可以使用我们的/他们的来解决单个文件的 Git 冲突



我在堆栈溢出和其他地方找到了许多使用OURS/THEIRS动态解决冲突的说明,如果您只想用另一个文件覆盖一个文件(特别是二进制文件)。然而,在我找到的几乎每个示例中,它总是被集体应用于所有冲突,而我只想将其应用于单个冲突文件。

我发现的一个假定的解决方案是使用 git mergetool 命令。但是合并工具给我带来了问题,我选择"选择左"或"选择右",但没有任何反应。

不管合并工具如何,我都想知道是否有办法从命令行执行此操作。我敢肯定有,如果有人让我知道命令,或者以其他方式将我链接到我一定找不到的 SO 问题,我将不胜感激。

我也尝试过使用...

GIT 结帐 --他们的路径/到/冲突/文件

但是当我输入"git 状态"时,它仍然将文件显示为冲突。

非常感谢您的时间。

TL;博士

您将需要git add最终分辨率,除非您使用其他方法来提取"我们的"或"他们的"版本。

每个合并工具都独立于 Git(Git 只是运行它们并让它们做自己的事情),因此对于这个问题的特定子部分,您必须查阅合并工具本身。

至于git checkout --oursgit checkout --theirs,嗯,这就是 Git 所谓的索引显示其全部复杂性的地方。 请记住,索引,否则有点神秘,也称为暂存区,有时也称为缓存,本质上是您和 Git 构建下一次提交的地方。

运行时:

git merge <commit-or-branch-specifier>

Git 找到三个提交:

  • 一个是你当前的提交,这是你在任何给定时间总是使用的提交,所以这并不特别,除了你可以用名称HEAD或单个字符@来引用它(例如,git rev-parse HEADgit rev-parse @来获取它的哈希ID)。
  • 一个是你刚刚命名的提交。 如果您运行了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:path来查看它。 第二阶段是"我们的"版本:有--ours,但你也可以跑git show :2:path去看看它。 第 3 阶段是"他们的"版本,可通过git show :3:path. 这三个阶段取代了现在缺少的正常阶段零条目。

实际上,当您运行git mergetool时,它的作用是在索引中找到三个版本,将它们提取到常规(非 Git 化)文件中,然后在这三个文件上运行实际的合并工具。 假设合并工具执行正确的操作(无论结果如何)将三个文件合并为一个合并文件,之后git mergetool可以对结果运行git add

但是,从命令行(这就是我进行合并的方式),您只需编辑带有冲突标记的工作树文件,并找出正确的结果是什么。 把它写出来,git add生成的文件,你很好,因为git add注意到该文件以三阶段版本形式存在,并删除了这三个版本,而是写入阶段编号零。

一旦出现阶段零(不再是阶段 1-3),该文件将被视为已解决。

现在,git checkout --ours --path只是告诉 Git:从索引中取出 stage-2 版本并将其放入工作树中。带有--theirs的版本告诉 Git 改用阶段 3 版本。 在这两种情况下,索引及其三个分阶段版本都单独保留。 这从索引中提取到工作树。 (这里的--只是以防path部分是一个名为--theirs的文件。 如果文件名与选项不同,则不需要--。 一直使用--是一种好习惯,但大多数人不会。

由于索引仍具有所有三个暂存版本,因此尚未解析该文件。 运行git add将工作树文件放在插槽 0 中,擦除 1 到 3 条目,现在该文件已解析。

奇怪的是,运行git checkout HEAD --pathgit checkoutotherbranch--path会导致文件已解析。 这是 Git 的一个工件,让实现决定接口:在内部,当你使用git checkoutname--path时,Git 必须首先在给定name中找到文件的 Git 形式(提交哈希或像HEADotherbranch这样的名称)。 然后它必须将该Git 表单复制到索引......此复制会清除插槽 1-3 条目,写入正常的插槽零条目。 最后,Git 然后将(Git 形式)文件从索引条目零提取到工作树。

这种"先写入索引,然后从索引提取到工作树"的副作用是,如果文件处于冲突状态(阶段 1-3 处于活动状态),则不再冲突! 因此:

git checkout --ours -- file

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

git checkout HEAD -- file

确实解析了文件(因为它从当前提交中提取,转到索引槽 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 的内部合并引擎——"merge as a verb"——也被git cherry-pickgit revertgit rebase使用。 使用这些提交时,"合并基"提交(填充插槽 1 的实体)是通过不同的过程选择的,而"我们的"和"他们的"提交(进入插槽 2 和 3 的内容,例如--ours,由git checkoutgit restore命名)并不总是简单的。 我将把细节留给其他StackOverflow问答,因为这已经足够消化了。

这是我对这个问题的回答的(略微)修改版本。

每当您需要帮助合并单个文件时,我发现最好编写一个自定义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)工具只需编辑差异标记和封闭部分,通过正则表达式检测它们。 这样做的好处是它保留了 merge 命令中的 diff 选项,并允许您手动解决一些冲突,然后自动解决其余冲突。 它的缺点是,如果文件中有其他冲突标记,则可能会混淆。

这些都由运行git mergetool -t (get|merge|keep)(ours|theirs) [<filename>]使用,如果未提供<filename>,它将处理所有冲突的文件。

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

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

注意:如果未diff3merge.conflictstyle,则需要改为/^=======/sed规则中的/^|||||||/模式。

最新更新