将上游分支合并到具有重写历史记录的分叉中



有什么好方法可以合并 HEAD 上具有相同文件/文件夹结构但历史记录不同的分叉存储库?不完全自动化的工作流程是可以接受的,因为它不会经常完成 - 但我希望有比复制所有文件和手动检查差异更好的方法:)

背景是我们必须从 10 年前的 TFS 存储库迁移到 Git。要求保留整个历史记录,但仅适用于主分支。从 TFS 迁移后 - 我们对 Git 存储库进行了一些清理,但它对于 Git 来说仍然太大了。

我们迁移了它,此分支仍用于当前的生产部署。在该生产分支中也进行了修复 - 因此在一段时间内无法放弃它,并且保留这些修复也很重要。

与此同时,我们正在一个单独的分支中进行重大重构,其中仍然使用许多当前的生产代码库,但另一方面 - 许多历史内容被删除或移动到不同的存储库。

我想做的是创建一个分支并重写历史记录(例如使用 BFG Repo-Cleaner),以清理所有已删除的项目/对象。

这个清理部分运行良好,但是我们还需要合并在当前生产分支上所做的更改的可能性(仅单向 - 从生产清理存储库)。我试图通过从旧存储库添加上游分支来做到这一点,但是将上游存储库合并到具有重写历史记录的存储库 - 使所有清理都毫无用处。它会重新添加所有已删除的对象。

有什么办法可以解决吗?也许这样的清理可以用某种完全不同的方式完成? 有很多类似的问题,但没有找到我需要的:)

更新- 在阅读评论并查看我的答案后,有一些事情可以澄清,一些调整可以使其更容易正确使用,以及一两个彻头彻尾的错误。 对此感到抱歉;随着文档的发展,最初的答案是"草稿"质量。 我将首先解决几个问题,但我确实建议您也查看下面编辑的答案。

上游配置- 每个存储库中分支之间的关系是此处发生的关键。 获取引用规范将管理这一点,只要它们设置正确,就不需要其他"上游"配置。

