Note: this article refers to “git pull -r” and “git pull –rebase” interchangeably. They are the same command, except the merge-preserving variation can only be specified via the long form: git pull --rebase=preserve
I’ve long known that “git pull –rebase” reconciles the local branch correctly against upstream amends, rebases, and reorderings. The official “git rebase” documentation attests to this:
‘Note that any commits in HEAD which introduce the same textual changes as a commit in HEAD.. are omitted (i.e., a patch already accepted upstream with a different commit message or timestamp will be skipped).’
Thanks to the git patch-id command it’s easy to imagine how this mechanism might work. Take two commits, look at their patch-ids, and if they’re the same, drop the local one.
But what about squashes and other force-pushes where git patch-id
won’t help? What does “git pull -r” do in those cases? I created a series of synthetic force-pushes to find out. I tried squashes, merge-squashes, dropped commits, merge-base adjustments, and all sorts of other force-push craziness.
I was unable to confuse “git pull –rebase,” no matter how hard I tried. It’s bulletproof, as far as I can tell.
The context here is not a master branch that’s advancing. The context is a feature branch that two people are working in parallel, where either person might force-push at any time. Something like this:
The starting state. Evangeline and Gabriel are working together on branch ‘feature’. Note: ‘evangeline/feature’ is actually Evangeline’s local ‘feature’ branch, and ‘gabriel/feature’ is Gabriel’s local ‘feature’ branch. R__ecreate it using the script below.
git init
echo 'a' > a; git add .; git commit -m 'a'
echo 'b' > b; git add .; git commit -m 'b'
echo 'c' > c; git add .; git commit -m 'c'
git checkout -b feature HEAD~1
echo 'd' > d; git add .; git commit -m 'd'
echo 'e' > e; git add .; git commit -m 'e'
echo 'f' > f; git add .; git commit -m 'f'
git checkout -b gabriel/feature
echo 'gf' > gf; git add .; git commit -m 'gf' --author='gabriel@mergebase.com'
git checkout -b evangeline/feature HEAD~1
echo 'ef' > ef; git add .; git commit -m 'ef'evangeline@mergebase.com'
git push --mirror [url-to-an-empty-git-repo]
In each scenario, Evangeline rewrites the history of origin/feature with a force-push of some kind, usually incorporating her own ‘ef‘ commit into her push. Meanwhile, Gabriel has already made his own ‘gf‘ commit to his local feature branch. For each scenario, we want to see if Gabriel can use “git pull –rebase” to correctly reconcile his work (his ‘gf‘ commit) against Evangeline’s most recent push.
For each scenario, we are on Gabriel’s local branch feature. The graph on the left shows both the state of origin/feature (thanks to Evangeline’s force-push), as well as the state of Gabriel’s local feature and how it relates to Evangeline’s force-push. The graph on the right shows the result of Gabriel typing “git pull -r.”
A scenario is deemed successful if “git pull -r” results in Gabriel’s _‘_gf‘ commit sitting on top of origin/feature. Since Gabriel does not push back in these scenarios, his ‘gf‘ commit remains confined to his local feature branch.
Result: Success! |
Result: Success! |
Result: Success! |
Result: Success! |
Result: Success! |
Result: Success! |
Supposedly, the golden rule of git is to never force-push public branches.
Of course, I would never force-push against ‘master’ and ‘release/*’. As a git admin, that’s always the first config I set for a new repo: disallow all rewrites for ‘master’ and ‘release/*’.
But all public branches? I find force-pushing feature branches incredibly useful.
Industry has arrived at a compromise: defer the rewrite to the final merge. Bitbucket, Gitlab, and Github now offer “rebase” and “squash” flavours of PR merge. But it’s a silly compromise because the golden rule itself is silly. Instead of building complex merge machinery to dance around the golden rule, I think we’d be better served by reworking the rule itself. Three reasons:
I propose a new golden git rule (in haiku form):
We never force-push master
or release. But always,
for all branches: git pull -r
Alternatively, you can make “git pull -r” the default behaviour:
git config --global pull.rebase true
Git graphs in this article were generated using the Bit-Booster – Rebase Squash Amend plugin for Bitbucket Server.