How do you merge two Git repositories?

后端 未结 23 3309
耶瑟儿~
耶瑟儿~ 2020-11-21 05:45

Consider the following scenario:

I have developed a small experimental project A in its own Git repo. It has now matured, and I\'d like A to be part of larger projec

23条回答
  •  既然无缘
    2020-11-21 06:16

    In my case, I had a my-plugin repository and a main-project repository, and I wanted to pretend that my-plugin had always been developed in the plugins subdirectory of main-project.

    Basically, I rewrote the history of the my-plugin repository so that it appeared all development took place in the plugins/my-plugin subdirectory. Then, I added the development history of my-plugin into the main-project history, and merged the two trees together. Since there was no plugins/my-plugin directory already present in the main-project repository, this was a trivial no-conflicts merge. The resulting repository contained all history from both original projects, and had two roots.

    TL;DR

    $ cp -R my-plugin my-plugin-dirty
    $ cd my-plugin-dirty
    $ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all
    $ cd ../main-project
    $ git checkout master
    $ git remote add --fetch my-plugin ../my-plugin-dirty
    $ git merge my-plugin/master --allow-unrelated-histories
    $ cd ..
    $ rm -rf my-plugin-dirty
    

    Long version

    First, create a copy of the my-plugin repository, because we're going to be rewriting the history of this repository.

    Now, navigate to the root of the my-plugin repository, check out your main branch (probably master), and run the following command. Of course, you should substitute for my-plugin and plugins whatever your actual names are.

    $ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all
    

    Now for an explanation. git filter-branch --tree-filter (...) HEAD runs the (...) command on every commit that is reachable from HEAD. Note that this operates directly on the data stored for each commit, so we don't have to worry about notions of "working directory", "index", "staging", and so on.

    If you run a filter-branch command that fails, it will leave behind some files in the .git directory and the next time you try filter-branch it will complain about this, unless you supply the -f option to filter-branch.

    As for the actual command, I didn't have much luck getting bash to do what I wanted, so instead I use zsh -c to make zsh execute a command. First I set the extended_glob option, which is what enables the ^(...) syntax in the mv command, as well as the glob_dots option, which allows me to select dotfiles (such as .gitignore) with a glob (^(...)).

    Next, I use the mkdir -p command to create both plugins and plugins/my-plugin at the same time.

    Finally, I use the zsh "negative glob" feature ^(.git|plugins) to match all files in the root directory of the repository except for .git and the newly created my-plugin folder. (Excluding .git might not be necessary here, but trying to move a directory into itself is an error.)

    In my repository, the initial commit did not include any files, so the mv command returned an error on the initial commit (since nothing was available to move). Therefore, I added a || true so that git filter-branch would not abort.

    The --all option tells filter-branch to rewrite the history for all branches in the repository, and the extra -- is necessary to tell git to interpret it as a part of the option list for branches to rewrite, instead of as an option to filter-branch itself.

    Now, navigate to your main-project repository and check out whatever branch you want to merge into. Add your local copy of the my-plugin repository (with its history modified) as a remote of main-project with:

    $ git remote add --fetch my-plugin $PATH_TO_MY_PLUGIN_REPOSITORY
    

    You will now have two unrelated trees in your commit history, which you can visualize nicely using:

    $ git log --color --graph --decorate --all
    

    To merge them, use:

    $ git merge my-plugin/master --allow-unrelated-histories
    

    Note that in pre-2.9.0 Git, the --allow-unrelated-histories option does not exist. If you are using one of these versions, just omit the option: the error message that --allow-unrelated-histories prevents was also added in 2.9.0.

    You should not have any merge conflicts. If you do, it probably means that either the filter-branch command did not work correctly or there was already a plugins/my-plugin directory in main-project.

    Make sure to enter an explanatory commit message for any future contributors wondering what hackery was going on to make a repository with two roots.

    You can visualize the new commit graph, which should have two root commits, using the above git log command. Note that only the master branch will be merged. This means that if you have important work on other my-plugin branches that you want to merge into the main-project tree, you should refrain from deleting the my-plugin remote until you have done these merges. If you don't, then the commits from those branches will still be in the main-project repository, but some will be unreachable and susceptible to eventual garbage collection. (Also, you will have to refer to them by SHA, because deleting a remote removes its remote-tracking branches.)

    Optionally, after you have merged everything you want to keep from my-plugin, you can remove the my-plugin remote using:

    $ git remote remove my-plugin
    

    You can now safely delete the copy of the my-plugin repository whose history you changed. In my case, I also added a deprecation notice to the real my-plugin repository after the merge was complete and pushed.


    Tested on Mac OS X El Capitan with git --version 2.9.0 and zsh --version 5.2. Your mileage may vary.

    References:

    • https://git-scm.com/docs/git-filter-branch
    • https://unix.stackexchange.com/questions/6393/how-do-you-move-all-files-including-hidden-from-one-directory-to-another
    • http://www.refining-linux.org/archives/37/ZSH-Gem-2-Extended-globbing-and-expansion/
    • Purging file from Git repo failed, unable to create new backup
    • git, filter-branch on all branches

提交回复
热议问题