git checkout --ours 不会从未合并的文件列表中删除文件

git checkout --ours does not remove files from unmerged files list

嗨,我需要像这样合并两个分支。

这只是一个例子,我处理了数百个需要解析的文件。

git merge branch1
...conflicts...
git status
....
# Unmerged paths:
#   (use "git add/rm <file>..." as appropriate to mark resolution)
#
#   both added:   file1
#   both added:   file2
#   both added:   file3
#   both added:   file4
git checkout --ours file1
git chechout --theirs file2
git checkout --ours file3
git chechout --theirs file4
git commit -a -m "this should work"
U   file1
fatal: 'commit' is not possible because you have unmerged files.
Please, fix them up in the work tree, and then use 'git add/rm <file>' as
appropriate to mark resolution and make a commit, or use 'git commit -a'.

当我执行 git merge tool 时,'ours' 分支中有正确的内容,当我保存它时,该文件从未合并列表中消失。但是因为我有数百个这样的文件,所以这不是一个选择。

我认为这种方法会把我带到我想去的地方——轻松地说出我想保留哪个分支的哪个文件。

但我想我误解了合并后 git checkout --ours/theirs 命令的概念。

能否请您提供一些信息,如何处理这种情况?我用 git 1.7.1

这主要是 git checkout 内部工作方式的一个怪癖。 Git 人倾向于让实现决定接口。

最终的结果是git checkout--ours--theirs之后,如果要解决冲突,也必须git add相同的路径:

git checkout --ours -- path/to/file
git add path/to/file

但是不是其他形式的情况git checkout:

git checkout HEAD -- path/to/file

或:

git checkout MERGE_HEAD -- path/to/file

(这些在多个方面略有不同)。在某些情况下,这意味着最快的方法是使用中间命令。 (顺便说一下,这里的 -- 是为了确保 Git 可以区分路径名和选项或 b运行ch 名。例如,如果你有一个名为 --theirs,它看起来像是一个选项,但 -- 会告诉 Git 不,它实际上是一个路径名。)

要了解这一切在内部是如何工作的,以及为什么您需要单独的 git add 除非您不需要,请继续阅读。 :-) 首先,让我们快速回顾一下合并过程。

合并,第 1 部分:合并如何开始

当你 运行:

$git合并<em>commit-or-branch</em>

Git 做的第一件事是找到命名提交和当前 (HEAD) 提交之间的合并基础。 (请注意,如果您在此处提供 b运行ch 名称,如 git merge otherbranch、Git t运行 将其命名为 commit-ID,即b运行ch。它保存最终合并日志消息的 b运行ch 名称参数,但需要提交 ID 才能找到合并基础。)

找到合适的合并基地后,1 Git 会生成两个 git diff 列表:一个从合并基地到 HEAD,一个从合并基础到您确定的提交。这得到 "what you changed" 和 "what they changed",Git 现在必须合并。

对于您进行了更改但没有更改的文件,Git 可以只采用您的版本。

对于他们进行了更改而您没有更改的文件,Git 可以采用他们的版本。

对于你们都进行了更改的文件,Git 必须进行一些真正的合并工作。它逐行比较更改,看是否可以将它们组合起来。如果它 可以 将它们结合起来,它就会这样做。如果合并看起来——再次基于纯粹的 line-by-line 比较——冲突,Git 为该文件声明一个 "merge conflict"(并继续尝试合并,但留下冲突标记到位)。

一旦 Git 合并了它所能合并的所有内容,它要么完成合并(因为没有冲突),要么因合并冲突而停止。


1如果绘制提交图,合并基础是显而易见的。不画图,有点神秘。这就是为什么我总是告诉人们绘制图形,或者至少尽可能多地画出有意义的图形。

技术上的定义是merge base是提交图中的"lowest common ancestor"(LCA)节点。用不太专业的术语来说,它是您当前的 b运行ch 与您正在合并的 b运行ch 合并的最新提交。也就是说,通过记录每个合并的父提交 ID,Git 能够找到两个 b运行ches 在一起的 last 时间,从而找出两者你做了什么,他们做了什么。但是,要使它完全起作用,Git 必须记录每次合并。具体来说,它必须将两个(或全部,对于 so-called "octopus" 合并)父 ID 写入新的合并提交。

在某些情况下,有不止一个合适的合并基础。然后,该过程取决于您的合并策略。默认的 recursive 策略将合并多个合并基础以产生 "virtual merge base"。这种情况很少见,您现在可以忽略它。


合并,第 2 部分:因冲突而停止,以及 Git 的 "index"

当Git确实以这种方式停止时,它需要给你一个机会来解决冲突。但这也意味着它需要记录冲突,这就是Git的"index"—也称为"the staging area",有时"the cache"——真是自得其乐。

