Ashley Drake

Software Engineer

[email protected]

Twitter Github

Git Rebase Tricks: Rebase Interactive

I have been using the git rebase command heavily lately, and thought it might be good to write a quick tips and tricks guide about it. I have found it incredibly useful for keeping my git history clean, organizing my thoughts about what I am writing and why, and grouping changes together as logical groups.

Why would I want to use this?

First, it might be useful to motivate why this tool is helpful.

Say you have a commit history that looks like this:

commit e3e96d237c2fb4f7a5fdgf97023652ae91697bde
Author: Ashley Drake <[email protected]>
Date:   Fri Jan 17 14:45:56 2020 +0000

    Add tests for feature Y.

commit 1ded87897327cdjke2743829b199a6eea1e64aeea0
Author: Ashley Drake <[email protected]>
Date:   Thu Jan 16 21:17:26 2020 +0100

    Implement feature X.

commit 17b1c8kjlk648b1ba890afjklddf71b93f9bfb80d4
Author: Ashley Drake <[email protected]>
Date:   Thu Jan 16 07:04:56 2020 -0800

    Write tests for feature X.


And then, you realize that you need to update some of your tests to handle a new edge case you discovered while developing feature Y. You fix it, and make a new commit.

commit 17b1cjkljsdk648b1ba780afjlkjdf71b93f9bfb390d4
Author: Ashley Drake <[email protected]>
Date:   Fri Jan 17 16:23:08 2020 -0800

    Fix tests for feature X to add new edge case.

However, this means the “logical change” of “adding a new test for X” is now split across 2 separate, non-consecutive commits. If someone tries to revert the first commit, this is what happens:

[email protected] ~/development/oss/project/tests $ git revert 17b1c8kjlk648b1ba890afjklddf71b93f9bfb80d4
error: could not revert 17b1c8... Add test.
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'

And then they (or you) have to comb back through commits to find the other one to revert first. You can imagine what this looks like if you're trying to revert a change to make a hotfix, and the clock is ticking.

git rebase allows you rewrite history (doesn't that sound nice!), and clean up your changes, so you can have clean histories like this, and easily revert what you need:

aabbcc Write tests for feature Y.
ddeecc Implement feature X.
ffddss Write tests for feature X.

What is rebase?

Rebase, in short, is an alternative to git merge, which merges 3 items together: the one common ancestor commit of the 2 branches being merged (where in history they diverge from one another) and the HEAD of each branch, respectively. A rebase, however, attempts to replay commits from one branch on top of the other - usually, it would replay commits from a feature branch onto a master or main branch. It will rewrite history. [1]

For example, if you branched your development branch from master at commit X, and made commits Y and Z in the feature branch, while in the meantime master progresses with new commits A and B; rebasing your development branch on top of master would apply your commits Y and Z after commits A and B, even if you committed those changes previous to A and B in time. It rewrites history, as if you had started your branch at commit B, instead of commit X.

However, the tricks I mention below are ones that you can use within one branch, i.e. rebasing a branch against itself. Since the rebase command just replays commits, you can essentially tell git, “I'd like to rewind to 10 commits ago, and change a few things.”

[1]Helpful summary of rebase

Combining and Reordering

You can combine 2 commits using the squash command. In our example, the commits we want to combine aren't next to each other - but that is no problem.

First, you want to re-order the commits, so that they are next to each other. This is done in the rebase editor, by moving the commit lines so that they are in the order you want. Pay attention, as the order goes from top to bottom.

Before:

1 pick 69e9993 Implement feature Y.
2 pick 2d96c71 Implement Feature Z.
3 pick 7dad83c Bugfix for feature Y.

After:

1 pick 69e9993 Implement feature Y.
3 pick 7dad83c Bugfix for feature Y.
2 pick 2d96c71 Implement Feature Z.

Then, you can either squash or fixup the more recent (lower) commit. Squash will retain the comments from each commit message; fixup will treat the more recent of the commits as a “bugfix” to the previous one, and ignore the commit message. Either way, the more recent commit is “smooshed” (yes, that is a technical term) into the one prior to it. The difference is how the commit messages are handled.

Here, I chose fixup, because I don't really care about retaining the “bugfix” commit message.

1 pick 69e9993 Implement feature Y.
3 fixup 7dad83c Bugfix for feature Y.
2 pick 2d96c71 Implement Feature Z.

This is a great tool if your commit history is like mine, and has lots of “bugfix … another bugfix… Fixing the bugfix…” type commits in it.

Editing a Commit and/or Commit Message

If all you want to do is edit the commit message, reword is your friend. It's like using git commit --amend without any code changes for multiple commits at the same time. Here, we fix a few spelling errors.

1 reword 69e9993 Commit message with a msipelling
2 pick 2d96c71 Bugfix.
3 reword 7dad83c Imlpement feature Y.
4 pick 27b1c8a Add new tests.
5 pick 78lkjsd Bugfix for feature Y.

What happens if you just want to make a small change in the commit itself, without having to (a) make a new commit, (b) reorder it, and (c) squash/fixup? You can tell git, “stop! I just need to fix a few things,” by using the edit command. The effect would be that git starts to apply commits … and when it gets to the commit marked with edit … it will stop, allowing you to make changes to the changeset represented by that commit.

Here, imagine we found a bug in feature A that is very simple to fix, maybe a misspelling. Instead of committing the bugfix separately and then going back to move it, squash it, etc., we just edit the commit interactively.

1 pick 69e9993 Implement feature A.
2 edit 7dad83c Implement feature Y.
3 pick 27b1c8a Add new tests for feature Y.

Deleting commits altogether

Added your AWS secrets to your repo? Committed your node_modules folder by mistake? It's okay! git rebase rewrites history so that even searching through old versions of the code, nobody can ever find the thing you deleted (unless they somehow managed to clone your repo at just the right time, or have it locally). Just removing the line altogehter, or marking the commit with drop, will get rid of it forever. (Depending on the situation, using edit might also be more effective.)

How might this change your development strategy?

For one, you don't need to be afraid of trying to undo mistakes. You can present a clean git history in a PR, which is also really handy when requesting reviews - if you can organize your commits in a logical order that even allows you to separate a range of commits and ask for feedback on smaller pieces, this is really handy.

I'm also more likely to work in one area at a time, make smaller commits that encompass one or two files at a time, knowing I can combine them in more representative ways later on. My workflow is to keep commits small, push to remote, and if needed, after a review, rebase and combine commits.

It also presents your work in a clean, professional manner. If you work in open source, or are asked to do a code project for a job application, it's a nice touch to present a clean series of commits to the reviewer.

There are a few other options available as well, but these are the tools I use frequently, and have gotten me out of a bind more than once! As always, git-scm has your back if you need more information.