How do I recover/resynchronise after someone pushes a rebase or a reset to a published branch?
Solution 1
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
Solution 2
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.
Solution 3
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 withgit checkout -b topic origin/master
, the history of remote-tracking branchorigin/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 commitsB3
,B2
,B1
and now it points atB
, and yourtopic
branch was started on top of it back whenorigin/master
was atB3
.
This mode uses the reflog of
origin/master
to findB3
as the fork point, so that thetopic
can be rebased on top of the updatedorigin/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<commit>
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 atB
, but earlier fetch observed that its tip used to beB3
and thenB2
and thenB1
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 betweenDerived
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 ofB
that is not patch-identical withB
; -
C*
andD*
are patch-identical toC
andD
respectively and conflict textually if applied in the wrong order; -
E
depends textually onD
.
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 C*
and D*
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 fixSigned-off-by: Alex Torok
[jc: revamped the fix and used Alex's tests]
Signed-off-by: Junio C Hamano [email protected]
"
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 underlyingget_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.
Comments
-
Aristotle Pagaltzis almost 2 years
We have all heard that one should never rebase published work, that it’s dangerous, etc. However, I have not seen any recipes posted for how to deal with the situation in case a rebase is published.
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(!).
One obvious solution that I have seen will work if you have no local commits on
foo
and it gets rebased:git fetch git checkout foo git reset --hard origin/foo
This will simply throw away the local state of
foo
in favour of its history as per the remote repository.But how does one deal with the situation if one has committed substantial local changes on that branch?
-
Aristotle Pagaltzis over 13 yearsAh, so it does. But though I now understand what it says, I would not have before, prior to figuring this out on my own. And there is no cookbook recipe (perhaps rightly so in such documentation). I will also put forth that calling the “hard case” hard is F.U.D. I submit that rewritten history is trivially manageable at the scale of most in-house development. The superstitious way in which this subject is always treated annoys me.
-
Cascabel over 13 years@Aristotle: You're right that it's very manageable, given that all developers know how to use git, and that you can effectively communicate to all developers. In a perfect world, that'd be the end of the story. But a lot of projects out there are big enough that an upstream rebase really is a scary thing. (And then there are places like my workplace, where most of the developers have never even heard of a rebase.) I think the "superstition" is just a way of providing the safest, most generic advice possible. No one wants to be the one who causes a disaster in someone else's repo.
-
Aristotle Pagaltzis over 13 yearsYes, I understand the motive. And I agree with it fully. But there is a world of difference between “don’t try this if you don’t understand the consequences” and “you should never do that because it’s evil”, and this alone I take issue with. It is always better to instruct than to instil fear.
-
Cascabel over 13 yearsQuick note: I think it's pretty intuitive, but if you don't know awk well... that one-liner is just looking through the output of
git reflog show origin/foo
for the first line saying "fetch: forced-update"; that's what git records when a fetch causes the remote branch to do anything but fast-forward. (You could just do it by hand, too - the forced update is probably the most recent thing.) -
Cascabel over 13 years@Aristotle: Agreed. I do try to tend toward the "make sure you know what you're doing" end, but especially online, I try to give it enough weight so that a casual visitor from google will take note. You're right, a lot of it should probably be toned down.
-
Iolo over 11 yearsIt's nothing like violence. Violence is occasionally fun
-
Dan Bechard over 10 years@iolo True, rebasing is always fun.
-
VonC over 10 yearsYou will be able, with Git2.0, to find the old origin of your branch (before the upstream branch was rewritten with a
push -f
): see my answer below. No moregit branch old-foo origin/foo
before fetching. If you forgot that step and only realizes the issue belatedly, you will be able to recover with the newgit merge-base --fork
! -
VonC over 10 yearsNote that a git push --force can now (git 1.8.5) be done more prudently: stackoverflow.com/a/18505634/6309
-
Bob Stein about 9 yearsLike violence, almost always avoid rebasing. But have a clue how.
-
Aristotle Pagaltzis about 9 yearsWell, avoid pushing a rebase where others will be affected.