题 你如何合并两个Git存储库?


请考虑以下情形:

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

如何将A合并到B中,而不会丢失任何一方的历史记录?


1222
2017-09-15 08:31


起源


如果您只是尝试将两个存储库合并为一个,而不需要保留两个存储库,请查看以下问题: stackoverflow.com/questions/13040958/... - Flimm
用于在自定义目录中合并git repo并保存所有comits使用 stackoverflow.com/a/43340714/1772410 - Andrey Izman


答案:


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

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

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

从提交添加'rails /' <rev>

哪里 <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之外,您可能需要自己安装它。


325
2018-02-20 23:44



@Brad Mace,git-subtree repo现在已经过时,因为它被包含在git本身中。看到 github.com/apenwarr/git-subtree/blob/master/... - Simon Perepelitsa
不要停止阅读......下面有更完整的答案。 - Ryan Shillington
以下是有关如何安装Git SubTree的说明(截至2013年6月): stackoverflow.com/a/11613541/694469  (我换了 git co v1.7.11.3  同 ... v1.8.3)。 - KajMagnus
或阅读Eric Lee的“将两个Git存储库合并到一个存储库而不丢失文件历史” saintgimp.org/2013/01/22/... - Jifeng Zhang
正如其他人所说, git subtree 可能没有你想的那样做!看到 这里 获得更完整的解决方案。 - Paul Draper


如果你想合并 project-a 成 project-b

cd path/to/project-b
git remote add project-a path/to/project-a
git fetch project-a
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 Documentation / --allow-unrelated-histories 


1284
2018-05-11 09:37



看来这需要一个工作副本: fatal: This operation must be run in a work tree,我想合并两个裸git存储库。 - LiuYan 刘研
这为我做了这件事。在.gitignore文件中只有一次冲突,就像魅力第一次一样!它完美地保留了提交历史。除了简单之外,其他方法的重大优势在于,不需要持续引用合并的仓库。但要注意的一件事 - 如果你是像我这样的iOS开发人员 - 要非常小心地将目标repo的项目文件放入工作区。 - Max MacLeod
谢谢。为我工作。我需要将合并的目录移动到子文件夹中,所以按照上面的步骤我只是使用 git mv source-dir/ dest/new-source-dir - Sid
该 git merge 步骤失败了 fatal: refusing to merge unrelated histories; --allow-unrelated-histories 修复了如中所解释的那样 文档。 - ssc
--allow-unrelated-histories 被引入 git 2.9。在早期版本中,它是默认行为。 - Douglas Royds


这有两种可能的解决方案:

子模块

将存储库A复制到较大项目B中的单独目录中,或者(可能更好)将存储库A克隆到项目B中的子目录中。然后使用 git子模块 使这个存储库成为一个 子模块 存储库B.

对于松散耦合的存储库来说,这是一个很好的解决方案,其中存储库A中的开发仍在继续,并且开发的主要部分是A中的单独独立开发。 SubmoduleSupport 和 GitSubmoduleTutorial Git Wiki上的页面。

子树合并

您可以使用存储库将存储库A合并到项目B的子目录中 子树合并 战略。这在中描述 子树合并和你 作者:Markus Prinz。

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

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

