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

考虑以下场景:

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

我如何将A和B合并,而不丢失任何一方的历史?

  • 如果您只是想将两个存储库合并为一个存储库,而不需要同时保留两个存储库,请查看以下问题:stackoverflow.com/questions/13040958/…
  • 要在定制目录中合并git repo并保存所有comits,请使用stackoverflow.com/a/43340714/1772410


如果你想合并project-aproject-b:

1
2
3
4
5
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合并文档/——允许不相关的历史记录

更新:根据@jstadler的建议添加--tags以保留标签。

  • 这似乎需要一个工作副本:fatal: This operation must be run in a work tree,我想合并两个裸git存储库。
  • @LiuYan刘研:对不起这是有点晚,但你可以创建一个工作副本通过查看一个项目然后procceed描述。
  • 注意,这不会合并子模块。
  • @PythonNut,你能提供一个解决方案的链接吗?
  • 这帮我做成了生意。在.gitignore文件中只有一个冲突的情况下,第一次工作起来很有魅力!它完美地保存了提交历史。与其他方法相比,除了简单性之外,最大的优点是不需要持续引用合并后的回购。但是,需要注意的一件事是——如果您是像我这样的iOS开发人员——要非常小心地将目标repo的项目文件放入工作区。
  • 谢谢。为我工作。我需要将合并后的目录移动到子文件夹中,因此在执行了上述步骤之后,我只使用了git mv source-dir/ dest/new-source-dir
  • @Sid或者其他任何人,你知道如何一次性将project-a放入project-b的子目录吗?如果两个项目的根目录中都有很多文件,那么对所有文件进行筛选并移动到需要的地方就有点麻烦了
  • @sg一种间接的方法是将project-a中的所有文件移动到project-a中的子目录中(这样项目的顶层-a只有一个目录),然后遵循上面的过程。
  • 合并之后,还可以运行git rebase -i [last commit of project-b]以使日志看起来更好。
  • git merge步骤在这里用fatal: refusing to merge unrelated histories失败;--allow-unrelated-histories修复了文档中解释的问题。
  • 证实了!对我来说很有魅力。我没有执行最后两行代码,而是使用sourcetree来完成我的工作,但是前三行代码确实很神奇。
  • 这是对主要问题/标题的一个更好的回答。我猜被接受的和投票最多的受访者只是在正文中选择了"在子目录中"的细节,然后带着它跑掉了。对我来说,"合并两个Git存储库"就是这个答案的确切含义,到目前为止,它似乎对我来说还不错。+ 1
  • 这只适用于git >2.8—对于linux,您需要ppa存储库才能获得它。但当我把它固定后它就是我想要的解了。100%解决问题!
  • 我得到了error: unknown option 'allow-unrelated-histories'
  • 但是停止使用yanan8起了作用
  • 在git 2.9中引入了--allow-unrelated-histories。在早期版本中,这是默认行为。
  • 短:git fetch /path/to/project-a master; git merge --allow-unrelated-histories FETCH_HEAD
  • @jthill可能会给你自己的答案?
  • 这是我所见过的最简洁的工作流配方。谢谢你!
  • 这太棒了。当我转向另一个分支而忘记将更改合并回基本分支时,我使用它来获取旧的提交历史记录。在本例中,我不希望接受来自正在合并的旧分支的任何更改,而只是获取历史记录。所以我使用"our"合并策略,git merge -s ours --allow-unrelated-histories project-a/master,它工作得很好!
  • 这完美的工作。对于正在做类似于我的事情的任何人的一个快速提示:在做上面的操作之前,我还遵循了这些指示,在所有project-a的提交消息中添加了一个类似Project A:的前缀,以便从project-b中清楚地显示消息的上下文。
  • 这仍然是最好的解决方案。我经常使用它来合并git repos。谢谢! !
  • --allow-unrelated-histories的文档。
  • 这非常方便。我使用的是git版本pre 2.9,并且我能够像上面所说的那样简单地执行远程添加和获取,然后将源项目直接合并到目标项目中。我还发现历史并没有消失。希望这对某些人有所帮助!
  • 在获取时,您还应该考虑获取标记,否则它们将丢失:git fetch project-a --tags
  • @James为什么回滚?
  • @DaniSpringer阅读了diff,它引入了一个错误
  • @JamesTaylor哦!现在固定。谢谢。
  • 但是现在,你如何撤销它呢?在我的工作中,显然有人在过去的某个时候决定这样做,但是他合并了Slim framework repo,所以我们的存储库历史几乎不可用,因为有一种方法可以进行许多不相关的提交。
  • 这将是一个新的、独立的问题的主题。去创建它并链接到这里


这里有两个可能的解决方案:

要么将存储库A复制到较大的项目B的单独目录中,要么(也许更好)将存储库A复制到项目B的子目录中,然后使用git子模块使该存储库成为存储库B的子模块。

