I encountered a scenario in which I don't understand the outcome.
My team is working on a feature branch with commits:
A--B--C
A co-worker pushes two new commits:
A--B--C--D--E
Me, not realizing anyone else is pushing code to this branch, I force push an ammended commit:
A--B--C'
After I realized my error, I suggested that he try (on his repo, where D and E are still present):
git pull -r
git push
My thinking was that his two commits (D, E) were no longer in the history for the branch and so, upon pull, git would try to merge them into the history. My expectation was that D and E would be rebased on top of C', but that didn't happen. Instead (paraphrased):
$ git pull -r
+ commit...commit branch -> origin/branch (forced update)
$ git push
Everything up-to-date
and D and E were no where to be seen. I thought that perhaps he'd done some other actions on his repo and so it might have been in some unknown state, so we found the hash for E in the reflog, and did git reset --hard <E>
a few times to try again (mostly because I was curious why it hadn't gone as I'd expected) and got the same result.
I'm sure I've misunderstood something, but I'm not sure what. Why didn't the git pull -r
rebase the "new" commits D and E (ie. the ones I'd inadvertently removed from history) on top of C'?
As a few have pointed out: C should've been included in my expectation as well. I wasn't thinking about that, but I understand why, and that makes sense.
My expectation was that the "new" commits (now updated to be C, D, E; even though, had it worked, would not have been what I wanted) would have been rebased, but they were not and I don't understand why.
@matt's comment about the commits having previously been pushed was interesting. If someone could elaborate on that mechanism that seems like a potential answer.
What 'git rebase' does is take all your commits out until it finds a common parent and then apply upstream changes(The branch that you are merging) first and then apply the changes you have in your current branch. This process takes a long time. You have to fix the conflicts each time after fixing the conflicts.
The Rebase Option But, instead of using a merge commit, rebasing re-writes the project history by creating brand new commits for each commit in the original branch.
Rebasing a branch on another "re-applies" (fairly smartly, these days) the commits of the currently checked out branch on top of the tip of the target. So, yes, if you do it over and over again then you will keep your current branch up to date.
What happens when you do git rebase is that the commits that are on the current branch but are not in upstream are saved. The current branch is reset to upstream and then the saved commits are replayed on top of this.
A merge commit is created to point to the latest local and remote commits. In case of a rebase, we use the command git pull --rebase. In a rebase, the unpublished local changes of the local branch are reapplied on top of the published changes of the remote repository.
Run git rebase --interactive $commit, where $commit is the commit prior to both the duplicated commits 2. Here we can outright delete the lines for the duplicates. 1 It doesn't matter which of the two you choose, either ba7688a or 2a2e220 work fine.
Each commit hash in Git is based on a number of factors, one of which is the hash of the commit that comes before it. If you reorder commits you will change commit hashes; rebasing (when it does something) will change commit hashes.
The reason this is happening is because git pull -r
(or git pull --rebase
) is not identical to the following commands:
git fetch
git rebase @{u}
If you had run those commands instead, you would have rebased C-D-E
on top of A-B-C'
, yielding:
A-B-C'-C-D-E
. (And perhaps C would have fallen out depending on what your amend did.)
Because pull -r
is not exactly those commands behind the scenes, after a force push the results will be as you witnessed.
This is yet another reason that force pushing is frowned upon. And also another reason I dislike pull in general. I would always prefer to type the two commands separately, just in case.
As a side note (and mentioned in a comment), had you used git push --force-with-lease
instead of git push --force
you would noticed the change on the remote branch and would have been able to rebase your branch instead of force pushing. I highly recommend always using --force-with-lease
(or perhaps the newer --force-if-includes
) unless you have a specific contrived scenario where --force
would be necessary.
git pull -r
uses git rebase --fork-point
. It picks E as the fork point.
When --fork-point is active, fork_point will be used instead of to calculate the set of commits to rebase, where fork_point is the result of git merge-base --fork-point command (see git-merge-base(1)). If fork_point ends up being empty, the will be used as a fallback.
See torek's answer for a detailed explanation.
We can reproduce this situation.
Remote repositories do not have to be over the network. I've tested this by setting up a remote with git init --bare
and two clones. Here's a setup script.
#!/bin/sh
# Remote repo, two clones.
git init --bare upstream.git
git clone upstream.git me.git
git clone upstream.git coworker.git
# Push A-B-C
cd me.git
git commit --allow-empty -m A
git commit --allow-empty -m B
git commit --allow-empty -m C
git push
cd ../coworker.git
git pull
# Co-worker adds D-E and pushes
git commit --allow-empty -m D
git commit --allow-empty -m E
git push
# You amend C and force push
cd ../me.git
git commit --amend --allow-empty -m C1
git push --force
cd ..
At this point if we pull -r their work will be blown away as you've observed.
cd coworker.git
git pull -r
If we instead do what is supposed to be equivalent, git fetch
and git rebase origin/main
, it works as expected.
cd coworker.git
git fetch
git rebase origin/main
Why? Running `git pull --rebase=interactive we see it's picking the wrong range.
$ git pull --rebase=interactive
noop
# Rebase 01431d7..01431d7 onto d914686 (1 command)
It's using the "fork-point" which git merge-base --fork-point origin/main
chooses as E, not C. torek's answer can explain why.
And, as others have said, this can be avoided with --force-with-lease
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With