或者你可以使用 git子树 工具(GitHub上的存储库由apenwarr(Avery Pennarun)在他的博客文章中宣布 Git子模块的新替代方案:git子树


我认为在你的情况下(A是大项目B的一部分),正确的解决方案是使用 子树合并


583
2017-09-15 08:38



这有效并且似乎保留了历史记录,但不是这样您可以使用它来区分文件或通过合并进行二等分。我错过了一步吗? - jettero
这是不完整的。是的,你得到一大堆提交,但他们不再引用正确的路径。 git log dir-B/somefile 除了合并之外不会显示任何内容。看到 Greg Hewgill的回答 引用这个重要问题。 - artfulrobot
重要提示:git pull --no-rebase -s子树Bproject master如果你不这样做,并且你自动将pull设置为rebase,你将最终得到“无法解析对象”。看到 osdir.com/ml/git/2009-07/msg01576.html - Eric Bowman - abstracto -
这个答案可能会让人感到困惑,因为它在问题A中有B作为合并子树。复制和粘贴的结果? - vfclists
如果您只是想将两个存储库粘合在一起,则子模块和子树合并是错误的工具,因为它们不会保留所有文件历史记录(正如其他评论者所指出的那样)。看到 stackoverflow.com/questions/13040958/...。 - Eric Lee


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

第一件事就是使用 git filter-branch 将第二个存储库中所有内容的名称重写到您希望它们结束的子目录中。而不是 foo.cbar.html, 你将会拥有 projb/foo.c 和 projb/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


188
2018-02-01 08:10



非常感谢,这正是我想要做的。 - static_rtti
子树合并将是更好的解决方案,并且不需要重写包含项目的历史记录 - Jakub Narębski
我想知道如何使用 git filter-branch 为了达成这个。在手册页中,它说的是相反的方式:使subdir /成为根,但不是相反。 - artfulrobot
如果它解释了如何使用filter-branch来实现所需的结果,那么这个答案会很棒 - Anentropic
我在这里找到了如何使用filter-branch: stackoverflow.com/questions/4042816/... - David Minor


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

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


就我而言,我想要两个子目录, projectA 和 projectB。在那种情况下,我也做了步骤(1)到B.


62
2018-06-11 14:31



看起来你复制了你的答案 stackoverflow.com/a/618113/586086? - Andrew Mao
@AndrewMao,我想是的......我实在记不起来了。我已经使用过这个脚本了。 - Paul Draper
我要补充一点\ t在OS X上不起作用,你必须输入<tab> - Muneeb Ali
"$GIT_INDEX_FILE" 必须引用(两次),否则您的方法将失败,例如路径包含空格。 - Rob W
如果您想知道,要在osx中​​插入<tab>,您需要 Ctrl-V <tab> - casey


如果两个存储库都具有相同类型的文件(例如,针对不同项目的两个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 mergetoolkdiff3 可以单独使用键盘,因此只需几分钟读取代码即可获得5个冲突文件。

记得完成合并:

git commit

39
2018-02-27 03:09





在使用merge时我一直在丢失历史记录,所以我最终使用了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的提交


20
2017-08-18 21:06



清爽简洁有效! - Ivan


就我而言,我有一个 my-plugin 存储库和a main-project 存储库,我想假装 my-plugin 一直在发展 plugins 子目录 main-project

基本上,我重写了历史 my-plugin 存储库,以便它看起来所有的发展都发生在 plugins/my-plugin 子目录。然后,我添加了开发历史 my-plugin 进入 main-project 历史,并将两棵树合并在一起。既然没有 plugins/my-plugin 目录已存在于 main-project 存储库,这是一个简单的无冲突合并。生成的存储库包含两个原始项目的所有历史记录,并且有两个根。

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|my-plugin) 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-plugin 和 plugins 无论你的名字是什么。

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

现在来解释一下。 git filter-branch --tree-filter (...) HEAD 跑了 (...) 每个可从中访问的提交的命令 HEAD。请注意,这直接对每个提交存储的数据进行操作,因此我们不必担心“工作目录”,“索引”,“暂存”等概念。

如果你跑了 filter-branch 命令失败,它会留下一些文件 .git 目录和下次你尝试 filter-branch 它会抱怨这个,除非你提供 -f 选项 filter-branch

至于实际的命令,我没有太多运气 bash 做我想做的事,所以我使用 zsh -c 制作 zsh 执行命令。首先我设置了 extended_glob 选项,这是什么使能 ^(...) 语法 mv 命令,以及 glob_dots 选项,允许我选择点文件(例如 .gitignore)用glob(^(...))。

接下来,我用了 mkdir -p 命令创建两者 plugins 和 plugins/my-plugin 与此同时。

最后,我用了 zsh “负面水珠”功能 ^(.git|my-plugin) 匹配存储库根目录中的所有文件,除了 .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 命令无法正常工作或已经有一个 plugins/my-plugin 目录 main-project

确保为任何未来的贡献者输入一个解释性提交消息,想知道hackery正在进行什么样的生成有两个根的存储库。

您可以使用上面的内容可视化新的提交图,该图应该有两个根提交 git log 命令。注意 只有 master 分支将合并。这意味着如果你在其他方面有重要的工作 my-plugin 要合并到的分支 main-project 树,你应该避免删除 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.0 和 zsh --version 5.2。你的旅费可能会改变。

参考文献:


13
2018-03-10 12:51



哪里 --allow-unrelated-histories 来自哪里? - Marcelo Filho
@MarceloFilho检查 man git-merge。 默认情况下,git merge命令拒绝合并不共享共同祖先的历史记录。在合并独立开始生命的两个项目的历史时,此选项可用于覆盖此安全性。由于这是一个非常罕见的场合,因此默认情况下不会启用任何配置变量来启用它,也不会添加。 - Radon Rosborough
应该可用 git version 2.7.2.windows.1? - Marcelo Filho
@MarceloFilho这是在2.9.0中添加的,但在旧版本中,您不必通过该选项(它将正常工作)。 github.com/git/git/blob/... - Radon Rosborough
这很好用。我能够使用过滤器分支在合并之前将文件名重写到我想要的树中。我想如果你需要移动除主分支之外的历史,还需要做更多的工作。 - codeDr