对于松耦合存储库,这是一个很好的解决方案,其中存储库a中的开发将继续进行,而开发的主要部分是在a中单独的独立开发。请参阅Git Wiki上的SubmoduleSupport和GitSubmoduleTutorial页面。

子树合并

您可以使用子树合并策略将存储库A合并到项目B的子目录中。这是由Markus Prinz在子树合并和You中描述的。

1
2
3
4
5
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上的repository),比如他在博客文章中宣布了git子模块的一个新替代方案:git子树。

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

  • 在"如何使用子树合并策略"how to之后,我找到了将本地repo放入远程repo子目录所需的所有内容。
  • 嗯,"git pull -s子树Bproject master"不会将历史中的Bproject/master标记更新到新的Bproject头部
  • 子树归并非常简单,完全符合我的需要。谢谢!
  • 这样做似乎可以保存历史,但不能使用它来区分文件或通过合并进行平分。我是不是漏了一步?
  • 这是不完整的。是的,您收到了大量提交,但是它们不再指向正确的路径。git log dir-B/somefile只显示合并。参见Greg Hewgill的回答,他提到了这个重要的问题。
  • 从1.7.11开始,apenwarr的git subtree split工具就在主流git中
  • 重要提示:git pull—no-rebase -s子树Bproject master如果不这样做,并且您已经将pull设置为自动rebase,您将以"无法解析对象"结束。看到osdir.com/ml/git/2009-07/msg01576.html
  • 这个答案可能令人困惑,因为当问题中的子树是a时,它的合并子树是B。
  • @artfulrobot是的,我刚刚经历了同样的问题,git日志在项目B的文件上被破坏了,很多提交被推送到混合repo中,我想知道是否有人有办法解决这个问题?谢谢。
  • 子树的另一个好链接:log.pardus.de/2012/08/ module -git-with-git-subtree.html
  • 如果您试图将两个存储库简单地粘合在一起,则使用子模块和子树合并是错误的工具,因为它们不能保存所有的文件历史记录(正如其他评论者所指出的)。看到stackoverflow.com/questions/13040958/…。
  • 还有git stree—应该比subtree更好:medium.com/@porteneuve/mastering-git-subtrees-943d29a798ec。


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

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

这将显示为一个提交,其中Rails主分支的所有文件都被添加到"Rails"目录中。然而,提交的标题包含对旧历史树的引用:

Add 'rails/' from commit

其中是SHA-1提交散列。你还能看到历史,怪一些变化。

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

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

1
2
3
4
5
# 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自制程序)。但是除了git之外,您可能还必须自己安装它。

  • @Brad Mace, git子树repo现在已经过时了,因为它包含在git本身中。看到github.com/apenwarr/git-subtree/blob/master/…
  • 不要停止阅读…下面是更完整的答案。
  • 以下是关于如何安装Git子树的说明(截至2013年6月):stackoverflow.com/a/11613541/694469(我用... v1.8.3替换了git co v1.7.11.3 )。
  • 谢谢你提醒我下面的答案。到目前为止,git 1.8.4仍然没有包含"子树"(至少在Ubuntu 12.04 git ppa中没有)。
  • 我可以确认,在此之后,git log rails/somefile将不会显示该文件的提交历史记录,除了合并提交。正如@artfulrobot所建议的,检查一下格雷格·休吉尔的答案。您可能需要在要包含的repo上使用git filter-branch
  • 或者阅读Eric Lee的"将两个Git存储库合并到一个存储库中而不丢失文件历史记录",saintgimp.org/2013/01/22/…
  • git子树包含在git-core ppa中,但默认情况下没有启用。启用:chmod +x /usr/share/doc/git/contrib/subtree/git-subtree.sh然后ln -s /usr/share/doc/git/contrib/subtree/git-subtree.sh /usr/lib/git-core/git-subtree作为根
  • 正如其他人所说,亚南可能不会做你认为的事情!这里有一个更完整的解决方案。
  • 你好。由于这个问题很受欢迎,我花了一些时间来扩展我的答案。希望它能回答这里提出的许多问题。投票最多的答案并不能直接回答问题。我不是git专家,花了很长时间才弄明白这个简单的命令。有些包管理器没有安装git-contrib,这很令人失望。
  • 使用"git子树"假定您正在组合相同的分支,在本例中是"master"。如果没有,git子树将失败。您必须遵循下面的第二组说明。
  • 子树并没有像预期的那样工作,在这个答案中,在合并了所有git历史记录之后,将保留原来的stackoverflow.com/a/43340714/1772410
  • Add 'rails/' from commit 是什么意思?显然没有Add命令。
  • 这是一个提交标题示例,而不是命令。为了清楚起见,我将它重新格式化为引用。


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

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

然后,您应该能够做如下事情:

