如何将Git仓库合并为线性历史记录?

16

我有两个Git存储库R1R2,其中包含产品开发的两个时期的提交记录:1995-1997和1999-2013年。 (我通过将现有的RCS和CVS存储库转换为Git来创建它们。)

R1:
A---B---C---D

R2:
K---L---M---N

我该如何将这两个代码库合并为一个,以便于获得项目线性历史的准确视图?

A---B---C---D---K---L---M---N

请注意,在R1R2之间有文件被添加、删除和重命名。

我尝试创建一个空的代码仓库,然后将它们的内容合并到其中。

git remote add R1 /vol/R1.git
git fetch R1

git remote add R2 /vol/R2.git
git fetch R2

git merge --strategy=recursive --strategy-option=theirs R1
git merge --strategy=recursive --strategy-option=theirs R2

然而,这样最终会留下那些在修订版K中没有出现, 但在修订版D中的文件。 我可以创建一个合成提交来删除合并之间的额外文件,但这似乎不够优雅。 此外,通过这种方法得到的最终结果包含了实际上没有发生的合并。


这听起来像是一次性的问题,是吗?如果是这样,我认为你可以选择合成提交并忘记它的不优雅之处。(从某种意义上说,所有导入的提交已经是合成的了,所以我认为这没那么糟糕)。 - Tamás Szelei
实际上,在我描述的情况下有超过两个仓库,而且我可以看到这个问题在未来可能再次发生(我正在尝试将多样化项目的历史记录重新创建为一个git仓库)。 - Diomidis Spinellis
这是生成的代码库:https://github.com/dspinellis/unix-history-repo - Diomidis Spinellis
4个回答

15

使用 git filter-branch

直接使用来自 git-filter-branch 手册中的技巧:

首先,创建一个新的仓库,并将两个原始仓库作为远程仓库添加,就像之前一样。 我假设它们都使用分支名称“master”。

git init repo
cd repo
git remote add R1 /vol/R1.git
git fetch R1
git remote add R2 /vol/R2.git
git fetch R2

接下来,将“master”(当前分支)指向R2的“master”的末端。

git reset --hard R2/master

现在我们可以将R1“主”分支的历史记录嫁接到开头。

git filter-branch --parent-filter 'sed "s_^\$_-p R1/master_"' HEAD

换句话说,我们在DK之间插入一个虚假的父提交,使得新的历史记录看起来像:

A---B---C---D---K---L---M---N

仅有的变化是 KN 的父指针发生了改变,因此所有的 SHA-1 标识符都会改变。提交消息、作者、时间戳等保持不变。

使用 filter-branch 将多个存储库合并在一起

如果你有超过两个要处理的存储库,比如从最老的 R1 到最新的 R5,只需按照时间顺序重复执行 git resetgit filter-branch 命令即可。

PARENT_REPO=R1
for CHILD_REPO in R2 R3 R4 R5; do
    git reset --hard $CHILD_REPO/master
    git filter-branch --parent-filter 'sed "s_^\$_-p '$PARENT_REPO/master'"' HEAD
    PARENT_REPO=$CHILD_REPO
done

使用移植点

作为使用--parent-filter选项对filter-branch进行替代的一种方法,您可以使用移植点机制。

考虑将R2/master附加为(即比)R1/master更新后的子级的原始情况。与之前一样,首先将当前分支(master)指向R2/master的末端。

git reset --hard R2/master

现在,不要运行filter-branch命令,而是在.git/info/grafts中创建一个“graft”(虚假的父级),将R2/master (K)的“根”(最早的)提交R1/master (D)的最新提交链接起来。(如果R2/master有多个根,则以下内容仅链接其中一个。)

ROOT_OF_R2=$(git rev-list R2/master | tail -n 1)
TIP_OF_R1=$(git rev-parse R1/master)
echo $ROOT_OF_R2 $TIP_OF_R1 >> .git/info/grafts

此时,您可以查看您的历史记录(例如通过 gitk)来确定是否正确。如果是,则可以通过以下方式将更改永久保存:

git filter-branch

最后,您可以通过删除移植文件来清理所有内容。

rm .git/info/grafts

使用嫁接方法比使用 --parent-filter 更费力,但它有一个优点,即可以使用单个 filter-branch 将多个历史记录进行嫁接。 (使用 --parent-filter 也可以实现相同的功能,但脚本会变得非常丑陋。) 它还可以让您在更改永久生效之前查看更改的效果; 如果看起来不好,只需删除嫁接文件即可中止。

使用嫁接将多个存储库合并在一起

