你如何检测git中的邪恶合并?

时间:2014-12-29 03:36:21

标签: git git-merge

我已经创建了一个简单的git repo来说明我的问题,可以在GitHub上找到:https://github.com/smileyborg/EvilMerge

以下是回购历史的说明:

master          A---B---D---E-----G-----I
                 \     /     \         /
another_branch    ----C       \       /
                               \     /
another_branch2                 F---H

(在GitHub上的实际回购中,D4a48c9I48349d。)

D 是一个“简单”的邪恶合并,其中合并提交“正确”解决合并冲突,但也会产生一个不相关的“邪恶”变化家长。通过在此提交中使用git show -c,可以发现此合并的“邪恶”部分,因为输出包括++--(而不是单+-)表示父级中不存在的更改(有关上下文,请参阅this answer)。

I 是一种不同类型的恶意合并,其中合并提交“正确”解决了合并冲突(由F更改为file.txt引起的与G)的更改发生冲突,但“evilly”会丢弃对完全不同的文件file2.txt所做的更改(有效撤消H的更改)。

你怎么知道I是邪恶的合并?换句话说,您可以使用哪些命令来发现I不仅可以手动解决冲突,还无法合并它应该具有的更改?

编辑/更新:什么是邪恶合并?

正如下面的René Link所指出的那样,很难(或许不可能)定义一套通用标准来识别“邪恶合并”。然而,就像Supreme Court Justice Stewart said about pornography一样,邪恶的合并是你所知道的。

因此,或许更好的问题是:您可以在合并提交中使用什么git命令来获取仅在合并提交本身中引入的所有新颖更改的差异输出。这种差异应该包括:

  • 所有合并冲突解决方案(至少,如果解决方案涉及的事情比选择一方父母的更改更复杂)
  • 父母双方都不存在的所有添加或删除(如D所示)
  • 其中一个父母确实存在的所有更改但合并提交丢弃(如I中所示)

这里的目标是能够让人类看看这个输出并知道合并是否成功或(意外或恶意)“邪恶”没有必须重新审查以前的所有已合并的更改(例如FH)正在合并中。

7 个答案:

答案 0 :(得分:7)

最简单的方法是使用合并自动解决冲突而不需要人为干预来区分冲突解决的结果。任何自动分辨率都将被忽略,因为它们将以完全相同的方式解决。

我看到两种方式可视化可能的“邪恶”决议。如果您将其添加到脚本中,请将&> /dev/null添加到您不关心输出的所有行的末尾。

1)使用两个单独的差异,一个支持第一个父级,另一个支持第二个父级。

MERGE_COMMIT=<Merge Commit>
git checkout $MERGE_COMMIT~
git merge --no-ff --no-edit -s recursive -Xours $MERGE_COMMIT^2
echo "Favor ours"
git diff HEAD..$MERGE_COMMIT
git checkout $MERGE_COMMIT~
git merge --no-ff --no-edit -s recursive -Xtheirs $MERGE_COMMIT^2
echo "Favor theirs"
git diff HEAD..$MERGE_COMMIT

2)对冲突合并的结果与仍然存在的冲突进行差异。

MERGE_COMMIT=<Merge Commit>
git checkout $MERGE_COMMIT~
git -c merge.conflictstyle=diff3 merge --no-ff $MERGE_COMMIT^2 --no-commit
git add $(git status -s | cut -c 3-)
git commit --no-edit
git diff HEAD..$MERGE_COMMIT

答案 1 :(得分:4)

在我们发现邪恶合并之前,我们必须定义邪恶合并是什么。

必须手动解决每个有冲突的合并。 为了解决冲突,我们可以

  1. 进行其中一项更改并省略另一项。
  2. 最终进行两项更改(在这种情况下,结果中的顺序可能很重要)
  3. 不使用它们并创建一个新的更改,即两者的合并。
  4. 不使用它们并省略它们。
  5. 那么现在什么是邪恶的合并?

    根据this blog,它是

      如果合并没有忠实地整合来自所有父母的所有变更,那么合并被认为是邪恶的。

    那么&#34;忠实的整合&#34 ;?我认为没有人可以给出一般答案,因为它取决于代码或文本的语义或合并的任何内容。

    Other say

      

    邪恶合并是一种合并,它引入了未出现在任何父级中的更改。

    使用此定义所有由

    解决的冲突
    1. 进行其中一项更改并省略另一项。
    2. 不使用它们并创建一个新的更改,即两者的合并。
    3. 不使用它们并省略它们。
    4. 是邪恶的合并。

      所以我们终于回答了问题。

      是否合法
      • 只进行其中一项更改并省略另一项?
      • 同时进行两项更改?
      • 不采取任何措施并创建一个新的变化,即两者的合并?
      • 不拿这些并省略两者?

      如果我们考虑章鱼合并,事情会变得更加复杂。

      我的结论

      我们可以检测到的唯一邪恶合并是没有冲突的合并。在这种情况下,我们可以重做合并并将其与已经完成的合并进行比较。如果存在的差异超过他/她应该引入的差异,我们可以确定这种合并是一种邪恶的合并。

      至少我认为我们必须手动检测邪恶合并,因为它取决于变化的语义,而且我无法形成邪恶合并的正式定义。

