【问题标题】:How does Git solve the merging problem? [closed]Git如何解决合并问题? [关闭]
【发布时间】:2023-11-07 13:39:01
【问题描述】:

SVN 使分支变得非常便宜,从而使分支变得更加容易,但合并仍然是 SVN 中的一个真正问题 - Git 应该可以解决这个问题。

Git 能做到这一点吗?如何做到的?

(免责声明:我对 Git 的所有了解均基于 Linus 讲座 - 此处的总 git noob)

【问题讨论】:

  • 回应:“SVN 使分支变得非常便宜,从而使分支变得更加容易”。你确定你没有不小心用 SVN 替换 Git 吗?我知道 Git 拥有的一大特点是便宜的分支......我听说 SVN 中的分支是一场噩梦,因为其中大部分是手动的(使用分支内容创建新目录等)。

标签: svn git version-control


【解决方案1】:

Git 不会防止合并中的冲突,但即使它们不共享任何父祖先,Git 也可以协调历史。
(通过The grafts file (.git/info/grafts),它是一个提交列表,每行一个,后面是其父项,您可以出于“和解”目的对其进行修改。)
好强大啊。

但是要真正了解“如何考虑合并”,can start by turning to Linus himself,并意识到这个问题与“算法”无关:

Linus:我个人,我想要一些可重复且不聪明的东西。我理解的某件事告诉我它做不到。
坦率地说,合并单个文件的历史而不考虑所有其他文件的历史会让我“哎呀”。

合并的重要部分不是它如何处理冲突(如果它们很有趣的话,无论如何都需要由人来验证),而是它应该将历史正确地融合在一起,这样你就有了一个未来合并的新坚实基础。

换句话说,重要的部分是微不足道的部分:父母的命名,并跟踪他们的关系。不是冲突。

看起来 99% 的 SCM 人似乎认为解决方案是更聪明地进行内容合并。这完全没有抓住重点。


所以 Wincent Colaiuta 补充说(强调我的):

不需要花哨的元数据、重命名跟踪等。
您唯一需要存储的是每次更改前后树的状态。

哪些文件被重命名了?复制了哪些?哪些被删除了?添加了哪些行?哪些被删除了?哪些线路内部发生了变化?哪些文本块从一个文件复制到另一个文件?
您不必关心这些问题中的任何一个,当然也不必保留特殊的跟踪数据来帮助您回答这些问题:对树的所有更改(添加、删除、重命名、编辑等) ) 隐式编码在树的两个状态之间的增量中;您只需跟踪内容是什么。

绝对可以(并且应该)推断出一切

Git 打破常规,因为它考虑的是内容,而不是文件。
它不跟踪重命名,它跟踪内容。它在整个树级别上这样做。
这与大多数版本控制系统完全不同。
它不会费心尝试存储每个文件的历史记录;而是将历史记录存储在树级别。
当您执行 diff 时,您是在比较两棵树,而不是两个文件。

另一个根本上聪明的设计决策是 Git 如何进行合并。
合并算法很聪明,但它们不会试图变得太聪明。明确的决定是自动做出的,但如果有疑问,则由用户决定。
这是应该的方式。您不希望机器为您做出这些决定。你永远不会想要它。
这是 Git 合并方法的基本见解:当所有其他版本控制系统都在努力变得更智能时,Git 很高兴地自我描述为“愚蠢的内容管理器”,而且它更适合它。

【讨论】:

  • 这让我印象深刻,因为它旨在帮助您从过去的错误中恢复过来。虽然这是一件崇高而美好的事情,但它并不能真正帮助你不犯错误。
  • @hansen j :树是 blob(SHA1 引用的内容)或子树和名称的列表。请注意,具有相同内容/大小的两个文件将具有相同的 SHA1。树仍然会列出 2 个文件(因为 2 个不同的名称),但 Git 只会存储唯一的内容一次!
  • @VonC “其他版本控制系统” - 这仍然正确吗? Mercurial 和 Bazaar 不也像 Git 那样做吗? (至少现在是 2011 年) 现在说 “集中式版本控制系统”不是更准确吗?
  • @Mike:他们通常会存储更多用于管理合并的信息,主要是围绕重命名检测,例如 hg addremove (thread.gmane.org/gmane.comp.version-control.git/177146/…),尽管重命名检测仍然受到 Linus 的强烈反对 (article.gmane.org/gmane.comp.version-control.git/177315 )。它们都进行了合并,但 Git 试图让它比其他的更简单。
  • @Mike:另外,Git 是唯一一个成为 content 管理器的人。所有其他都是文件管理器。请参阅blog.daemon.com.au/blog-post/know-subversion-git-or-mercurial 了解更多信息。
