Reminder about `git rebase onto`

Posted on

This is something that I haven’t had to do in a while, so I lost it as a part of my “git reflexes”. I had to do it today for the first time in over a year, so it kind of jogged my memory.

Using git rebase

Many folks are already familiar with basic git rebase, but as a recap:

Let’s say that I’m working in a feature branch off of main, but some other engineer gets their code merged to main first (how dare they!). So, because I’m awesome, I want to make sure I test my code with the latest. I also like a tidy git history, so I don’t just merge main into my feature branch. I do the following:

git checkout main
git pull
git checkout feat/JIRA-123
git rebase main

The above doesn’t mean I won’t have conflicts, but it does mean that any commits I have in feat/JIRA-123 get taken off the top of the commit log and then stacked on top of the commit log of main.

Using git rebase --onto

But what if there is another level of indirection?

Let’s say I am working in a monorepo and I am working on an Epic to allow users to manage their filesystem. I might break that into two work items that can ship separately. For example maybe feat/JIRA-123 represents creation of a shared library for doing the low-level filesystem stuff and feat/JIRA-321 represents the work to add screens in the user interface that leverage the library. Even though I may have unit tests in feat/JIRA-123 to test the library, sometimes it is useful to work on both at the same time to have a faster feedback loop.

To do this, I have the following branches chained up:

main -> feat/JIRA-123 -> feat/JIRA-321

As long as I’ve kept my commit history linear between the two feature branches, the commit history for feat/JIRA-123 might look like this:

917605a (HEAD -> feat/JIRA-123) chore: deprecate cross-compile for OS/360 and VAX 9000
3fa8d4c chore: makefile cross-compiles to OS/360 and VAX 9000
a8f5fef fix: library no longer deletes root filesystem
738bf4e feat: initial code for new library
0c1764f (origin/main, main) fix: temporary hack until new filesystem library is in place

This shows four commits in feat/JIRA-123 on top of main. Pretty standard stuff.

The commit history for feat/JIRA-321 might look like this:

76951b6 (HEAD -> feat/JIRA-321) chore: update README
d4d2b9f feat: add new screen that leverages new filesystem library
917605a (feat/JIRA-123) chore: deprecate cross-compile for OS/360 and VAX 9000
3fa8d4c chore: makefile cross-compiles to OS/360 and VAX 9000
a8f5fef fix: library no longer deletes root filesystem
738bf4e feat: initial code for new library
0c1764f (origin/main, main) fix: temporary hack until new filesystem library is in place

This is telling us that we have two commits ontop of the feat/JIRA-123 branch, which in turn sits on top of main.

Once my feat/JIRA-123 branch is ready to go into main I might do a “Squash and Merge”. This will take my four commits in feat/JIRA-123 and turn them into a single commit when it hits main. After the squash and merge my commit log in main looks like:

commit 4106ee08a29ea773711811c85e2c1bd70d05ce31 (HEAD -> main, origin/main)
Author: David Newman <some@email.com>
Date:   Fri May 6 14:08:07 2022 -0400

    feat/JIRA-123

    * chore: deprecate cross-compile for OS/360 and VAX 9000

    * chore: makefile cross-compiles to OS/360 and VAX 9000

    * fix: library no longer deletes root filesystem

    * feat: initial code for new library

Now I want to get feat/JIRA-321 ready to go to main and because I want to keep my job I make sure to test feat/JIRA-321 with the latest code from main.

If I just do git rebase main then I will have to deal with conflicts because the orignal commits from feat/JIRA-123 are still in the feat/JIRA-321 branch which makes git think that we are modifying the same files.

One way of avoiding the conflicts is:

git checkout main
git pull
git checkout feat/JIRA-321
git rebase --onto main HEAD~2

That last command is the important one and it says: “take two commits off the top of the currently checked out branch (feat/JIRA-321) and place them on top of the git history of main.

At this point any commits “between” the HEAD commit of main and the HEAD~2 commits of feat/JIRA-321 are eliminated from the history of the feat/JIRA-321 branch. In this case, that’s fine, because we know that the HEAD commit of main contains all the same code/work/whatever from feat/JIRA-123.

Now, this all goes out the window if I have been doing git merge or git pull between my two feature branches because I will have a non-linear commit history. However, the fact that the above is kinda simple once you know it is why I try and avoid that kind of thing at all costs.