答案 2 :(得分:4)

我扩展了answer from Joseph K. Strauss以创建两个完整的shell脚本,可以很容易地为给定的合并提交获取有意义的diff输出。

此GitHub Gist中提供了这些脚本:https://gist.github.com/smileyborg/913fe3221edfad996f06

第一个脚本detect_evil_merge.sh使用的策略是在不解决任何冲突的情况下再次自动重做合并,然后将其差异化为实际合并。

第二个脚本detect_evil_merge2.sh使用两次重新自动重做合并的策略,一次解决与第一个父版本的冲突,第二次使用第二个父版本解决冲突,然后差异化每个那些实际的合并。

任何一个脚本都可以完成这项工作,这只是个人偏好,您可以通过哪种方式更容易理解冲突是如何解决的。

答案 3 :(得分:3)

免责声明:正如@smileyborg所指出的,此解决方案不会检测到一个恶意合并完全恢复由父母之一引入的更改的情况。出现此缺陷是因为根据-c选项的Git文档

  

此外,它仅列出了从所有父母修改过的文件。

我最近发现了一个比目前任何答案都简单得多的解决方案。

基本上,合并提交的git show的默认行为应该可以解决您的问题。如果合并两侧的修改没有触及,并且没有做出“邪恶”的改变,那么将没有差异输出。我以前认为git show从不显示合并提交的差异。但是,如果合并提交涉及混乱冲突或恶意合并,则差异将以组合格式显示。

要查看使用log -p的多个提交补丁时的组合格式,只需添加参数--cc

在问题中从GitHub给出的示例中显示以下内容(我的评论散布在一起):

$ git show 4a48c9

