编辑:如果想要采用基于日期的方法来处理这个问题,这样做会相当简单,但是前提是两个仓库中的一个将“控制”另一个仓库的提交。详见jthill的回答。你最终得到的提交历史将完全匹配“项目”历史,可能会压缩一些“测试”历史。下面的答案更适合在需要为两个历史记录集添加前缀或者想要交错它们的情况下使用(例如需要两个不同的“测试”更新来针对同一个“项目”提交)。
phd的答案很好,但如果我自己做并想使其更加整洁和清晰,我会采用不同的方法。
如果两个仓库的树没有重叠,那么肯定可以实现这一点,并且通过绕过常规的Git机制,并直接使用底层的git read-tree
命令,你可以自动化这个过程。(这也是VonC最近的评论,拒绝了我关于Git和Mercurial非常相似的说法是正确的:如果你绕过顶级的Git命令,你会得到在Mercurial中很难得到的东西。)
就像phd的答案中一样,你将通过git fetch
合并两个仓库的提交数据库开始这个过程。(你可以在第三个仓库中进行此操作,我建议这么做,因为这样如果你决定调整某些参数,或者将仓库A添加到仓库B,或者将仓库B添加到仓库A,那么重新启动该过程会更容易。)但是,在此之后,一切都会发生变化。
现在你有了两个不相交的提交DAG:
D--...--K
/ \
A--B--C M--N <-- repoA/master
\ /
E--...--L
O--P--Q--...--Z <-- repoB/master
(如果 repoA 和 repoB 都有多个分支尖端,请绘制与其提交相关的任何简化图形。)
你需要做的下一步是枚举这两个不相交DAG中所有提交,可以使用
git rev-list --topo-order --reverse
命令和其他喜欢的排序选项。是否需要使用
--topo-order
取决于拓扑结构和其他排序信息,但通常你希望父提交在其子提交之前列出。
得到这两个线性化的提交哈希ID列表后,你现在需要完成困难的部分:构建你想要提交的新组合树的图形。每个新提交都将通过结合两个旧图中的一个提交来完成。如果其中一个图具有复杂的分支和合并(如上面的repoA),而另一个则没有(如上面的repoB),这可能会特别棘手。
我为此制定了自己的设置,其中我有一个非常简单的图形:
A
O
在我的简化设置中,我想要在新主分支上进行的第一个提交是将
A
和
O
的树合并为提交
C
:
C <
然后,作为我在master
分支上的第二次提交,我想要将A
和P
进行组合(不是A
和O
也不是B
和O
),并且作为我的最后一次提交,将B
和P
进行组合,以便最终得到:
C
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
$ git ls-files
100644 7a1c6130c652b6ea92f4d19183693727e32c9ac4 0 A/file
$ git read-tree
$ git ls-files
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
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。
- 使用适当的
-p
参数对git commit-tree
进行设置,以设置新提交的父项。 提供适当的(合并或其他)提交消息文本。 使用环境变量GIT_AUTHOR_NAME
、GIT_AUTHOR_EMAIL
、GIT_AUTHOR_DATE
、GIT_COMMITTER_NAME
、GIT_COMMITTER_EMAIL
和GIT_COMMITTER_DATE
来控制作者和提交者名称和日期。 git commit-tree
的输出是哈希ID,它是某个后续提交的父项。
整个过程完成后,任何特定分支或一组分支的最后一个提交都是进入这些分支的哈希ID,因此现在您可以运行:
git branch <name> <hash>
对于每个这样的哈希ID。