编辑:对于一种基于日期的方法,它使这变得非常容易,但假设两个存储库之一将“控制”来自另一个存储库的提交,请参阅jthill's answer。您最终会得到与“项目”历史完全匹配的提交历史,可能会压缩一些“测试”历史。如果您需要为 both 组历史记录添加前缀,或者想要将它们交错(例如,需要对同一个“项目”提交进行两个不同的“测试”更新),则下面的答案更合适。
phd's answer 很好,但如果我自己这样做并且想让它真正整洁干净,我会使用不同的方法。
如果两个存储库的树不重叠,当然可以做到这一点——通过绕过通常的 Git 机制,直接使用底层的git read-tree 命令,您可以自动执行它。 (这就是 VonC's recent comment 拒绝我声称 Git 和 Mercurial 非常相似的说法是正确的:如果您绕过顶级 Git 命令,您会得到一些在 Mercurial 中几乎无法轻易获得的东西。)
就像在phd's answer 中一样,您可以通过git fetch 组合两个存储库提交数据库来启动此过程。 (您可以在第三个存储库中执行此操作,我建议这样做,因为如果您决定要调整某些参数,或者通过将存储库 A 添加到存储库 B 或将存储库 B 添加到repo A.) 但在那之后,一切都不同了。
您现在有两个不相交的提交 DAG:
D--...--K
/ \
A--B--C M--N <-- repoA/master
\ /
E--...--L
O--P--Q--...--Z <-- repoB/master
(如果 repoA 和 repoB 都有多个分支提示,请绘制更合适的提交简化图。)
您的下一步是使用git rev-list --topo-order --reverse 和您喜欢的任何其他排序选项枚举两个不相交的 DAG 中的每一个中的所有提交。何时以及是否需要 --topo-order 取决于拓扑结构和其他排序信息,但通常您希望在其任何子项之前列出父提交。
鉴于这两个提交哈希 ID 的线性化列表,您现在遇到了困难的部分:构建您希望提交的新组合树的图。每个 new 提交都将通过组合两个旧图表中的每一个中的一个提交来进行。如果其中一张图很复杂(如上面的 repoA),带有分支和合并,而一张不是(如上面的 repoB),这可能特别棘手。
我为此做了自己的设置,其中有一个非常简单的图表:
A--B <-- A/master
O--P <-- B/master
在我的简化设置中,我想让我的新主人的第一次提交是提交C,它结合了A 和O 的树:
C <-- master
然后,作为我对master 的第二次提交,我想组合A 和P(不是A 和O,也不是B 和O) ,作为我的最后一次提交,B 和 P 的组合,所以我最终得到:
C--D--E <-- master
with:
C = A+O
D = A+P
E = B+P
所以,这里我们在一个新的空存储库中,除了我们在项目 A 和 B 中读取:
$ git log --all --graph --decorate --format='%h%d %s' --name-status | sed '/^[| ] $/d'
* 7b9921a (B/master) commit-P
| A B/another
* 51955b1 commit O
A B/start
* 69597d3 (A/master) commit-B
| A A/new
* ff40069 commit-A
A A/file
(我不小心没有对提交 O 进行连字符,而是对所有其他文件进行了连字符。在这种情况下,sed 是为了删除一些对阅读没有帮助的空行。)
$ git status
On branch master
No commits yet
nothing to commit (create/copy files and use "git add" to track)
现在我们构建新的提交,一次一个,使用git read-tree 填充索引以进行提交。我们从一个空索引开始(我们现在有):
$ git status
On branch master
No commits yet
nothing to commit (create/copy files and use "git add" to track)
我们希望我们的第一个提交结合A 和O,所以现在让我们将这两个提交读入索引。如果我们必须为 A 中的树添加前缀,我们可以在这里这样做:
$ git read-tree --prefix= ff40069
$ git ls-files --stage
100644 7a1c6130c652b6ea92f4d19183693727e32c9ac4 0 A/file
$ git read-tree --prefix= 51955b1
$ git ls-files --stage
100644 7a1c6130c652b6ea92f4d19183693727e32c9ac4 0 A/file
100644 f6284744575ecfc520293b33122d4a99548045e4 0 B/start
我们现在可以进行我们需要的提交了:
$ git commit -m combine-A-and-O
[master (root-commit) 7c629d8] combine-A-and-O
2 files changed, 2 insertions(+)
create mode 100644 A/file
create mode 100644 B/start
现在我们需要进行下一次提交,这意味着我们需要在索引中建立正确的树。为此,我们首先必须将其清理干净;否则下一个 git read-tree --prefix 将失败并抱怨文件重叠和 Cannot bind. 所以现在我们清空索引,然后读取提交 A 和 P:
$ git read-tree --empty
$ git read-tree --prefix= ff40069
$ git read-tree --prefix= 7b9921a
如果您愿意,可以再次使用git ls-file --stage 检查结果:
$ git ls-files --stage
100644 7a1c6130c652b6ea92f4d19183693727e32c9ac4 0 A/file
100644 d7941926464291df213061d48784da98f8602d6c 0 B/another
100644 f6284744575ecfc520293b33122d4a99548045e4 0 B/start
无论如何,它们现在可以作为新的提交提交:
$ git commit -m 'combine A and P'
[master eb8fa3c] combine A and P
1 file changed, 1 insertion(+)
create mode 100644 B/another
(您现在可以看到我如何以不一致的连字符结尾:-))。最后,我们通过清空索引、读取两个期望的提交 (B+P) 并提交结果来重复该过程:
$ git read-tree --empty
$ git read-tree --prefix= A/master
$ git read-tree --prefix= B/master
$ git ls-files --stage
100644 7a1c6130c652b6ea92f4d19183693727e32c9ac4 0 A/file
100644 8e0c97794a6e80c2d371f9bd37174b836351f6b4 0 A/new
100644 d7941926464291df213061d48784da98f8602d6c 0 B/another
100644 f6284744575ecfc520293b33122d4a99548045e4 0 B/start
$ git commit -m 'combine B and P'
[master fad84f8] combine B and P
1 file changed, 1 insertion(+)
create mode 100644 A/new
(我在这里使用符号名称来获取最后两个提交,但是来自 git rev-list 的哈希 ID 当然可以很好地工作。)我们现在可以看到三个提交,都在 master:
$ git log --decorate --oneline --graph
* fad84f8 (HEAD -> master) combine B and P
* eb8fa3c combine A and P
* 7c629d8 combine-A-and-O
现在可以安全地删除 A/master 和 B/master 引用(以及两个遥控器)。有一个特点:因为我们直接在索引中完成了所有工作,而没有打扰工作树,工作树仍然是完全空的:
$ ls
$ git status -s
D A/file
D A/new
D B/another
D B/start
最后要解决这个问题,我们应该运行git checkout HEAD -- .:
$ git checkout HEAD -- .
$ git status -s
$ git status
On branch master
nothing to commit, working tree clean
如何编写自己的自动化脚本
在实践中,您可能希望使用git write-tree 和git commit-tree 而不是git commit 来进行新的提交。您将编写一个小脚本(使用您喜欢的任何语言)来运行 git rev-list 以收集要组合的提交的哈希 ID。脚本必须检查这些提交——例如,通过查看作者身份和日期,或文件内容,或其他任何东西——来决定如何交织提交。然后,在决定了交织以及要提供哪些分支和合并结构之后,脚本可以开始重复执行这些步骤的过程:
- 清空索引。
- 从 repo-A 的子图中的提交中提取树,使用任何合适的
--prefix 选项 - 在您的情况下,这是 --prefix=,即空字符串,但在其他情况下它将是带有尾部斜杠的目录名称)。
- 从 repo-B 的子图中的提交中提取树,并使用另一个适当的
--prefix,以便 A 和 B 的条目之间没有冲突。
- 使用
git write-tree 写入树。它的输出是下一步的树哈希 ID。
- 使用
git commit-tree 和适当的-p 参数来设置新提交的父级。为它提供适当的(组合或其他)提交消息文本。使用环境变量GIT_AUTHOR_NAME、GIT_AUTHOR_EMAIL、GIT_AUTHOR_DATE、GIT_COMMITTER_NAME、GIT_COMMITTER_EMAIL 和GIT_COMMITTER_DATE 来控制作者和提交者的名称和日期。 git commit-tree 的输出是哈希 ID,它是一些后续提交的父级。
当整个事情完成后,为任何特定分支或一组分支所做的 last 提交是进入这些分支的哈希 ID,因此您现在可以运行:
git branch <name> <hash>
对于每个这样的哈希 ID。