我如何告诉 git 从一个特定的分支中获取所有冲突的文件?
将分支 1 合并到分支 2 时出现合并冲突。我可以说从 branch1 中获取所有冲突的文件,而不是添加每个文件。
我如何告诉 git 从一个特定的分支中获取所有冲突的文件?
将分支 1 合并到分支 2 时出现合并冲突。我可以说从 branch1 中获取所有冲突的文件,而不是添加每个文件。
你可以。
你可能不应该,但你可以。(最多,您可能应该使用-X ours
or -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
, 并且由于——即,与共同合并基础提交相比——前三个 ( a.a
, b.b
, 和c.c
) 被修改,后branch1
三个 ( b.b
, c.c
, 和d.d
) 在 中进行了修改branch2
。显然,对 的更改a.a
没有冲突,对 的更改d.d
没有冲突,但可能会b.b
发生冲突,在分支 2 中进行c.c
了几个修复和一个不同的修复,但是这两个修复重叠。发生了类似的事情,导致那里发生冲突。branch1
b.b
b.b
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
这些与使用名称和的命令几乎相同,但有两个很大的不同。我们稍后会谈到这些。git checkout
branch1
branch2
我在上面提到了合并基础,没有定义它。了解合并基础是一个好主意,这样您就知道 Git 在做什么,从而知道您在做什么。有一个精确的定义(也有点复杂),但简单地说,当我们查看两个分支上的提交链时,合并基础是最近的共同祖先提交。也就是说,如果我们绘制(部分)提交图,我们通常会看到如下内容:
o--o--o--o <-- branch1
/
...--o--*
\
o--o--o <-- branch2
这两个分支有两个不同的提示提交(分支名称指向)。这些提交指向它们的父提交,它们指向它们的父提交,依此类推。也就是说,每个o
(代表提交图中的提交节点)都有一个指向其父提交的左箭头。(合并提交有两个或更多这样的箭头;上面显示了一个简单的情况,其中没有合并。)最终这两个分支应该(通常)会合,并且从那个点向左 - 在时间上向后 - 现在所有提交在两个分支上。
*
我画的最右边的提交o
是合并基础。
在我们进入自动部分之前,我们需要看看 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 保存了 的branch2
版本,b.b
而插槽 3 保存了 的branch1
版本b.b
(因为我们做了git merge branch1
)。同时,插槽 1 包含来自 commit 的合并基础版本*
。
其中一些插槽可能是空的:例如,如果new.new
在两个分支中都添加了一个名为的文件,因此它没有合并基础版本,插槽 1 将是空的;将只有 slot-2 和 slot-3 条目。
和标志告诉使用槽 2 或槽 3。没有用于提取版本 1(合并基础版本)的标志--ours
,但可以获得它;有关语法,请参阅gitrevisions 文档。--theirs
git checkout
git checkout
--
:n:path
当然,您也可以使用分支名称,但这有两种微妙的不同,我们稍后会看到。
要查找未合并的文件,我们只需要找到处于此未合并状态的任何路径。一种方法是使用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
force --stage
,正如文档所说)。我们可以使用它来代替检查阶段编号。
一旦我们有了一个未合并文件的列表,我们只需在它们上运行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
etc的不那么微妙的问题假设上面确实如此git checkout --ours -- b.b
。然后我们必须标记文件已解决,即通过将工作树版本放入阶段槽零git add b.b
来清除阶段 1-3 。b.b
但是,如果我们这样做git checkout branch2 -- b.b
,我们就不必这样做git add b.b
。
你可能想知道为什么。我当然愿意。我可以告诉你,这是因为git checkout branch2 -- b.b
将版本b.b
从提交到哪个branch2
点复制到阶段槽零,清除槽 1-3,然后从那里复制到工作树;但将stage-slot-3 ( )git checkout --ours -- b.b
的版本复制到工作树中。b.b
--ours
也就是说,git checkout
有时复制到索引中,然后复制到工作树,有时只是从索引槽复制到工作树。我无法解释的是为什么 git checkout
有时必须先写入索引,有时则不需要,除了“在编写了一堆其他代码之后以这种方式编写代码更容易”。换句话说,这里没有明显的主要设计原则在起作用。您可以编造一个:“如果checkout
要从树中读取,则应写入索引,该索引写入零槽并清除1-3;但如果只能从索引中读取,则不应写入索引。” 但这似乎很随意。
--ours
etc的更微妙的问题当 Git 准备合并并设置所有这些索引槽时,它也会重命名 detection。假设我们有,和, 而不是a.a
通过的文件。假设变成in但变成in 。z.z
a.a
sillyname.b
c.c
sillyname
b.b
branch1
bettername.b
branch2
Git 通常会自动检测这种重命名。然后它将使用特定提交中命名的文件内容填充插槽 1、2 和 3 。由于我们打开branch2
并且文件现在命名为bettername.b
,因此索引名称将为bettername.b
. 插槽 1 中的条目将是 commit 的*
条目sillyname.b
。插槽 2 中的条目,即我们的版本,将用于我们的bettername.b
. 插槽 3 中的条目,即另一个 ( --theirs
) 版本,将用于b.b
.
同样,它们都在我们的新名称下,所以我们需要git checkout --ours -- bettername.b
or git checkout --theirs -- bettername.b
。如果我们尝试这样做git checkout branch1 -- bettername.b
,Git 会抱怨它无法bettername.b
在branch1
. 事实上,它不能:它在b.b
那里被调用。
有时您可能不在乎,或者可能想发现这种重命名检测。在这种情况下,您可能希望使用分支名称,而不是--ours
or--theirs
参数。你只需要意识到差异。
-X ours
和-X theirs
不同(可能是你想要的)b.b
我在上面提到了对in有多个修复的情况branch1
,并且在 中只有一个修复(与 中的修复冲突branch1
)branch2
。
冲突可能只发生在一个领域。也就是说,假设有两个与更主要的修复无关的小拼写错误。如果我们采用 的--ours
版本b.b
,我们将失去两个拼写修复,即使我们采用了高级代码修复。
如果我们告诉合并命令使用-X ours
,这表示在发生冲突的情况下,使用我们的更改,但是如果对b.b
我们没有做任何事情的地方进行了一些更改,请继续进行更改。在这种情况下,我们将选择他们的两个拼写修复。
因此,git merge -X ours
与后来的git checkout --ours
.
如果你想把合并冲突放回去——如果你解决了一个文件,但后来又重新考虑了——<code>git checkout 有另一个标志,-m
,它会这样做:
git checkout -m b.b
这会“重新合并”文件,将冲突标记放回原处。(它实际上使用一组特殊的“撤消”索引条目,因为合并基础版本可能是虚拟合并基础而不是单个提交。它可以只能在提交合并之前使用;之后,您必须重新执行整个合并。)
-s/--strategy
命令选项指定的合并策略git merge
可以告诉git应该使用哪个分支。你也可以看这里