在有人将 rebase 或 reset 推到已发布的分支后,如何恢复/重新同步?

我们都听说过,一个人永远不应该重新定位已发表的作品,它是危险的,等等。然而,我还没有看到任何食谱张贴如何处理的情况下,一个基础 重新发布的情况。

Now, do note that this is only really feasible if the repository is only cloned by a known (and preferably small) group of people, so that whoever pushes the rebase or reset can notify everyone else that they will need to pay attention next time they fetch(!).

我看到的一个显而易见的解决方案是,如果你在 foo上没有本地提交,并且它重新基于:

git fetch
git checkout foo
git reset --hard origin/foo

这将简单地抛弃 foo的本地状态,而按照远程存储库使用它的历史记录。

但是,如果已经在该分支上进行了实质性的本地更改,该如何处理这种情况呢?

17151 次浏览

Getting back in synch after a pushed rebase is really not that complicated in most cases.

git checkout foo
git branch old-foo origin/foo # BEFORE fetching!!
git fetch
git rebase --onto origin/foo old-foo foo
git branch -D old-foo

Ie. first you set up a bookmark for where the remote branch originally was, then you use that to replay your local commits from that point onward onto rebased remote branch.

Rebasing is like violence: if it doesn’t solve your problem, you just need more of it. ☺

You can do this without the bookmark of course, if you look up the pre-rebase origin/foo commit ID, and use that.

This is also how you deal with the situation where you forgot to make a bookmark before fetching. Nothing is lost – you just need to check the reflog for the remote branch:

git reflog show origin/foo | awk '
PRINT_NEXT==1 { print $1; exit }
/fetch: forced-update/ { PRINT_NEXT=1 }'

This will print the commit ID that origin/foo pointed to before the most recent fetch that changed its history.

You can then simply

git rebase --onto origin/foo $commit foo

I'd say the recovering from upstream rebase section of the git-rebase man page covers pretty much all of this.

It's really no different from recovering from your own rebase - you move one branch, and rebase all branches which had it in their history onto its new position.

Starting with git 1.9/2.0 Q1 2014, you won't have to mark your previous branch origin before rebasing it on the rewritten upstream branch, as described in Aristotle Pagaltzis's answer:
See commit 07d406b and commit d96855f :

After working on the topic branch created with git checkout -b topic origin/master, the history of remote-tracking branch origin/master may have been rewound and rebuilt, leading to a history of this shape:

                  o---B1
/
---o---o---B2--o---o---o---B (origin/master)
\
B3
\
Derived (topic)

where origin/master used to point at commits B3, B2, B1 and now it points at B, and your topic branch was started on top of it back when origin/master was at B3.

This mode uses the reflog of origin/master to find B3 as the fork point, so that the topic can be rebased on top of the updated origin/master by:

$ fork_point=$(git merge-base --fork-point origin/master topic)
$ git rebase --onto origin/master $fork_point topic

That is why the git merge-base command has a new option:

--fork-point::

Find the point at which a branch (or any history that leads to <commit>) forked from another branch (or any reference) <ref>.

This does not just look for the common ancestor of the two commits, but also takes into account the reflog of <ref> to see if the history leading to ABC1 forked from an earlier incarnation of the branch <ref>.


The "git pull --rebase" command computes the fork point of the branch being rebased using the reflog entries of the "base" branch (typically a remote-tracking branch) the branch's work was based on, in order to cope with the case in which the "base" branch has been rewound and rebuilt.

For example, if the history looked like where:

  • the current tip of the "base" branch is at B, but earlier fetch observed that its tip used to be B3 and then B2 and then B1 before getting to the current commit, and
  • the branch being rebased on top of the latest "base" is based on commit B3,

it tries to find B3 by going through the output of "git rev-list --reflog base" (i.e. B, B1, B2, B3) until it finds a commit that is an ancestor of the current tip "Derived (topic)".

Internally, we have get_merge_bases_many() that can compute this with one-go.
We would want a merge-base between Derived and a fictitious merge commit that would result by merging all the historical tips of "base (origin/master)".
When such a commit exist, we should get a single result, which exactly match one of the reflog entries of "base".


Git 2.1 (Q3 2014) will add make this feature more robust to this: see commit 1e0dacd by John Keeping (johnkeeping)

correctly handle the scenario where we have the following topology:

    C --- D --- E  <- dev
/
B  <- master@{1}
/
o --- B' --- C* --- D*  <- master

where:

  • B' is a fixed-up version of B that is not patch-identical with B;
  • C* and D* are patch-identical to C and D respectively, and conflict textually, if applied in the wrong order;
  • E depends textually on D.

The correct result of git rebase master dev is that B is identified as the fork-point of dev and master, so that C, D, E are the commits that need to be replayed onto master; but C and D are patch-identical with B0 and B1 and so can be dropped, so that the end result is:

o --- B' --- C* --- D* --- E  <- dev

If the fork-point is not identified, then picking B onto a branch containing B' results in a conflict and if the patch-identical commits are not correctly identified, then picking C onto a branch containing D (or equivalently D*) results in a conflict.


The "--fork-point" mode of "git rebase" regressed when the command was rewritten in C back in 2.20 era, which has been corrected with Git 2.27 (Q2 2020).

See commit f08132f (09 Dec 2019) by Junio C Hamano (gitster).
(Merged by Junio C Hamano -- gitster -- in commit fb4175b, 27 Mar 2020)

rebase: --fork-point regression fix

Signed-off-by: Alex Torok
[jc: revamped the fix and used Alex's tests]
Signed-off-by: Junio C Hamano gitster@pobox.com

"git rebase --fork-point master" used to work OK, as it internally called "git merge-base --fork-point" that knew how to handle short refname and dwim it to the full refname before calling the underlying get_fork_point() function.

This is no longer true after the command was rewritten in C, as its internall call made directly to get_fork_point() does not dwim a short ref.

Move the "dwim the refname argument to the full refname" logic that is used in "git merge-base" to the underlying get_fork_point() function, so that the other caller of the function in the implementation of "git rebase" behaves the same way to fix this regression.


With Git 2.31 (Q1 2021), "git rebase --[no-]fork-point"(man)" gained a configuration variable rebase.forkPoint so that users do not have to keep specifying a non-default setting.

See commit 2803d80 (23 Feb 2021) by Alex Henrie (alexhenrie).
(Merged by Junio C Hamano -- gitster -- in commit 682bbad, 25 Feb 2021)

rebase: add a config option for --no-fork-point

Signed-off-by: Alex Henrie

Some users (myself included) would prefer to have this feature off by default because it can silently drop commits.

git config now includes in its man page:

rebase.forkPoint

If set to false set --no-fork-point option by default.