要使用嫁接方法将 R1 (最旧的) 到 R5 (最新的) 进行嫁接,只需在嫁接文件中添加多行即可。(运行 echo 命令的顺序无关紧要。)

git reset --hard R5/master

PARENT_REPO=R1
for CHILD_REPO in R2 R3 R4 R5; do
    ROOT_OF_CHILD=$(git rev-list $CHILD_REPO/master | tail -n 1)
    TIP_OF_PARENT=$(git rev-parse $PARENT_REPO/master)
    echo "$ROOT_OF_CHILD" "$TIP_OF_PARENT" >> .git/info/grafts
    PARENT_REPO=$CHILD_REPO
done

Git rebase怎么样?

有些人建议使用git rebase R1/master代替上面的git filter-branch命令。这将会取空提交和K之间的差异,然后尝试将其应用到D上,结果如下:

A---B---C---D---K'---L'---M'---N'

如果在 DK 之间删除了文件,这很可能会导致合并冲突,并且甚至可能会在 K' 中创建虚假文件。唯一能够奏效的情况是 DK 的树是相同的。

(另一个轻微的区别是,git rebase 通过 N' 改变了 K' 的提交者信息,而 git filter-branch 没有改变。)


最后一步可能只是 git rebase R1/master - vonbrand
@vonbrand,我更新了我的答案,解释了为什么那样做不起作用。 - Mark Lodato
好的,我刚刚测试了一下,如果旧仓库的开发真的停止了,那么即使新仓库删除了文件,在重新设置基础时Git使用的默认三路递归合并算法也应该能够干净地应用补丁,而无需手动解决冲突。不会有“虚假”的文件。所以我认为重新设置基础的解决方案并不是一个问题。如果出现冲突,你甚至可以通过执行git rm .命令来替换旧历史的工作目录树,然后再检出新仓库第一个提交的目录。 - user456814
@DiomidisSpinellis 对于你的14000次提交合并,我有一个问题:如果repo1有file1,repo2有file2,在最终合并的仓库中,你是否同时有file1和file2?我问这个问题是因为filter-branch不像rebase那样重写历史记录,所以你是否有真正的组合历史记录? - Flavius
最终,我手动创建了一个 git-fast-import 流,其中文件在添加下一个版本时同时存在于隐藏目录中。请参见 https://www2.dmst.aueb.gr/dds/pubs/jrnl/2016-EMPSE-unix-history/html/unix-history.html - Diomidis Spinellis
显示剩余3条评论

2
原帖作者表示:
R1:
A---B---C---D

R2:
K---L---M---N

How can I combine the two repositories into a single one that contains an accurate view of the project's linear history?

How can I combine the two repositories into a single one that contains an accurate view of the project's linear history?

A---B---C---D---K---L---M---N

Note that between R1 and R2 files have been added, deleted, and renamed.

我知道,如果新的存储库 K 的第一个提交与旧的存储库 D 的最后一次提交完全相同或略有修改,那么你可以简单地将 R1 的历史记录获取到 R2 中,然后将 R2 的提交图重新基于 R1 的图进行变基:

# From R2
git fetch R1
git checkout master
git rebase --onto R1/master --root

非线性历史(当您有合并提交时)

这是假设R2的图形是线性的。如果有合并提交,则可以尝试通过指定要保留合并提交来执行相同的操作。

git rebase --preserve-merges --onto R1/master --root

然而,如果您曾经在任何这些合并中解决过冲突,那么在进行变基时,您可能需要重新解决它们,这可能会很麻烦。

如何合并两个完全不同的历史记录?

原帖作者说:

请注意,在 R1R2 之间添加、删除和重命名了文件。

如我上面所指出的,如果新存储库的第一个提交 K 与旧存储库的最后一个提交 D 相同或只有轻微的差异,则简单的变基应该可以工作。如果实际上 KD 明显不同,那么相同的变基是否能够干净地工作我就不确定了。我想,在最坏的情况下,在变基期间的第一次应用程序期间,您可能需要解决许多冲突。

文档


注意事项,如果在rebase的第一个补丁中出现痛苦的冲突,请添加如何优先使用较新repo中的冲突解决方案。 - user456814

1
这是我所做的,它起作用了:

git init
git remote add R1 /vol/R1.git
git fetch R1
git remote add R2 /vol/R2.git
git fetch R2
git co -B master R2/master
git rebase R1/master
git push -f

0
你所需要的全部就是: git rebase,然后加上你要rebase的分支名称。
简而言之,rebase会将该分支的所有提交记录回退,并将它们与你正在rebase的分支的提交记录合并。
根据两个分支之间的差异程度,你可能会遇到冲突。但使用其他任何方法都无法避免相同的冲突。
祝你好运!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接