(示例中 D

commit 4a48c9d0bbb4da5fb30e1d24ae4e0a4934eabb8d
Merge: 0fbc6bb 086c3e8
Author: Tyler Fox <Tyler_Fox@intuit.com>
Date:   Sun Dec 28 18:46:08 2014 -0800

    Merge branch 'another_branch'

    Conflicts:
        file.txt

diff --cc file.txt
index 8be441d,f700ccd..fe5c38a
--- a/file.txt
+++ b/file.txt
@@@ -1,9 -1,7 +1,9 @@@
  This is a file in a git repo used to demonstrate an 'evil merge'.

以下几行并不邪恶。第一个父级所做的更改由最左侧列中的+ / -表示;第二个父项所做的更改由第二列中的+ / -表示。

- int a = 0;
- int b = 1;
+ int a = 1;
+ int b = 0;
 +int c = 2;
- a = b;
+ b = a;
  a++;

这是邪恶的部分:++两个父母更改为--。请注意前导--++表示这些更改都来自父母双方,这意味着有人在此提交中引入了尚未反映在其中一个父级中的新更改。 请勿将前导的差异生成的++ / --与作为文件内容一部分的尾随++ / --混淆。 < / p>

--b++;
++b-- ;

邪恶的结束。

 +c++;

快速查看可能存在问题的所有合并提交:

git log --oneline --min-parents=2 --cc -p --unified=0

所有不感兴趣的合并将显示在一行上,而杂乱的合并将显示组合差异。

说明:

  • -p - 显示补丁
  • --oneline - 在一行显示每个提交标题
  • --min-parents=2 - 仅显示合并。
  • --cc - 显示组合差异,但仅适用于父母双方都有重叠的地方
  • --unified=0 - 显示0行上下文;修改数字以便更积极地找到邪恶的合并。

或者,添加以下内容以消除所有不感兴趣的提交:

-z --color=always | perl -pe 's/^([^\0]*\0\0)*([^\0]*\0\0)(.*)$/\n$2\n$3/'
  • -z - 在提交日志结束时显示NUL而不是换行符
  • --color=always - 管道到perl时不要关闭颜色
  • perl -pe 's/^([^\0]*\0\0)*([^\0]*\0\0) - 按住输出以隐藏带有空差异的日志条目

答案 4 :(得分:3)

初步说明:我正在使用Linus Torvalds&#34; Evil Merge&#34; definition在这里,Junio Hamano notes有时可能是一件好事(例如,解决语义冲突而不是文本冲突)。这是Linus的定义:

  &#34;邪恶的合并&#34;是一种使得来自任何一方并且实际上都没有解决冲突的变革[来源:LKML]

@joseph-k-strauss中提到的his answer,任何邪恶合并检测的问题完全基于&#34; -c&#34;或&#34; - cc&#34;是这样的:

  

&#34;此外,它仅列出了从所有父母修改过的文件。&#34; [来源:man git-log]

为了检测 的邪恶,我们需要找到一些修改过的文件,但不是全部 ,其父母。

我认为干净的合并具有对称属性。考虑这个图:

enter image description here

在干净合并中,对角线是相同的: b1 == m2 b2 == m1 。更改的行集仅在发生冲突时重叠,并且干净的合并没有冲突。因此, b2 中的一组更改必须匹配 m1 ,因为 b2 的重点是要重播<在parent2之上的em> m1 ,使parent2与parent1同步(并且记住---没有冲突)。反之亦然 m2 b1

考虑这种对称性的另一种方式:当我们进行rebase时,我们基本上扔掉 b1 并用 m2 替换它。

所以如果你想检测邪恶的合并,你可以使用&#34; git show -c&#34;对于父母双方都改变的文件,以及使用&#34; git diff --name-only&#34;来检查图的四个部分的对称性是否成立。

如果我们假设图中的 merge 是HEAD(例如,让我们看看我刚刚提交的合并是否是邪恶的),我们使用花哨的&#34;三点&# 34; git diff notation为你计算merge-base,我想你只需要这四行:

git diff --name-only HEAD^2...HEAD^1 > m1
git diff --name-only HEAD^1...HEAD^2 > b1
git diff --name-only HEAD^1..HEAD    > m2
git diff --name-only HEAD^2..HEAD    > b2

然后分析内容以查看 m1 == b2 b1 == m2 。如果他们不匹配,那么你就是邪恶的!

来自其中任何一个的任何输出都表示邪恶,因为如果我们cat b1 m2 并对它们进行排序,则每行应该出现两次。

cat b1 m2 | sort | uniq -c | grep -v ' 2 '
cat b2 m1 | sort | uniq -c | grep -v ' 2 '

对于EvilMerge示例,提交 输出以下内容:

cat b2 m1 | sort | uniq -c | grep -v ' 2 '
      1 file2.txt

编辑&#34; file2.txt&#34;仅在 b2 m1 对角线之间发生一次。合并不是对称的,因此它不是一个干净的合并。邪恶成功地被检测到了!

答案 5 :(得分:1)

最简单的可能就是最好:将一次性未校正(和不完整)automerge的结果区分开来,如果有任何冲突,则不会解决冲突,并带有实际的合并结果。

普通的我们/他们的决议将显示为删除所有3个(3diff的4个)冲突标记线,并且还删除了一侧或另一个变化,这很容易引人注目。

任何一个分支变化的任何变化都会显示为奇怪的混合,例如任何无偿添加或删除的帅哥都会出现在冲突标记之外。

在示例仓库中,在

之后
git clone https://github.com/smileyborg/EvilMerge

git checkout master^
git merge --no-commit master^2   # --no-commit so  w/ or w/o conflict work the same

运行建议的差异

$ git diff -R master    # -R so anything master adds shows up as an add
diff --git b/file.txt a/file.txt
index 3835aac..9851407 100644
--- b/file.txt
+++ a/file.txt
@@ -1,12 +1,6 @@
 This is a file in a git repo used to demonstrate an 'evil merge'.

-<<<<<<< HEAD
-int a = 3;
-||||||| merged common ancestors
-int a = 1;
-=======
-int d = 1;
->>>>>>> master^2
+int d = 3;
 int b = 0;
 int c = 2;
 b = a;
diff --git b/file2.txt a/file2.txt
index d187a25..538e79f 100644
--- b/file2.txt
+++ a/file2.txt
@@ -4,6 +4,6 @@ int x = 0;
 int y = 1;
 int z = 2;
 x = y;
-x--;
-y--;
-z--;
+x++;
+y++;
+z++;

并且它立刻清楚了一些可疑的东西:在file.txt中,两个分支上的变化都被丢弃,一条线从无处插入。而在file2.txt中从未发生过冲突,合并只是无条件地改变了代码。一点点挖掘表明这是一个提交回复,但这并不重要,关键是通常的变化遵循易于识别的模式,任何不寻常的东西都很容易被发现并值得检查。

同样,在

之后
git branch -f wip 4a48
git checkout wip^
git merge --no-commit wip^2

运行建议的差异

$ git diff -R wip
diff --git b/file.txt a/file.txt
index 3e0e047..fe5c38a 100644
--- b/file.txt
+++ a/file.txt
@@ -1,19 +1,9 @@
 This is a file in a git repo used to demonstrate an 'evil merge'.

-<<<<<<< HEAD
-int a = 0;
-int b = 1;
-int c = 2;
-a = b;
-||||||| merged common ancestors
-int a = 0;
-int b = 1;
-a = b;
-=======
 int a = 1;
 int b = 0;
+int c = 2;
 b = a;
->>>>>>> wip^2
 a++;
-b++;
+b--;
 c++;

再一次奇怪的跳出来:wip为int c = 2分支的更改添加了wip^2,并且无处不在地将b--切换为b++

你可以从这里变得可爱并自动化一些可预测的东西,使批量审查更快,但这真的是一个单独的问题。

答案 6 :(得分:0)

如何重复“虚拟”合并并比较结果?换句话说

伪代码:

  1. 从我开始
  2. 得到2个父母:G,H
  3. git checkout E
  4. git merge H
  5. 现在你有了新的我。
  6. 使用git diff或比较git show Igit show new-I
  7. 的输出,比较我和新I

    特别是最后一步将是艰苦的工作,如果你想完全自动完成,至少如果提交中存在冲突