命名分支与多个存储库

我们目前正在一个相对较大的代码库上使用 subversion。每个发行版都有自己的分支,对主干执行修复并使用 svnmerge.py迁移到发行版分支

我相信是时候进行更好的源代码控制了,我已经玩弄 Mercurial 一段时间了。

在使用 Mercurial 管理这样的发布结构方面,似乎有两派观点。要么每个发行版都有自己的回购,然后针对发行版分支进行修复,并推送到主分支(以及任何其他较新的发行版分支)或在单个存储库(或多个匹配副本)中使用命名分支

无论哪种情况,我似乎都可能使用类似于移植的方法来精确选择更改,以便将其包含在发布分支中。

我问你,每种方法的相对优点是什么?

20069 次浏览

据我所知,主要的区别在于您已经说过了: 命名分支位于单个存储库中。有名字的分支在一个地方什么都有。单独的回购协议规模较小,易于流动。关于这个问题之所以有两派观点,是因为没有明确的赢家。无论哪一方的观点对你来说最有意义,你都应该支持他们,因为他们的环境很可能和你的最相似。

最大的区别在于分支名称是如何记录在历史中的。对于已命名的分支,每个变更集中的分支名称是 嵌入了,因此将成为历史记录中不可变的部分。对于克隆,将有一个特定变更集来自哪里的 不是永久的记录。

这意味着克隆对于不想记录分支名称的快速实验非常有用,而命名分支对于长期分支(“1.x”、“2.x”和类似的名称)也很有用。

还要注意,单个存储库可以轻松地容纳 Mercurial 中的多个轻量级分支。这种存储库内的分支可以加入书签,这样您就可以很容易地再次找到它们。假设您已经克隆了公司存储库,它看起来是这样的:

[a] --- [b]

你把 [x][y]砍掉:

[a] --- [b] --- [x] --- [y]

当有人将 [c][d]放入存储库时,所以当你拉动它们时,你会得到一个如下的历史图:

[x] --- [y]
/
[a] --- [b] --- [c] --- [d]

这里一个存储库中有两个头。您的工作副本将始终反映单个变更集,即所谓的工作副本父变更集。看看这个:

% hg parents

假设它报告 [y]。你可以看到头部

% hg heads

这将报告 [y][d]。如果您希望将存储库更新为 [d]的干净签出,那么只需这样做(用 [d]的修订号替换 [d]) :

% hg update --clean [d]

然后您将看到 hg parents报告 [d]。这意味着您的下一次提交将以 [d]作为父级。因此,您可以修复您在主分支中注意到的 bug,并创建变更集 [e]:

[x] --- [y]
/
[a] --- [b] --- [c] --- [d] --- [e]

要只推送变更集 [e],您需要

% hg push -r [e]

其中 [e]是变更集散列。默认情况下,hg push只是比较存储库,发现缺少 [x][y][e],但是您可能还不想共享 [x][y]

如果 bug 修复也影响到你,你需要把它和你的特性分支合并:

% hg update [y]
% hg merge

这将使您的存储库图形看起来如下:

[x] --- [y] ----------- [z]
/                       /
[a] --- [b] --- [c] --- [d] --- [e]

其中 [z][y][e]之间的合并。你也可以选择扔掉分支:

% hg strip [x]

我对这个故事的主要观点是: 一个单独的克隆可以很容易地代表几个发展轨迹。对于不使用任何扩展的“普通 hg”来说,情况总是如此。书签扩展是一个很大的帮助,虽然。它将允许您为变更集分配名称(书签)。在上面的例子中,您需要在开发头部和上游头部分别设置一个书签。书签可以是带有 Mercurial 1.6的 又推又拉,并且已经成为 Mercurial 1.8的内置特性。

如果你选择制作两个克隆,你的开发克隆在制作 [x][y]之后看起来会是这样:

[a] --- [b] --- [x] --- [y]

你的上游克隆体将包含:

[a] --- [b] --- [c] --- [d]

现在,您将注意到这个 bug 并修复它。在这里,您不必使用 hg update,因为上游克隆已经准备好使用了。你提交并创建 [e]:

[a] --- [b] --- [c] --- [d] --- [e]

为了在你的开发克隆中包含 bug,你把它放在那里:

[a] --- [b] --- [x] --- [y]
\
[c] --- [d] --- [e]

然后合并:

[a] --- [b] --- [x] --- [y] --- [z]
\                   /
[c] --- [d] --- [e]

图表可能看起来不同,但它具有相同的结构,最终结果也是相同的。使用克隆人,你不得不少做一点心理簿记。

这里没有提到命名分支,因为它们是可选的。在我们转向使用命名分支之前,Mercurial 本身已经使用两个克隆开发了多年。除了“ default”分支之外,我们还维护了一个名为“ Stability”的分支,并基于该分支发布我们的版本。有关推荐工作流的说明,请参见 wiki 中的 标准分支页面。

我觉得你想要一次性了解整个历史。产生短期回购是为了进行短期实验,而不是像发行这样的大事件。

Mercurial 的一个令人失望的地方是,似乎没有简单的方法来创建一个短期存在的分支、使用它、放弃它并收集垃圾。树枝是永恒的。我很同情从不想放弃历史,但是超级便宜的一次性分支是 git的一个特性,我真的很想在 hg中看到。

