Git:查看分支自上次合并以来的更改情况以解决合并冲突

4

我认为我的情况应该很普遍,必须有更简单的方法来做我所做的事情。 假设有两个分支,currentnext,用于两行开发。 目前阶段,next 接收所有来自 current 分支的更改。 作为 next 分支的维护人员,我正在按照以下方式同步更改(因为需要进行 alpha 版本发布):

$ git co origin/next -b next
$ git merge origin/current

一些文件在next上更改了其格式,自上次同步合并以来对这几个文件的每个更改都导致冲突。为解决此冲突,我需要查看从上一次合并以来current分支中的更改。通常会有很多文件已更改,但只有1或2个文件(如我所提到的)发生了冲突。

示例

假设current分支上的文件baz.dat包含方括号中的单词,例如
[real]
[programmers]
[use]
[pascal]

next分支中,语法更改要求单词用冒号括起来。
:real:
:programmers:
:use:
:pascal:

current 分支上的更改已经向文件中添加了一行:

[real]
[programmers]
[don't]
[use]
[pascal]

每当合并冲突导致以下差异合并标记时:
<<<<<<< ours
:real:
:programmers:
:use:
:pascal:
=======
[real]
[programmers]
[don't]
[use]
[pascal]
>>>>>>>> theirs

整个文件内容被删除并替换,因为它从某种意义上说就是这样。

同样地,只有 git diff 显示所有传入的“their”行被删除,“our”行被添加:

$ git diff
<usual unified diff preamble>
+  :real:
... more lines with "+ " (our new) or " -" (their deleted) mark, 
 - [pascal]

我需要的是什么

一种查看自上次合并以来当前(“他们”的)分支中发生了什么更改的方法,以便我可以手动将更改合并到下一个(“我们的”)分支中。

$ git diff --magically-from-merge-point --theirs
<usual unified diff preamble>
+ [don't]

换句话说,我希望在这个例子中只添加一行,这样我也可以将其插入到新格式中。

(我的真实案例是领域特定语言的更改,但本质上与这个简单的例子非常相似)。

当前解决方案

我正在使用一系列相当笨拙的命令:

$ git status 
. . . .
      both modified:   foo/bar/baz.dat <== copy/paste every conflict filename
$ git diff `git merge-base HEAD origin/current`..origin/current -- foo/bar/baz.dat

这里展示的是我想要的,但比较复杂,每次都需要构建和输入。用bash脚本编写很容易,但在此之前,我想问一下是否有更简单的方法来查看合并基础中的冲突变更?

(next)$ git merge origin/current
. . . 
CONFLICT (content): foo/bar/baz.dat
(next|MERGING)$ git diff --magic-switch

而diff只会显示冲突文件的更改,作为合并基础和合并点之间的增量(在理想情况下,我会进一步限制使用例如--magic-switch --theirs

是否存在这样的魔法开关?看起来我错过了一些显而易见的东西!


1
在冲突合并时,运行不带参数的 git diff 命令会显示组合差异。但我发现这种方式难以阅读:我更喜欢将 merge.conflictstyle 设置为 diff3,这样可以将基础(第一阶段)冲突文本保留在冲突文件中,同时也包含第二阶段(HEAD)和第三阶段(--theirs)的文本。 - torek
@torek:这不是我要找的。每次合并时,由于文件格式的更改,该文件的差异显示所有旧行已删除并添加了所有新行。这很好,但我需要找到标记为已删除的部分中添加的一两个文件,自上次合并以来。合并时默认的差异行为对我来说难以理解,而diff3根本没有帮助(在2个版本中没有共同的相同行,因此我基本上看到的是相同的东西,只是在中间添加了一排||||||| :( )。 - kkm
这种diff3输出意味着您看到的冲突是添加/添加冲突。这意味着没有文件的基本版本!但是,您不会得到“内容”冲突,而是“添加/添加”冲突,并且对基本版本进行差异化也行不通,因为它不存在。我需要看到一个实际的例子才能说更多。(一个障碍是:我不明白您所说的“每个点都被合并”的含义。) - torek
@torek:我在问题中添加了一个“示例”部分以进行详细说明。你能再看一下吗? - kkm
https://dev59.com/RW025IYBdhLWcg3wwYz4#5818279 - max630
2个回答

1

啊,有了这个例子我们就能进一步理解了。

这不是 Git 内置的功能,因为这通常是一个非常困难的问题。但如果我们稍微限制一下,我们可以编写一个脚本、工具或者过程来处理它(而且发现那部分其实已经内置在 Git 中了!...嗯,有点像)。让我们看看我们能描述哪些限制:

  • 这是一个标准的三方合并,有一个标准的修改冲突,因此有一个基础版本(第一阶段),一个本地/HEAD/--ours版本(第二阶段)和一个远程/其他/--theirs版本(第三阶段)。

  • 合并的一侧触及每一行,但以可重复且可识别的模式进行,我们可以将其回退,或许是暂时的。(让我们给这个更改取个名字:称之为“系统性增量”或SD,+SD表示添加或保留该增量,而-SD表示撤销/删除它。SD可能是不可逆转的更改,即-SD可能无法完全撤消它;如果是这样,我们仍然可以自动处理它。)它可能也具有一些与基础版本相比的额外更改。

  • 合并的另一侧只触及少数行,甚至没有触及任何行,并且缺乏可重复且可识别的模式更改,我们可以将其添加到其中,或许是暂时的。

我们需要考虑一些额外的问题和决策,可能是根据特定情况或系统性地进行。这些将影响我们编写脚本、工具或程序的方式。它们包括:
- SD是否可逆?也就是说,一旦我们发现SD,base + SD - SD是否能够重现原始base? - 我们是否希望在结果中包含SD? - 我们是否能够检测到SD的存在?(可能没有必要,但如果我们能够检测到,那会很方便。)
如果SD是可逆的,那么我们就可以得到任何想要的结果。如果不是,我们就必须将SD应用于缺少它的一侧:只有当我们希望在结果中包含SD时,这样才是可行的。
假设我们现在想要在结果中使用+SD。在这种情况下,我们很顺利:只需调用应用SD的过程即可将文件“标准化”。或者,如果我们想要在结果中使用-SD,并且SD是可逆的,那么我们可以称执行-SD为“标准化”。此外,如果我们能够判断文件是否应用了SD,我们可以编写一个“标准化过滤器”,并将所有内容都通过该过滤器运行。
Git中就内置了这样的功能:它有“清洗”和“污染”过滤器,如 gitattributes文档所述。此外,我们可以指示git merge在执行合并之前对每个文件运行这两个过滤器(但我们可以使它们都成为“标准化”,或者留下其中一个未设置,以适应我们特定的目的)。由于“标准化”给出了我们想要的最终形式,因此它使Git执行了我们想要完成的合并。
如果SD不可逆,我们需要更聪明些,但只要我们想在结果中使用+SD,我们仍然可以做到。我们可以编写自己的合并驱动程序,而不是使用gitattributes中描述的merge.renormalize设置。同一手册中还有进一步的文档记录此方法。我不会在这里详细介绍,因为清理/污染过滤器方法更容易且可能适用于您的情况,但本质上是我们提取三个阶段,对需要它的任何阶段应用+SD(通过测试或先前知识选择“需要它的文件”),然后使用git-merge-file在三个(现在都是+SD)输入上实现所需的合并。
请注意,当您运行git merge时,工作树中存在的.gitattributes文件以及您在.gitconfig中选择的任何配置项或使用-c命令行选项到git,都会控制过滤和重新规范化。这个.gitattributes文件甚至不需要被检入到任何地方。这意味着您可以在不同的时间使用不同的.gitattributes文件,通过将其变为普通的跟踪文件并切换提交和/或分支(以便Git为您更新它),或者根据需要手动更新它。

感谢您的好解释!我明白我的问题可能没有通用的解决方案,这也是这种合并类型不可用的原因之一。至于第二部分,我知道清理和染色,但它们在我的情况下并不能完全帮助解决问题(分支之间DSL的变化实际上有点太复杂,无法可靠地进行脚本编写)。从实际角度来看,我接受了(LeGEC的回答)[https://dev59.com/N5zha4cB1Zd3GeqPDVWw#40502751]。 - kkm

1

git mergetool会逐一打开有冲突的文件,使用您选择的差异编辑器(meld、vimdiff、kdiff3、winmerge等),作为三方合并的三个版本之间的比较:

  • local:当前分支中的版本(在您的情况下:next的版本)
  • base:合并基础提交中的版本
  • remote:合并后的分支中的版本(在您的情况下:origin/current的版本)

如果您编辑+保存中央文件,则git将标记此冲突已解决,并将您保存的内容添加到索引中。


如果您的合并由于冲突而停止,git会将合并提交的引用存储在.git/MERGE_HEAD中。这意味着您可以在git命令中使用字符串"MERGE_HEAD"作为有效引用:
git log -1 MERGE_HEAD            # view last commit on the merged branch
git merge-base MERGE_HEAD HEAD   # no need for the name of the branch

您可以构建一个更简单的别名:

theirs = 'git diff $(git merge-base MERGE_HEAD HEAD) MERGE_HEAD'
# usage :
git theirs                  # complete diff between 'base' and 'theirs'
git theirs -w -- this/file  # you can add any option you would pass to 'git diff'

谢谢,但这并没有解决问题,只是更换了工具。我可能没有描述问题清楚吗?让我重新修改一下问题,并感激您对其可读性的任何反馈! - kkm
关于可读性:第一个版本已经很容易阅读,重写的版本更清晰地表达了目标,并且非常易读。因此加一分。 - LeGEC
谢谢,这正是我要用的!(我不知道MERGE_HEAD;这大大简化了脚本。我将-- $(git diff --name-only --diff-filter=U)放在命令的末尾,以获取我想要的内容,即仅有冲突文件中的更改。虽然不像您的别名那样灵活,但我需要完全相同的东西太多了。) - kkm
请注意,如果存在多个合并基础,则git merge-base MERGE_HEAD HEAD将从所有合并基础中随机选择一个合并基础。这基本上是进行完整比较的最佳方法,因为没有简单的方法可以访问由“递归”策略创建的虚拟合并基础。然而,对于单个文件,虚拟合并基础版本就是基础(插槽1)索引条目中的版本,因此您可能希望在此处使用git diff [options] :1:path/to/file :3:path/to/file - torek
@kkm:我一直依赖于git status来查看冲突文件,你额外的git diff ...是一个不错的快捷方式。谢谢 :) - LeGEC
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接