In my previous Git tutorial, we covered the basics of version control - initialising repos, making commits, pushing to remotes, and creating branches. Now it's time to wade a little deeper into Git's more powerful features. I'm going to show you how to fix mistakes, manage your work in progress, and manipulate your commit history like a pro.
Fixing Mistakes with revert
and reset
Let's face it - we all make mistakes. Maybe you committed code with a bug, or accidentally included a giant log file. Git provides multiple ways to walk back these errors.
Using git revert
The revert
command creates a new commit that undoes the changes made in a previous commit. It's the safest option for fixing mistakes as it doesn't alter the repository's history - it just adds a new commit that reverses the unwanted changes.
git revert <commit-hash>
This is perfect for when you've already pushed your commits to a remote repository. It's non-destructive and won't cause issues for others who have pulled your code.
Using git reset
Unlike revert
, reset
actually moves the current branch pointer to a different commit, effectively removing commits from your current branch history. There are three main modes:
git reset --soft <commit-hash> # Keeps changes staged
git reset --mixed <commit-hash> # Default - keeps changes but unstages them
git reset --hard <commit-hash> # Discards all changes since that commit
--soft
is helpful when you want to recommit your changes with a different commit message or structure. --mixed
gives you a clean slate while preserving your work. --hard
is the nuclear option - use it carefully as it permanently deletes uncommitted changes!
One common use of reset is to undo local commits that haven't been pushed:
git reset --soft HEAD~1 # Move back one commit but keep changes staged
⚠️ Important: Never use reset
on commits that have been pushed to a shared repository. This will cause major headaches for your team as your local history will diverge from the shared history.
Temporarily Shelving Changes with stash
Ever been working on a feature when your colleague reports a critical bug that needs immediate attention? You're not ready to commit your half-finished changes, but you need a clean working directory to fix the bug. Enter git stash
.
git stash save "WIP: Feature X implementation"
This command takes all your uncommitted changes (both staged and unstaged) and saves them on a temporary shelf, reverting your working directory to match the HEAD commit. After fixing that bug, you can bring your shelved changes back:
git stash apply # Apply the most recent stash but keep it in the stash list
git stash pop # Apply the most recent stash and remove it from the stash list
You can have multiple stashes:
git stash list # See all stashes
git stash apply stash@{2} # Apply a specific stash
git stash drop stash@{1} # Remove a specific stash
git stash clear # Remove all stashes
I've found stashing particularly handy when I need to switch branches but have uncommitted changes that would be overwritten by the checkout.
Rewriting History with rebase
Sometimes your commit history gets messy - lots of small commits, typo fixes, or "WIP" commits. Rebasing lets you clean this up by rewriting your branch's history.
The basic interactive rebase command is:
git rebase -i <commit-hash>
This opens an editor showing a list of commits since the specified hash. You can then:
pick
- Keep the commit as isreword
- Change the commit messageedit
- Stop and amend the commitsquash
- Combine with previous commit and keep both messagesfixup
- Combine with previous commit and discard this commit's messagedrop
- Remove the commit entirely
For example, to squash your last three commits into one cleaner commit:
git rebase -i HEAD~3
Then change pick
to squash
(or s
for short) for the commits you want to combine, save, and edit the combined commit message.
Rebasing is also useful for incorporating upstream changes:
git fetch origin
git rebase origin/main
This is similar to git pull
, but instead of merging (which creates a merge commit), it rewrites your branch's history to appear as if you started from the latest main branch.
Warning: Just like reset
, never rebase commits that have been pushed and shared with others unless you're absolutely sure no one has based work on them.
Cherry-picking Commits
Sometimes you only want to grab a specific commit from another branch without merging the entire branch. That's where cherry-pick
comes in.
git cherry-pick <commit-hash>
This takes the changes from the specified commit and applies them to your current branch, creating a new commit with the same message (which you can edit if needed).
It's particularly useful when:
- You need to backport a bug fix to a release branch
- A colleague made a useful change in their feature branch that you want in yours
- You need to restore a specific change that was lost during a complex merge
For example, to grab a specific fix from a feature branch:
git checkout release-branch
git cherry-pick abc123def # The hash of the bug fix commit
Practical Workflow Examples
Let's wrap these commands together in a few real-world scenarios:
Scenario 1: Oops, that last commit had a mistake!
# Fix the mistake in your code
git add .
git commit --amend # Amends your last commit with the new changes
If you've already pushed:
git revert HEAD # Creates a new commit undoing the previous one
git push # Safely push the revert
Scenario 2: My feature branch got messy with lots of small commits
git rebase -i HEAD~5 # Assuming you want to clean up the last 5 commits
# Change commits to 'squash' or 'fixup' as needed
git push --force-with-lease # Only push if no one else has updated the branch
Scenario 3: I need to switch tasks but my work isn't ready to commit
git stash save "Halfway through refactoring user authentication"
git checkout -b hotfix/critical-bug
# Fix the bug
git commit -m "Fix critical bug in payment processing"
git checkout previous-branch
git stash pop # Resume where you left off
Scenario 4: I need a specific bugfix from another branch
git cherry-pick 789abcdef # Apply just that commit to your current branch
Pro Tips
- Always name your stashes with descriptive messages. Future you will thank past you.
- Use
git reflog
to find commit hashes after a bad reset or rebase. - Consider
--force-with-lease
instead of--force
when pushing rewritten history - it's safer. - Combine
git add -p
with these commands to selectively stage parts of files. - Keep in mind that
rebase
andreset
change commit hashes, so make sure you're not disrupting collaborators.
Wrapping Up
These intermediate Git commands give you more control over your workflow and can help you maintain a cleaner, more purposeful commit history. Remember that with great power comes great responsibility - especially when rewriting history that others might be working with.
In the next tutorial, we'll dive into more advanced techniques like submodules, bisect, and hooks. Until then, happy committing!