Ken Y-N
Ken Y-N

Reputation: 15009

Make two git branches into one

Given this situation

A--B
|  B1
|  B2
|  ...
|  Bn label:b-branch
|
C--D1
|  D2
|  ...
|  Dn label:d-branch
|
E

I would like to hook Bn to D1 so we have a nice clean path, even though Bn and D1 are totally unrelated. I don't need to do any fancy merging or otherwise affect any actual source code as the D branch completely replaces the B branch, I just want to somehow say that Bn is now another parent of D1 along with C, that is:

A--B
|  B1
|  B2
|  ...
|  Bn label:b-branch
|  |  <- Only this link is new
C--D1
|  D2
|  ...
|  Dn label:d-branch
|
E

The code is hosted on a private GitLab instance, so I am aware everyone will have to sync up when we change history.

Upvotes: 0

Views: 30

Answers (1)

torek
torek

Reputation: 488183

Start with git replace. I'm not quite sure which way to read your graph, so I'll just say that you can use this with --edit or with --graft. That will make a replacement for some commit, with the replacement having whatever changes you like.

The new replacement is not actually in the graph! For instance, suppose I the following graph, which we read by starting at the right side and working leftwards to go back in time:

A <-B <-C <-D <-E   <-- master

Commit E has D as its parent, which has C as its parent, and so on.

We can make a new commit C' whose parent is A using git replace --graft hash-of-C hash-of-A, giving:

A--B--C--D--E   <-- master
 \
  C'   <-- refs/replace/hash-of-C

When git log or other similar commands walk the graph, they might start at E and show it, then move to D and show it. The tricky bit happens next: they move to C, but at this point, they notice that refs/replace/hash-of-C exists. They let go of C entirely and move to C' instead, and show that. They they move to C's parent A and show that. The result is that B seems to be gone. The same trick will work for your case: if you want to introduce two parents where there was just one, you make a replacement that has those two parents instead of just the one.

The drawback to this kind of replacement commit is that git clone normally omits the refs/replace/ namespace and its commits. So someone cloning the repository sees the original commits—their Git has no refs/replace/hash at all, and never copied the replacement commits.

You can arrange to get those cloned, but instead of doing that, you can now use an otherwise-no-op git filter-branch to "cement" the replacements in place in a new repository. For instance, after:

git filter-branch --tag-name-filter cat -- --all

on the five-commits-and-one-replacement repository I drew above, you would have:

A--B--C--D--E   <-- refs/original/refs/heads/master
 \
  C'   <-- refs/replace/hash-of-C
    \
     D'-E'  <-- master

Discarding the refs/original/ and refs/replace/ namespace names by making another clone, you wind up with:

A--C'-D'-E'  <-- master

which has made the grafting permanent. So you can now either clone the repository and use the clone as the replacement repository, or just discard the refs/replace/ and refs/original/ names from the original repository to make it look the same as the proposed cloned replacement (except that the original objects tend to linger for some time—basically, until the garbage collector gets around to cleaning them up).

Upvotes: 2

Related Questions