您如何合并两个 Git 存储库?

请考虑以下情形:

我已经在自己的 Git 仓库中开发了一个小型实验项目 A。它现在已经成熟,我希望 A 成为较大项目 B 的一部分,该项目 B 具有自己的大型存储库。我现在想将 A 添加为 B 的子目录。

如何将 A 合并为 B,而又不会丢失任何历史记录?

答案

如果要将project-a合并到project-b

cd path/to/project-b
git remote add project-a path/to/project-a
git fetch project-a --tags
git merge --allow-unrelated-histories project-a/master # or whichever branch you want to merge
git remote remove project-a

来自: git 合并不同的存储库?

这种方法对我来说效果很好,它更短,而且我认为它更干净。

注意: -- --allow-unrelated-histories参数仅从 git> = 2.9 开始存在。参见Git-git merge 文档 / --allow-unrelated-histories

更新--tags建议添加--tags以保留标签。

这是两个可能的解决方案:

子模块

可以将存储库 A 复制到较大项目 B 中的单独目录中,或者(也许更好)将存储库 A 复制到项目 B 中的子目录中。然后使用git submodule将此存储库作为存储库 B 的子模块

这是松耦合的仓库,其中一个仓库继续发展一个很好的解决方案,以及发展的主要部分是又见一个单独的独立发展SubmoduleSupportGitSubmoduleTutorial上的 Git 维基网页。

子树合并

您可以使用子树合并策略将存储库 A 合并到项目 B 的子目录中。这在 Markus Prinz 撰写的 Subtree Merging and You 中进行了描述。

git remote add -f Bproject /path/to/B
git merge -s ours --allow-unrelated-histories --no-commit Bproject/master
git read-tree --prefix=dir-B/ -u Bproject/master
git commit -m "Merge B project as our subdirectory"
git pull -s subtree Bproject master

(对于 Git> = 2.9.0,需要选项--allow-unrelated-histories 。)

或者,您可以使用 apenwarr(Avery Pennarun)的git 子树工具( GitHub 上的存储库 ),例如在他的博客文章A Git 子模块的新替代品中宣布:git subtree


我认为在您的情况下(A 将成为较大项目 B 的一部分),正确的解决方案是使用subtree merge

可以将另一个存储库的单个分支轻松放置在保留其历史记录的子目录下。例如:

git subtree add --prefix=rails git://github.com/rails/rails.git master

这将显示为一次提交,其中 Rails master 分支的所有文件都添加到 “rails” 目录中。但是,提交的标题包含对旧历史树的引用:

从提交<rev>添加 “rails /”

其中<rev>是 SHA-1 提交哈希。您仍然可以看到历史,怪一些变化。

git log <rev>
git blame <rev> -- README.md

请注意,您无法从此处看到目录前缀,因为这是一个完整的实际旧分支。您应该像对待通常的文件移动提交一样对待它:到达它时,您将需要一个额外的跳转。

# finishes with all files added at once commit
git log rails/README.md

# then continue from original tree
git log <rev> -- README.md

还有其他更复杂的解决方案,例如手动执行此操作或按照其他答案所述重写历史记录。

git-subtree 命令是官方 git-contrib 的一部分,某些数据包管理器默认安装(OS X Homebrew)。但是除了 git 之外,您可能还必须自己安装它。

如果要单独维护项目,则子模块方法很好。但是,如果您确实要将两个项目合并到同一个存储库中,那么您还有更多工作要做。

第一件事是使用git filter-branch重写第二个存储库中所有内容的名称,该名称位于您希望它们结束的子目录中。因此,而不是foo.cbar.html ,你就必须projb/foo.cprojb/bar.html

然后,您应该可以执行以下操作:

git remote add projb [wherever]
git pull projb

git pull将执行git fetch然后进行git merge 。如果要拉到的存储库还没有projb/目录,则应该没有冲突。

进一步的搜索表明进行了类似的操作以将gitk合并为git 。 Junio C Hamano 在这里写到: http : //www.mail-archive.com/git@vger.kernel.org/msg03395.html

git-subtree很不错,但可能不是您想要的。

例如,如果projectA是在 B 中创建的目录,则在git subtree

git log projectA

列出一次提交:合并。合并项目中的提交用于不同的路径,因此不会显示。

格雷格 · 休吉尔(Greg Hewgill)的回答最接近,尽管它实际上并未说明如何重写路径。


解决方案非常简单。

(1)在 A 中,

PREFIX=projectA #adjust this

