Skip to content

Instantly share code, notes, and snippets.

@badukaire
Created August 23, 2017 16:49
Show Gist options
  • Save badukaire/e0bd67b7b366a8a818d878b3065c2501 to your computer and use it in GitHub Desktop.
Save badukaire/e0bd67b7b366a8a818d878b3065c2501 to your computer and use it in GitHub Desktop.

rebase a subtree

Sometimes we want to move (rebase) a set of branches that form a subtree, where some of them diverge, and others even are merged together. Rebasing them 1-by-1 can be slow and painful.

In these cases, a nice trick is to merge (join) all the branches to be moved into a final commit node. After that, use rebase with the --preserve-merges option for moving the resulting enclosed subtree (set of branches).

Creating a closed subtree that contains all the branches, exposes 2 nodes (start and end) that are used as input parameters for the rebase command.

The end of the closed subtree is an artificial node that may be deleted after moving the tree, as well as the other nodes that may have been created for merging other branches.

example

Let's see the following case.

The developer wants to insert a new commit (master) into other 3 development branches (b11, b2, b3). One of these (b11) is a merge of a feature branch b12, both based on b1. The other 2 branches (b2, b3) diverge.

Of course the developer could cherry-pick that new commit into each one of these branches, but the developer may prefer not to have the same commit in 3 different branches, but just 1 commit before the branches diverge.

* baa687d (HEAD -> master) new common commit
| * b507c23 (b11) b11
| *   41849d9 Merge branch 'b12' into b11
| |\
| | * 20459a3 (b12) b12
| * | 1f74dd9 b11
| * | 554afac b11
| * | 67d80ab b11
| |/
| * b1cbb4e b11
| * 18c8802 (b1) b1
|/
| * 7b4e404 (b2) b2
| | * 6ec272b (b3) b3
| | * c363c43 b2 h
| |/
| * eabe01f header
|/
* 9b4a890 (mirror/master) initial
* 887d11b init

preparation

For that, the first step is to create a common merge commit that includes the 3 branches. For that a temporary branch called pack is used.

Merging into pack may create conflicts, but that is not important since these merges will later be discarded. Just instruct git to automatically solve them, adding options -s recursive -Xours.

$ git checkout -b pack b11 # create new branch at 'b11' to avoid losing original refs
$ git merge -s recursive -Xours b2 # merges b2 into pack
$ git merge -s recursive -Xours b3 # merges b3 into pack

This is the whole tree after merging everything into the pack branch:

*   b35a4a7 (HEAD -> pack) Merge branch 'b3' into pack
|\
| * 6ec272b (b3) b3
| * c363c43 b2 h
* |   60c9b7c Merge branch 'b2' into pack
|\ \
| * | 7b4e404 (b2) b2
| |/
| * eabe01f header
* | b507c23 (b11) b11
* |   41849d9 Merge branch 'b12' into b11
|\ \
| * | 20459a3 (b12) b12
* | | 1f74dd9 b11
* | | 554afac b11
* | | 67d80ab b11
|/ /
* | b1cbb4e b11
* | 18c8802 (b1) b1
|/
| * baa687d (master) new common commit
|/
* 9b4a890 initial
* 887d11b init

now move it

Now it's time to move the subtree that has been created. For that the following command is used:

$ git rebase --preserve-merges --onto master master^ pack

The reference master^ means the commit before master (master's parent), 9b4a890 in this case. This commit is NOT rebased, it is the origin of the 3 rebased branches. And of course, pack is the final reference of the whole subtree.

There may be some merge conflicts during the rebase. In case there had already been conflicts before doing the merge these will arise again. Be sure to solve them the same way. For the the artificial commits created for merging into the temporary node pack, don't bother and solve them automatically.

After the rebase, that would be the resulting tree:

*   95c8d3d (HEAD -> pack) Merge branch 'b3' into pack
|\
| * d304281 b3
| * ed66668 b2 h
* |   b8756ee Merge branch 'b2' into pack
|\ \
| * | 8d82257 b2
| |/
| * e133de9 header
* | f2176e2 b11
* |   321356e Merge branch 'b12' into b11
|\ \
| * | c919951 b12
* | | 8b3055f b11
* | | 743fac2 b11
* | | a14be49 b11
|/ /
* | 3fad600 b11
* | c7d72d6 b1
|/
* baa687d (master) new common commit
|
* 9b4a890 initial
* 887d11b init

retouch

Sometimes the old branch references may not be relocated (even if the tree relocates without them). In that case you can recover or change some reference by hand.

It's also time to undo the pre-rebase merges that made possible rebasing the whole tree. After some delete, reset/checkout, this is the tree:

* f2176e2 (HEAD -> b11) b11
*   321356e Merge branch 'b12' into b11
|\
| * c919951 (b12) b12
* | 8b3055f b11
* | 743fac2 b11
* | a14be49 b11
|/
* 3fad600 b11
* c7d72d6 (b1) b1
| * d304281 (b3) b3
| * ed66668 b2 h
| | * 8d82257 (b2) b2
| |/
| * e133de9 header
|/
* baa687d (mirror/master, mirror/HEAD, master) new common commit
* 9b4a890 initial
* 887d11b init

Which is exactly what the developer wanted to achieve: the commit is shared by the 3 branches.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment