了解 git 冲突
Understanding git conflicts
我想我可能对 git 处理事情的方式有一些误解,因此,我面临着一些相当烦人的冲突。
我从 master 开始一个新的分支 A,并开始创建新文件。最终,我从分支 A 创建了分支 B,并开始处理它。一个分支 A 继续开发,B 需要在分支 A 中所做的更改,所以我将 A 合并到 B,并继续在它们两个上工作。
随着时间的推移,分支A继续开发,直到合并到master。在这一点上,我认为,现在它已经合并到 master 中,我可以简单地将 master 合并到 B 中,并从 A 和其他所有人那里获得所有更改。
问题是,现在我正在尝试这样做,我遇到了 "Both added"、"Both modified" 等的多重冲突 - 但我没有更改文件。我完全理解我更改的文件中的冲突 - 我造成了它们,我非常清楚。
认为我的解释令人困惑,我冒险进入 Google 幻灯片并创建了这个 "amazing" 绘图来说明我的场景(其中箭头代表合并,如 "merged master into A",除非它们指向分支中的第一个提交 - 在这种情况下它们表示 "branched off from here";也就是说,除了箭头方向之外,非常标准的 git 表示法 - 此处不确定):
其他人修改的文件,我不明白为什么列为冲突。也就是说,有一个文件在我创建分支 A 时已经更改,当我尝试将 master 合并到 B 时,同一个文件又被更改了。当然,分支 B 仍然有该给定文件的超旧版本,但是,仍然 git 不应该认识到它们在我的提交中没有被更改,而只是覆盖它们,或者它做了什么?我在这里错过了什么?
编辑:澄清一下,我是分支 A 和 B 的唯一贡献者。
shouldn't git recognize that they have not been changed in my commits, and just overwrite them?
没有
你没改文件也没关系。 git 的脑子里没有关于 who 创建提交或 who 获得优先权的事情。 master
和 b
在某种程度上都不是 "better"。
重点只是 master
已知的文件和 b
已知的文件存在无法自动协调的不同之处。
Git 显然可以自动调和一些差异;例如,如果文件在一个分支中因第 1 行已更改而不同,而在另一个分支中因第 1000 行已更改而不同。但有时它只是举手让你解决。那不是因为 谁 更改了文件,而是因为差异的性质。
至于 区别是什么,我认为阅读 会有所帮助 — 这是我读过的关于什么是合并以及如何合并的最好的文章有用。这完全是关于 LCA 以及从它到分支末端的差异。在你的图表中,LCA 是从最左边开始的第二个提交,一直回到 A 第一次分支的点。该提交与 master
末尾之间的所有差异,以及该提交与 b
末尾之间的所有差异都必须考虑在内,以便合并。好吧,git 告诉您对于这个文件它不能这样做。如果您考虑文件从那里到 master
末尾以及从那里到 b
末尾的变化情况,您就会明白为什么。
谁 更改了文件或如何绘制图表都没有关系。有一条路径描述(在您的脑海中)发生的事情的历史并不重要。重要的是三种状态:早期提交时的状态、master
结束时的状态以及 b
结束时的状态.
你的图表很漂亮,但是有点误导。
让我们从一个问题开始:蓝色提交在哪个分支上?
这是一个技巧问题。它们不在 a 分支上,它们在 several branches 上,复数形式。如果你说 他们在分支 A
,嗯,这是真的,但他们 也在 master
其中大部分 也在 分支 B
.
在您的绘图中,每个提交都没有标识符。这让他们很难谈论:我可以说 "the second-from-left grey commit" 或 "the rightmost blue commit",但这有点笨拙,所以让我将图形重新绘制为文本,在每次提交中使用单个大写字母。我也不会在这里使用箭头,因为它们在文本中很难做到。
A--B--C--D--E--F--G--H--I--J--K--L <-- master
\ \ /
M--N--O--P--Q---R---S <-- branch-A
\ \
T--U--V--W--X--Y--Å--Ø--Z <-- branch-B
也就是说,master
的提示提交是提交L
。 branch-A
的 提示提交 是提交 S
。 branch-B
的 提示提交 是提交 Z
.
在 Git 中,分支 name,如 master
,总是指向一个单独的提交。您可以选择哪个提交;你select这种方式的提交是分支的提示提交。 可从到达的任何较早的提交也在分支上。因此,从 L
开始并向后工作——在这些图中向左——沿着从提交到提交的箭头,1 我们从 L
到 K
,然后从 K
到 J
。但是 J
是一个 合并提交: 它有两个 parent,而不是一个。所以从 J
我们去 S
和 I
。从这两个,我们转到 H
和 R
,然后转到 G
和 Q
、F
、、、P
、E
。有两种方法可以到达 E
,但我们只访问了一次。从这里我们可以继续O
和D
、N
和C
、M
和B
以及A
。因此,该提交列表是 master
.
上的一组提交
(请注意,我们不能从 Q
到 W
,尽管我们 可以 走另一条路,从 W
到 Q
:所有的箭头都是单向的,指向后方。)
branch-A
上的提交集从 S
开始,然后倒退到 R
,然后是 Q
,然后是 P
,然后是 E
和 O
以及 D
和 N
等等。所有这些提交也在 master
.
上
branch-B
上的提交集从 Z
开始,通过 Ø
和 Å
以及 Y
和 X
向后移动W
,然后选择 P
和 V
,然后是 E
和 O
以及 U
,依此类推。因此 branch-B
上有四个提交不在任何其他分支上。
1从技术上讲,Git 的所有内部箭头都指向 向后 。因此,您通过向前绘制箭头来向后绘制箭头。出现这种情况是因为提交完全 read-only:parent 无法提前知道其最终 children 可能具有的哈希 ID,但 child 提交确实知道,提前/在创建时,child 具有的 parent 哈希 ID。所以箭头必须指向后面。
(为了向前移动,你告诉 Git 你想要结束的地方,然后它从那里向后移动,以确保它可以从其他起点结束到那里。)
git merge
的工作原理,大大简化了
当你运行:
git checkout master
git merge branch-B
Git 必须找到提交 L
和 Z
的 合并基础 提交。为此,它像大多数 Git 操作一样向后工作,从这些分支提示提交中找到 最佳共享提交: 从两个分支提示都可以访问的提交,因此在两个分支上,但实际上是 "closest to the tips" 。在这种情况下,虽然可能不是很明显,但这就是提交 Q
。从 L
开始,通过 K
返回到 J
,然后向下返回到 S
,然后是 R
,然后是 Q
。同时,从 Z
开始,回到 W
直到 Q
。提交 P
、E
、O
等也在两个分支上,但提交 Q
是 "better",因为它是最后一个这样的提交:它是所有这些提交的后代。
Git 现在将在内部 运行 两个 git diff
命令。这将比较合并基础——提交 Q
——与两个分支提示:
git diff --find-renames <hash-of-Q> <hash-of-L> # what we changed
git diff --find-renames <hash-of-Q> <hash-of-Z> # what they changed
在这两个 diff 列表中,如果某些文件在 both 分支提示中显示为 newly-created,您将得到该文件的 add/add 冲突.该文件不在 Q
中,但它 是 在 L
和 Z
中。
对于所有三个提交中的文件,Git 将尝试合并两组差异中显示的任何更改。如果这些更改不重叠(并且 "touch at the edges" 也不重叠),Git 可以将它们组合起来。它们确实重叠但重叠完全相同的地方——覆盖相同的原始行 iQ
,并进行 相同的 更改——Git 只需复制一份更改即可将其合并。所有其他重叠都会导致合并冲突。
此时你的工作是解决所有冲突,以你喜欢的任何方式。例如,在add/add whole-file 冲突中,如果两个文件匹配,则只选择其中一个的内容。如果没有,请以某种方式合并这两个文件。完成后,使用 git add
将每个冲突文件的最终内容写入索引。这将索引冲突标记为 "resolved",您现在可以 运行 git merge --continue
或 git commit
完成合并。2
2git merge --continue
检查以确保您正在完成合并。如果没有,它会出错。如果你是,它只是 运行s git commit
——所以这两种方式都没有真正的区别,除非你实际上没有完成冲突的合并。
我想我可能对 git 处理事情的方式有一些误解,因此,我面临着一些相当烦人的冲突。
我从 master 开始一个新的分支 A,并开始创建新文件。最终,我从分支 A 创建了分支 B,并开始处理它。一个分支 A 继续开发,B 需要在分支 A 中所做的更改,所以我将 A 合并到 B,并继续在它们两个上工作。
随着时间的推移,分支A继续开发,直到合并到master。在这一点上,我认为,现在它已经合并到 master 中,我可以简单地将 master 合并到 B 中,并从 A 和其他所有人那里获得所有更改。
问题是,现在我正在尝试这样做,我遇到了 "Both added"、"Both modified" 等的多重冲突 - 但我没有更改文件。我完全理解我更改的文件中的冲突 - 我造成了它们,我非常清楚。
认为我的解释令人困惑,我冒险进入 Google 幻灯片并创建了这个 "amazing" 绘图来说明我的场景(其中箭头代表合并,如 "merged master into A",除非它们指向分支中的第一个提交 - 在这种情况下它们表示 "branched off from here";也就是说,除了箭头方向之外,非常标准的 git 表示法 - 此处不确定):
其他人修改的文件,我不明白为什么列为冲突。也就是说,有一个文件在我创建分支 A 时已经更改,当我尝试将 master 合并到 B 时,同一个文件又被更改了。当然,分支 B 仍然有该给定文件的超旧版本,但是,仍然 git 不应该认识到它们在我的提交中没有被更改,而只是覆盖它们,或者它做了什么?我在这里错过了什么?
编辑:澄清一下,我是分支 A 和 B 的唯一贡献者。
shouldn't git recognize that they have not been changed in my commits, and just overwrite them?
没有
你没改文件也没关系。 git 的脑子里没有关于 who 创建提交或 who 获得优先权的事情。 master
和 b
在某种程度上都不是 "better"。
重点只是 master
已知的文件和 b
已知的文件存在无法自动协调的不同之处。
Git 显然可以自动调和一些差异;例如,如果文件在一个分支中因第 1 行已更改而不同,而在另一个分支中因第 1000 行已更改而不同。但有时它只是举手让你解决。那不是因为 谁 更改了文件,而是因为差异的性质。
至于 区别是什么,我认为阅读 master
末尾之间的所有差异,以及该提交与 b
末尾之间的所有差异都必须考虑在内,以便合并。好吧,git 告诉您对于这个文件它不能这样做。如果您考虑文件从那里到 master
末尾以及从那里到 b
末尾的变化情况,您就会明白为什么。
谁 更改了文件或如何绘制图表都没有关系。有一条路径描述(在您的脑海中)发生的事情的历史并不重要。重要的是三种状态:早期提交时的状态、master
结束时的状态以及 b
结束时的状态.
你的图表很漂亮,但是有点误导。
让我们从一个问题开始:蓝色提交在哪个分支上?
这是一个技巧问题。它们不在 a 分支上,它们在 several branches 上,复数形式。如果你说 他们在分支 A
,嗯,这是真的,但他们 也在 master
其中大部分 也在 分支 B
.
在您的绘图中,每个提交都没有标识符。这让他们很难谈论:我可以说 "the second-from-left grey commit" 或 "the rightmost blue commit",但这有点笨拙,所以让我将图形重新绘制为文本,在每次提交中使用单个大写字母。我也不会在这里使用箭头,因为它们在文本中很难做到。
A--B--C--D--E--F--G--H--I--J--K--L <-- master
\ \ /
M--N--O--P--Q---R---S <-- branch-A
\ \
T--U--V--W--X--Y--Å--Ø--Z <-- branch-B
也就是说,master
的提示提交是提交L
。 branch-A
的 提示提交 是提交 S
。 branch-B
的 提示提交 是提交 Z
.
在 Git 中,分支 name,如 master
,总是指向一个单独的提交。您可以选择哪个提交;你select这种方式的提交是分支的提示提交。 可从到达的任何较早的提交也在分支上。因此,从 L
开始并向后工作——在这些图中向左——沿着从提交到提交的箭头,1 我们从 L
到 K
,然后从 K
到 J
。但是 J
是一个 合并提交: 它有两个 parent,而不是一个。所以从 J
我们去 S
和 I
。从这两个,我们转到 H
和 R
,然后转到 G
和 Q
、F
、、、P
、E
。有两种方法可以到达 E
,但我们只访问了一次。从这里我们可以继续O
和D
、N
和C
、M
和B
以及A
。因此,该提交列表是 master
.
(请注意,我们不能从 Q
到 W
,尽管我们 可以 走另一条路,从 W
到 Q
:所有的箭头都是单向的,指向后方。)
branch-A
上的提交集从 S
开始,然后倒退到 R
,然后是 Q
,然后是 P
,然后是 E
和 O
以及 D
和 N
等等。所有这些提交也在 master
.
branch-B
上的提交集从 Z
开始,通过 Ø
和 Å
以及 Y
和 X
向后移动W
,然后选择 P
和 V
,然后是 E
和 O
以及 U
,依此类推。因此 branch-B
上有四个提交不在任何其他分支上。
1从技术上讲,Git 的所有内部箭头都指向 向后 。因此,您通过向前绘制箭头来向后绘制箭头。出现这种情况是因为提交完全 read-only:parent 无法提前知道其最终 children 可能具有的哈希 ID,但 child 提交确实知道,提前/在创建时,child 具有的 parent 哈希 ID。所以箭头必须指向后面。
(为了向前移动,你告诉 Git 你想要结束的地方,然后它从那里向后移动,以确保它可以从其他起点结束到那里。)
git merge
的工作原理,大大简化了
当你运行:
git checkout master
git merge branch-B
Git 必须找到提交 L
和 Z
的 合并基础 提交。为此,它像大多数 Git 操作一样向后工作,从这些分支提示提交中找到 最佳共享提交: 从两个分支提示都可以访问的提交,因此在两个分支上,但实际上是 "closest to the tips" 。在这种情况下,虽然可能不是很明显,但这就是提交 Q
。从 L
开始,通过 K
返回到 J
,然后向下返回到 S
,然后是 R
,然后是 Q
。同时,从 Z
开始,回到 W
直到 Q
。提交 P
、E
、O
等也在两个分支上,但提交 Q
是 "better",因为它是最后一个这样的提交:它是所有这些提交的后代。
Git 现在将在内部 运行 两个 git diff
命令。这将比较合并基础——提交 Q
——与两个分支提示:
git diff --find-renames <hash-of-Q> <hash-of-L> # what we changed
git diff --find-renames <hash-of-Q> <hash-of-Z> # what they changed
在这两个 diff 列表中,如果某些文件在 both 分支提示中显示为 newly-created,您将得到该文件的 add/add 冲突.该文件不在 Q
中,但它 是 在 L
和 Z
中。
对于所有三个提交中的文件,Git 将尝试合并两组差异中显示的任何更改。如果这些更改不重叠(并且 "touch at the edges" 也不重叠),Git 可以将它们组合起来。它们确实重叠但重叠完全相同的地方——覆盖相同的原始行 iQ
,并进行 相同的 更改——Git 只需复制一份更改即可将其合并。所有其他重叠都会导致合并冲突。
此时你的工作是解决所有冲突,以你喜欢的任何方式。例如,在add/add whole-file 冲突中,如果两个文件匹配,则只选择其中一个的内容。如果没有,请以某种方式合并这两个文件。完成后,使用 git add
将每个冲突文件的最终内容写入索引。这将索引冲突标记为 "resolved",您现在可以 运行 git merge --continue
或 git commit
完成合并。2
2git merge --continue
检查以确保您正在完成合并。如果没有,它会出错。如果你是,它只是 运行s git commit
——所以这两种方式都没有真正的区别,除非你实际上没有完成冲突的合并。