One of my core tenants as a developer is that git history should be a tool for solving problems. To me, that means that it should take the shape of a single continuous line of commits, which are delineated into groups (which usually represent pull requests) by merge commits.
The one struggle I’ve found with this approach is that in order to release some code and not release other code for whatever reason, it is sometimes necessary to rearrange the order of the pull requests after they have been merged in. My old solution for this was very time consuming and involved a lot of manual cherry-picking and merging.
But THEN in 2019, the
But when I used that flag for the first time I was super confused by the output, and shied away from it for awhile until I took the time to sit down and parse the complicated output. This blog post is what I’ve learned from that process and in the years since.
This post is intended for people who are already very comfortable with git interactive rebasing;
git rebase -i.
The syntax is nearly identical to the standard git rebase command, with the addition of the
My example in this post will be based on this sample git history:
If you’d like to you can follow along by cloning the repo from GitHub.
Understanding the output
In my opinion, the most complicated part of
-i --rebase-merges is the confusing grouping that git outputs. You can find each one of your original pull requests in its output, but but git doesn’t group them that way by default.
More on that in a moment, but first let’s see the output of the command:
Just like standard interactive rebasing, each line of the output corresponds to a command, with the first word being the command and the rest of the line being the arguments. Unlike standard interactive rebasing, each line does not correspond to a single commit (the
pick lines still do represent a single commit though).
pick <commit> command you should be familiar with from regular interactive rebasing (it just means “use this commit”), but the rest are new.
label <label>is used to give a temporary name to the commit that HEAD currently points to when the command is processed.
merge -C <commit> <label>is used to create a merge commit from HEAD to the specified label, with the same message as the specified commit.
reset <label>is similar to the
git resetcommand, and resets the HEAD pointer to the commit that was labeled with the specified label.
Step 1: Rearrange Git’s Default Output
You can see from the output above that the blank lines split up the output into 5 sections, three of them being larger than the others. You might assume that the three big ones correspond to your three pull requests, but that’s not entirely true; the blank lines don’t properly correspond to the separation between the pull requests.
Git puts a blank line before every
reset command. It also adds a full line comment before the
reset command (which I find more distracting than helpful). To make the groups correctly correspond to the pull requests, we want the blank lines to come after the
merge commands, like this:
All I’ve done is move some blank lines around and delete the full line comments, but now each one of these groups of commands delineates one of the pull requests.
This works because each group of commands takes this form:
Each group adds a label at its start, then (cherry-)picks each of its commits, then adds another label at its end, moves HEAD back to start, and finally creates a merge commit between the start and end.
Step 2: Rearrange the pull requests
Now that we have three groups of commands, each of which actually defines one pull request, all that’s required to change the order of the pull requests in git history is to rearrange those groups of commands in your text editor!
Because in this example repo I’ve ensured that there can’t be any merge conflicts, you can rearrange the groups in any order of your choosing. Then save and close the file, and git will do the rest.
For example, if you reverse the order of the groups, you’ll get this git history: