Question

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?

Was it helpful?

Solution

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

OTHER TIPS

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

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top