【解决方案2】:

现在人们普遍同意 3 路合并算法(可能具有诸如重命名检测和处理更复杂的历史记录等增强功能),它考虑了当前分支('ours')上的版本,合并分支上的版本( “他们的”)和合并分支的共同祖先版本(“祖先”)是(从实际角度来看)解决合并的最佳方法。在大多数情况下,对于大多数内容树级合并(采用哪个版本的文件)就足够了;很少需要处理内容冲突,那么diff3算法就足够了。

要使用 3 路合并,您需要知道合并分支的共同祖先(也称为合并基)。为此,您需要了解这些分支之间的完整历史记录。 (当前)1.5 版之前的 Subversion(没有 SVK 或 svnmerge 等第三方工具)缺少的是 merge tracking,即为合并提交记住什么父项(什么提交)用于合并。如果没有这些信息,就无法在存在重复合并的情况下正确计算共同祖先。

考虑下图:

---.---a---.---b---d---.---1
        \        /
         \-.---c/------.---2

(可能会被破坏……如果有能力在这里绘制 ASCII 艺术图就好了)
当我们合并提交'b'和'c'(创建提交'd')时,共同的祖先是分支点,提交'a'。但是当我们想要合并提交'1'和'2'时,现在共同的祖先是提交'c'。如果不存储合并信息,我们将不得不错误地得出它是 commit 'a' 的结论。

Subversion(1.5 之前的版本)和更早的 CVS 使合并变得困难,因为您必须自己计算共同祖先,并在进行合并时手动提供有关祖先的信息。

Git 将有关提交的所有父级(在合并提交的情况下为多个父级)的信息存储在提交对象中。这样你就可以说 Git 存储修订的 DAG(直接无环图),存储和记住提交之间的关系。


(我不确定 Subversion 如何处理下面提到的问题)

在 Git 中额外合并可以处理两个额外的复杂问题:文件重命名(当一方重命名文件,而另一方没有;我们想要重命名,我们想要获得更改应用于正确的文件)和交叉合并(更复杂的历史,当有多个共同祖先时)。

  • 合并期间的文件重命名使用基于启发式相似度得分进行管理(同时考虑文件内容的相似性和路径名的相似性)重命名检测。 Git 检测合并分支(和祖先)中哪些文件相互对应。在实践中,它适用于真实案例。
  • 交叉合并,参见definition at revctrl.org wiki,(以及多个合并基础的存在)通过使用递归合并策略进行管理/strong>,生成单个虚拟共同祖先。

【讨论】:

  • 我尝试改进图表,将其格式化为块引用......我希望我没有因为理解不足而破坏它,在这种情况下我很抱歉。
【解决方案3】:

上面的答案都是正确的,但我认为他们错过了 git 对我来说容易合并的中心点。 SVN 合并要求您跟踪并记住合并的内容,这是一个巨大的 PITA。从他们的文档中:

svn merge -r 23:30 file:///tmp/repos/trunk/vendors

现在这不是杀手,但如果你忘记它是 23-30 包容性还是 23-30 独占性,或者你是否已经合并了其中的一些提交,你就会被淹没,你必须去弄清楚避免重复或丢失提交的答案。如果你分支一个分支,上帝会帮助你。

使用 git 它只是 git merge 并且所有这一切都无缝地发生,即使您已经挑选了几个提交或完成了任何数量的梦幻般的 git-land 事情。

【讨论】:

  • 我想你忘记了 svn 最近的合并跟踪。
  • 没错,我对新的合并内容没有太多经验。从远处看,它看起来很笨拙“一旦从分支到主干完成了 --reintegrate 合并,该分支不再可用于进一步的工作。它无法正确吸收新的主干更改......”当然总比没有好。跨度>
【解决方案4】:

据我所知,合并算法并不比其他版本控制系统中的算法更智能。但是,由于 git 的分布式特性,不需要集中合并工作。每个开发人员都可以随时将其他开发人员的小更改合并到他的树中,因此出现的冲突往往更小。

【讨论】:

    【解决方案5】:

    Git 只是让通过错误的合并搞砸其他所有人的存储库变得更加困难。

    唯一真正的好处是 Git 的合并速度要快得多,因为一切都在本地完成并且是用 C 编写的。

    SVN,使用得当,完全可用。

    【讨论】:

    • Git 也有不同的差异。它着眼于内容差异,而不是逐个文件行编辑。