Git Reset Vs Revert Vs Rebase

Git Reset vs. Revert vs. Rebase: Mastering Git History Manipulation
Understanding the distinctions and appropriate use cases for git reset, git revert, and git rebase is crucial for effective Git history management. These commands offer different approaches to modifying commit history, ranging from destructive rewrites to non-destructive additions. Choosing the right tool depends on the desired outcome, the stage of the commit (local vs. pushed), and the potential impact on collaborators. This article will delve into each command, providing detailed explanations, practical examples, and guidance on when to employ them.
Git Reset: Rewriting Local History
git reset is a powerful and potentially destructive command that modifies the commit history of a branch. It works by moving the current branch pointer (HEAD) to a specified commit. Critically, git reset can also alter the staging area and the working directory, depending on the mode used. There are three primary modes for git reset: soft, mixed (the default), and hard.
git reset --soft <commit-ish>
The git reset --soft command moves the branch pointer (HEAD) to the specified <commit-ish> (a commit hash, tag, or branch name) but leaves the staging area and the working directory unchanged. This means that all the changes from the commits that were "un-done" by the reset will now appear as staged changes. This is particularly useful when you want to recommit changes with a different commit message or combine multiple commits into a single one.
Example: Suppose you have the following commit history:
A -- B -- C (main)
You’ve just made commit C, but you realize you want to combine B and C into a single, cleaner commit, or perhaps just change the commit message of C.
git reset --soft HEAD~1
After this command, your history will look like:
A -- B (main)
However, your staging area will contain all the changes that were in commit C. You can then make new changes, stage them, and create a new commit.
git commit -m "New combined commit message"
This effectively replaces commit C with a new commit, potentially incorporating changes from B if you re-staged them.
Use Cases for git reset --soft:
- Amending a commit: When you need to change the commit message of the most recent commit, or add/remove files from it.
- Combining commits: To squash multiple recent commits into a single logical commit.
- Preparing for a rebase: To un-stage changes from previous commits before rebasing onto a new branch.
git reset --mixed <commit-ish> (Default)
git reset --mixed (or simply git reset <commit-ish>) moves the branch pointer to the specified <commit-ish> and unstages the changes. The working directory remains untouched. This means that the changes introduced by the commits that are now behind HEAD will be present in your working directory as modified but unstaged files.
Example: Continuing from the previous example:
A -- B -- C (main)
You want to undo commit C and reconsider the changes.
git reset --mixed HEAD~1
Your history becomes:
A -- B (main)
And the changes from commit C are now in your working directory, but they are not staged. You can then choose to re-stage specific parts of the changes, discard some changes, or commit them as a new commit.
Use Cases for git reset --mixed:
- Unstaging changes: If you accidentally staged files,
git reset --mixed HEADwill unstage them without discarding the modifications. - Reconsidering changes: To undo recent commits and have the modifications available in your working directory to be selectively re-staged and committed.
- Cleaning up messy commits: Before pushing, you might use this to break down a large commit into smaller, more manageable ones.
git reset --hard <commit-ish>
git reset --hard is the most destructive form of git reset. It moves the branch pointer to the specified <commit-ish> and discards all changes in both the staging area and the working directory that are not part of the target commit. Use this command with extreme caution, as any uncommitted or unstaged changes will be permanently lost.
Example:
A -- B -- C (main)
You decide you absolutely want to go back to commit B and discard all work done in commit C.
git reset --hard HEAD~1
Your history becomes:
A -- B (main)
And all the changes from commit C are gone from your working directory and staging area.
Use Cases for git reset --hard:
- Discarding unwanted commits and changes: When you are certain that the commits you are undoing and all subsequent changes are incorrect and should be erased.
- Cleaning up a feature branch before merging: If a feature branch has gone completely off track,
git reset --hardcan be used to discard all the work on that branch and return to a clean state. - Recovering from a bad merge: In some cases,
git reset --hardcan be used to undo a problematic merge, butgit revertis generally safer for undoing merges that have already been pushed.
Important Note on git reset: git reset rewrites history. If you have pushed the commits you are resetting away to a remote repository, using git reset locally and then attempting to git push --force can cause significant problems for collaborators who have already pulled those commits. It’s generally considered bad practice to git reset commits that have been pushed and shared.
Git Revert: Creating New Commits to Undo Changes
git revert is a non-destructive way to undo changes introduced by previous commits. Instead of removing commits from history, git revert creates new commits that reverse the effects of a specified commit. This makes it a much safer option for undoing changes, especially on branches that have been pushed to a shared remote repository.
How it works: When you git revert <commit-ish>, Git analyzes the changes introduced by <commit-ish> and creates a new commit that applies the inverse of those changes. For example, if <commit-ish> added a line of text, git revert will create a commit that removes that line.
Example: Consider the following commit history:
A -- B -- C -- D (main)
Let’s say commit C introduced a bug. You want to undo the changes from commit C.
git revert C
Git will open your default editor to allow you to edit the commit message for the new revert commit. The default message will usually indicate that it’s reverting commit C. After saving and closing the editor, your history will look like this:
A -- B -- C -- D -- E (main)
Where commit E contains the inverse changes of commit C, effectively undoing the bug introduced by C.
Use Cases for git revert:
- Undoing specific commits on shared branches: This is the primary and safest use case. If a commit has been pushed,
git revertallows you to undo its effects without rewriting history, preventing conflicts for collaborators. - Undoing a merge: You can revert a merge commit to undo all the changes brought in by that merge. This will create a new commit that undoes the merge.
- Selective undoing of changes: If a commit introduced multiple changes, you can revert specific changes within that commit by cherry-picking or editing the revert commit.
Reverting Ranges of Commits: You can revert a range of commits using git revert <older-commit>^..<newer-commit>. The ^ is important to include the older commit in the range.
Example: To revert commits C and D:
git revert C..D
This will create two new revert commits, one for D and one for C (in that order).
Key Advantage of git revert: It preserves history. This means that the original commits remain in the history, and the revert commit clearly indicates what was undone. This makes it easier to track the evolution of the codebase and understand why certain changes were made or unmade.
Git Rebase: Integrating Changes by Moving Commits
git rebase is a powerful command used to integrate changes from one branch onto another by replaying commits. Unlike merge, which creates a new merge commit, git rebase rewrites history by moving commits from one branch to another. It effectively moves the base of your branch to a new commit.
How it works: When you rebase branch feature onto branch main (git rebase main while on feature), Git finds the common ancestor of feature and main. It then takes all the commits that are unique to feature (since the common ancestor) and reapplies them, one by one, on top of the latest commit of main.
Example: Consider this scenario:
You are working on a feature branch:
A -- B -- C (main)
D -- E (feature)
While you were working on feature, new commits were added to main:
A -- B -- C -- F -- G (main)
D -- E (feature)
If you merge main into feature now, you’ll get a merge commit:
A -- B -- C -- F -- G -- H (main)
/
D -- E -----------
Instead, you can rebase feature onto main:
git checkout feature
git rebase main
Git will take commits D and E from feature and reapply them on top of G:
A -- B -- C -- F -- G (main)
D' -- E' (feature)
Notice that D' and E' are new commits with the same changes as D and E, but their parent commit is now G. This results in a linear history, which is often considered cleaner.
Interactive Rebase (git rebase -i <commit-ish>):
The interactive mode of git rebase (git rebase -i) is incredibly powerful for cleaning up your local commit history before pushing. It allows you to:
pick: Use the commit as is.reword: Edit the commit message.edit: Amend the commit’s content.squash: Combine the commit into the previous one, but edit the combined commit message.fixup: Combine the commit into the previous one, discarding the commit’s message.drop: Remove the commit entirely.
Example of Interactive Rebase:
Let’s say you have a feature branch with several small, unpolished commits.
A -- B -- C -- D -- E -- F (main)
G -- H -- I -- J (feature)
You want to combine H, I, and J into a single commit, and reword the commit message of G.
git checkout feature
git rebase -i HEAD~4 # To rebase the last 4 commits of the feature branch
This will open an editor with something like:
pick G Some initial work
pick H More work on feature
pick I Fix a typo
pick J Finalizing feature
# Rebase 5a3b1c7..f9e8d2a onto 5a3b1c7 (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
You would change it to:
reword G Some initial work
pick H More work on feature
squash I Fix a typo
squash J Finalizing feature
After saving, Git will first ask you to reword commit G. Then, it will apply H, and then squash I and J into H, allowing you to edit the combined commit message. The final history might look like:
A -- B -- C -- F -- G' (main)
H'' (feature)
Where G' has a new message and H'' is a combination of H, I, and J with a new, consolidated commit message.
Use Cases for git rebase:
- Keeping feature branches up-to-date: Regularly rebasing a feature branch onto the main development branch (e.g.,
mainordevelop) helps to avoid large, complex merge conflicts later on. - Creating a clean, linear history: For public or shared branches, a linear history can be easier to read and understand than one filled with merge commits.
- Cleaning up local commits before sharing: Interactive rebase is invaluable for squashing, rewording, and reordering commits to present a polished and logical commit history.
Crucial Warning about git rebase: git rebase rewrites history. Never rebase commits that have already been pushed to a shared remote repository. Doing so will cause significant disruption for collaborators who have based their work on those original commits. If you need to undo pushed commits, use git revert. If you absolutely must rebase a shared branch (e.g., correcting a mistake in a very recent, unaccepted pull request), be prepared for forceful pushes (git push --force-with-lease) and clear communication with your team.
Comparing and Contrasting: Reset vs. Revert vs. Rebase
| Feature | git reset |
git revert |
git rebase |
|---|---|---|---|
| History | Rewrites history (removes commits) | Preserves history (adds new commits) | Rewrites history (moves/rewrites commits) |
| Destructive? | Potentially destructive (--hard) |
Non-destructive | Potentially destructive (rewrites history) |
| Purpose | Undo local commits, clean up local history | Undo specific changes, undo pushed commits | Integrate changes, linearize history, clean local history |
| Impact on Collaboration | High risk if used on pushed commits | Safe for pushed commits | High risk if used on pushed commits |
| Staging Area | Can affect staging area and working dir | Does not affect staging area or working dir | Can affect working dir, but staged changes are often kept |
| New Commits | No new commits (unless creating new ones after reset) | Creates new commits | Creates new commits (with potentially different SHA-1s) |
| Best For | Correcting mistakes in local, unpushed work | Undoing published changes or specific past changes | Keeping feature branches updated, creating a clean history |
When to Use Which Command: Practical Scenarios
-
You just made a commit locally and want to change its message or add/remove a file:
- Use
git commit --amend(which is essentially agit reset --soft HEAD~1followed by a new commit).
- Use
-
You made a few commits locally and realize they are all part of the same logical change:
- Use
git rebase -itosquashorfixupthe commits into one. - Alternatively,
git reset --soft HEAD~N(where N is the number of commits to squash) followed by a singlegit commit.
- Use
-
You made a commit locally and want to discard it entirely, along with any changes:
- Use
git reset --hard HEAD~1. Be absolutely sure you want to lose these changes.
- Use
-
You made a commit locally and want to undo it, but keep the changes available to re-stage or modify:
- Use
git reset --mixed HEAD~1.
- Use
-
You accidentally committed sensitive information to a branch that has already been pushed:
- Do NOT use
git reset. - Use
git revert <commit-hash>for the commit containing the sensitive data. - Then, you might need to use a tool like
git filter-repo(a more advanced and safer alternative tobfg-repo-cleaner) to completely remove the sensitive data from history, but this requires careful execution and communication.
- Do NOT use
-
You are working on a feature branch and want to incorporate the latest changes from the main development branch (
mainordevelop):- Use
git rebase main(while on your feature branch). This will keep your history linear and cleaner.
- Use
-
A colleague pushed a commit that introduced a bug, and you need to undo its effects:
- Use
git revert <commit-hash>. This is the safest way to undo published changes.
- Use
-
You want to create a clean, linear history for a pull request:
- Use
git rebase -ion your feature branch to clean up commit messages, squash small commits, and reorder if necessary.
- Use
Mastering git reset, git revert, and git rebase offers significant control over your Git workflow. Prioritizing safety, especially concerning pushed commits, will prevent unnecessary complications and ensure a smoother development process for yourself and your team.



