6

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.

Allen
  • 478
  • 9
  • 21
  • 4
    "I force push an ammended commit" This is why there is "force with lease", so that you can't make that sort of mistake. – matt Mar 08 '21 at 19:19
  • TIL about "force with lease" – Allen Mar 08 '21 at 19:22
  • 3
    Looks like a dup: [git pull --rebase lost commits after coworker's git push --force](https://stackoverflow.com/questions/42536989/git-pull-rebase-lost-commits-after-coworkers-git-push-force) – TTT Mar 08 '21 at 20:58

3 Answers3

4

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.

TTT
  • 22,611
  • 8
  • 63
  • 69
  • 2
    I've confirmed this. It's so strange, the docs say `git pull -r` will fetch and rebase. What is `git pull -r` doing? – Schwern Mar 08 '21 at 20:30
  • 2
    @Schwern I'll have to confirm when I have time to dig into it. But I think it uses a different fork point setting. – TTT Mar 08 '21 at 20:32
  • Perhaps it's recognizing that the `git push --force` blew away D-E and `git pull -r` won't resurrect them? – Schwern Mar 08 '21 at 20:37
  • [My answer](https://stackoverflow.com/a/66536945/14660) has a setup script to make experimenting easier. – Schwern Mar 08 '21 at 20:45
  • 1
    There are some shenanigans where git uses the reflog to determine where a branch "relaly" diverged. I think it'll be related to that. (If so, that behavior can be suppressed when you know you're trying to recover from a bad force push, which - at the risk of being indelicate - is what happened here.) But I didn't realize that this was different for `pull -r` vs `rebase`; I'll have to look at that later – Mark Adelsberger Mar 08 '21 at 20:47
  • 2
    Oh, and the default assumption of `--fork-point` is different if an upstream is explicitly provided to `git rebase` vs. if it's not. So this is probably no so much "`git pull -r` not being equivalent to the two commands" as "`git pull -r` using an implicit upstream which makes the command behave differently." – Mark Adelsberger Mar 08 '21 at 20:50
  • @Schwern I think if we wait long enough torek will come explain it to us. :D – TTT Mar 08 '21 at 20:51
  • 2
    @MarkAdelsberger LOL. I must have had a premonition. There I go trying to make a joke and [look what I found](https://stackoverflow.com/q/42536989/184546). I guess this is a dup. – TTT Mar 08 '21 at 20:56
  • 1
    @MarkAdelsberger You're correct, `git rebase --fork-point` recreates the problem. – Schwern Mar 08 '21 at 20:56
  • 1
    So my comment (now deleted) about what was happening was correct even though I was just guessing. I'm glad I was right but I'm glad I deleted it. :) – matt Mar 08 '21 at 21:21
2

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.

Schwern
  • 153,029
  • 25
  • 195
  • 336
-1

After you force push, your remote will probably look like this:

A--B--C'(remote/master)
    \
     --C--D--E

try git fetch then git log --oneline --graph --all and see if it shows.

I am not that knowledgeable on rebasing, but here is the official git tutorial on it : https://git-scm.com/book/en/v2/Git-Branching-Rebasing

tek27
  • 43
  • 1
  • 4