The easiest way to rearrange merge-commit pull requests

WhySection titled: Why
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.
* 22fb86c (HEAD -> main) Merge PR #3|\ | * 468bc63 ten| * ef0893d nine| * 5a8e851 eight| * 95b1937 seven| * bee57ff six|/ * d89d835 Merge PR #2|\ | * 02561c3 five| * f8d4ec5 four| * d738586 three|/ * 815a983 Merge PR #1|\ | * 3b5c0d2 two| * 7c62c07 one|/ * 87f90a3 Initial commit
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.
AudienceSection titled: Audience
This post is intended for people who are already very comfortable with git interactive rebasing; git rebase -i
.
SyntaxSection titled: Syntax
The syntax is nearly identical to the standard git rebase command, with the addition of the --rebase-merges
flag.
git rebase -i --rebase-merges <commit-ish>
SandboxSection titled: Sandbox
My example in this post will be based on this sample git history:
➜ git log --graph --oneline --decorate
* 22fb86c (HEAD -> main) Merge PR #3|\ | * 468bc63 ten| * ef0893d nine| * 5a8e851 eight| * 95b1937 seven| * bee57ff six|/ * d89d835 Merge PR #2|\ | * 02561c3 five| * f8d4ec5 four| * d738586 three|/ * 815a983 Merge PR #1|\ | * 3b5c0d2 two| * 7c62c07 one|/ * 87f90a3 Initial commit
If you’d like to you can follow along by cloning the repo from GitHub.
Understanding the outputSection titled: 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:
git rebase -i --rebase-merges 87f90a3
label onto
# Branch Merge-PR-1reset ontopick 9006a2d onepick 117b0ff twolabel Merge-PR-1
# Branch Merge-PR-2reset ontomerge -C 428d254 Merge-PR-1 # Merge PR #1label branch-pointpick 0808cce threepick 67c0c1f fourpick 5d04848 fivelabel Merge-PR-2
# Branch Merge-PR-3reset branch-point # Merge PR #1merge -C 9d3c988 Merge-PR-2 # Merge PR #2label branch-point-2pick 81091b0 sixpick 86faedf sevenpick b5c836c eightpick f5bbb45 ninepick 0b325e9 tenlabel Merge-PR-3
reset branch-point-2 # Merge PR #2merge -C 621bc14 Merge-PR-3 # Merge PR #3
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).
The 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 thegit reset
command, and resets the HEAD pointer to the commit that was labeled with the specified label.
Step 1: Rearrange Git’s Default OutputSection titled: 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:
label ontoreset onto # doesn't actually do anything (HEAD is already at this label) -- could deletepick 9006a2d onepick 117b0ff twolabel Merge-PR-1reset ontomerge -C 428d254 Merge-PR-1 # Merge PR #1
label branch-pointpick 0808cce threepick 67c0c1f fourpick 5d04848 fivelabel Merge-PR-2reset branch-point # Merge PR #1merge -C 9d3c988 Merge-PR-2 # Merge PR #2
label branch-point-2pick 81091b0 sixpick 86faedf sevenpick b5c836c eightpick f5bbb45 ninepick 0b325e9 tenlabel Merge-PR-3reset branch-point-2 # Merge PR #2merge -C 621bc14 Merge-PR-3 # Merge PR #3
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:
label startpick commit1pick commit2pick commit3label endreset startmerge -C origCommitToCopyMsgFrom end
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 requestsSection titled: 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:
* 746e3ba (HEAD -> main) Merge PR #1|\ | * 54c55d9 two| * 6c49806 one|/ * 7201f4d Merge PR #2|\ | * 0a59673 five| * e797c7f four| * 27cd869 three|/ * 874fea3 Merge PR #3|\ | * 5173212 ten| * b157e78 nine| * d7c129e eight| * 6e766e3 seven| * eea2949 six|/ * 87f90a3 Initial commit