1
2
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 pull步骤可能会抱怨无法找到一个共同的祖先。其他一些细节可能也需要解决。
  • 子树合并将是更好的解决方案,并且不需要重写包含的项目的历史
  • 我想知道如何使用git filter-branch来实现这一点。在手册页中,它描述了相反的方法:使subdir/成为根目录,但不是相反的方法。
  • @artfulrobot:一定要查看Git -subtree,寻找能够帮助完成此任务的工具(Git -subtree最近也添加到了标准Git发行版中)。
  • 如果它解释了如何使用filter-branch来实现期望的结果,那么这个答案将非常好
  • 我在这里找到了如何使用filter-branch: stackoverflow.com/questions/4042816/…
  • 有关Greg's outline的实现,请参阅此答案。
  • 这里有一个更详细的解释,我认为是相同的解决方案。
  • 尝试了这个方法,结果出现了这个错误:"您要求从远程''中提取,但是没有指定分支。因为这不是当前分支的默认配置远程,所以必须在命令行上指定一个分支。"
  • 对于任何寻找git filter-branch示例将根目录移动到子目录的人:git filter-branch -f --index-filter 'git ls-files -s | awk -v prefix="SUBDIRECTORY/""{ sub(/\t/, "\t" prefix); print}" | GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info && mv"$GIT_INDEX_FILE.new""$GIT_INDEX_FILE"' HEAD(用实际目录名替换子目录)


git-subtree很好,但是它可能不是你想要的那个。

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

1
git log projectA

只列出一个提交:合并。合并项目的提交针对不同的路径,因此它们不会出现。

格雷格?休吉尔(Greg Hewgill)的答案最为接近,不过他并没有具体说明如何重写路径。

解决方案出奇地简单。

(1)在一个,

1
2
3
4
5
6
7
8
PREFIX=projectA #adjust this

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

注意:这将重写历史记录,因此如果您打算继续使用这个repo A,您可能想首先克隆(复制)它的一次性副本。

然后在B中,运行

1
git pull path/to/A

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

在我的例子中,我需要两个子目录,projectAprojectB。在这种情况下,我也做了步骤(1)到B。

  • 看起来你是从stackoverflow.com/a/618113/586086复制答案的。
  • @AndrewMao,我想是的……我不记得了。我经常使用这个脚本。
  • 无论如何,原版都在git-scm.com/docs/git-filter-branch上,这个链接是由另一个问题链接的。这本书读起来很有趣,但也令人困惑。
  • 我要添加 在OS X上不起作用,您必须输入
  • 这工作得很好,虽然在Windows上我需要把脚本放在一个单独的文件中,像这样调用它:stackoverflow.com/questions/7798142/…
  • "$GIT_INDEX_FILE"必须被引用(两次),否则你的方法将失败,例如路径包含空格。
  • 如果您想在osx中插入,您需要Ctrl-V
  • "注意:这重写了历史,所以如果您打算继续使用这个repo B,您可能想首先克隆(复制)它的一次性副本。"这一步不就是在A中重写历史吗?
  • @JosephBlair,是的。"使用这个回购B"应该是"使用这个回购A"。固定的。


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

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

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

1
git merge --allow-unrelated-histories branch_name

如果Git版本小于2.9,则删除--allow-unrelated-histories

在此之后,可能会发生冲突。您可以使用git mergetool解析它们。kdiff3可以单独使用键盘,所以5冲突文件读取代码时只需几分钟。

记住要完成合并:

1
git commit

在使用merge时,我不断丢失历史记录,所以我最终使用了rebase,因为在我的例子中,这两个存储库足够不同,不会在每次提交时都合并:

1
2
3
4
5
6
7
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

=>解决冲突,然后继续,需要多少次…

1
git rebase --continue

这样做将导致一个项目拥有来自projA的所有提交,然后是来自projB的提交

  • 清爽简单有效!


在我的例子中,我有一个my-plugin存储库和一个main-project存储库,我想假装my-plugin一直是在main-projectplugins子目录中开发的。

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

博士

TL;

1
2
3
4
5
6
7
8
9
$ cp -R my-plugin my-plugin-dirty
$ cd my-plugin-dirty
$ git filter-branch -f --tree-filter"zsh -c 'setopt extended_glob &amp;&amp; setopt glob_dots &amp;&amp; mkdir -p plugins/my-plugin &amp;&amp; (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

1
$ git filter-branch -f --tree-filter"zsh -c 'setopt extended_glob &amp;&amp; setopt glob_dots &amp;&amp; mkdir -p plugins/my-plugin &amp;&amp; (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 -czsh执行命令。首先,我设置了extended_glob选项,它支持mv命令中的^(...)语法,以及glob_dots选项,它允许我使用glob (^(...))选择dotfiles(例如.gitignore)。

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

最后,我使用zsh"负glob"特性^(.git|plugins)匹配存储库根目录中的所有文件,除了.git和新创建的my-plugin文件夹。(这里可能不需要排除.git,但是尝试将目录移动到目录本身是一个错误。)

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

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

