【发布时间】:2016-06-09 03:02:21
【问题描述】:
我如何告诉 git 从一个特定的分支中获取所有冲突的文件?
我在将 branch1 合并到 branch2 时发生合并冲突。我可以说从 branch1 中取出所有冲突的文件而不是添加每个文件吗?
【问题讨论】:
标签: git git-branch git-merge git-merge-conflict
我如何告诉 git 从一个特定的分支中获取所有冲突的文件?
我在将 branch1 合并到 branch2 时发生合并冲突。我可以说从 branch1 中取出所有冲突的文件而不是添加每个文件吗?
【问题讨论】:
标签: git git-branch git-merge git-merge-conflict
你可以。
你可能不应该,但你可以。 (最多,您可能应该使用-X ours 或-X theirs。)
在所有设置之后,确保阅读此答案的任何人都了解正在发生的事情,以下是简单(尽管有时太)简单的方法来选择“我们的”或“他们的”文件每个冲突。
$ git status # make sure everything is clean
[git status is here]
$ git checkout branch2 # get onto branch2
[git's message about checking out the branch is here]
$ git merge branch1 # merge from branch1 into branch2, as in your text
... conflict messages ...
您现在有一些成功的合并和一些冲突。例如,也许通用合并基础有文件a.a、b.b、c.c 等到z.z,并且由于——即与通用合并基础提交相比,前三个(@ 987654333@、b.b 和 c.c)在 branch1 中进行了修改,后三个(b.b、c.c 和 d.d)在 branch2 中进行了修改。显然对a.a 的更改没有冲突,对d.d 的更改也没有冲突,但可能是b.b 和c.cconflict,在branch1 到b.b 中进行了几个修复,以及一个不同的修复b.b 在 branch2 中制作,但两个修复重叠。 c.c 发生了类似的事情,导致那里发生冲突。
您现在想要放弃在branch2(或branch1)中所做的所有修复,方法是从branch1(或branch2)获取文件版本。你可以使用这个命令:
git checkout branch1 -- b.b c.c
或:
git checkout branch2 -- b.b c.c
但这需要知道有问题的两个文件是b.b 和c.c。
您也可以这样做:
git checkout --ours -- b.b c.c
或:
git checkout --theirs -- b.b c.c
这些几乎与使用名称 branch1 和 branch2 的 git checkout 命令相同,但有两个很大的不同。我们稍后会谈到这些。
我在上面提到了合并基础,没有定义它。了解合并基础是一个好主意,这样您就可以知道 Git 在做什么以及 您 在做什么。有一个precise definition (which is also a bit complicated),但简单地说,当我们查看两个分支上的提交链时,合并基础是最近的共同祖先提交。也就是说,如果我们绘制(部分)提交图,我们通常会看到如下内容:
o--o--o--o <-- branch1
/
...--o--*
\
o--o--o <-- branch2
两个分支有两个不同的提示提交(分支 names 指向)。这些提交指向它们的父提交,它们指向它们的父提交,依此类推。也就是说,每个o(代表提交图中的一个提交节点)都有一个指向其父提交的左箭头。 (一个 merge 提交有两个或多个这样的箭头;上面显示了一个简单的情况,其中没有合并。)最终这两个分支应该(通常)会合,并且从那个点向左 - 向后及时——所有提交现在都在两个分支上。
最右边的提交是merge base。
在进入自动部分之前,我们需要看看 Git 如何定义索引或暂存区域。
索引本质上是“Git 将在您所做的下一个 提交中放入的内容”。也就是说,对于提交中的每个文件,它都有一个条目。当您刚刚提交时,索引对每个(跟踪的)文件都有一个条目,并且该条目与您刚刚提交的跟踪文件匹配。每次提交都有您工作树的完整快照(无论如何,所有跟踪文件的快照),并且该快照是根据索引制作的,并且索引仍然与刚才相同。
当您git add 一个文件时,您将索引版本替换为工作树中的版本。也就是说,您可以编辑b.b 以添加一些新内容,然后git add b.b 将新 b.b 内容放入索引中。现有的a.a 和c.c 等都保持不变,但b.b 被替换为工作树版本。
(如果你 git add 一个全新的文件,例如 README,那个新文件进入索引,现在 README 被“跟踪”。如果你 git rm 一个文件,Git 会放置一个特殊的“ white-out” 进入索引,将文件标记为“将被排除在下一次提交之外”,即使它当前仍在跟踪。在大多数情况下,您可以忽略这些小细节,只需将索引视为“不过,您可以进行下一次提交”。)
不过,在合并期间,索引扮演了一个新角色。
事实上,索引中有 四个 槽,而不仅仅是一个,用于每个跟踪的文件。通常 Git 只使用零槽 (0),这是正常的、非合并冲突的文件所在的位置。当 Git 在合并过程中遇到冲突时,它会将每个文件的多个副本放入索引中,位于插槽 1、2 和/或 3 中,并将插槽 0 留空。
插槽 1 用于文件的合并基础版本。插槽 2 保存本地(当前或--ours)分支版本,插槽 3 保存另一个(待合并或--theirs)版本。在上面的示例中,我们使用了git checkout branch2,因此插槽 2 保存了b.b 的branch2 版本,而插槽 3 保存了b.b 的branch1 版本(因为我们使用了git merge branch1)。同时,插槽 1 保存合并基础版本,来自提交 *。
其中一些插槽可能是空的:例如,如果在两个分支中都添加了一个名为 new.new 的文件,因此它没有合并基础版本,插槽 1 将是空的;将只有 slot-2 和 slot-3 条目。
--ours 和 --theirs 标志 git checkout 告诉 git checkout 使用插槽 2 或插槽 3。没有 -- 标志来提取版本 1(合并基础版本),但它 是可能得到它;有关:<em>n</em>:path 语法,请参见the gitrevisions documentation。
您当然也可以使用分支names,但这有两种微妙的不同,我们稍后会看到。
要查找未合并的文件,我们只需要找到任何处于此未合并状态的路径即可。一种方法是使用git status,但这有点棘手。更好的方法是使用所谓的“管道命令”,尤其是git ls-files。 ls-files 命令有--stage 以显示所有索引条目及其阶段编号。我们想知道在第 1 阶段、第 2 阶段和/或第 3 阶段中存在哪些文件:
git ls-files --stage
产生这样的输出:
[mass snip]
100755 2af1beec5f6f330072b3739e3c8b855f988173a9 0 t/t6020-merge-df.sh
100755 213deecab1e81685589f9041216b7044243acff3 0 t/t6021-merge-criss-cross.sh
100755 05ebba7afa29977196370d178af728ec1d0d9a81 0 t/t6022-merge-rename.sh
[more mass snip]
输出格式具体有阶段编号(0 到 3)后跟一个文本制表符,后跟文件名,所以我们要排除阶段 0,收集阶段 1-3 的名称。有很多方法可以做到这一点,例如grep -v $'0\t',但我们还需要拆分文件名并使其唯一(因为如果它在所有 3 个插槽中会出现多次)。我们可以使用awk 一次性完成所有操作:
git ls-files --stage | awk '$3 != 0 { path[$4]++; } END { for (i in path) print i }'
(此代码仍有一些缺陷,因为它对名称包含空格的文件做了错误的事情。修复它留作练习。)
更复杂的 awk(或其他语言)脚本会检查每个路径实际上是否存在三个版本,并针对其他情况执行某些操作(具体取决于您的目标)。
请注意,git ls-files 有一个标志 -u 或 --unmerged,用于显示仅未合并的文件。输出仍然是--stage 格式(实际上--unmerged 强制--stage,正如the documentation 所说)。我们可以使用它来代替检查阶段编号。
一旦我们有了一个未合并文件的列表,我们只需对它们运行git checkout --ours -- 或git checkout --theirs --:
git ls-files --unmerged | \
awk '{ path[$4]++; } END { for (i in path) print i }' |
xargs git checkout --ours --
在此之后我们仍然需要将它们标记为已解决,如果我们使用--ours。
--ours 等的不那么微妙的问题假设上面是git checkout --ours -- b.b。然后我们必须git add b.b 来标记文件已解析,即通过将b.b 的工作树版本放入stage-slot-zero 来清除阶段1-3。
如果我们使用git checkout branch2 -- b.b,我们不必必须git add b.b。
你可能想知道为什么。我当然愿意。我可以告诉你,这是因为git checkout branch2 -- b.b 将b.b 的版本从branch2 指向的提交复制到阶段槽零,清除槽1-3,然后从那里复制到工作树;但是git checkout --ours -- b.b 将b.b 的版本从stage-slot-3 (--ours) 复制到工作树中。
也就是说,git checkout 有时将复制到索引然后复制到工作树,有时只是从索引槽复制到工作树.我无法解释的是 为什么 git checkout 有时必须先写入索引,有时则不需要,除了“在编写了一堆其他代码之后以这种方式编写代码更容易。 "换句话说,这里没有明显的主要设计原则在起作用。你可以编一个:“如果checkout要从一棵树中读取,它应该写入索引,它写入slot-0并清除1-3;但是如果它只能从索引中读取,它就不应该写入到索引。”但这似乎很随意。
--ours 等更微妙的问题当 Git 准备合并并设置所有这些索引槽时,它还会进行 重命名检测。假设我们有a.a、sillyname.b 和c.c,而不是文件a.a 到z.z。假设sillyname 在branch1 中变成了b.b,但在branch2 中变成了bettername.b。
Git 通常会自动检测这种重命名。然后它将用文件的内容填充插槽 1、2 和 3,在该特定提交中命名。因为我们在branch2 并且文件现在命名为bettername.b,所以索引name 将是bettername.b。插槽 1 中的条目将是提交 * 的 sillyname.b 的条目。插槽 2 中的条目,即我们的版本,将用于我们的bettername.b。插槽 3 中的条目,即另一个 (--theirs) 版本,将用于 b.b。
同样,它们都在我们的新名称下,所以我们需要git checkout --ours -- bettername.b 或git checkout --theirs -- bettername.b。如果我们尝试git checkout branch1 -- bettername.b,Git 会抱怨在branch1 中找不到bettername.b。事实上,它不能:它在那里被称为b.b。
有时您可能不关心,或者可能想发现这种重命名检测。在这种情况下,您可能希望使用分支名称,而不是 --ours 或 --theirs 参数。您只需要注意区别。
-X ours 和 -X theirs 不同(可能是你想要的)我在上面提到了branch1 中有多个对b.b 的修复,而branch2 中只有一个修复(与branch1 中的修复冲突)的情况。
冲突可能只发生在一个领域。也就是说,假设有两个与更主要的修复无关的小拼写错误。如果我们采用 b.b 的 --ours 版本,我们将失去两个拼写修复,即使我们采用了高级代码修复。
如果我们告诉 merge 命令使用-X ours,这表示在发生冲突的情况下,使用我们的更改,但在我们所做的b.b 的某些更改的情况下什么也不做,继续他们的改变。在这种情况下,我们将选择他们的两个拼写修复。
因此,git merge -X ours 与后来的git checkout --ours非常不同。
如果你想重新解决合并冲突——如果你解决了一个文件,但后来又重新考虑了——git checkout 有另一个标志 -m,这样做:
git checkout -m b.b
这“重新合并”文件,将冲突标记放回。(它实际上使用一组特殊的“撤消”索引条目,因为合并基础版本可能是虚拟合并基础而不是单个提交. 只能在提交合并之前使用;之后,您必须重新进行整个合并。)
【讨论】:
git merge命令的-s/--strategy选项指定的合并策略可以告诉git应该使用哪个分支。你也可以看here
【讨论】:
-s ours 应该谨慎使用,如果有的话。