可以使用"Ours/Theirs"解决单个文件的Git冲突吗?

15

我在Stack Overflow和其他地方找到了许多关于使用OURS/THEIRS动态解决冲突的指南,适用于你只想用另一个文件覆盖一个文件(特别是二进制文件)的情况。但是,在几乎所有的例子中,它都被应用于所有冲突,而我只想将其应用于单个有冲突的文件。

我找到了一种所谓的解决方法,即使用git mergetool命令。然而,mergetool给我带来了问题,我选择“选择左侧”或“选择右侧”,但没有任何反应。

无论如何,不论是否使用mergetool,我都想知道是否有一种方法可以通过命令行完成这项工作。我相信一定有,如果有人能告诉我该命令,或者链接我到我找不到的SO问题,我会非常感激。

我还尝试过使用...

git checkout --theirs PATH/TO/CONFLICTED/FILE

但是,当我输入 'git status' 时,它仍然显示该文件存在冲突。

非常感谢您的时间。

2个回答

24

TL;DR

To extract the "ours" or "theirs" version, use a merge tool independent of Git. To add the final resolution, use git add.

Long

Each merge tool operates independently of Git, so for this specific aspect of the question, it's necessary to refer to the merge tool documentation.

When using git checkout --ours or git checkout --theirs, the complexity of the Git index becomes apparent. The Git index, also called the staging area or cache, is where you and Git construct the next commit to make. To add the final resolution, use the git add command.

For more detailed instructions and explanations, refer to the full text.

git merge <commit-or-branch-specifier>

Git找到了三个提交:

  • 其中一个是您的当前提交,这是您随时在使用的提交,因此除了您可以通过名称HEAD或单字符@(例如,git rev-parse HEADgit rev-parse @)引用它的外,它并没有什么特殊之处。
  • 另一个是您刚刚命名的提交。如果您运行了git merge otherbranch,则可以运行git rev-parse otherbranch查看其当前的提交哈希ID。(分支名称具有移动属性:即现在由分支名称标识的提交不一定与昨天或明天由该名称标识的提交相同。分支名称的这种移动是分支增长的方式。)当然,如果您运行了git merge a123456,那么另一个提交,即--theirs所用的提交,就是哈希IDa123456
  • 最后一个提交是合并基础,Git会自动为您找到。它通过使用从您的提交和其他提交的父链接来向后工作,直到找到两个分支首次重新汇合的适当点为止,从而找到此提交。

在找到这三个提交后,Git会实际运行:

git diff --find-renames <merge-base> <ours>    # see what we changed
git diff --find-renames <merge-base> <theirs>  # see what they changed

合并进程——以“合并”作为动词,可以说是找到这三个提交,执行差异并组合更改的过程。当两组更改影响相同行时,会发生合并冲突。

对于没有合并冲突的文件,Git 将结果放入您的工作树(作为普通文件)和索引(作为文件的特殊 Git 形式,准备提交)。因此,对于未发生冲突的文件,通常不需要进行其他操作。

然而,在出现合并冲突时,Git 会执行两个异常操作:首先,它将合并冲突版本写入工作树,使您可以将其作为普通文件进行编辑。其次,它写入到索引中,不是文件的一个版本,而是所有三个版本:合并基础版本、我们的版本和他们的版本。

Git 称这些额外版本为“高级阶段”。第一阶段是合并基础,没有“--base”选项访问它,但您可以使用“git show:1:

实际上,当您运行“git mergetool”时,它会在索引中找到三个版本,将它们提取为常规(非 Git 化)文件,并在这三个文件上运行实际的合并工具。合并工具假定要做正确的事情(无论结果如何),将这三个文件组合成一个合并的文件,之后,“git mergetool”可以在结果上运行“git add”。

不过,从命令行——这是我进行合并的方式——您只需编辑带有冲突标记的工作树文件,并找出正确的结果。将其写出,使用“git add”添加生成的文件,就可以了,因为“git add”注意到文件以三个阶段版本的形式存在并擦除这三个版本,改为写入零阶段。

一旦有了零阶段(不再有 1-3 阶段),则认为该文件已解决。

现在,git checkout --ours -- path 告诉 Git:将阶段2的版本从索引中取出并放到工作树中。带有 --theirs 的版本告诉 Git 选择使用阶段3的版本。在两种情况下,索引和它的三个版本都保持不变。这个命令 只能 从索引中提取到工作树。如果文件名不像一个选项,那么这里的 -- 只是为了防止文件名与选项混淆。使用 -- 是一个好习惯,但大多数人都不这样做。

由于索引仍然有所有的三个版本,因此该文件尚未解决。运行 git add 命令将工作树文件放入 slot zero,擦除1-3号条目,此时该文件已经解决。

奇妙的是,运行git checkout HEAD -- 路径git checkout otherbranch -- 路径 会导致该文件被解决。这是 Git 的一种实现方式: 在内部,当您使用git checkout name -- path 时,Git 首先要定位给定name(提交哈希或像HEADotherbranch这样的名称)中文件的 Git 形式。然后,它必须将该 Git 形式的文件 复制 到索引中…这个复制操作会擦除1-3号条目,并写入普通的0号条目。最后,Git 将从索引条目零提取文件(Git 格式)到工作树。