现在,导航到您的main-project存储库并检出您想合并到的任何分支。将my-plugin存储库的本地副本(其历史记录已修改)作为main-project的远程添加到:

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

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

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

要合并它们,请使用:

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

注意,在pre-2.9.0 Git中,不存在--allow-unrelated-histories选项。如果您正在使用这些版本中的一个,只需省略该选项:--allow-unrelated-histories阻止的错误消息也添加在2.9.0中。

您不应该有任何合并冲突。如果这样做,可能意味着要么filter-branch命令没有正确工作,要么main-project中已经有一个plugins/my-plugin目录。

请确保输入一条解释性的提交消息,以供未来的贡献者了解hackery将如何创建具有两个根的存储库。

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

可选地,在您合并了所有您想要从my-plugin中保留的内容之后,您可以使用以下方法删除my-plugin远程:

1
$ git remote remove my-plugin

现在,您可以安全地删除您更改了历史记录的my-plugin存储库的副本。在我的例子中,在合并完成并推送之后,我还向实际的my-plugin存储库添加了一个弃用通知。

测试在Mac OS X El Capitan与git --version 2.9.0zsh --version 5.2。你的里程可能不同。

引用:

https://git-scm.com/docs/git-filter-branchhttps://unix.stackexchange.com/questions/6393/how-do-you-move-all-files-including-hidden-from-one-directory-to-anotherhttp://www.refining-linux.org/archives/37/ZSH-Gem-2-Extended-globbing-and-expansion/从Git repo清除文件失败,无法创建新的备份git,所有分支上的过滤器分支

  • 亚南从哪里来?
  • @MarceloFilho检查man git-merge。默认情况下,git merge命令拒绝合并不共享同一祖先的历史记录。当合并两个独立启动的项目的历史记录时,可以使用此选项覆盖此安全性。由于这是非常罕见的情况,因此默认情况下不存在任何启用此功能的配置变量,并且不会添加该配置变量。
  • 应该在git version 2.7.2.windows.1上可用吗?
  • @MarceloFilho这是在2.9.0中添加的,但在较早的版本中,您不应该必须传递该选项(它将正常工作)。github.com/git/git/blob/…
  • 这工作得很好。我可以使用filter分支将文件名重写到合并之前我想要在树中的位置。我想,如果你需要移动历史,除了主分支,还需要做更多的工作。
  • @codeDr我已经更新了我的答案,以解决重写所有分支的问题。
  • @TJ改变,谢谢。


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

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

我建议你先在B分行工作。

以下是没有分支的步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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

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

1
git log --follow A/<file>

这篇文章帮助我做到了这一点:

Merging Two Git Repositories Into One Repository Without Losing File History


我在这里收集了许多关于stack&c;OverFlow等的信息,并设法编写了一个脚本来解决这个问题。

需要注意的是,它只考虑每个存储库的"开发"分支,并将其合并到一个全新存储库中的单独目录中。

标记和其他分支被忽略——这可能不是您想要的。

该脚本甚至处理特性分支和标记——在新项目中重新命名它们,以便您知道它们来自何处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
#!/bin/bash
#
################################################################################
## Script to merge multiple git repositories into a new repository
## - The new repository will contain a folder for every merged repository
## - The script adds remotes for every project and then merges in every branch
##&nbsp;  and tag. These are renamed to have the origin project name as a prefix
##
## Usage: mergeGitRepositories.sh <new_project> <my_repo_urls.lst>
## - where <new_project> is the name of the new project to create
## - and <my_repo_urls.lst> is a file contaning the URLs to the respositories
##   which are to be merged on separate lines.
##
## Author: Robert von Burg
##            eitch@eitchnet.ch
##
## Version: 0.3.2
## Created: 2018-02-05
##
################################################################################
#

# disallow using undefined variables
shopt -s -o nounset

# Script variables
declare SCRIPT_NAME="${0##*/}"
declare SCRIPT_DIR="$(cd ${0%/*} ; pwd)"
declare ROOT_DIR="$PWD"
IFS=$'
'

# Detect proper usage
if ["$#" -ne"2" ] ; then
  echo -e"ERROR: Usage: $0 <new_project> <my_repo_urls.lst>"
  exit 1
fi


## Script variables
PROJECT_NAME="${1}"
PROJECT_PATH="${ROOT_DIR}/${PROJECT_NAME}"
TIMESTAMP="$(date +%s)"
LOG_FILE="${ROOT_DIR}/${PROJECT_NAME}_merge.${TIMESTAMP}.log"
REPO_FILE="${2}"
REPO_URL_FILE="${ROOT_DIR}/${REPO_FILE}"


# Script functions
function failed() {
  echo -e"ERROR: Merging of projects failed:"
  echo -e"ERROR: Merging of projects failed:">>${LOG_FILE} 2>&amp;1
  echo -e"$1"
  exit 1
}

