【问题标题】:Git merging strategyGit合并策略
【发布时间】:2020-11-21 04:53:58
【问题描述】:

我在 git 中创建了一个名为 release-1.0.0 的分支,我一直向该分支提交代码。现在,有一个重要的未来版本,它在设计和体系结构上发生了重大变化,称为release-2.0.0。这个新分支是从release-1.0.0 创建的。这将与 release-1.0.0 进行一些更改,但由于设计差异,无法合并该分支的某些更改。

release-1.0.0 中所做的更改移动到release-2.0.0 的正确策略是什么?合并是正确的做法吗?还是应该手动将代码复制粘贴到release-2.0.0?或者我们是否应该为此创建一个单独的存储库:-O

最后release-1.0.0release-2.0.0 将在完成后合并到master。 请分享您的想法。我不确定,这是否是正确的问题。但我看到有人问过其他类似的问题here

【问题讨论】:

  • 好像你想签出release-2.0.0 并在选择提交时执行git cherry-pick
  • 顺便说一句,release_2.0.0 没有包含来自release_1.0.0 的所有更改似乎很奇怪
  • 如果使用了不同的架构和设计风格,可能会有我们不能按原样使用的场景?
  • 在不知道两个分支的结构的情况下,如何回答“什么是正确的策略”这个问题?
  • @Daemon Painter,你能告诉我你所说的分支结构是什么意思吗?第 2 版分支最初是从第 1 版本身创建的

标签: git


【解决方案1】:

如果这类问题只有一个正确答案,那么每个人都会使用它并且它会众所周知。没有。但是我们可以说一些一般的事情:

或者我们是否应该为此创建一个单独的存储库:-O

单独的存储库只不过是一个分支——其内容没有人可以看到,除非他们有权访问该单独的存储库。 (嗯,从技术上讲,它是 Git 中的一整套分支,正弦分支名称是存储库本地的。)在 Git 中创建分支的成本非常低,所以如果这对你有帮助,那是一件好事,无论你是否将其放在单独的存储库中。

我们可以肯定地说:

  • 从本质上讲,Git 完全是关于提交

  • 每个提交都有编号。这些数字不是简单的连续数字——它们不会向上计数,1、2、3 等等——而是看似随机的哈希 ID,但它们仍然是唯一编号的。

  • 哈希 ID 的计算对于让 Git 工作至关重要:这里的秘密是 每个 Git 都会为 相同的对象计算 相同的哈希 ID提交内容。所以这意味着两个 Git 在相互交谈时,只需要比较 hash IDs 来查看它们是否有相同的提交。 (您不需要为眼前的问题关心这个,这只是一个有用的知识。)

  • 提交的内容分为两部分:

    • 每次提交都有每个文件的完整快照。这些文件是一种特殊的、只读的、仅限 Git 的、压缩的和去重复的形式,通常只有 Git 可以读取。 (重复数据删除意味着由于大多数提交主要重用了之前提交的文件,因此新提交几乎不会占用任何额外空间。即使每个提交都有每个文件的完整副本,这些提交实际上 share 单一副本。)

    • 与快照一起,每个提交都有一些元数据,或者关于提交本身的信息。元数据包括提交人的姓名和电子邮件地址、一些日期和时间戳,以及他们提交该提交的原因的日志消息。该元数据中有一部分是 Git 本身专用的,Git 自己维护:每个提交记录其 父级 的哈希 ID(提交号)(或者,对于合并,父级,复数) .

最后一部分是像master 这样的分支名称 如何以及为什么只存储一件事:last 提交的哈希 ID。提交本身就是并存储了项目的历史。

请注意,提交不会存储更改。他们存储快照。但是因为每个提交都会记住它的前一个提交——它的父提交——Git 可以接受任何提交并后退一步并查看它的父提交。在父级中,大多数文件可能相同,并且通过重复数据删除进行共享。因此,Git 可以直接跳过这些文件,而只需比较两次提交之间不同的文件。通过比较不同的文件,Git 可以在您询问时计算这些文件中的更改,从而向您显示该提交中的更改。