也就是说,我在下面所做的最大更改是将桥接存储库中清理的分支移动到它们自己的clean/*命名空间,以便将正确的引用获取到干净的存储库要简单得多

BFG 删除原始分支- 这是正确的,但是在配置桥存储库的origin获取 refspec 之后,后续fetch will recreate the original branches under theprod/*' 命名空间。

至于你的最后一条评论——我认为你之前的尝试只是成为原始答案的"粗略草案"问题引起的错误的受害者。 获得正确的结果是绝对可能的,我想作为一个完全熟悉这里的工具和技术的人,我会自动忽略"即时"更正,这将使它起作用。 但希望这次重写至少会让你更接近你想要做的事情......


您提到可能需要将生产存储库中的更改合并清理的存储库。 这不是一个糟糕的问题,但请注意,如果您需要让更改双向流动 - 即,如果您想使用清理后的存储库中的更改更新生产分支 - 这会使事情复杂化,并且可能有利于不同的方法。

此外,如果所有更改都从生产存储库上的单个分支流向干净存储库,则这是最简单的。 (是否在生产存储库中使用分支并不重要,但理想情况下,您希望将它们全部合并到单个分支中,这将成为干净存储库中单个分支的更改源。 如果没有,同样的原则可以适用,但执行起来更难。

请注意,任何方法都与将补丁从生产应用到清理的代码库的能力一样好。 在某种程度上,清理仅包括删除某些文件,这没有问题。 但是,如果存储库分歧很大,那么无论您尝试什么方法,应用更改时的冲突都将成为一个日益严重的问题。


对于单向流(生产存储库 ->已清理的存储库),可以保留一个同时具有"原始"和"已清理"历史记录的存储库。 这可以是生产存储库本身,也可以是专用的"桥接存储库"。 (它不能是已清理的存储库,因为它将包含您尝试从中删除的大型历史记录。

究竟如何从你所在的位置到达那个状态,取决于你所在位置的细节。 出于说明目的,如果您一开始就考虑到这种方法,它可能是这样的:

您在<prod-url>处有您的生产存储库。 您克隆它,此克隆将用于创建桥接存储库。

$ git clone `<prod-url>` bridge
$ cd bridge

您在bridge中运行 BFG,然后克隆它以创建真正的"干净"存储库。 然后(再次在bridge中)重新配置origin,以便其分支可以映射到bridge存储库中的prod命名空间。

$ git config remote.origin.fetch refs/heads/*:refs/heads/prod/*

现在,当您从源获取到桥存储库时,git 将尝试在prod/命名空间中推进一组分支,而不是更新远程跟踪引用。 但是您不希望将这些prod/*分支提取到干净的存储库中;解决此问题的最简单方法是将清理的分支移动到clean/命名空间,并重新配置干净存储库以仅提取clean/*分支。

bridge中,有几种方法可以移动分支。 如果不是很多,您可以手动完成

$ git checkout master
$ git checkout -b clean/master
$ git branch -D master

对于许多分支,您可以编写此脚本(也许使用git for-each-ref来启动操作)。 或者,您可能以某种方式滥用filter-branch备份引用机制。

无论如何,一旦分支被移动,转到干净的存储库并

$ git config remote.origin.fetch +refs/heads/clean/*:refs/remotes/origin/*

现在退后一步,与最后一个命令不同,当我在桥存储库中origin提供 fetch refspec 时,我省略了通常用于 fetch refspecs 的前导+;这意味着如果一个prod分支经历历史重写,fetch 会抱怨,你会知道你有一个潜在的头痛需要解决。 稍后会详细介绍。

所以接下来,在桥存储库中,你可以运行

$ git fetch origin

这将重新加载prod/命名空间下的原始分支。

现在您拥有两个原始分支(例如refs/heads/prod/master)和干净的树枝(例如refs/heads/clean/master)。 可以这样画

A' -- B' -- C' -- D' <--(clean/master)
A -- B -- C -- D <--(prod/master)

历史是无关的,你需要保持这种状态。 但是,您还希望"知道"clean/master分支是通过D提交prod/master的方式"最新的",以便于合并未来的更改。 一种方法是创建两个额外的分支 - 我们称它们为bridge-prodbridge-clean.

bridge-clean分支将指向我们从prod进行更改的最后一次提交。clean/分支本身可能会发生新的更改,但bridge-clean会记得单独清理prod会是什么样子。

$ git checkout clean/master
$ git branch bridge-clean

然后bridge-prod的工作是拥有与bridge-clean相同的内容,直到它收到来自prod/master的新更改 - 之后它将再次用作更新bridge-clean的参考。

因此,为了初始化它,我们创建一个父级为DD'的副本。

git checkout prod/master
git checkout -b bridge-prod
git rm -r ':/'
git checkout bridge-clean -- ':/'
git commit

现在你有

A' -- B' -- C' -- D' <--(bridge-clean)(clean/master)
D" <--(bridge-prod)
/
A -- B -- C -- D <--(prod/master)

其中D'D"具有相同的内容(这是D的"清理"版本)。 由于D"D作为其父级,因此您可以将prod/master的未来更改合并到bridge-prod(D将是合并基础)。 所以一段时间后你有

... x <--(clean/master)
/
A' -- B' -- C' -- D' <--(bridge-clean)
D" <--(bridge-prod)
/
A -- B -- C -- D ... H <--(prod/master)

这两个...可以包括许多提交、分支、合并等等;它没有太大的区别。 重要的是,bridge-prodbridge-clean仍然表示存储库之间的最后集成。

所以接下来你要合并prod/masterbridge-prod.

... x <--(clean/master)
/
A' -- B' -- C' -- D' <--(bridge-clean)
D" -- H"<--(bridge-prod)
/     /
A -- B -- C -- D ... H <--(prod/master)

您希望H"表示H的清理状态。 为此,有两个条件需要担心:

如果prod/master分支更新由清理删除的文件,则合并将发生冲突。 幸运的是,这些删除是合并的"我们"方面的唯一更改,我们知道我们希望将它们保留在prod/master可能对这些文件所做的任何操作之上。 所以当我们合并时,我们可以说

git checkout bridge-prod
git merge -X ours prod/master

-X ours选项不应与-s ours. 虽然-s ours会使用"我们的合并策略",完全忽略prod/master的变化,但-X ours使用默认的合并策略和"我们的策略选项"(感谢,git,清澈如泥的命名)。

这意味着,此命令将尝试正常合并,但每次发生冲突时,该大块代码的bridge-prod版本将占上风。 由于bridge-prod唯一的更改是删除我们不想要的文件,这很好。

另一个问题是,如果prod/master可能添加了应从清理中排除的新文件。 如果你知道这不可能发生,没问题。 如果它可能发生,那么您需要检查它。 例如,在合并之前,您可以说

git diff prod/master prod/master^

并查看是否有任何您不需要的新文件出现在干净的存储库中。 如果是这样,那么为了您的合并,请

git checkout bridge-prod
git merge -X ours --no-commit prod/master
# remove the unwanted files
git add ':/'
git commit

现在,因为D"D'相同的内容,这意味着H"在下一个bridge-clean提交中具有您想要的TREE

git checkout bridge-clean
git rm -r ':/'
git checkout bridge-prod -- ':/'
git commit

这给你

... x <--(clean/master)
/
A' -- B' -- C' -- D' -- H' <--(bridge-clean)
D" -- H"<--(bridge-prod)
/     /
A -- B -- C -- D ... H <--(prod/master)

H'具有与H"相同的内容 - 这是经过净化的内容,通过H更新。 此外,H'已经清理了历史记录(它的父级是D',我们在开始时清理了它),因此它可以安全地包含在干净的存储库中。 您可以将bridge-clean合并到master,更改传输完成。

这在概念上有点复杂,并且需要一些前期设置(并且可能编写一些脚本以用于每次更改集成)。 但是一旦全部设置完毕,它就会最大限度地减少手动摆弄,并让您最好地使用git提供的合并机制。

但是,这是一座单向桥。 如果要将bridge-prod合并回prod/master,则几乎肯定会删除要保存在prod/master中的文件。

如果必须从干净存储库中获取更改并将其应用于生产存储库,则可以在干净存储库上生成修补程序。 考虑到干净的存储库内容是生产存储库内容的子集,修补程序应该不会有太多麻烦地应用。 下次将更改从生产合并到干净时,可能会导致一些虚假冲突。

最后一点(上面提到过,但后来被遗忘了) - 这一切都假设你不会在prod存储库中进行历史重写(或者至少不会经常)。 如果要进行这样的重写,那么就像其他用户的克隆无法干净地拉取更改一样,桥也无法正常工作,无法将更改集成到干净的存储库中。 您必须根据具体情况制定一个程序。

最新更新