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

sta*_*tti 1517 git merge repository git-subtree

请考虑以下情形:

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

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

And*_*erj 1753

如果要合并project-aproject-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
Run Code Online (Sandbox Code Playgroud)

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

这种方法对我来说效果很好,它更短,在我看来更清洁.

注意:--allow-unrelated-histories参数仅在git> = 2.9时存在.请参阅Git-git merge Documentation/--allow-unrelated-histories

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

  • 谢谢.为我工作.我需要将合并的目录移动到子文件夹中,所以按照上面的步骤后我只使用`git mv source-dir/dest/new-source-dir` (28认同)
  • `--allow-unrelated-histories`在[git 2.9](https://github.com/git/git/blob/master/Documentation/RelNotes/2.9.0.txt)中介绍.在早期版本中,它是默认行为. (17认同)
  • `git merge`步骤在这里失败,因为`致命:拒绝合并不相关的历史记录'; `--allow-unrelated-histories`修复了[docs](https://git-scm.com/docs/git-mergea)中的解释. (12认同)
  • 这为我做了这件事.在.gitignore文件中只有一次冲突,就像魅力第一次一样!它完美地保留了提交历史.除了简单之外,其他方法的最大优点是,不需要持续引用合并的仓库.但要注意的一件事 - 如果你是像我这样的iOS开发人员 - 要非常小心地将目标repo的项目文件放入工作区. (7认同)
  • 更短:`git fetch/path/to/project-a master; git merge --allow-unrelated-histories FETCH_HEAD`. (7认同)
  • @Sid或其他任何人,您知道如何一次将project-a放入project-b的子目录中吗?如果两个项目的根目录中都有很多文件,那么筛选所有文件并在必要时移动是很痛苦的 (3认同)
  • 合并后,你也可以运行`git rebase -i [project-b]的最后一次提交`来使日志看起来更好. (3认同)
  • 请注意,“git filter-repo”不适用于 Mojave 10.14.6 上的标准 Apple 开发工具。该平台上的`git`版本是2.21.1 (3认同)
  • 它似乎需要一个工作副本:`致命:这个操作必须在工作树中运行`,我想合并两个裸git存储库. (2认同)
  • @LiuYan刘研:讽刺这有点晚了但你可以通过检查其中一个项目创建一个工作副本,然后按照描述进行处理. (2认同)
  • 请注意,这不会合并子模块. (2认同)
  • @sg一种间接方法是将project-a中的所有这些文件移动到project-a中的子目录中(这样,project-a的顶层只有一个目录),然后执行上述过程。 (2认同)
  • WTF,这是对主要问题/标题的出色回答。我想被接受和投票最多的受访者只是在正文中“在子目录中”选择了细节,然后就跑掉了。对我来说,“合并两个Git存储库”意味着这个答案的确切含义,到目前为止,它似乎对我来说还不错。+1 (2认同)
  • 但是,除了" - 允许 - 不相关的历史"之外 (2认同)
  • 这是我见过的最简洁的工作流程配方.谢谢! (2认同)
  • 这非常有效.给我做类似事情的人快速提示:在做上述事情之前,我也遵循[这些说明](http://www.janosgyerik.com/rewrite-git-history-to-prefix-a-range-of- commit-messages /)在所有`project-a`的提交消息中添加类似`Project A:`的前缀,以便从`project-b`中清除消息的上下文. (2认同)
  • 获取时,您还应该考虑获取标签,否则它们会丢失:`git fetch project-a --tags` (2认同)
  • 为我节省了一天。我没有使用第 4 步,而是使用 `git rebase project-a/master` 来获得线性历史记录。 (2认同)
  • 很好的答案!如果您还添加了一个关于如何将项目 A 作为项目 B 中的子文件夹的版本,那就太好了。项目 A 在根目录中有很多文件。 (2认同)
  • 我认为 @Norfeldt 提出了与原始问题完全相同的问题:“我现在想将 A 添加为 B 的 **子目录**。” (2认同)
  • 有没有办法将前缀添加到“project-a”的标签中?因为项目 a 和项目 b 有相似的标签,现在我得到了这个`![已拒绝] 0.1.0 -> 0.1.0 (会破坏现有标签)` 运行 `git fetch project-a --tags` 时 (2认同)
  • @jstadler我不知道为什么你必须将`--tags`传递给`git fetch`,因为这是默认的。我至少可以追溯到[2007年](https://github.com/git/git/commit/02f571c73bfdf1d950848ed24229caea438f1f72) (`git-1.5.0`)。@Shinebayar `git filter-repo --tag-rename :project-a-` 有关示例,请参阅[this](https://gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731)。 (2认同)

Jak*_*ski 602

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

子模块

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

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

子树合并

您可以使用子树合并策略将存储库A合并到项目B的子目录中.Markus Prinz 在Subtree Merging和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
Run Code Online (Sandbox Code Playgroud)

(--allow-unrelated-historiesGit> = 2.9.0需要选项.)

或者你可以使用apenwarr(Avery Pennarun)的git子树工具(GitHub上的存储库),例如在他的博客文章中公布了一个新的替代Git子模块:git子树.


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

  • **这是不完整的**.是的,你得到一大堆提交,但他们不再引用正确的路径.除了一个合并之外,`git log dir-B/somefile`不会显示任何内容.参见[Greg Hewgill的回答](http://stackoverflow.com/a/1425914/623519)引用了这个重要问题. (54认同)
  • 如果您只是想将两个存储库粘合在一起,则子模块和子树合并是错误的工具,因为它们不会保留所有文件历史记录(正如其他评论者所指出的那样).请参阅http://stackoverflow.com/questions/13040958/merge-two-git-repositories-without-breaking-file-history/. (11认同)
  • 这个答案可能会让人感到困惑,因为它在问题A中有B作为合并子树.复制和粘贴的结果? (4认同)
  • 重要提示:git pull --no-rebase -s子树Bproject master如果你不这样做,并且你自动将pull设置为rebase,你将最终得到"无法解析对象".请参见http://osdir.com/ml/git/2009-07/msg01576.html (2认同)

Sim*_*tsa 391

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

git subtree add --prefix=rails git://github.com/rails/rails.git master
Run Code Online (Sandbox Code Playgroud)

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

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

<rev>SHA-1提交哈希在哪里.你仍然可以看到历史,指责一些变化.

git log <rev>
git blame <rev> -- README.md
Run Code Online (Sandbox Code Playgroud)

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

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

# then continue from original tree
git log <rev> -- README.md
Run Code Online (Sandbox Code Playgroud)

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

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

  • 不要停止阅读......下面有更完整的答案. (830认同)
  • 或阅读Eric Lee的"将两个Git存储库合并到一个存储库而不丢失文件历史"http://saintgimp.org/2013/01/22/merging-two-git-repositories-into-one-repository-without-losing-file -历史/ (5认同)
  • 正如其他人所说,"git subtree"可能不会按你的想法行事!有关更完整的解决方案,请参见[此处](http://stackoverflow.com/questions/1425892/how-do-you-merge-two-git-repositories/21495718#21495718). (3认同)
  • 以下是有关如何安装Git SubTree的说明(截至2013年6月):http://stackoverflow.com/a/11613541/694469(我将`git co v1.7.11.3`替换为`... v1.8.3 `). (2认同)

Gre*_*ill 193

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

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

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

git remote add projb [wherever]
git pull projb
Run Code Online (Sandbox Code Playgroud)

git pull会做一个git fetch接着一个git merge.如果您要提取的存储库还没有projb/目录,则不应存在冲突.

进一步的搜索表明,类似的东西做合并gitkgit.Junio C Hamano在这里写到:http://www.mail-archive.com/git@vger.kernel.org/msg03395.html

  • 如果它解释了如何使用filter-branch来实现所需的结果,那么这个答案会很棒 (30认同)
  • 我在这里找到了如何使用filter-branch:http://stackoverflow.com/questions/4042816/how-can-i-rewrite-history-so-that-all-files-are-in-a-subdirectory (14认同)
  • 我想知道如何使用`git filter-branch`来实现这一目标.在手册页中,它说的是相反的方式:使subdir /成为根,但不是相反. (7认同)
  • 子树合并将是更好的解决方案,并且不需要重写包含项目的历史记录 (4认同)
  • 有关Greg大纲的实施,请参阅[此答案](http://stackoverflow.com/questions/1425892/how-do-you-merge-two-git-repositories/21495718#21495718). (3认同)

Pau*_*per 68

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

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

git log projectA
Run Code Online (Sandbox Code Playgroud)

列出一个提交:合并.合并项目的提交是针对不同的路径,因此它们不会显示.

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
Run Code Online (Sandbox Code Playgroud)

注意:这会重写历史记录,因此如果您打算继续使用此回购A,您可能希望首先克隆(复制)它的一次性副本.

(2)然后在B中,运行

git pull path/to/A
Run Code Online (Sandbox Code Playgroud)

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


在我的情况下,我想要两个子目录,projectAprojectB.在那种情况下,我也做了步骤(1)到B.

  • 我补充说,\ t在OS X上不起作用,你必须输入<tab> (6认同)
  • 如果您想知道,要在osx中​​插入<tab>,您需要`Ctrl-V <tab>` (4认同)
  • `"$ GIT_INDEX_FILE"`必须引用(两次),否则如果路径包含空格,则方法将失败. (2认同)

Sma*_*mar 47

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

git fetch git://repository.url/repo.git master:branch_name
Run Code Online (Sandbox Code Playgroud)

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

git merge --allow-unrelated-histories branch_name
Run Code Online (Sandbox Code Playgroud)

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

在此之后,可能会发生冲突.您可以使用例如解决它们git mergetool.kdiff3可以单独使用键盘,因此只需几分钟读取代码即可获得5个冲突文件.

记得完成合并:

git commit
Run Code Online (Sandbox Code Playgroud)

  • 我喜欢这个解决方案的简单性,它似乎是我正在寻找的,但它基本上不等于“git pull --allow-unrelated-histories”吗? (2认同)

Cal*_*had 23

我在使用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
Run Code Online (Sandbox Code Playgroud)

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

git rebase --continue
Run Code Online (Sandbox Code Playgroud)

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


Rad*_*ugh 21

在我的情况下,我有一个my-plugin存储库和一个main-project存储库,我想假装my-plugin总是在plugins子目录中开发main-project.

基本上,我重写了my-plugin存储库的历史记录,以便所有开发都发生在plugins/my-plugin子目录中.然后,我将历史的发展历史添加my-pluginmain-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|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
Run Code Online (Sandbox Code Playgroud)

长版

首先,创建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
Run Code Online (Sandbox Code Playgroud)

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

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

至于实际的命令,我没有太多的运气bash去做我想要的事情,所以我用它zsh -czsh执行一个命令.首先我设置extended_glob选项,这是启用命令^(...)语法mvglob_dots选项,以及允许我.gitignore用glob(^(...))选择dotfiles(例如)的选项.

接下来,我用mkdir -p命令同时创建plugins,并plugins/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:

$ git remote add --fetch my-plugin $PATH_TO_MY_PLUGIN_REPOSITORY
Run Code Online (Sandbox Code Playgroud)

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

$ git log --color --graph --decorate --all
Run Code Online (Sandbox Code Playgroud)

要合并它们,请使用:

$ git merge my-plugin/master --allow-unrelated-histories
Run Code Online (Sandbox Code Playgroud)

请注意,在2.9.0之前的Git中,该--allow-unrelated-histories选项不存在.如果您使用的是其中一个版本,请忽略该选项:--allow-unrelated-histories2.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
Run Code Online (Sandbox Code Playgroud)

您现在可以安全地删除my-plugin其更改历史记录的存储库副本.在我的情况下,我还在my-plugin合并完成并推送后向真实存储库添加了弃用通知.


经测试在Mac OS X埃尔卡皮坦与git --version 2.9.0zsh --version 5.2.你的旅费可能会改变.

参考文献:

  • @MarceloFilho检查`man git-merge`._By默认情况下,git merge命令拒绝合并不共享共同祖先的历史记录.在合并独立开始生命的两个项目的历史时,此选项可用于覆盖此安全性.由于这是一个非常罕见的场合,因此默认情况下不存在任何默认启用此配置变量的配置变量. (3认同)
  • @MarceloFilho这是在2.9.0中添加的,但是在较旧的版本中,您不必传递该选项(它将起作用)。https://github.com/git/git/blob/a28705da929ad746abcb34270947f738549d3246/Documentation/RelNotes/2.9.0.txt#L11 (2认同)

Ria*_*ian 8

我一直试图做同样的事情好几天,我正在使用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
Run Code Online (Sandbox Code Playgroud)

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

git log --follow A/<file>
Run Code Online (Sandbox Code Playgroud)

这是帮助我这样做的帖子:

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


小智 7

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

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

git remote add B ../B # Add repo B as a new remote.
git fetch B
Run Code Online (Sandbox Code Playgroud)

现在我们在repo A中创建一个全新的分支(只有一个提交),我们称之为new_b_root.生成的提交将包含在repo B的主分支的第一次提交中提交但放在名为的子目录中的文件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))"
Run Code Online (Sandbox Code Playgroud)

说明:--orphancheckout命令的选项从A的主分支中检出文件,但不创建任何提交.我们可以选择任何提交,因为接下来我们清除了所有文件.然后,在没有提交yet(-n)的情况下,我们从B的主分支中挑选第一个提交.(cherry-pick保留了原始的提交消息,直接签出似乎没有.)然后我们创建子树,我们要放置来自repo B的所有文件.然后我们必须移动所有文件樱桃挑选子树.在上面的示例中,只有一个README要移动的文件.然后我们提交B-repo根提交,同时,我们还保留原始提交的时间戳.

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

git checkout -b b B/master
git rebase -s recursive -Xsubtree=path/to/b-files/ new_b_root
Run Code Online (Sandbox Code Playgroud)

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

git checkout master
git merge --allow-unrelated-histories --no-commit b
git commit -m 'Merge repo B into repo A.'
Run Code Online (Sandbox Code Playgroud)

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

git remote remove B
git branch -D new_b_root b
Run Code Online (Sandbox Code Playgroud)

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

在此输入图像描述


jet*_*ero 6

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

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 ] && git commit -m "merge made by $me"
        cd ..

    x=$(( x + 1 ))
done
Run Code Online (Sandbox Code Playgroud)

  • 这正是我想要的.谢谢!但是,我必须将第22行更改为:if [[$ dirname =〜^.*\.git $]]; then` (2认同)
  • ^.*blarg $浪费贪婪RE.最好说.blarg $并跳过前锚. (2认同)

Eri*_*Lee 6

如果您只是想将两个存储库粘合在一起,则子模块和子树合并是错误的工具,因为它们不会保留所有文件历史记录(正如人们在其他答案中所指出的那样).请在此处查看此答案,以获得简单而正确的方法.

  • 您的解决方案仅适用于新存储库,但是如何将存储库合并到另一个有文件冲突的存储库中? (2认同)

eit*_*tch 6

我已经在Stack OverFlow上收集了很多信息,并设法将一个脚本放在一起,为我解决了问题.

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

标签和其他分支被忽略 - 这可能不是你想要的.

该脚本甚至可以处理功能分支和标记 - 在新项目中重命名它们,以便您知道它们来自何处.

#!/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
##   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=$'\n'

# 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>&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>&1
  else
    echo -e "INFO:   Committing ${sub_project}..."
    echo -e "INFO:   Committing ${sub_project}..." >>${LOG_FILE} 2>&1
    if ! git commit -m "[Project] Merged branch '$1' of ${sub_project}" >>${LOG_FILE} 2>&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>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&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>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&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>&1
  echo -e "----------------------------------------------------"
  echo -e "----------------------------------------------------" >>${LOG_FILE} 2>&1

  # Fetch the project
  echo -e "INFO:   Fetching ${sub_project}..."
  echo -e "INFO:   Fetching ${sub_project}..." >>${LOG_FILE} 2>&1
  git remote add "${sub_project}" "${url}"
  if ! git fetch --tags --quiet ${sub_project} >>${LOG_FILE} 2>&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>&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>&1

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

    # Merge the project
    echo -e "INFO:   Merging ${sub_project}..."
    echo -e "INFO:   Merging ${sub_project}..." >>${LOG_FILE} 2>&1
    if ! git merge --allow-unrelated-histories --no-commit "remotes/${sub_project}/${branch_name}" >>${LOG_FILE} 2>&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>&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>&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})


  # checkout master of sub probject
  if ! git checkout "${sub_project}/master" >>${LOG_FILE} 2>&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>&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>&1
    if ! git tag "${tag_new_name}" "${tag_ref}" >>${LOG_FILE} 2>&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>&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>&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>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&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>&1
  if ! git merge --allow-unrelated-histories --no-commit "${sub_project}/master" >>${LOG_FILE} 2>&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>&1
