How to merge two branches without a common ancestor?

20,701

Solution 1

Disclaimer: I've only used "graft points" myself once in a toy repository. But it is an obscure feature which you may not have heard of, and which _may_ be helpful in your situation.

You could use "graft points" to fake the ancestry information. See, e.g., What are .git/info/grafts for? or proceed immediately to the git wiki entry on graft points.

In essence, you would create a file .git/info/grafts that tricks git into thinking that commit M1 is an ancestor of commit M2:

$ cat .git/info/grafts
<your M2 commit hash> <your M1 commit hash>

Subsequently, it would look like M2 was an empty commit that just merged I2 and M1 into a common tree.

The major downside: the graft point is not committed; therefore, it is not checked out, but needs to be added to each local working copy of the repository manually.



Update: use git replace --graft instead.

Graft points, as described above, have been superseded. Run

git replace --graft <your M2 commit hash> <your M1 commit hash>

to create the graft. This is stored in .git/refs/replace/. Although git does not fetch, or push, these refs by default, they can be synchronized between repositories using:

git push origin 'refs/replace/*'
git fetch origin 'refs/replace/*:refs/replace/*'

(StackOverflow: How to push 'refs/replace' without pushing any other refs in git?)

Solution 2

I'd do the following:

git checkout M1
git cherry-pick I1
git cherry-pick I2

That adds .gitignore and .gitattributes to your branch containing the nicer history.

And then just set the new commits on top of that one:

git filter-branch --parent-filter 'if $GIT_COMMIT = $hash_of_N; then printf -- '-p
$hash_of_cherrypicked_I2\n'; else cat; fi'

The downside of this is you rewrite history.

So an alternative approach is to create a script similar to the one for the Linux kernel and put that in your repository.

Solution 3

The most elegant solution would be to rebase the changes introduced by N .. Z commits on top of svn branch, but I didn't found yet the required syntax for two branches without a common ancestor.

Try to first cherry-pick I1 and I2 onto M1, and after that use the command git rebase --onto M1' M2 Z (where M1' is the M1-I1-I2 branch). I'm not sure whether rebase --onto works when there are no common ancestors, but if it doesn't, there is the possibility of using patches. Use the git format-patch to generate the patches of M2..Z and then git am to apply them on top of M1. Here are some experience reports on using it in converting old SVN and CVS repositories.

Share:
20,701
alexandrul
Author by

alexandrul

.

Updated on June 29, 2020

Comments

  • alexandrul
    alexandrul almost 4 years

    I have started using Git in the middle of my project, where the first two commits are just some initial settings (.gitignore and .gitattributes), and the third commit M2 adds the content of the SVN trunk:

    I1 -- I2 -- M2 -- N -- .. -- Z
    

    I have imported the SVN history in a branch named svn, where M1 is the SVN trunk (with the same content as M2, except .gitignore and .gitattributes):

    A -- B -- ... -- K -- L -- M1
    

    Q: What is the best approach in merging both branches?

    I could merge M1 and M2 into M3, and then rebase, but I don't know how to delete the I1 and I2 commits and if I can safely remove the M3 commit (I have found some advices to preserve the merge commits, but in this case M3 it's not necessary anymore).

    A -- B -- ... -- K -- L -- M1
                                 \
                                  M3 -- N' -- .. -- Z'
                                 /
                   I1 -- I2 -- M2 -- N -- .. -- Z
    

    Another way would be to cherry-pick the N .. Z commits into svn branch by hand, but I would like to avoid this approach.

    The most elegant solution would be to rebase the changes introduced by N .. Z commits on top of svn branch, but I didn't found yet the required syntax for two branches without a common ancestor.

  • Anonigan
    Anonigan over 14 years
    You can use "git filter-branch" to rewrite history according to grafts, turning grafted parentage into real parentage. But that rewrites history.
  • alexandrul
    alexandrul over 14 years
    @Jakub: would you give an example of using filter-branch with grafts (as an answer in order to be able to upvote) ? I'm ok with rewriting history.
  • Esko Luontola
    Esko Luontola over 13 years
    IIRC, in the grafts file you need to mention the the current commit and all of its parents. So on one line there would be the hashes of M2, M1 and I2 - unless you want to get rid of commits I1 and I2. By having only M2 and M1 in the grafts line, it'll look like M2 committed the contents of I1 and I2 on top of M1.
  • user1750701
    user1750701 over 11 years
    It's worth noting that grafts have been superseded by "git replace" as a way to fake a common ancestor.
  • Ivan Danilov
    Ivan Danilov over 8 years
    @stsquad and git replace actually is committed to repository. Or at least can be.