对于您 work-tree 中的每个暂存文件,索引有 最多四个 条目,而不是只有一个条目。其中最多三个实际使用过,但有四个插槽,编号为 03

槽零用于解析文件。当您使用 Git 而不进行合并时,仅使用 插槽零。当你在工作树中编辑一个文件时,它有 "unstaged changes",然后你 git add 文件和更改被写入存储库,更新槽零;您的更改现在是 "staged".

插槽 1-3 用于未解析的文件。当 git merge 必须因合并冲突而停止时,它会将槽 0 留空,并将所有内容写入槽 1、2 和 3。文件的 merge base 版本是记录在插槽 1 中,--ours 版本记录在插槽 2 中,ad --theirs 版本记录在插槽 3 中。这些非零插槽条目是 Git 知道文件未解析的方式。2

解析文件时,您 git add 它们会擦除所有插槽 1-3 条目并写入 slot-zero、staged-for-commit 条目。这就是 Git 知道文件已解析并准备好进行新提交的方式。 (或者,在某些情况下,您 git rm 文件,在这种情况下 Git 将一个特殊的 "removed" 值写入槽零,再次擦除槽 1-3。)


2在少数情况下,这三个插槽之一也是空的。假设文件 new 在合并库中不存在,并且在我们的和他们的中都添加了。然后:1:<em>new</em>留空,:2:<em>new</em>:3:<em>new</em>记录了add/add冲突。或者,假设文件 f 确实存在于 base 中,在我们的 HEAD b运行ch 中被修改,并在它们的 b运行ch 中被删除.然后:1:<em>f</em>记录基础文件,:2:<em>f</em>记录了我们的文件版本,而:3:<em>f</em>为空,记录了modify/delete冲突。

对于modify/modify冲突,三个槽都被占用;只有当一个文件丢失时,这些插槽之一才会为空。逻辑上不可能有两个空位:没有 delete/delete 冲突,也没有 nocreate/add 冲突。但是 rename 冲突有些奇怪,我在这里省略了,因为这个答案足够长了!在任何情况下,正是插槽 1、2、and/or 3 中某些值的存在将文件标记为未解析。


合并,第 3 部分:完成合并

一旦所有文件都被解析——所有条目只在zero-numbered槽中——你可以git commit合并结果。如果 git merge 能够在没有帮助的情况下进行合并,它通常会为您 运行s git commit,但实际提交仍然由 运行ning git commit 完成.

commit 命令的工作方式与往常一样:它将索引内容转换为 tree 对象并写入新的提交。合并提交的唯一特殊之处在于它有多个父提交 ID。3 额外的父提交来自 git merge 留下的文件。默认的合并消息也来自一个文件(实际上是一个单独的文件,尽管原则上它们可以合并)。

请注意,在所有情况下,新提交的内容都由索引的内容决定。此外,一旦新的提交完成,索引仍然是满的:它仍然包含相同的内容。默认情况下,git commit 此时不会进行另一个新提交,因为它发现索引与 HEAD 提交匹配。它调用此 "empty" 并要求 --allow-empty 进行额外提交,但索引根本不是 empty。它仍然很满——它只是充满了 HEAD 提交相同的东西


3这假设您正在进行真正的合并,而不是挤压合并。在进行压缩合并时,git merge 故意 将额外的父 ID 写入额外的文件,以便新的合并提交只有一个父。 (出于某种原因,git merge --squash 也抑制了自动提交,就好像它也包含 --no-commit 标志一样。不清楚为什么,因为你 可以 只是 运行 git merge --squash --no-commit 如果你想要 自动提交被抑制。)

壁球合并不记录其其他父项。这意味着如果我们再次合并 ,一段时间后,Git 将不知道 从哪里开始差异 。这意味着如果您打算放弃另一个 b运行ch,您通常应该只 squash-merge。 (有一些巧妙的方法可以结合挤压合并和真正的合并,但它们超出了这个答案的范围。)


如何git结帐<em>b运行ch</em>使用索引

完成所有这些之后,我们还必须看看 git checkout 如何使用 Git 的索引。请记住,在正常使用中,只有零号槽被占用,并且索引对每个暂存文件都有一个条目。此外,该条目 与当前 (HEAD) 提交 匹配,除非您修改了文件并 git add-ed 结果。它还 匹配 work-tree 中的文件,除非您修改了该文件。4

如果你在一些 b运行ch 并且你 git checkout 一些 other b运行ch,Git 尝试切换到另一个 b运行ch。为了成功,Git 必须用另一个 b运行ch.

的条目替换每个文件的索引条目