function commit_merge() {
  current_branch="$(git symbolic-ref HEAD 2>/dev/null)"
  if [[ ! -f".git/MERGE_HEAD" ]] ; then
    echo -e"INFO:   No commit required."
    echo -e"INFO:   No commit required.">>${LOG_FILE} 2>&amp;1
  else
    echo -e"INFO:   Committing ${sub_project}..."
    echo -e"INFO:   Committing ${sub_project}...">>${LOG_FILE} 2>&amp;1
    if ! git commit -m"[Project] Merged branch '$1' of ${sub_project}">>${LOG_FILE} 2>&amp;1 ; then
      failed"Failed to commit merge of branch '$1' of ${sub_project} into ${current_branch}"
    fi
  fi
}


# Make sure the REPO_URL_FILE exists
if [ ! -e"${REPO_URL_FILE}" ] ; then
  echo -e"ERROR: Repo file ${REPO_URL_FILE} does not exist!"
  exit 1
fi


# Make sure the required directories don't exist
if [ -e"${PROJECT_PATH}" ] ; then
  echo -e"ERROR: Project ${PROJECT_NAME} already exists!"
  exit 1
fi


# create the new project
echo -e"INFO: Logging to ${LOG_FILE}"
echo -e"INFO: Creating new git repository ${PROJECT_NAME}..."
echo -e"INFO: Creating new git repository ${PROJECT_NAME}...">>${LOG_FILE} 2>&amp;1
echo -e"===================================================="
echo -e"====================================================">>${LOG_FILE} 2>&amp;1
cd ${ROOT_DIR}
mkdir ${PROJECT_NAME}
cd ${PROJECT_NAME}
git init
echo"Initial Commit"> initial_commit
# Since this is a new repository we need to have at least one commit
# thus were we create temporary file, but we delete it again.
# Deleting it guarantees we don't have conflicts later when merging
git add initial_commit
git commit --quiet -m"[Project] Initial Master Repo Commit"
git rm --quiet initial_commit
git commit --quiet -m"[Project] Initial Master Repo Commit"
echo