此“先写入索引,然后从索引提取到工作树”的副作用是,如果该文件处于冲突状态-有1-3号版本处于活动状态-那么它就不再处于冲突状态了!因此:

git checkout --ours -- file

不会解析该文件(因为它从索引槽2中提取),但是:

git checkout HEAD -- file

does指的是通过从当前提交中提取文件,进入索引槽位0,清除1-3;然后提取刚刚写入的槽位0条目。

编辑,2022年6月:自我撰写以上内容以来,Git增加了一对新命令git switchgit restore,将git checkout曾经结合的多个不同作用分开。 如果您的Git版本为2.23或更高版本,则可能希望使用git restore而不是git checkout来提取单个文件。(使用旧的git checkout,很容易以错误的方式调用它:即使有经验的Git用户有时也会在这里出错。)不过,git restore--ours--theirs标志与git checkout的工作方式相同:它们始终选择槽位2和3,即使在重新设置基准期间也是如此(请参见下面的附言)。

附言

以上描述了由git merge命令调用的合并操作流程。 Git的内部合并引擎——“合并作为动词”——也被git cherry-pickgit revertgit rebase(等等)使用。在使用这些命令时,通过不同的过程选择填充槽位1的“合并基础”提交,而“我们的”和“他们的”提交——例如通过--oursgit checkoutgit restore配合使用的提交——并不总是简单明了。我将把详细信息留给其他StackOverflow问题和答案,因为这已经足够消化了。


1
这是一个压倒性的解释,但还是谢谢 :) - Adi Prasetyo
非常棒的答案,让我“理解”而不是“复制粘贴”。非常感谢! - Andrey Yerokhin
在变基而非合并时,可能需要添加一个警告,即我们的变基是他们的,他们的变基是我们的。 - Patrick Fromberg
@PatrickFromberg:这比那更加复杂:在变基中,--theirs 总是“我们的”,但 --ours 就...嗯,它相当令人困惑:在要被复制的 第一个 提交上,--ours 明显是他们的。之后,--ours 也是我们的,因为已经修改以适应他们的!我在关于变基的答案中涵盖了这一点。这就是我更喜欢在这里提到“索引槽”的原因。 - torek
@PatrickFromberg:我稍微编辑了一下,提到了Git 2.23以及在变基(或者挑选或还原)过程中可能会出现的问题。 - torek

0

这是我对这个问题的回答进行(轻微)修改后的版本。

每当您需要帮助合并单个文件时,我发现编写一个自定义的mergetool最好,以便实现您想要的功能。 为此,我建议将以下行添加到您的.gitconfig文件(或等效文件)中:

[merge]
  conflictstyle = diff3
[mergetool.getours]
  cmd = git-checkout --ours ${MERGED}
  trustExitCode = true
[mergetool.mergeours]
  cmd = git-merge-file --ours ${LOCAL} ${BASE} ${REMOTE} -p > ${MERGED}
  trustExitCode = true
[mergetool.keepours]
  cmd = sed -I '' -e '/^<<<<<<</d' -e '/^|||||||/,/^>>>>>>>/d' ${MERGED}
  trustExitCode = true
[mergetool.gettheirs]
  cmd = git-checkout --theirs ${MERGED}
  trustExitCode = true
[mergetool.mergetheirs]
  cmd = git-merge-file --theirs ${LOCAL} ${BASE} ${REMOTE} -p > ${MERGED}
  trustExitCode = true
[mergetool.keeptheirs]
  cmd = sed -I '' -e '/^<<<<<<</,/^=======/d' -e '/^>>>>>>>/d' ${MERGED}
  trustExitCode = true

get(ours|theirs) 工具将保留文件的对应版本,并丢弃其他版本中的所有更改(因此不进行合并)。这是您针对二进制文件请求的方法。

merge(ours|theirs) 工具重新执行从本地、基础和远程文件版本的三方合并,选择以给定方向解决冲突。这有一些注意事项,特别是:它忽略了传递给合并命令的差异选项(例如算法和空格处理);从原始文件干净地合并(因此任何手动更改都会被丢弃,这可能是好事或坏事);并且它的优点是不能被应该在文件中的 diff 标记所混淆。

keep(ours|theirs) 工具只是通过正则表达式检测出 diff 标记和封闭部分的位置并将其删除。这样做的优点是它保留了合并命令中的差异选项,并允许您手动解决一些冲突,然后自动解决剩余的冲突。它的缺点是如果文件中存在其他冲突标记,则可能会混淆。

这些都是通过运行git mergetool -t (get|merge|keep)(ours|theirs) [<filename>]来使用的,如果没有提供<filename>,则处理所有冲突文件。

一般来说,假设您知道没有差异标记会混淆正则表达式(并且您不处理无法合并的二进制文件),则命令的keep*变体最为强大。如果将mergetool.keepBackup选项设置为未设置或true,则在合并后,您可以将*.orig文件与合并结果进行比较以检查其是否合理。例如,在提交之前,我运行以下命令来检查更改:

for f in `find . -name '*.orig'`; do vimdiff $f ${f%.orig}; done

注意:如果merge.conflictstyle不是diff3,那么sed规则中的/^|||||||/模式需要改为/^=======/


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