Or alternative title - How I learned to love Git rebase
When I first started working with Git, I was indoctrinated into the school of merge and was told to never rebase. Rebasing lets you re-write history. The whole point of Git is to track history. Therefore, rebasing is bad.
But there was one workflow that truly cemented my conversion and has become a regular part of my code writing process - rebasing on my active branch.
Even with linters and a spell checker extension installed in my code editor, from time to time, I'll catch a typo I've committed to git. Or I'll forget a change in a lingering file. And because the basic push workflow has become muscle memory at this point, I would push the commit before I noticed the mistake. I would fix it and do one of these...
$ git commit -m "fix typo"
Gross.
But we can fix this quickly with an interactive rebase!
Fixup First
After fixing my mistakes, I'll stage the file like normal. Then instead of -m
and the cringy message, I mark the commit with the --fixup
option. The command expects a commit SHA-1 to attach the fix to.
$ git add .
$ git commit --fixup 710f0f8
Another neat trick is to refer to the previous commit as the parent of the current one. HEAD~
, HEAD^
will both work, as would HEAD~2
to refer to the commit before last, or the grandparent of the current commit. Note that ~
and ^
are not interchangable. Git is even smart enought to be able to find the first words of a commit message.
# these will all fixup your commit to the previous one.
$ git commit HEAD~
$ git commit HEAD^
$ git commit :/update
When I run git log
, the history will look like this:
f7f3f6d (HEAD) fixup! update variable name
310154e update variable name
a5f4a0d (master, origin/master, origin/HEAD) working code on master
Let's rebase!
We add -i
to run the rebase in interactive mode and supply an argument of the parent of the last commit you want to edit. I use this as a rule: add 1 to the number of commits I need to go back. Adding --autosquash
will pick up any commits prefaced with fixup!
and set your interactive rebase session with the commands filled in.
$ git rebase -i --autosquash HEAD~3
The result of that command will be a list of your commits in ascending order (my default opens in vim) along with the action git should apply when running the commit. Note that the last commit already has fixup
command attached.
pick a5f4a0d working code on master
pick 310154e update variable name
fixup f7f3f6d fixup! update variable name
# Rebase a5f4a0d..f7f3f6d onto a5f4a0d (3 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
# 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
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# 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.
#
#
# Note that empty commits are commented out
At this point, if I'm doing a final clean up to push to a remote branch, I'll maybe reword
a commit message or maybe even squash
some extraneous commits together. It's worth having a read through the comments because this is a really powerful workflow, but fixup
is the one I use the most.
Once you complete the rebase, your revised git log
should have a new singular commit that has combined your fixup commit with the previous one. bows and accepts applause
2231360 update variable name
a5f4a0d (master, origin/master, origin/HEAD) working code on master
Autosquash magic
--autosquash
will also pick up commits with --squash
option, but I tend to not want to keep that message, so fixup works just fine for me. Squash might be a good option if you have a significant amount of new code but want only one atomic commit.
You can also set the following git config setting to omit having to include the autosquash option every time you run an interactive rebase.
$ git config --global rebase.autosquash true
My setups always have this set to true, which helped make fixups and squashing commits feel like second nature since to enter a rebase session, I only have to type git rebase -i HEAD~3
or however many commits I think I need to clean up.
And that's how I converted to rebase!
Other helpful resources include this git tutorial on revising history. Once I digested that, I found it easier to understand the full doc on rebase.
Top comments (11)
Your alternative title is better. You should never rebase after you push to a remote. Ever.
I would clarify that as never rebase after you push to a remote shared with others. I push and rebase my feature branch all the time. It's especially helpful when reviewing pull requests.
A feature branch pushed up to a shared repository is available to others, so at this point, it becomes a matter of communication and etiquette. I wouldn't force push to a remote branch if someone else is actively committing or has an upstream based off of mine, but that situation is rare on the teams I work on.
If we end up with a feature branch that other developers would work on (pretty rare), we would just treat it like we do for master. Create a new branch off it, rebase and merge. Same workflow.
While it may be rare, I prefer a workflow that doesn't have any gotchas. If we never rebase anything that has been pushed, our workflow never requires the developer to wonder if anyone else might be affected by a rebase. My devs have enough to think about. The more I can take off their plate, the better.
That's the nice thing about git. It supports different workflows. So whatever works best for your team is what you should do. There is no right or wrong way.
As someone who makes many typos for a living, these are great!!
Really great of fixing multiple mistakes along the history.
One question, if it's just the latest commit you need to fix why not use
git commit --amend
?I often like to fixup and then keep going with my work. Then I'll rebase when I feel that I'm at a natural stopping point. But
git commit --amend
is an option!😳 That’s a lotta work to fix a sepling error! Good for you, though. This level of attention to detail implies that you are a polished developer.
Thanks! It's not only typos. Sometimes I may commit a "WIP", but fixup or squash the commit ass I get closer to opening a pull request. Getting comfortable with this technique has taught me the value of presenting polished work, to borrow your description. :)