# Merge all projects into the branches of this project
echo -e"INFO: Merging projects into new repository..."
echo -e"INFO: Merging projects into new repository...">>${LOG_FILE} 2>&amp;1
echo -e"===================================================="
echo -e"====================================================">>${LOG_FILE} 2>&amp;1
for url in $(cat ${REPO_URL_FILE}) ; do

  if [["${url:0:1}" == '#' ]] ; then
    continue
  fi

  # extract the name of this project
  export sub_project=${url##*/}
  sub_project=${sub_project%*.git}

  echo -e"INFO: Project ${sub_project}"
  echo -e"INFO: Project ${sub_project}">>${LOG_FILE} 2>&amp;1
  echo -e"----------------------------------------------------"
  echo -e"----------------------------------------------------">>${LOG_FILE} 2>&amp;1

  # Fetch the project
  echo -e"INFO:   Fetching ${sub_project}..."
  echo -e"INFO:   Fetching ${sub_project}...">>${LOG_FILE} 2>&amp;1
  git remote add"${sub_project}""${url}"
  if ! git fetch --tags --quiet ${sub_project} >>${LOG_FILE} 2>&amp;1 ; then
    failed"Failed to fetch project ${sub_project}"
  fi

  # add remote branches
  echo -e"INFO:   Creating local branches for ${sub_project}..."
  echo -e"INFO:   Creating local branches for ${sub_project}...">>${LOG_FILE} 2>&amp;1
  while read branch ; do
    branch_ref=$(echo $branch | tr"""\t" | cut -f 1)
    branch_name=$(echo $branch | tr"""\t" | cut -f 2 | cut -d / -f 3-)

    echo -e"INFO:   Creating branch ${branch_name}..."
    echo -e"INFO:   Creating branch ${branch_name}...">>${LOG_FILE} 2>&amp;1

    # create and checkout new merge branch off of master
    if ! git checkout -b"${sub_project}/${branch_name}" master >>${LOG_FILE} 2>&amp;1 ; then failed"Failed preparing ${branch_name}" ; fi
    if ! git reset --hard ; then failed"Failed preparing ${branch_name}">>${LOG_FILE} 2>&amp;1 ; fi
    if ! git clean -d --force ; then failed"Failed preparing ${branch_name}">>${LOG_FILE} 2>&amp;1 ; fi

    # Merge the project
    echo -e"INFO:   Merging ${sub_project}..."
    echo -e"INFO:   Merging ${sub_project}...">>${LOG_FILE} 2>&amp;1
    if ! git merge --allow-unrelated-histories --no-commit"remotes/${sub_project}/${branch_name}">>${LOG_FILE} 2>&amp;1 ; then
      failed"Failed to merge branch 'remotes/${sub_project}/${branch_name}' from ${sub_project}"
    fi

    # And now see if we need to commit (maybe there was a merge)
    commit_merge"${sub_project}/${branch_name}"

    # relocate projects files into own directory
    if ["$(ls)" =="${sub_project}" ] ; then
      echo -e"WARN:   Not moving files in branch ${branch_name} of ${sub_project} as already only one root level."
      echo -e"WARN:   Not moving files in branch ${branch_name} of ${sub_project} as already only one root level.">>${LOG_FILE} 2>&amp;1
    else
      echo -e"INFO:   Moving files in branch ${branch_name} of ${sub_project} so we have a single directory..."
      echo -e"INFO:   Moving files in branch ${branch_name} of ${sub_project} so we have a single directory...">>${LOG_FILE} 2>&amp;1
      mkdir ${sub_project}
      for f in $(ls -a) ; do
        if  [["$f" =="${sub_project}" ]] ||
            [["$f" =="." ]] ||
            [["$f" ==".." ]] ; then
          continue
        fi
        git mv -k"$f""${sub_project}/"
      done

      # commit the moving
      if ! git commit --quiet -m "[Project] Move ${sub_project} files into sub directory" ; then
        failed"Failed to commit moving of ${sub_project} files into sub directory"
      fi
    fi
    echo
  done < <(git ls-remote --heads ${sub_project})


  #&nbsp;checkout master of sub probject
  if ! git checkout"${sub_project}/master">>${LOG_FILE} 2>&amp;1 ; then
    failed"sub_project ${sub_project} is missing master branch!"
  fi

  # copy remote tags
  echo -e"INFO:   Copying tags for ${sub_project}..."
  echo -e"INFO:   Copying tags for ${sub_project}...">>${LOG_FILE} 2>&amp;1
  while read tag ; do
    tag_ref=$(echo $tag | tr"""\t" | cut -f 1)
    tag_name_unfixed=$(echo $tag | tr"""\t" | cut -f 2 | cut -d / -f 3)

    # hack for broken tag names where they are like 1.2.0^{} instead of just 1.2.0
    tag_name="${tag_name_unfixed%%^*}"

    tag_new_name="${sub_project}/${tag_name}"
    echo -e"INFO:     Copying tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}..."
    echo -e"INFO:     Copying tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}...">>${LOG_FILE} 2>&amp;1
    if ! git tag"${tag_new_name}""${tag_ref}">>${LOG_FILE} 2>&amp;1 ; then
      echo -e"WARN:     Could not copy tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}"
      echo -e"WARN:     Could not copy tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}">>${LOG_FILE} 2>&amp;1
    fi
  done < <(git ls-remote --tags --refs ${sub_project})

  # Remove the remote to the old project
  echo -e"INFO:   Removing remote ${sub_project}..."
  echo -e"INFO:   Removing remote ${sub_project}...">>${LOG_FILE} 2>&amp;1
  git remote rm ${sub_project}

  echo
done


# Now merge all project master branches into new master
git checkout --quiet master
echo -e"INFO: Merging projects master branches into new repository..."
echo -e"INFO: Merging projects master branches into new repository...">>${LOG_FILE} 2>&amp;1
echo -e"===================================================="
echo -e"====================================================">>${LOG_FILE} 2>&amp;1
for url in $(cat ${REPO_URL_FILE}) ; do

  if [[ ${url:0:1} == '#' ]] ; then
    continue
  fi

  # extract the name of this project
  export sub_project=${url##*/}
  sub_project=${sub_project%*.git}

  echo -e"INFO:   Merging ${sub_project}..."
  echo -e"INFO:   Merging ${sub_project}...">>${LOG_FILE} 2>&amp;1
  if ! git merge --allow-unrelated-histories --no-commit"${sub_project}/master">>${LOG_FILE} 2>&amp;1 ; then
    failed"Failed to merge branch ${sub_project}/master into master"
  fi

  # And now see if we need to commit (maybe there was a merge)
  commit_merge"${sub_project}/master"

  echo
done


# Done
cd ${ROOT_DIR}
echo -e"INFO: Done."
echo -e"INFO: Done.">>${LOG_FILE} 2>&amp;1
echo

exit 0

您也可以从http://paste.ubuntu.com/11732805获得它

首先创建一个文件与每个存储库的URL,例如:

1
2
3
git@github.com:eitchnet/ch.eitchnet.parent.git
git@github.com:eitchnet/ch.eitchnet.utils.git
git@github.com:eitchnet/ch.eitchnet.privilege.git

然后调用脚本,给出项目的名称和脚本的路径:

1
./mergeGitRepositories.sh eitchnet_test eitchnet.lst

脚本本身有很多注释,可以解释它的功能。

  • 不要引导读者找到答案,请在这里发布答案(也就是把你在评论中说的话编辑成这个答案)。
  • 当然,我只是觉得最好不要重复我的话……=)
  • 如果您认为这个问题与另一个问题相同,那么您可以使用问题本身下方的"flag"链接将其标记为一个副本,并指示另一个问题。如果这不是一个重复的问题,但你认为完全相同的答案可以用来解决这两个问题,那么就发布两个问题相同的答案(正如你现在所做的)。谢谢你的贡献!
  • 神奇的!没有工作在Windows bash提示符,但它完美地运行形成一个流浪框运行ubuntu。真是节省时间!
  • 乐意为您服务=)


我知道那是很久以后的事了,但是我对我在这里找到的其他答案不满意,所以我写了这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
me=$(basename $0)

TMP=$(mktemp -d /tmp/$me.XXXXXXXX)
echo
echo"building new repo in $TMP"
echo
sleep 1

set -e

cd $TMP
mkdir new-repo
cd new-repo
    git init
    cd ..

x=0
while [ -n"$1" ]; do
    repo="$1"; shift
    git clone"$repo"
    dirname=$(basename $repo | sed -e 's/\s/-/g')
    if [[ $dirname =~ ^git:.*\.git$ ]]; then
        dirname=$(echo $dirname | sed s/.git$//)
    fi

    cd $dirname
        git remote rm origin
        git filter-branch --tree-filter \
           "(mkdir -p $dirname; find . -maxdepth 1 ! -name . ! -name .git ! -name $dirname -exec mv {} $dirname/ \;)"
        cd ..

    cd new-repo
        git pull --no-commit ../$dirname
        [ $x -gt 0 ] &amp;&amp; git commit -m"merge made by $me"
        cd ..

    x=$(( x + 1 ))
done

  • 这正是我要找的。谢谢!但是,我不得不将第22行改为:if [[ $dirname =~ ^.*\.git$ ]]; then
  • ^。blarg$是浪费而贪婪的。


如果您试图将两个存储库简单地粘合在一起,则使用子模块和子树合并是错误的工具,因为它们不能保存所有的文件历史记录(正如人们在其他答案中指出的那样)。看这里的答案,找到简单而正确的方法。

  • 您的解决方案只适用于新的存储库,但是如何将repo合并到另一个具有文件冲突的存储库中呢?


如果您想将repo B中的分支中的文件放到repo a的子树中并保存历史,请继续阅读。(在下面的例子中,我假设我们希望repo B的主分支合并到repo A的主分支中。)

在回购A中,首先执行以下步骤使回购B可用:

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

现在我们在repo a中创建了一个全新的分支(只有一个commit),我们称之为new_b_root。生成的提交将包含在repo B主分支的第一次提交中提交的文件,但这些文件将放在名为path/to/b-files/的子目录中。

1
2
3
4
5
6
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的主分支签出文件,但不创建任何提交。我们可以选择任何提交,因为接下来我们将清除所有文件。然后,在还没有提交(-n)的情况下,我们从B的主分支中选择第一个提交。(chry -pick保留了原始的提交消息,而直接签出似乎不会这样做。)然后,我们创建子树,将repo b中的所有文件放到子树中。然后,我们必须将chry -pick中引入的所有文件移动到子树中。在上面的例子中,只需要移动一个README文件。然后我们提交B-repo根提交,同时,我们还保存原始提交的时间戳。

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

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

现在,我们将我们的b分支合并为A/master:

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

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

1
2
git remote remove B
git branch -D new_b_root b

最终的图表结构如下:

enter image description here

  • 伟大的回答,谢谢!在Andresch Serj的"git子树"或"merge——allow-unrelated- history"的其他答案中,我确实遗漏了子目录没有日志。


我遇到过类似的挑战,但在我的例子中,我们在repo a中开发了一个版本的代码库,然后将其克隆到一个新的repo, repo B,用于产品的新版本。在修复了repo A中的一些bug后,我们需要将更改FI到repo b中。

向指向repo a的repo B添加远程(git远程添加…)绘制当前分支(我们没有使用master进行bug修复)(git绘制remoteForRepoA bugFixBranch)推动合并到github

工作待遇:)


将a a合并到B中:

1)在项目A中

1
git fast-export --all --date-order > /tmp/ProjectAExport

2)在项目B中