echo

exit 0
Run Code Online (Sandbox Code Playgroud)

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

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

git@github.com:eitchnet/ch.eitchnet.parent.git
git@github.com:eitchnet/ch.eitchnet.utils.git
git@github.com:eitchnet/ch.eitchnet.privilege.git
Run Code Online (Sandbox Code Playgroud)

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

./mergeGitRepositories.sh eitchnet_test eitchnet.lst
Run Code Online (Sandbox Code Playgroud)

脚本本身有很多注释,可以解释它的作用.


Tur*_*adg 5

与@Smar 类似,但使用文件系统路径,在 PRIMARY 和 SECONDARY 中设置:

PRIMARY=~/Code/project1
SECONDARY=~/Code/project2
cd $PRIMARY
git remote add test $SECONDARY && git fetch test
git merge test/master
Run Code Online (Sandbox Code Playgroud)

然后你手动合并。

(改编自Anar Manafov 的帖子


小智 5

我也遇到了类似的挑战,但就我而言,我们在软件库A中开发了一个版本的代码库,然后将其克隆到新的软件库中,用于产品的新版本。修复了存储库A中的一些错误之后,我们需要将更改FI集成到存储库B中。最终执行以下操作:

  1. 向指向仓库A的仓库B添加一个远程服务器(git remote add ...)
  2. 拉当前分支(我们未使用master进行错误修复)(git pull remoteForRepoA bugFixBranch)
  3. 推送合并到github

工作了请客:)


小智 5

合并 2 个仓库

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
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

435026 次

最近记录:

6 年,9 月 前