git filter-branch --index-filter '
    git ls-files -s |
    sed "s,\t,&'"$PREFIX"'/," |
    GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info &&
    mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE
' HEAD

注意:这将重写历史记录,因此,如果您打算继续使用此存储库 A,则可能要先克隆(复制)该存储库的一次性副本。

(2)然后在 B 中运行

git pull path/to/A

瞧!您在 B 中有一个projectA目录。如果运行git log projectA ,您将看到来自 A 的所有提交。


就我而言,我需要两个子目录projectAprojectB 。在这种情况下,我也对 B 执行了步骤(1)。

如果两个存储库具有相同类型的文件(例如,两个用于不同项目的 Rails 存储库),则可以将辅助存储库的数据提取到当前存储库中:

git fetch git://repository.url/repo.git master:branch_name

然后将其合并到当前存储库:

git merge --allow-unrelated-histories branch_name

如果您的 Git 版本小于 2.9,请删除--allow-unrelated-histories

此后,可能会发生冲突。您可以使用git mergetool例如解决它们。 kdiff3只能与键盘一起使用,因此仅几分钟即可读取 5 个冲突文件。

记住要完成合并:

git commit

使用合并时,我一直丢失历史记录,因此最终使用了 rebase,因为在我的情况下,两个存储库的差异足够大,以致于每次提交时都不会合并:

git clone git@gitorious/projA.git projA
git clone git@gitorious/projB.git projB

cd projB
git remote add projA ../projA/
git fetch projA 
git rebase projA/master HEAD

=> 解决冲突,然后根据需要继续多次...

git rebase --continue

这样做会导致一个项目具有 projA 的所有提交,然后是 projB 的提交

就我而言,我有一个my-plugin仓库和一个main-project仓库,我想假装my-plugin总是在main-projectplugins子目录中main-project

基本上,我重写了my-plugin存储库的历史,以便所有开发工作都发生在plugins/my-plugin子目录中。然后,我将my-plugin的开发历史记录添加到main-project历史记录中,并将两棵树合并在一起。由于main-project存储库中不存在plugins/my-plugin目录,因此这是微不足道的无冲突合并。生成的存储库包含两个原始项目的所有历史记录,并且有两个根。

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

长版

首先,创建my-plugin存储库的副本,因为我们将要重写该存储库的历史记录。

现在,导航到my-plugin存储库的根目录,检出主分支(可能是master ),然后运行以下命令。当然,无论您的实际名字是什么,都应该用my-pluginplugins代替。

$ 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

现在进行解释。 git filter-branch --tree-filter (...) HEAD运行(...)上的每个提交即从到达命令HEAD 。请注意,这直接针对每次提交存储的数据进行操作,因此我们不必担心 “工作目录”,“索引”,“登台” 等概念。

如果您运行失败的filter-branch命令,它将在.git目录中留下一些文件,并且下次您尝试filter-branch ,它将抱怨此问题,除非您为filter-branch提供了-f选项。

至于实际的命令,我没有很多运气让bash可以执行我想要的操作,所以我改用zsh -c使zsh执行命令。首先,我设置了extended_glob选项,该选项启用了mv命令中的^(...)语法,以及glob_dots选项,该选项使我能够选择一个具有 glob( ^(...) )的点文件(例如.gitignore^(...) )。

接下来,我使用mkdir -p命令同时创建pluginsplugins/my-plugin

最后,我使用zsh “negative glob” 功能^(.git|plugins)匹配存储库根目录中的所有文件,除了.git和新创建的my-plugin文件夹。 (在这里可能不需要排除.git ,但是尝试将目录移入自身是错误的。)

在我的存储库中,初始提交不包含任何文件,因此mv命令在初始提交时返回错误(因为没有可用的移动)。因此,我添加了|| true以便git filter-branch不会中止。

--all选项告诉filter-branch重写存储库中所有分支的历史记录,而额外的--必须告诉git将其解释为选项列表的一部分以供分支重写,而不是作为选项filter-branch本身。

现在,导航到您的main-project存储库,并检查要合并到的分支。使用以下命令将my-plugin存储库的本地副本(已修改其历史记录)添加为main-project的远程副本:

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

现在,您的提交历史记录中将有两个不相关的树,您可以使用以下命令很好地可视化它们:

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

要合并它们,请使用:

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

请注意,在 2.9.0 之前的 Git 中,-- --allow-unrelated-histories选项不存在。如果您使用的是这些版本之一,则只需忽略该选项:-- --allow-unrelated-histories阻止的错误消息也已在 2.9.0 中添加。