为了具体起见,假设您在 master 并且正在做 git checkout branch。 Git 将每个当前索引条目与它需要在 b运行ch branch 的 tip-most 提交上的索引条目进行比较。日问题是,文件 README.txtmaster 内容 branch 的内容 相同,还是不同?

如果内容相同,Git可以放轻松,继续下一个文件。如果内容不同,Git 必须对索引条目做一些事情。 (正是在这一点附近,Git 检查 work-tree 文件是否也与索引条目不同。)

具体来说,如果 branch 的文件与 master 的不同,git checkout 必须 替换 索引条目使用来自 branch 的版本——或者,如果 README.txtbranch 的提示提交中 不存在 ,Git 必须删除 索引条目。而且,如果git checkout要修改或删除索引项,还需要修改或删除work-tree文件。 Git 确保这是一件安全的事情,即 work-tree 文件与 master 提交的文件相匹配,然后它会让你切换 b运行ches.

换句话说,这就是 Git 确定是否可以更改 b运行ches 的方式(以及原因)——无论您是否有修改会因从 [=55 切换而被破坏=] 到 branch。如果您在 work-tree、 中进行了修改,但 修改后的文件 在两个 b运行 中相同, Git 可以只保留索引和 work-tree 中的修改。它可以并且会提醒您这些修改过的文件 "carried over" 进入新的 b运行ch:很简单,因为无论如何它都必须检查这个。

一旦所有测试都通过并且 Git 决定可以从 master 切换到 branch — 或者如果您指定 --forcegit checkout 实际上用所有更改(或删除)的文件更新索引,并更新 work-tree 以匹配。

请注意,所有此操作都使用了槽零。根本没有插槽 1-3 条目,因此 git checkout 不必删除任何此类内容。您不在有冲突的合并中,您 运行 git checkout branch 不只是检查 一个文件 ,而是检查整个 set 个文件并切换 b运行ches.

另请注意,您可以签出特定提交,而不是签出 b运行ch。例如,这就是您查看之前提交的方式:

$ git log
... peruse log output ...
$ git checkout f17c393 # let's see what's in this commit

此处的操作与检出 b运行ch 相同,只是不使用 b运行ch 的 tip 提交, Git 检查任意提交。你现在不是 "on" 新的 b运行ch,而是 no b运行ch:5Git给你一个"detached HEAD"。要重新装上你的头,你必须 git checkout mastergit checkout branch 才能取回 "on" b运行ch。


4索引条目可能与 work-tree 版本不匹配,如果 Git 正在进行特殊的 CR-LF 结束修改,或应用污迹过滤器.这变得非常先进,最好的办法是暂时忽略这种情况。 :-)

5更准确地说,这使您进入一个 anonymous(未命名)b运行ch,它将从当前提交。如果您进行新的提交,您将保持分离的 HEAD 模式,一旦您 git checkout 其他提交或 b运行ch,您将切换到那里并且 Git 将 "abandon" 你所做的承诺。这种分离的 HEAD 模式的要点是让你环顾 让你做出新的提交,如果你不这样做, 消失不要采取特殊行动来拯救他们。但是,对于 Git 相对较新的任何人来说,提交 "just go away" 并不是一件好事——所以无论何时,请确保您知道自己处于这种 "detached HEAD" 模式。

git status 命令会告诉您是否处于分离 HEAD 模式。经常使用它。6 如果你的 Git 是旧的(OP 的是 1.7.1,现在已经很旧了),git status 没有它那么有用在 Git 的现代版本中,但总比没有好。

6一些程序员喜欢将密钥 git status 信息编码到每个 command-prompt 中。我个人不会走这么远,但不失为一个好主意。


签出特定文件,以及为什么它有时会解决合并冲突

不过,git checkout 命令还有其他操作模式。特别是,您可以 运行 git checkout [flags etc] -- path [path ...] 检出特定文件。这就是事情变得奇怪的地方。请注意,当您使用 this 形式的命令时,Git 不会 检查以确保您没有覆盖文件。7

现在,您不是更改 b运行ches,而是告诉 Git 从某处获取一些特定文件,并将它们放入 work-tree,覆盖无论那里有什么,如果有的话。棘手的问题是:Git 从哪里获取这些文件?

一般来说,Git保存文件的地方有3个:

  • 在提交中;8
  • 在索引中;
  • 并在 work-tree.

checkout 命令可以从前两个地方读取,并且总是将结果写入work-tree。

git checkout 从提交中获取文件时,它首先 将其复制到索引 。每当它这样做时,它都会将文件写入槽零。如果插槽 1-3 已被占用,则写入插槽 0 会清除它们。 git checkout 从索引中获取文件时,不必将其复制到索引中。 (当然不是:它已经存在了!)这就是当您 不是 在合并过程中时 git checkout 的工作方式:您可以 git checkout -- path/to/file 获得返回索引版本。9

