How do I recover/resynchronise after someone pushes a rebase or a reset to a published branch?
Asked Answered
I

3

92

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?

Inapposite answered 3/11, 2010 at 7:8 Comment(4)
+1 for the simple case recipe. It's ideal for personal synchronisation between machines, especially if they have different OS's. It's something that should be mentioned in the manual.Seppala
The ideal recipe for personal synchronisation is git pull --rebase && git push. If you work on master only, then this will very near unfailingly do the right thing for you, even if you’ve rebased and pushed at the other end.Inapposite
Because I'm synchronising and developing between a PC and a Linux machines I find that using a new branch for every rebase/update works well. I also use the variant git reset --hard @{upstream} now that I know that magic refspec incantation for "forget what I have/had, use what I fetched from the remote" See my final comment to https://mcmap.net/q/12353/-command-to-determine-the-upstream-ref-of-the-current-headSeppala
You 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 belowStun
I
76

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
Inapposite answered 3/11, 2010 at 7:8 Comment(6)
Quick 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.)Improvisator
It's nothing like violence. Violence is occasionally funDeus
@iolo True, rebasing is always fun.Closefisted
You 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 more git 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 new git merge-base --fork!Stun
Like violence, almost always avoid rebasing. But have a clue how.Slam
Well, avoid pushing a rebase where others will be affected.Inapposite
I
11

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.

Improvisator answered 3/11, 2010 at 15:39 Comment(4)
Ah, 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.Inapposite
@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.Improvisator
Yes, 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.Inapposite
@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.Improvisator
S
11

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 <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 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 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 fix

Signed-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 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.

Stun answered 6/12, 2013 at 11:43 Comment(1)
Note that a git push --force can now (git 1.8.5) be done more prudently: https://mcmap.net/q/11861/-git-how-to-ignore-fast-forward-and-revert-origin-branch-to-earlier-commitStun

© 2022 - 2024 — McMap. All rights reserved.