1
2
git checkout -b projectA
git fast-import --force < /tmp/ProjectAExport

在这个分支中,执行所有需要执行的操作并提交它们。

C)然后回到大师和经典的融合之间的两个分支:

1
2
git checkout master
git merge projectA

合并2回购

1
2
3
4
5
6
7
8
git clone ssh://<project-repo> project1
cd project1
git remote add -f project2 project2
git merge --allow-unrelated-histories project2/master
git remote rm project2

delete the ref to avoid errors
git update-ref -d refs/remotes/project2/master

类似于@Smar,但使用文件系统路径,设置在主要和次要:

1
2
3
4
5
PRIMARY=~/Code/project1
SECONDARY=~/Code/project2
cd $PRIMARY
git remote add test $SECONDARY &amp;&amp; git fetch test
git merge test/master

然后手动合并。

(改编自Anar Manafov的文章)


当您想在一次提交中合并三个或更多的项目时,按照其他答案(remote add -fmerge)中描述的步骤执行。然后,(soft)将索引重置为old head(没有合并的地方)。添加所有文件(git add -A)并提交它们(消息"将项目A、B、C和D合并到一个项目中")。这现在是master的commit-id。

现在,用以下内容创建.git/info/grafts:

1
<commit-id of master> <list of commit ids of all parents>