不过,假设您 处于冲突合并的中间,并且将要 git checkout 某些路径,可能 --ours。 (如果你不在合并的中间,插槽 1-3 中没有任何内容,--ours 没有意义。)所以你 运行 git checkout --ours -- path/to/file.

这个 git checkout 从索引中获取文件——在本例中,从索引槽 2 中获取文件。因为它已经在索引中,所以 Git 不会将 写入 的索引,刚好到work-tree。所以文件没有解决!

git checkout --theirs也是如此:它从索引(插槽 3)获取文件,但不解析任何内容。

但是:如果你git checkout HEAD -- path/to/file,你是在告诉git checkoutHEAD提交中提取。由于这是一个 commit,Git 首先将文件内容写入索引。这将写入插槽 0 并擦除 1-3。现在文件已解析!

因为在冲突合并期间,Git 将 being-merged 提交的 ID 记录在 MERGE_HEAD 中,您还可以 git checkout MERGE_HEAD -- path/to/file 从其他提交中获取文件。这也是从提交中提取的,因此它写入索引,解析文件。


7我经常希望 Git 为此使用不同的 front-end 命令,因为我们可以明确地说 git checkout 是安全的,它不会覆盖没有 --force 的文件。但是这种 git checkout 故意覆盖文件!

8这是一个谎言,或者至少是一个谎言:提交不直接包含文件。相反,提交包含一个指向 tree 对象的(单个)指针。此树对象包含其他树对象和 blob 对象的 ID。 blob 对象包含实际文件内容。

事实上,索引也是如此。每个索引槽包含的不是实际文件 内容,而是存储库中 blob 对象的哈希 ID。

不过,就我们的目的而言,这并不重要:我们只要求 Git 检索 commit:path 并找到树和我们的 blob ID。或者,我们要求 Git 检索 :n:path,它会在 path[=395 的索引条目中找到 blob ID =] 插槽 n。然后它获取文件的内容,我们就可以开始了。

此 colon-and-number 语法适用于 Git,而 --ours--theirs 标志仅适用于 git checkoutgitrevisions.

中描述了有趣的冒号语法

9git checkout -- path的use-case是这样的:假设,不管你是否合并,你对一个文件做了一些修改,测试,发现这些更改有效,然后 运行 git add 在文件上。然后你决定做更多的改变,但没有再运行 git add。您测试第二组更改并发现它们是错误的。要是你能把文件的 work-tree 版本设置回你刚才 git add 编辑的版本就好了....啊哈,你 可以 : 你 git checkout -- path 和 Git 复制索引版本,从槽零,回到 work-tree.


细微行为警告

但是请注意,使用 --ours--theirs 除了 "extract from index and therefore don't resolve" 行为之外还有另一个细微的差别。假设在我们的冲突合并中,Git 检测到某个文件被 重命名 。也就是说,在合并库中,我们有文件 doc.txt,但现在在 HEAD 中我们有 Documentation/doc.txtgit checkout --ours 我们需要的路径是 Documentation/doc.txt。这也是HEAD提交中的路径,所以git checkout HEAD -- Documentation/doc.txt.

就可以了

但是,如果在我们合并的提交中,doc.txt 没有 重命名怎么办?在这种情况下,我们应该10能够git checkout --theirs -- Documentation/doc.txt从索引中得到他们的doc.txt。但是如果我们尝试 git checkout MERGE_HEAD -- Documentation/doc.txt,Git 将无法找到该文件:它不在 Documentation 中,在 MERGE_HEAD 提交中。我们必须 git checkout MERGE_HEAD -- doc.txt 才能获取他们的文件……而那 不会 解析 Documentation/doc.txt。事实上,它只会创建 ./doc.txt(如果它被重命名,几乎可以肯定没有 ./doc.txt,因此 "create" 比 "overwrite" 更好。

因为合并使用了 HEAD 的名字,所以一步到 git checkout HEAD -- path 到 extract-and-resolve 通常是足够安全的。如果您正在处理文件并且已经 运行ninggit status,你应该知道他们是否有一个重命名的文件,因此通过放弃你自己的更改一步到 git checkout MERGE_HEAD -- path 到 extract-and-resolve 是否安全。但是您仍然应该意识到这一点,并且知道如果 需要关注重命名该怎么做。


10我在这里说"should",而不是"can",因为Git目前忘记了重命名有点太早了。因此,如果使用 --theirs 获取您在 HEAD 中重命名的文件,您也必须在此处使用旧名称,然后在 work-tree.

中重命名该文件