我认为这显然是一个务实的决定,取决于当前的情况,例如一个功能的大小/重新设计。我认为分叉对于那些尚未成为提交者的贡献者来说是非常好的,他们可以通过可忽略的技术开销来证明自己的能力,从而加入开发人员团队。

你应该做 都有

从@Norman 提供的可接受的答案开始: 使用一个存储库,每个发行版有一个命名的分支。

然后,每个发布分支有一个用于构建和测试的克隆。

一个关键的注意事项是,即使您使用多个存储库,您也应该避免使用 transplant在它们之间移动变更集,因为1)它改变散列(hash) ,2)当您移植的变更集和目标分支之间存在冲突变更时,它可能会引入很难检测到的 bug。你想要做的是通常的合并(而不是预先合并: 总是直观地检查合并) ,这将导致@mg 在他的回答结尾说:

图表可能看起来不同,但它具有相同的结构,最终结果也是相同的。

更详细地说,如果您使用多个存储库,“主干”存储库(或默认的、主要的、开发的,等等)在 全部存储库中包含 全部变更集。每个版本/分支存储库只是主干中的一个分支,所有分支都以一种方式或另一种方式合并回主干,直到您想要留下一个旧版本为止。因此,在命名分支方案中,主要回购与单个回购之间的唯一真正区别仅在于是否命名了分支。

这就很明显地说明了我为什么说“从一个回购开始”。那单一的回购是唯一的地方,你将永远需要寻找 任何版本中的任何变更集。您可以在发布分支上进一步标记变更集以进行版本控制。它在概念上清晰而简单,使系统管理更加简单,因为它是唯一一个必须始终可用且可恢复的东西。

但是,您仍然需要为每个分支/版本维护一个需要构建和测试的克隆。这很简单,因为您可以使用 hg clone <main repo>#<branch> <branch repo>,然后分支 repo 中的 hg pull只会在该分支上提取新的变更集(加上被合并的早期分支上的祖先变更集)。

这种设置最适合于 单人拖拉机单人拖拉机的 linux 内核提交模型(像 Lord Linus 那样行事不是很好吗。在我们公司,我们把这个角色称为 积分器) ,因为主要的回购是开发人员需要克隆的唯一东西,而拉动者需要进入。分支回购协议的维护纯粹是为了发布管理,可以完全自动化。开发人员永远不需要从/推到分支回购。


下面是@mg 为这个设置重新设置的示例。起点:

[a] - [b]

当您到达 alpha 版本时,为发布版本创建一个命名的分支,比如“1.0”。提交 bug 修复:

[a] - [b] ------------------ [m1]
\                 /
(1.0) - [x] - [y]

(1.0)不是一个真正的变更集,因为在提交之前命名的分支不存在。(您可以进行简单的提交,比如添加一个标记,以确保正确地创建了命名的分支。)

合并 [m1]是此设置的关键。不像一个开发者仓库可以有无限数量的头,你不希望有多个头在你的主要回购(除了旧的,死亡的发布分支,如前所述)。因此,无论何时在发布分支上有新的变更集,都必须立即将它们合并回默认分支(或稍后的发布分支)。这保证了一个版本中的任何 bug 修复也包含在所有以后的版本中。

与此同时,对默认分支的开发继续向下一个版本发展:

          ------- [c] - [d]
/
[a] - [b] ------------------ [m1]
\                 /
(1.0) - [x] - [y]

和往常一样,你需要把两个头合并到默认分支上:

          ------- [c] - [d] -------
/                         \
[a] - [b] ------------------ [m1] - [m2]
\                 /
(1.0) - [x] - [y]

这是1.0分支的克隆版本:

[a] - [b] - (1.0) - [x] - [y]

现在是添加下一个发布分支的练习。如果它是2.0,那么它肯定会分支默认。如果是1.1,你可以选择分支1.0或默认。无论如何,1.0上的任何新变更集都应该首先合并到下一个分支,然后才是默认的。如果没有冲突,这可以自动完成,结果只是一个空合并。


我希望这个例子能让我前面的观点更加清晰,总之,这种方法的优点是:

  1. 包含完整变更集和版本历史的单个权威存储库。
  2. 清晰和简化的发布管理。
  3. 为开发人员和集成商提供清晰和简化的工作流程。
  4. 促进工作流迭代(代码审查)和自动化(自动空合并)。

更新 hg 本身 会这样: 主要回购包含默认的和稳定的分支,而 稳定的回购是稳定分支的克隆。但是,它不使用版本化分支,因为沿着稳定分支的版本标记对于它的发布管理目的来说已经足够好了。

我真的建议不要在版本中使用命名分支。这就是标签的作用。命名分支意味着长期持续的转移,如 stable分支。

那么为什么不使用标签呢? 一个基本的例子:

  • 开发在单个分支上进行
  • 无论何时创建发布,都要相应地对其进行标记
  • 发展只是从那里继续
  • 如果在某个版本中有一些 bug 需要修复(或者其他什么) ,你只需要更新它的标记,做出修改并提交

这将创建一个新的,未命名的头在 default分支,即。一个匿名分支,在 hg 完全没问题。然后,您可以在任何时候将 bug 修复提交合并回主开发轨道。不需要命名分支。