My colleague Chris recently wrote about how to take advantage of Git rebase. In this post we'll explain how you can take these techniques, and apply them to daily developer life.
Fixup
Imagine you have created a merge request, and there are some pipeline failures and some comments from reviews, and suddenly your commit history looks something like this:
$ git log --oneline
8f8ef5af (HEAD -> my-change) More CI fixes
e4fb7935 Apply suggestion from reviewer
c1a1bec6 Apply suggestion from reviewer
673222be Make linter happy
a0c30577 Fix CI failure for X
5ff160db Implement feature Y
f68080e3 Implement feature X
3cdbc201 (origin/main, origin/HEAD, main) Merge branch 'other-change' into 'main'
...
In this example there are 2 commits implementing feature X and Y, followed by a handful of commits that aren't useful on their own. We used the fixup feature of Git rebase to get rid of them.
Finding the commit
The idea of this technique is to integrate the changes of these follow-up commits into the commits that introduced each feature. This means for each follow-up commit we need to determine which commit they belong to.
Based on the filename you may already know which commits belong together, but if you don't you can use git-blame to find the commit.
git blame <revision> -L<start>,<end> <filename>
With the option -L
we'll specify a range of a line numbers we're interested in.
Here <end>
cannot be omitted, but it can be the same as <start>
. You can
omit <revision>
, but you probably shouldn't because you want to skip over the
commits you want to rebase away. Your command will look something like this:
$ git blame 5ff160db -L22,22 app/model/user.rb
f68080e3 22) scope :admins, -> { where(admin: true) }
This tells us line 22
was touched by f68080e3 Implement feature X
.
Now repeat this step until you know the commit for each of the commits you want to rebase out.
Interactive rebase
The next step is to start the interactive rebase:
$ git rebase -i main
Here you're presented with the list of instructions in your $EDITOR
:
pick 8f8ef5af More CI fixes
pick e4fb7935 Apply suggestion from reviewer
pick c1a1bec6 Apply suggestion from reviewer
pick 673222be Make linter happy
pick a0c30577 Fix CI failure for X
pick 5ff160db Implement feature Y
pick f68080e3 Implement feature X
Now you'll need to change these instructions to something like this:
fixup 8f8ef5af More CI fixes
fixup e4fb7935 Apply suggestion from reviewer
fixup 673222be Make linter happy
pick 5ff160db Implement feature Y
fixup c1a1bec6 Apply suggestion from reviewer
fixup a0c30577 Fix CI failure for X
pick f68080e3 Implement feature X
As you can see I've reordered the commits, and I've changed some occurrences of
pick
to fixup
.
The Git rebase will process this list bottom-to-top. It takes each line with
pick
and uses its commit message. On each line starting with fixup
it
integrates the changes into the commit below. When you've saved this file and
closed your $EDITOR
, the Git history will look something like this:
$ git log --oneline
e880c726 (HEAD -> my-change) Implement feature Y
e088ea06 Implement feature X
3cdbc201 (origin/main, origin/HEAD, main) Merge branch 'other-change' into 'main'
...
Autosquash
Using autosquash can be an alternative technique to the above. First we'll uncommit all the commits we want to get rid of.
git checkout f68080e3
Now all changes only exist in your working tree, and are gone from the commit
history. You can use git add
or git add -p
to stage all changes related to
e088ea06 Implement feature X
. Instead of running git commit
or git commit -m
we'll use the --fixup
option:
$ git commit --fixup e088ea06
Now the history will look something like:
$ git log --oneline
e744646b (HEAD -> my-change) fixup! Implement feature X
5ff160db Implement feature Y
f68080e3 Implement feature X
3cdbc201 (origin/main, origin/HEAD, main) Merge branch 'other-change' into 'main'
...
All remaining changes should now belong to 5ff160db Implement feature Y
so we
can run:
$ git add .
$ git commit --fixup 5ff160db
$ git log --oneline
18c0fff9 (HEAD -> my-change) fixup! Implement feature Y
e744646b fixup! Implement feature X
5ff160db Implement feature Y
f68080e3 Implement feature X
3cdbc201 (origin/main, origin/HEAD, main) Merge branch 'other-change' into 'main'
...
You can now review the fixup!
commits and if you're happy with it, run:
$ git rebase -i --autosquash main
You see we provide the extra option --autosquash
. This option will look for
fixup!
commits and automatically reorder those and set their instruction to
fixup
. Normally there's nothing for you to be done now, and you can just close
the instruction list in your editor. If you type git log
now you'll see the
fixup!
commits are gone.
Alternatives
Finally, there are some tools that allow you to absorb commits more easily, for example:
- lib.rs/crates/git-absorb
- github.com/MrFlynn/git-absorb
- gitlab.com/bertoldia/git-absorb
- github.com/tummychow/git-absorb
- github.com/torbiak/git-autofixup
Cover image by Yung Chang on Unsplash.