您不应有任何合并冲突。如果这样做,则可能意味着filter-branch命令无法正常运行,或者main-project已经存在plugins/my-plugin目录。

确保输入任何将来的贡献者都想知道的黑客正在做什么的信息,以解释为什么黑客将要建立一个具有两个根的存储库。

您可以使用上面的git log命令可视化新的提交图,该图应具有两个根提交。注意, 只有master分支将被合并 。这意味着,如果您要在要合并到main-project树中的其他my-plugin分支上进行重要工作,则应避免删除my-plugin远程操作,直到完成这些合并为止。如果您不这样做,那么来自那些分支的提交仍将保留在main-project存储库中,但是其中一些将无法访问,并且容易受到最终垃圾收集的影响。 (此外,您将必须通过 SHA 引用它们,因为删除远程对象会删除其远程跟踪分支。)

(可选)在合并了要保留在my-plugin ,可以使用以下方法删除my-plugin远程:

$ git remote remove my-plugin

现在,您可以安全地删除历史记录已更改的my-plugin存储库的副本。就我而言,在合并完成并推送之后,我还向真正的my-plugin存储库添加了弃用通知。


在 Mac OS X El Capitan 上使用git --version 2.9.0zsh --version 5.2 。你的旅费可能会改变。

参考文献:

几天来,我一直在尝试做同样的事情,我正在使用 git 2.7.2。子树不保留历史记录。

如果您将不再使用旧项目,则可以使用此方法。

我建议您先分支 B,然后在分支中工作。

以下是不分支的步骤:

cd B

# You are going to merge A into B, so first move all of B's files into a sub dir
mkdir B

# Move all files to B, till there is nothing in the dir but .git and B
git mv <files> B

git add .

git commit -m "Moving content of project B in preparation for merge from A"


# Now merge A into B
git remote add -f A <A repo url>

git merge A/<branch>

mkdir A

# move all the files into subdir A, excluding .git
git mv <files> A

git commit -m "Moved A into subdir"


# Move B's files back to root    
git mv B/* ./

rm -rf B

git commit -m "Reset B to original state"

git push

如果现在将任何文件记录在子目录 A 中,则将获得完整的历史记录

git log --follow A/<file>

这是帮助我做到这一点的帖子:

http://saintgimp.org/2013/01/22/merging-two-git-repositories-into-one-repository-without-losing-file-history/

如果要将文件的分支中的文件放到存储库 A 的子树中的存储库 B 中, 并且还保留历史记录,请继续阅读。 (在下面的示例中,我假设我们希望存储库 B 的主分支合并到存储库 A 的主分支。)

在仓库 A 中,首先执行以下操作以使仓库 B 可用:

git remote add B ../B # Add repo B as a new remote.
git fetch B

现在,我们在 repo A 中创建一个全新的分支(仅提交一次),我们将其称为new_b_root 。生成的提交将包含在回购 B 的 master 分支的第一次提交中提交的文件,但会将这些文件放在名为path/to/b-files/的子目录中。

git checkout --orphan new_b_root master
git rm -rf . # Remove all files.
git cherry-pick -n `git rev-list --max-parents=0 B/master`
mkdir -p path/to/b-files
git mv README path/to/b-files/
git commit --date="$(git log --format='%ai' $(git rev-list --max-parents=0 B/master))"

说明:checkout 命令的--orphan选项从 A 的 master 分支中检出文件,但不创建任何提交。我们可以选择任何提交,因为接下来我们还是要清除所有文件。然后,在尚未提交( -n )的情况下,我们从 B 的 master 分支中挑选第一个提交。 (cherry-pick 保留了原来的提交消息,而直接检出似乎没有。)然后,我们在该子树中创建了要存储库 B 中所有文件的子树。然后,我们必须移动该文件中引入的所有文件。樱桃采摘到子树。在上面的示例中,只有一个README文件要移动。然后,我们提交 B-repo 根提交,与此同时,我们还保留原始提交的时间戳。

现在,我们将在新创建的new_b_root之上创建一个新的B/master分支。我们称新分支为b

git checkout -b b B/master
git rebase -s recursive -Xsubtree=path/to/b-files/ new_b_root

现在,我们将b分支合并到A/master

git checkout master
git merge --allow-unrelated-histories --no-commit b
git commit -m 'Merge repo B into repo A.'

最后,您可以删除B远程和临时分支:

git remote remove B
git branch -D new_b_root b

最终的图形将具有以下结构:

在此处输入图片说明