樱桃采摘与合并

要从单个提交中进行更改,您可以使用git cherry-pick。在内部,这实际上使用了 Git 的合并机制,但简单的描述使这一切都有意义:

  • Git 将提交与其父级进行比较,以查看提交中的 更改 内容。

  • 然后,Git 将 相同的更改 应用到 当前 提交。

如果应用程序运行顺利,那么您刚刚进行了相同的更改,Git 将自行提交。进行新提交的人当然是,只是刚才,但是消息 也是从原始提交中复制的。从新提交到其父级的差异将与从樱桃挑选的提交到 父级的差异相同。但是新的提交与原来的不一样,1所以它有一个不同的哈希 ID(提交号)。

这与合并非常不同。当你使用git merge 时,你告诉 Git:*找到两个特定提交的最佳共享祖先。将共享祖先提交与两个分支提示中的每一个进行比较。作为说明,考虑以下相对简单的分支历史:

          I--J   <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

在这里,我们“打开”分支 branch1,如附加的特殊名称 HEAD (HEAD) 所示。我们运行git merge branch2,告诉Git 这两个commits是commit J——我们当前的commit——和commit L。 Git 会自行找到最佳共享提交 H。 Git 将此称为合并基础。然后,Git 比较 H-vs-J 以查看 我们 发生了什么变化,这会获取在 IJ 两个提交中所做的更改,并比较 H-vs-@ 987654336@ 以查看 他们 发生了什么变化,这会获取在提交 KL 中所做的更改。

合并过程合并两组更改,将合并后的更改应用到来自提交H 的快照,即合并基础。如果一切顺利,生成的组合更改会正确应用,Git 会自行生成新的 merge commit M

          I--J
         /    \
...--G--H      M   <-- branch1 (HEAD)
         \    /
          K--L   <-- branch2

因为我们在branch1,Git 将新合并提交的哈希 ID 写入 name branch1,自动更新该名称以便 last 提交分支branch1 现在是M。因为M两个 父母,而不是只有一个,这将一切联系在一起。如果我们在branch2 上进行更多提交,则返回branch1,如下所示:

          I--J
         /    \
...--G--H      M   <-- branch1 (HEAD)
         \    /
          K--L----N--O   <-- branch2

并要求 Git 再次合并,这次最好的共享提交不是H,而是L(提交L在两个分支上)。所以这一次 Git 将比较 LM 来看看我们改变了什么——毕竟这是因为H-vs-J 而进行的改变——然后比较L-vs-@ 987654358@ 以查看他们在branch2 上所做的更改。 Git 将合并这些更改,将它们应用到L 中的快照,并生成一个新的合并:

          I--J
         /    \
...--G--H      M-------P   <-- branch1 (HEAD)
         \    /       /
          K--L----N--O   <-- branch2

现在提交 P 将从 N-O 获取更改,并且未来的合并将使用提交 O 作为新的合并基础。

如果我们回过头来将其与樱桃采摘进行比较,我们会发现它们有很大的不同:

          I--J   <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

假设我们现在在提交 L 时运行 git cherry-pick,方法是提供其哈希 ID 或使用名称 branch2。 Git 会将提交K 的快照与提交L 的快照进行比较,将这些更改应用到提交J,并进行我们将调用L' 的新提交——表明它是L 的副本— 将提交 J 作为其(单)父:

          I--J--L'  <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

我们没有从提交 K 获得任何更改。

如果我们在 this 点运行 git merge branch2,Git 仍会找到 H 作为合并基础,并将比较 HL' 以查看我们更改了什么,以及 @ 987654381@ vs K 看看他们改变了什么,和以前一样。这一次,当 Git 合并这些更改时,我们已经有了 K-vs-L 更改,但 Git 通常足够聪明,可以说哦,我看到我们和他们都做了同样的事情,所以我只复制一份更改


