Rebasing after squash and merge on GitHub
the situation
When we use the Squash and merge on GitHub, the commits are squashed into a
single commit and placed at the HEAD of the branch.
In the diagram below, the feature branch's {a,b,c} changes has been squashed and merged into main.
hint: use these commands to replicate the git history above.
rm -rf /tmp/example.0
cd /tmp; mkdir example.0 && cd "$_";
git init;
touch foobar; git add foobar; git commit -m "init";
git switch -c feature
echo a >> foobar; git commit -am "a";
echo b >> foobar; git commit -am "b";
echo c >> foobar; git commit -am "c";
git switch master # "main" in the diagram.
git merge --squash feature
git commit -m 'abcd'
git switch feature
sed -i '2c // b' foobar; git commit -am 'd';If the feature branch is reused for future development, then we might encounter conflicts when we try to rebase our branch or merge these newer changes.
What happens when we rebase or merge?
When we run the rebase or merge git command, git looks for the common
ancestor between both branches, known as the merge base, and cherry picks
commits from that point until the branch's HEAD.
We can find this out ourselves with the merge-base git command:
git merge-base master feature # prints commit id of "init".We will encounter conflicts if we naively rebase:
git switch feature; git -c merge.conflictstyle=diff3 rebase mastermerge conflict:
diff --cc foobar
index de98044,7898192..0000000
--- a/foobar
+++ b/foobar
@@@ -1,3 -1,1 +1,8 @@@
++<<<<<<< HEAD
a
+b
+c
++||||||| parent of 94cb3aa (a)
++=======
++a
++>>>>>>> 94cb3aa (a)
The plumbing events leading to this conflict:
gitswitches into a copy of the master branchgitbegins cherry picking commits starting with commit a- conflict: commit a is adding "a" to the foobar file, commit abc is adding "a\nb\nc" and the foobar is empty in the parent commit of a. Who has priority?
rebase to the rescue
Under ideal circumstances, we would git reset our feature branch to main
as soon as the changes are merged.
To my demise, I often forget to do this which leaves me to create a temporary new branch, cherry-pick the newer commits and switcheroo the temporary branch to the old one.
Fortunately, this sequence of operations is nicely packed into single rebase command:
git rebase --onto origin/master origin/feature feature
This command follows the first form of the git-rebase command:
git rebase [-i | –interactive] [<options>] [–exec <cmd>] [–onto <newbase> | –keep-base] [<upstream> [<branch>]]
Destructuring our command's arguments, we get:
- <newbase> : origin/master
- <upstream> : origin/feature
- <branch> : feature
The git-rebase(1) manual explains the side-effects of this command, but it is
a bit heavy to understand as it tries to explain all forms at the same time.
In short, git will switch into the feature branch, and find all the commits
since it was last pushed to remote, that is origin/feature. It then resets the
branch to origin/master, and then cherry picks all the commits it had found in the
previous step.
bringing the knowledge to the origin
So far, we've only assumed local branches. Going back to the initial situation now: we've merged a pull request using the Squash and merge strategy, and we would like to continue using the branch for further pull requests.. what do?
If we have newer changes that have not yet pushed to remote (GitHub), simply
reference the outdated origin as the base for a git rebase operation:
git fetch -a; git rebase --onto origin/master origin/feature feature
If we're diligent, immediately run a git reset --{TBD} origin/main.