As a developer, I often find myself in situations where I need to rewrite Git history to fix mistakes, clean things up, and reorganize my commits. While Git provides powerful commands like reset, revert, and rebase for these scenarios, their differences can be confusing for many developers.
In this comprehensive guide, we‘ll demystify these commands with visual diagrams, clear explanations, and actionable tips you can apply right away. Whether you‘re a junior dev getting started with Git or a seasoned engineer looking to master advanced techniques, this guide has you covered! Let‘s dive in.
Reset: Rewinding Time
Ah, git reset. This versatile command lets you travel back in time to undo commits and make changes as if they never happened.
Resetting is like rewinding history – it moves the current branch pointer backward to the selected commit. The implications depend on whether you use --soft, --mixed, or --hard mode:
| Reset Type | Branch Pointer | Staging Area | Working Directory |
|---|---|---|---|
--soft |
Rewind | Preserved | Preserved |
--mixed |
Rewind | Rewind | Preserved |
--hard |
Rewind | Rewind | Rewind |
As you can see, --soft merely repositions the branch pointer without losing changes. This lets you easily amend commits before sharing them publicly.
--mixed resets both the branch pointer and staging area while keeping the working directory changes intact. This lets you unstage commits without losing work in progress.
Finally, --hard rewinds everything – branch, staging area, working directory – to match the target commit. This obliterates all uncommitted changes, so use with care!
Now let‘s see reset in action. Imagine we have a commit history that looks like:
A - B - C (master)
We make some changes, add them, and commit:
$ git add .
$ git commit -m "Commit D"
A - B - C - D (master)
But now we realize D was a mistake! Let‘s undo it with reset:
$ git reset --hard C
A - B - C (master)
Boom! Commit D is gone and the files now match C. This is the power of reset – it lets you remove commits and reland the branch wherever you want.
Common Reset Scenarios
Here are some common scenarios where you‘ll want to reach for git reset:
-
Uncommit staged changes:
git reset --mixed HEAD^ -
Undo the last commit:
git reset --hard HEAD^ -
Abort merge conflicts in progress:
git reset --merge -
Revert an entire feature branch:
git reset --hard origin/main
As you can see, reset gives you ultimate control over your local branch history. The ability to rewrite commits is a superpower – but use it judiciously!
Revert:Undoing Changes Safely
While resetting erases history, git revert takes a safer approach: creating a new commit that reverses an existing one.
Revert is used to undo changes that have already been made public through pushing. It applies a mirror-image commit that cancels out the unwanted commit:
$ git revert C
A - B - C - C‘ (master)
Here, C‘ is the inverse of C effectively removing the changes. But the original C commit remains intact in the history. This helps avoid disrupting other developers working on the branch.
Revert is meant to be used for undoing public commits. For private local work, reset is usually a better choice.
Let‘s see an example. Say we have some commits on master:
A - B - C - D (master)
We realize D broke something, but it‘s already been pushed to the remote repository!
No problem, we‘ll revert it:
$ git revert D
A - B - C - D - D‘ (master)
Commit D is reverted with new commit D‘. Our teammates will get D‘ which neutralizes D without having to reset/rebase anything.
Revert creates a safe, auditable history – essential for shared repositories. Use it instead of reset for public commits.
Rebase: Rearranging History
While reset and revert modify history, rebase rewrites it. Rebasing replays commits from one branch onto another new base. This moves the entire branch to begin on the new commit.
For example, say we have:
A - B - C (master)
/
D - E - F (feature)
We can rebase the feature branch onto master like this:
(feature) $ git rebase master
Result:
A - B - C (master)
\
D - E - F (feature)
This fast-forwards feature by removing D-E-F and replaying them after C.
The result is a linear history – but rebasing rewrites IDs and overwrites existing commits. This can be dangerous if the commits have already been made public.
Some other advantages of rebasing:
- Cleans up local experiments and consolidates commits before merging
- Incorporates upstream changes from the main branch
- Resolves conflicts related to overlapping commits
But rebase carefully to avoid unintended consequences for others. And avoid rebasing commits that exist outside your local repository.
Interactive Rebasing
For more advanced rebasing, you can use the interactive mode. This lets you alter individual commits in exciting ways!
To rebase interactively:
$ git rebase -i HEAD~5
This will launch an editor where you can:
- Reorder commits just by changing their order
- Squash commits together by deleting lines
- Edit commit messages and content by changing ‘pick‘ to ‘edit‘
- And more!
Interactive rebasing gives total control over your local commits before sharing with others. Definitely a power-user Git skill worth learning.
Putting It All Together
We‘ve covered the key differences between resetting, reverting and rebasing. Here‘s a quick recap:
- Reset removes commits from current branch
- Revert creates inverse commits to undo changes
- Rebase moves branch to new base by replaying commits
To help cement these concepts, let‘s walk through a hypothetical workflow using all three commands.
Imagine we‘re working on a new feature in a dedicated branch:
A - B (main)
\
C - D (feature)
We decide to incorporate some upstream changes from main to resolve potential conflicts early:
(feature)$ git rebase main
A - B - E (main)
\
C - D (feature)
Rebasing replays our feature commits onto the updated main. So far so good!
We make some more progress on the feature and commit our changes:
A - B - E (main)
\
C - D - F (feature)
But later we realize F introduced a major bug! We could reset it, but the commits are isolated to our private feature branch. So instead we‘ll just revert it:
(feature) $ git revert F
A - B - E (main)
\
C - D - F - F‘ (feature)
F‘ undoes the changes from F. Finally we finish up our feature and it‘s time to merge to main:
$ git checkout main
$ git merge feature
A - B - E - G (main)
\
C - D - F - F‘ (feature)
Our nice linear history is preserved thanks to rebasing and reverting earlier. What a clean feature branch!
This shows how rebasing, reverting, and resetting all have their place depending on the situation at hand. A Git master knows when and how to use each one.
Git Superpowers Unlocked
Whew, we covered a lot of ground here today! Here are some key takeways:
- Reset removes commits from current branch and resets working tree
- Revert creates inverse commits to reverse changes
- Rebase replays commits onto a new base
- Rewriting public history can cause issues for collaborators
- Interactive rebase provides fine-grained commit control
Learning these commands unlocks Git superpowers allowing you to manipulate history like never before. Start practicing with some throwaway branches to get comfortable.
The ability to rewrite commits takes Git skills to the next level. I hope this guide gives you the knowledge and confidence to tame even the most tangled commit histories! Let me know if you have any other topics you‘d like me to cover. Happy Gitting!