1不同之处包括提交者时间戳是“现在”,而您要复制的提交的提交者时间戳可能是过去的某个时间。但也有这样的:新提交的父提交是曾经是您正在挑选的分支上的最后一次提交的提交。您选择的提交的父级是不同的。因此,即使您设法在进行原始提交的同一秒内进行挑选,新的提交也将至少略有不同,并且会产生完全不同的哈希 ID。


重大的结构变化使 Git 变得困难

未来有一个重大版本,它在设计和架构上发生了重大变化......

要了解这是如何成为问题的,请坐下来为自己画一张简化图:

          D--X--E--F   <-- redesigned
         /
...--B--C
         \
          G--H--Y--I   <-- somebranch

假设提交 X 和 Y 进行了相当彻底的更改。然后在两个分支上提交C,在两个分支上完全相同,因为它实际上只是一个提交C。您显然不希望将提交 XY 复制到 other 分支——这些是主要的重新设计——所以你绝对想要合并提交FI 以任何方式。

您可以很容易地将git cherry-pick 提交GHredesigned,因为这些提交应用于提交C 本身,或直接从C 派生的东西。您可以将git cherry-pick 提交Dsomebranch,因为该提交应用于C 本身。但是,如果您尝试挑选EFI,那么,这些都是 主要的重新设计提交之后。他们不太可能轻易申请。

Git 不能做的工作变成了必须做的工作

如果提交EF 和/或I 永远 中的内容必须移动到“其他分支”,那很好。但是,如果您在 EF 中所做的某件事对 I 很重要,那么现在您就有问题了。

这里没有王道,但请注意这一点。假设您有一个 fix 来解决在任何重大更改提交之前 的提交中出现的问题:

          D--X--E--F   <-- redesigned
         /
...--B--C
         \
          G--H--Y--I   <-- somebranch

假设提交BCDG 和/或H 存在缺陷。进一步假设我们可以通过在缺陷出现的位置创建一个分支来修复该缺陷。为简单起见,让我们创建一个fix123 分支,现在使用git checkout -b fix123 <em>hash-of-C</em> 指向提交C

          D--X--E--F   <-- redesigned
         /
...--B--C   <-- fix123 (HEAD)
         \
          G--H--Y--I   <-- somebranch

现在让我们通过提交新的提交J 来修复提交C 中出现的错误,该错误与两个分支共享:

          D--X--E--F   <-- redesigned
         /
...--B--C--J   <-- fix123 (HEAD)
         \
          G--H--Y--I   <-- somebranch

这使我们能够运行git checkout redesigned; git merge fix123git checkout somebranch; git merge fix123 以将修复程序合并到两个 分支中。完成后,我们得到了这样的结果:

          D--X--E--F--K   <-- redesigned
         /           /
...--B--C-----------J   <-- fix123
         \           \
          G--H--Y--I--L   <-- somebranch

其中KL合并提交。这让我们看到修复已应用于两个分支。有问题的提交 XY 仍然在分支 redesignedsomebranch 上。但是,共享提交 C 之后是共享提交 J

如果G 或者修复,需要进入redesigned,我们可以创建一个直接指向提交G 的新分支,对此进行修复,然后将其合并到redesigned .生成的图表太复杂了,我无法在此处尝试绘制,但所有内容都将记录在 Git 中,以备日后提取。

这些合并中的每一个都可能会带来一些困难(因为结构性重写提交),并且通常很容易在每个分支提示中使用单独的修复。这也没有本质上的错误,特别是如果您事先不知道两个分支都可能需要修复。

【讨论】:

    猜你喜欢
    • 2018-09-08
    • 2020-02-05
    • 2012-07-05
    • 2012-05-09
    • 2023-03-28
    • 1970-01-01
    • 1970-01-01
    • 2019-05-03
    • 2014-06-02
    相关资源
    最近更新 更多