运行git filter-branch -- head^..head head^2..head head^3..head。如果您有三个以上的分支,只需添加与您的分支相同多的head^n..head。若要更新标记,请添加--tag-name-filter cat。不要总是添加它,因为这可能导致重写一些提交。有关详细信息,请参见过滤器-分支的手册页,搜索"grafts"。

现在,您的上一个提交关联了正确的父类。

  • 等等,为什么要在一次提交中合并三个项目?
  • 我从repository、repository-client和modeler作为单独的git项目开始。这对同事来说很难,所以我加入了他们的一个git项目。为了能够使新项目的"根"来自其他三个项目,我希望进行单次合并提交。


给定命令是我建议的最佳解决方案。

1
git subtree add --prefix=MY_PROJECT git://github.com/project/my_project.git master


我想把一个小项目移到一个大项目的子目录中。由于我的小项目没有很多提交,所以我使用了git format-patch --output-directory /path/to/patch-dir。然后在更大的项目中,我使用了git am --directory=dir/in/project /path/to/patch-dir/*

这感觉比滤网树枝要干净得多,也不那么可怕。当然,它可能并不适用于所有情况。


该函数将远程repo克隆到本地repo目录,合并所有提交后保存,git log将显示原始提交和正确路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function git-add-repo
{
    repo="$1"
    dir="$(echo"$2" | sed 's/\/$//')"
    path="$(pwd)"

    tmp="$(mktemp -d)"
    remote="$(echo"$tmp" | sed 's/\///g'| sed 's/\./_/g')"

    git clone"$repo""$tmp"
    cd"$tmp"

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

    cd"$path"
    git remote add -f"$remote""file://$tmp/.git"
    git pull"$remote/master"
    git merge --allow-unrelated-histories -m"Merge repo $repo into master" --edit"$remote/master"
    git remote remove"$remote"
    rm -rf"$tmp"
}

如何使用:

1
2
cd current/package
git-add-repo https://github.com/example/example dir/to/save

如果做一些小小的改变,你甚至可以把合并后的回购文件/dirs移动到不同的路径,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
repo="https://github.com/example/example"
path="$(pwd)"

tmp="$(mktemp -d)"
remote="$(echo"$tmp" | sed 's/\///g' | sed 's/\./_/g')"

git clone"$repo""$tmp"
cd"$tmp"

GIT_ADD_STORED=""

function git-mv-store
{
    from="$(echo"$1" | sed 's/\./\\./')"
    to="$(echo"$2" | sed 's/\./\\./')"

    GIT_ADD_STORED+='s,\t'"$from"',\t'"$to"',;'
}

# NOTICE! This paths used for example! Use yours instead!
git-mv-store 'public/index.php' 'public/admin.php'
git-mv-store 'public/data' 'public/x/_data'
git-mv-store 'public/.htaccess' '.htaccess'
git-mv-store 'core/config' 'config/config'
git-mv-store 'core/defines.php' 'defines/defines.php'
git-mv-store 'README.md' 'doc/README.md'
git-mv-store '.gitignore' 'unneeded/.gitignore'

git filter-branch --index-filter '
    git ls-files -s |
    sed"'"$GIT_ADD_STORED"'" |
    GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &amp;&amp;
    mv"$GIT_INDEX_FILE.new""$GIT_INDEX_FILE"
' HEAD

GIT_ADD_STORED=""

cd"$path"
git remote add -f"$remote""file://$tmp/.git"
git pull"$remote/master"
git merge --allow-unrelated-histories -m"Merge repo $repo into master" --edit"$remote/master"
git remote remove"$remote"
rm -rf"$tmp"

通知路径通过sed替换,因此请确保合并后它在正确的路径中移动。--allow-unrelated-histories参数只存在于git >= 2.9之后。


我稍微手动地合并项目,这允许我避免处理合并冲突。

首先,按照您希望的方式从其他项目复制文件。

1
2
cp -R myotherproject newdirectory
git add newdirectory

历史上的下一站

1
git fetch path_or_url_to_other_repo

告诉git在最后获取的东西的历史中合并

1
echo 'FETCH_HEAD' > .git/MERGE_HEAD

现在按照您通常的方式提交

1
git commit