git checkout --ours不会从未合并的文件列表中删除文件 [英] git checkout --ours does not remove files from unmerged files list

查看:214
本文介绍了git checkout --ours不会从未合并的文件列表中删除文件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

你好,我需要合并两个分支。



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

  git merge branch1 
...冲突...
git status
....
#未合并路径:
#(使用git add / rm< file> ...适当标记分辨率)

#两者都添加:file1
#同时添加:file2
#同时添加:file3
#同时添加:file4
git checkout --ours file1
git chechout - 他们的文件2
git checkout --oursour file3
git chechout - 他们的文件4
git commit -a -m这应该工作
U file1
致命的:'commit'是不可能的,因为你有未合并的文件。
请修正它们在工作树中,然后使用'git add / rm< file>'作为
来标记分辨率并进行提交,或使用'git commit -a'。

当我做 git merge tool 时,那里是来自'我们'分支的正确内容,当我保存它时,文件从未合并列表中消失。但是,因为我有这样的数百个文件,所以这不是一个选项。



我认为这种方法会使我想到我想要的地方 - 轻松地从哪个文件哪个分支我想保留。



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



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

解决方案

git checkout 可以在内部使用。最终的结果是,在 git checkout 之后的结果是 code> - 我们的或 - 他们的,如果您想解决冲突,您还必须 git add 相同的路径:

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

但这是 not 其他形式的 git checkout

  git checkout HEAD  -  -  path / to / file 

或者:

  git checkout MERGE_HEAD  -  path / to / file 

(这些以多种方式微妙地不同)。在某些情况下,这意味着最快的方法是使用中间命令。 (顺便提一句,这里的 - 是为了确保Git能够区分路径名和选项或分支名称。例如,如果你有一个名为 - 他们的,它看起来像一个选项,但 - 会告诉Git不,它真的是一个路径名。



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



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



运行时:



$ git merge commit-or-branch

Git所做的第一件事是找到命名提交和当前( HEAD 合并基 c $ c>)提交。 (注意,如果你在这里提供了一个分支名称,比如在 git merge otherbranch 中,Git将它转换为一个提交ID,即分支的顶端,它保存分支最终合并日志消息的名称参数,但需要提交ID来查找合并基础。)



找到合适的合并基础 1 Git然后生成两个 git diff 列表:一个从合并基址到 HEAD ,另一个从merge base到您确定的提交。这会得到你改变了什么和他们改变了什么,Git现在必须合并。



对于你进行了更改而没有更改的文件,Git只需要你的版本。



对于他们进行了更改但没有更改的文件,Git可以只取其版本。



对于你们都做过修改的文件,Git必须做一些真正的合并工作。它逐行比较变化,看它是否可以合并它们。如果它可以组合它们,它就是这样做的。如果合并似乎是基于纯粹的逐行比较再次发生冲突,那么Git会为该文件声明一个合并冲突(并继续尝试合并,但留下冲突标记)。



一旦Git合并了它所能做的所有事情,它就会结束合并 - 因为没有冲突 - 或者因合并冲突而停止。






1 如果您绘制提交图形,则合并基数很明显。没有绘制图表,这有点神秘。这就是为什么我总是告诉人们绘制图表,或者至少是根据需要绘制图表来理解。

技术定义是合并基数是提交图中的最低共同祖先(LCA)节点。用不太专业的术语来说,这是当前分支与您正在合并的分支加入的最新提交。也就是说,通过记录每个合并的父提交ID,Git能够找到两个分支在一起的 last 时间,因此找出你做了什么以及他们做了什么。尽管如此,Git必须记录每个合并。具体来说,它必须将两个(或全部,为所谓的章鱼合并)父ID写入新的合并提交。



在某些情况下,一个合适的合并基地。该流程取决于您的合并策略。默认的递归策略将合并多个合并基础以产生虚拟合并基础。






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



当Git以这种方式停止时,它需要给您一个解决冲突的机会。但是这也意味着它需要记录这些冲突,这就是Git的索引(也称为暂存区域),有时甚至是高速缓存 - 的地方,它实际上是它的存在。 p>

对于工作树中的每个分段文件,索引最多有4个条目,而不仅仅是一个条目。其中至多三个实际上正在使用,但有四个插槽,编号为 0 3

插槽零用于解析文件。当你使用Git而不进行合并时,仅使用零槽被使用。当你在工作树中编辑一个文件时,它有未分离的变化,然后你 git add 这个文件,并且这些变化被写入到存储库中,您的更改现在已上演。



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



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






2 有几种情况,其中三个插槽中的一个也是空的。假设文件 new 在合并库中不存在,并且在我们和他们的两个版本中都添加了。然后:1: new 留空,:2: new :3: new 记录添加/添加冲突。或者,假设文件 f 确实存在于基础中,在我们的HEAD分支中被修改,并且在其分支中被删除。然后:1: f 记录基本文件:2: f 记录我们的文件版本,并且:3: f 为空,记录修改/删除冲突。



对于修改/修改冲突,所有三个插槽都被占用;仅当缺少一个文件时,其中一个插槽为空。逻辑上不可能有两个空插槽:不存在删除/删除冲突,以及创建/添加冲突。但是有一些与重命名冲突有关的问题,我在这里省略了,因为这个答案足够长!在任何情况下,在第1,2和/或3号插槽中存在的某些值会将文件标记为未解决。




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



一旦所有文件解析完成 - 所有条目只在零编号的位置上 - code> git commit 合并结果。如果 git merge 能够在没有帮助的情况下执行合并,它通常为您运行 git commit ,但实际提交仍然通过运行 git commit



完成commit命令的工作方式与以往一样:将索引内容放入 tree 对象中并写入新的提交。合并提交的唯一特殊之处在于它有多个父提交ID。 3 额外的父节点来自文件 git merge 离开背后。默认的合并信息也来自一个文件(实际上是一个单独的文件,虽然原则上它们可能已经合并了)。注意,在所有情况下,新提交的内容由索引的内容决定。而且,一旦新的提交完成,索引仍然是完整的:它仍然包含相同的内容。默认情况下, git commit 此时不会再创建一个新的提交,因为它看到索引匹配 HEAD 承诺。它调用这个空,并且要求 - allow-empty 来做额外的提交,但索引不是。它还是很完整的 - 它只是与 HEAD 提交完全一样。






3 这假定您正在进行真正的合并,而不是压缩合并。当进行压缩合并时, git merge 故意不会 将额外的父编号写入额外的文件,这样新的合并提交只有一个单身父母。 (出于某些原因, git merge --squash 也会禁止自动提交,就好像它包含了 - no-commit 运行 git merge --squash --no-commit ,如果你 >想要自动提交被抑制。)



一个壁球合并不会记录其他的父目录。这意味着,如果我们再次合并 ,一段时间后,Git将不知道从哪里开始比较。这意味着如果您打算放弃其他分支,通常应该只进行压缩合并。 (有些棘手的方法可以将压缩合并和真正的合并结合起来,但它们远远超出了这个答案的范围。)






git checkout 分支 使用索引



那么我们必须看看 git checkout 如何使用Git的索引。请记住,在正常使用情况下,只有槽0被占用,并且索引对每个分阶段文件都有一个条目。此外,该条目与当前( HEAD )提交匹配,除非您修改了文件并且 git add - 结果。它也可以匹配工作树中的文件,除非您修改了文件。 4



如果你在某个分支上,而你 git checkout 某些其他分支,Git会尝试切换到另一个分支。为了成功,Git必须用每个文件的索引条目替换其他分支的条目。



比方说,仅仅为了具体性,继续 master ,你正在做 git checkout branch 。 Git会将每个当前索引条目与它需要在分支 branch 的提示最多提交时使用的索引条目进行比较。也就是说,对于文件 README.txt master 内容与 相同对于分支,或者它们是不同的?



如果内容相同, Git可以很容易地转移到下一个文件。如果内容不同,Git必须对索引条目执行一些操作。 (Git检查工作树文件是否与索引条目不同)

具体来说,在分支的文件不同于 master 的, git checkout 必须<如果 README.txt doesn'将索引条目替换为分支 t 存在分支的提示提交中,Git必须删除索引条目。此外,如果 git checkout 要修改或删除索引条目,它还需要修改或删除工作树文件。 Git确保这是一件安全的事情,也就是说,在它允许您切换分支之前,工作树文件与 master commit文件相匹配。



换句话说,这是Git如何(以及为什么)发现是否可以更改分支 - 无论您是否具有通过从 master切换而被破坏的修改分支。如果您的工作树中有修改,,两个分支中的修改后的文件都是相同的, Git可以在索引和工作树中保留修改。它可以也会提醒你这些修改过的文件结转到新的分支中:easy,因为它必须检查这个。

一旦所有的测试都有并且Git已经决定可以从 master 切换到分支 - 或者如果您指定了 - - git checkout 实际上会更新所有更改(或删除)文件的索引,并更新工作树以匹配。



请注意,所有这些操作都使用了零槽。根本没有插槽1-3条目,所以 git checkout 不必删除任何这样的内容。你并没有处于冲突的合并中,而是运行 git checkout branch 来检查一个文件,而是整个 set 文件和转换分支。



还要注意,您可以不检出分支,而是检出特定的提交。例如,你可能会看到以前的提交:

  $ git log 
... peruse log输出...
$ git checkout f17c393#让我们看看这个提交中有什么

这与检查分支是一样的,除了不使用分支的 tip 提交外,Git检出任意提交。现在不用在新的分支上,你现在在 no 分支上: 5 Git为你提供了一个分离的HEAD。要重新连接你的头,你必须 git checkout master git checkout branch 来取回分支。 / p>




4 如果Git执行特殊的CR,索引条目可能与工作树版本不匹配-LF结束修改或应用涂抹过滤器。这是相当先进的,最好的情况是现在忽略这种情况。 : - )

5 更准确地说,这会让你进入一个匿名(未命名)分支,当前提交。如果你进行了新的提交,你将会保持独立的HEAD模式,并且只要你 git checkout 其他提交或分支,你就会在那里切换,Git将放弃您提交的提交。这种分离的HEAD模式的重点在于让你环顾,让你可以做出新的提交,如果你不采取特殊措施保存, 就会消失。他们。不过,对于Git相对较新的人来说,提交刚刚离开并不是那么好 - 所以确保你知道你处于这种分离的HEAD模式,无论你何时进入。



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



6 一些程序员喜欢将 git status 编码到每个命令提示符中的信息。我个人不会走这么远,但可以是一个好主意。






检查特定文件,以及为什么有时解决合并冲突



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



现在,您不需要更改分支,而是让Git从某个地方获取某些特定文件,然后将它们放入工作树中,覆盖所有内容,如果任何东西。棘手的问题是: Git获取这些文件的位置是什么?



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




  • 提交; 8

  • 在索引中;

  • 和工作树中。


结帐命令可以从前两个地方读取,总是将结果写入工作树。



> git checkout 从提交中获取文件时,它首先将其复制到索引。每当它做到这一点,它将文件写入零槽。写入插槽零将抹掉插槽1-3,如果它们被占用。当 git checkout 从索引获取文件时,它不必将其复制到索引。 (当然不是:它已经存在了!)当 在合并过程中时, git checkout code> git checkout - path / to / file 以获取索引版本。 9



假设你在冲突的合并过程中 git checkout 有一些路径,可能是 - 我们的。 (如果你不在合并的中间,slot 1-3没有任何内容,并且 - 我们的没有任何意义。)所以你运行 git checkout --ours - path / to / file



这个 git checkout 从索引中获取文件 - 在这种情况下,从索引插槽2获取文件。由于这已经在索引中,所以Git不会将索引写入,而是写入工作树。所以这个文件没有被解析!



对于 git checkout也是如此 - 他们的:它从索引(插槽3)获取文件,并且不解决任何问题。



但是:if git checkout HEAD - path / to / file ,告诉 git checkout HEAD 提交。由于这是一个 commit ,因此Git通过将文件内容写入索引开始。这写入插槽0并擦除1-3。由于在冲突合并期间,Git会在 MERGE_HEAD ,你也可以 git checkout MERGE_HEAD - path / to / file 从其他提交中获取文件。这也从提交中提取,所以它写入索引,解析文件。






7 我经常希望Git为此使用了一个不同的前端命令,因为我们可以毫不含糊地说git checkout是安全的,它不会覆盖没有的文件 - force 。但这种 git checkout 确实会覆盖文件!



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



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



,这并不重要:我们只是要求Git检索 commit:path ,它会为我们找到树和blob ID。或者,我们要求Git检索 :n:path ,并在索引条目中找到 路径 插槽 n 。然后它得到我们文件的内容,我们很好去。



这个冒号和数字语法在Git中无处不在,而 - 我们的 - 他们的标志只能在 git checkout 中工作。有趣的冒号语法在 gitrevisions



9 git checkout - 路径的用例是这样的:假设,不管你是否合并,你对文件进行了一些修改,测试,发现这些修改已经工作,然后运行 git add 在文件上。然后你决定进行更多的更改,但没有再次运行 git add 。您测试第二组更改并发现它们是错误的。如果只有你可以将文件的工作树版本设置回你刚才的 git add -ed的版本.... Aha,你可以:你 git checkout - path 并且Git将索引版本从零槽复制回工作树。






细微行为警告



请注意,使用 - -ours - 他们的除了从索引中提取并因此不解决行为之外,还有一些细微差别。假设在我们发生冲突的合并中,Git检测到某个文件被重命名了。也就是说,在合并基础中,我们有文件 doc.txt ,但现在在 HEAD 中,我们有文档/ doc.txt 。我们需要的路径 git checkout --ours Documentation / doc.txt 。这也是 HEAD 提交中的路径,所以 git checkout HEAD - Documentation / doc.txt

但是如果在我们合并的提交中, doc.txt did not 重新命名?在这种情况下,我们应该 10 能够 git checkout - 他们的 - Documentation / doc.txt em> doc.txt 。但是,如果我们尝试 git checkout MERGE_HEAD - Documentation / doc.txt ,Git将无法找到该文件:它不在文档中,在 MERGE_HEAD 提交中。我们必须 git checkout MERGE_HEAD - doc.txt 来获取他们的文件...并且不会解决文档/doc.txt 。实际上,它只会创建 ./ doc.txt (如果它被重命名,几乎可以肯定没有 ./ doc.txt HEAD '',所以创建是一个比覆盖更好的猜测)

它通常足够安全,可以在一步之内提取和解析 git checkout HEAD - path 。如果您正在解析文件并且正在运行 git status ,您应该知道它们是否有重命名的文件,因此是否可以安全地保存到 git checkout MERGE_HEAD - path 通过放弃自己的更改来一步抽取和解析。但是你仍然应该意识到这一点,并且知道如果 是需要关注的重命名操作。






10 我在这里说应该,而不是可以,因为Git当前很少忘记重命名。因此,如果使用 - 他们来获取您在 HEAD 中重命名的文件,则必须在此使用旧名称然后重命名工作树中的文件。


Hi I need to merge two branches like this.

This is just an example what is happening, I work with hundreds of files which need resolution.

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'.

When I do git merge tool, there is the correct content just from the 'ours' branch and when I save it, the file disappears from the unmerged list. But since I have hundreds of files like this, this is not an option.

I thought that this approach will bring me where I want to be - easily say which file from which branch I want to keep.

But I guess I misunderstood the concept of the git checkout --ours/theirs commands after a merge.

Could you please provide me some info, how to handle this situation? I use git 1.7.1

解决方案

It's mostly a quirk of how git checkout works internally. The Git folks have a tendency to let implementation dictate interface.

The end result is that after git checkout with --ours or --theirs, if you want to resolve the conflict, you must also git add the same paths:

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

But this is not the case with other forms of git checkout:

git checkout HEAD -- path/to/file

or:

git checkout MERGE_HEAD -- path/to/file

(these are subtly different in multiple ways). In some cases this means the fastest way is to use the middle command. (Incidentally, the -- here is to make sure Git can distinguish between a path name and an option or branch name. For instance, if you have a file named --theirs, it will look like an option, but -- will tell Git that no, it's really a path name.)

To see how this all works internally, and why you need the separate git add except when you don't, read on. :-) First, let's do a quick review of the merge process.

Merge, part 1: how merge begins

When you run:

$ git merge commit-or-branch

the first thing Git does is find the merge base between the named commit and the current (HEAD) commit. (Note that if you supply a branch name here, as in git merge otherbranch, Git translates that to a commit-ID, namely the tip of the branch. It saves the branch name argument for the eventual merge log message, but needs the commit ID to find the merge base.)

Having found a suitable merge base,1 Git then produces two git diff listings: one from the merge base to HEAD, and one from the merge base to the commit you identified. This gets "what you changed" and "what they changed", which Git now has to combine.

For files where you made a change and they didn't, Git can just take your version.

For files where they made a change and you didn't, Git can just take their version.

For files where you both made changes, Git must do some real merge work. It compares the changes, line by line, to see if it can combine them. If it can combine them, it does so. If the merges seem—based, again, on purely line-by-line comparisons—to conflict, Git declares a "merge conflict" for that file (and goes ahead and tries to merge anyway, but leaves conflict markers in place).

Once Git has merged everything it can, it either finishes the merge—because there were no conflicts—or stops with a merge conflict.


1The merge base is obvious if you draw the commit graph. Without drawing the graph, it's kind of mysterious. This is why I always tell people to draw the graph, or at least, as much of it as needed to make sense.

The technical definition is that the merge base is the "lowest common ancestor" (LCA) node in the commit graph. In less technical terms, it's the most recent commit where your current branch joins up with the branch you're merging. That is, by recording each merge's parent commit IDs, Git is able to find the last time the two branches were together, and hence figure out both what you did, and what they did. For this to work at all, though, Git has to record each merge. Specifically, it has to write both (or all, for so-called "octopus" merges) parent IDs into the new merge commit.

In some cases, there's more than one suitable merge base. The process then depends on your merge strategy. The default recursive strategy will merge the multiple merge bases to produce a "virtual merge base". This is rare enough that you can ignore it for now.


Merge, part 2: stopping with a conflict, and Git's "index"

When Git does stop this way, it needs to give you a chance to resolve the conflicts. But this also means that it needs to record the conflicts, and this is where Git's "index"—also called "the staging area", and sometimes "the cache"—really earns its existence.

For every staged file in your work-tree, the index has up to four entries, rather than just one entry. At most three of these are ever actually in use, but there are four slots, which are numbered, 0 through 3.

Slot zero is used for resolved files. When you're working with Git and not doing merges, only slot zero gets used. When you edit a file in the work tree, it has "unstaged changes", and then you git add the file and the changes are written to the repository, updating slot zero; your changes are now "staged".

Slots 1-3 are used for unresolved files. When git merge has to stop with a merge conflict, it leaves slot zero empty, and writes everything to slots 1, 2, and 3. The merge base version of the file is recorded in slot 1, the --ours version is recorded in slot 2, and the --theirs version is recorded in slot 3. These nonzero slot entries are how Git knows that the file is unresolved.2

As you resolve files, you git add them, which erases all the slot 1-3 entries and writes a slot-zero, staged-for-commit entry. This is how Git knows the file is resolved and ready for a new commit. (Or, in some cases, you git rm the file, in which case Git writes a special "removed" value to slot zero, again erasing slots 1-3.)


2There are a few cases where one of these three slots is also empty. Suppose file new does not exist in the merge base and is added in both ours and theirs. Then :1:new is left empty and :2:new and :3:new record the add/add conflict. Or, suppose file f does exist in the base, is modified in our HEAD branch, and is removed in their branch. Then :1:f records the base file, :2:f records our version of the file, and :3:f is empty, recording the modify/delete conflict.

For modify/modify conflicts, all three slots are occupied; only when one file is missing is one of these slots empty. It's logically impossible to have two empty slots: there's no such thing as a delete/delete conflict, nor a nocreate/add conflict. But there is some weirdness with rename conflicts, which I've omitted here as this answer is long enough! In any case, it's the very existence of some value(s) in slots 1, 2, and/or 3 that mark the file as unresolved.


Merge, part 3: finishing the merge

Once all files are resolved—all entries are only in the zero-numbered slots—you can git commit the merge result. If git merge is able to do the merge without assistance, it normally runs git commit for you, but the actual commit is still done by running git commit.

The commit command works the same way as it always does: it turns the index contents into tree objects and writes a new commit. The only thing special about a merge commit is that it has more than one parent commit ID.3 The extra parents come from a file git merge leaves behind. The default merge message also comes from a file (a separate file in practice, although in principle they could have been combined).

Note that in all cases, the new commit's contents are determined by the index's contents. Moreover, once the new commit is done, the index is still full: it still contains the same contents. By default, git commit won't make another new commit at this point because it sees that the index matches the HEAD commit. It calls this "empty" and requires --allow-empty to make an extra commit, but the index is not empty at all. It's still quite full—it just is full of the same thing as the HEAD commit.


3This assumes you are making a real merge, not a squash merge. When making a squash merge, git merge deliberately does not write the extra parent ID to the extra file, so that the new merge commit has only a single parent. (For some reason, git merge --squash also suppresses the automatic commit, as if it included the --no-commit flag as well. It's not clear why, since you could just run git merge --squash --no-commit if you want the automatic commit suppressed.)

A squash merge does not record its other parent(s). This means that if we go to merge again, some time later, Git won't know where to start the diffs from. This means you should generally only squash-merge if you plan to abandon the other branch. (There are some tricky ways to combine squash merges and real merges but they're well out of the scope of this answer.)


How git checkout branch uses the index

With all that out of the way, we then have to look at how git checkout uses Git's index, too. Remember, in normal usage, only slot zero is occupied, and the index has one entry for every staged file. Moreover, that entry matches the current (HEAD) commit unless you've modified the file and git add-ed the result. It also matches the file in the work-tree unless you've modified the file.4

If you are on some branch and you git checkout some other branch, Git tries to switch to the other branch. For this to succeed, Git has to replace the index entry for each file with the entry that goes with the other branch.

Let's say, just for concreteness, that you're on master and you are doing git checkout branch. Git will compare each current index entry with the index entry it would need to be on the tip-most commit of branch branch. That is, for file README.txt, are the master contents the same as those for branch, or are they different?

If the contents are the same, Git can take it easy and just move on to the next file. If the contents are different, Git has to do something to the index entry. (It's around this point that Git checks to see if the work-tree file differs from the index entry, too.)

Specifically, in the case where branch's file differs from master's, git checkout has to replace the index entry with the version from branch—or, if README.txt doesn't exist in the tip commit of branch, Git has to remove the index entry. Moreover, if git checkout is going to modify or remove the index entry, it also needs to modify or remove the work-tree file. Git makes sure this is a safe thing to do, i.e., that the work-tree file matches the master commit's file, before it will let you switch branches.

In other words, this is how (and why) Git finds out whether it's OK to change branches—whether you have modifications that would be clobbered by switching from master to branch. If you have modifications in your work-tree, but the modified files are the same in both branches, Git can just leave the modifications in the index and work-tree. It can and will alert you to these modified files "carried over" into the new branch: easy, since it had to check for this anyway.

Once all the tests have passed and Git has decided that it's OK to switch from master to branch—or if you specified --forcegit checkout actually updates the index with all the changed (or removed) files, and updates the work-tree to match.

Note that all this action has used slot zero. There are no slot 1-3 entries at all, so that git checkout does not have to remove any such things. You're not in the middle of a conflicted merge, and you ran git checkout branch to not just check out one file, but rather an entire set of files and switch branches.

Note also that you can, instead of checking out a branch, check out a specific commit. For instance, this is how you might look at a previous commit:

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

The action here is the same as for checking out a branch, except that instead of using the tip commit of the branch, Git checks out an arbitrary commit. Instead of now being "on" the new branch, you're now on no branch:5 Git gives you a "detached HEAD". To reattach your head, you must git checkout master or git checkout branch to get back "on" the branch.


4The index entry may not match the work-tree version if Git is doing special CR-LF ending modifications, or applying smudge filters. This gets pretty advanced and the best thing is to ignore this case for now. :-)

5More accurately, this puts you on an anonymous (unnamed) branch that will grow from the current commit. You will stay in detached HEAD mode if you make new commits, and as soon as you git checkout some other commit or branch, you'll switch there and Git will "abandon" the commits you've made. The point of this detached HEAD mode is both to let you look around and to let you make new commits that will just go away if you don't take special action to save them. For anyone relatively new to Git, though, having commits "just go away" is not so good—so make sure you know that you're in this "detached HEAD" mode, whenever you are in it.

The git status command will tell you if you're in detached HEAD mode. Use it often.6 If your Git is old (the OP's is 1.7.1, which is very old now), git status is not as helpful as it is in modern versions of Git, but it's still better than nothing.

6Some programmers like to have key git status information encoded into each command-prompt. I personally do not go this far, but can be a good idea.


Checking out specific files, and why it sometimes resolves merge conflicts

The git checkout command has other modes of operation, though. In particular, you can run git checkout [flags etc] -- path [path ...] to check out specific files. This is where things get weird. Note that when you use this form of the command, Git does not check to make sure you are not overwriting your files.7

Now, instead of changing branches, you're telling Git to get some particular file(s) from somewhere, and drop them into the work-tree, overwriting whatever is there, if anything. The tricky question is: just where is Git getting these files?

Generally speaking, there are three places that Git keeps files:

  • in commits;8
  • in the index;
  • and in the work-tree.

The checkout command can read from either of the first two places, and always writes the result to the work-tree.

When git checkout gets a file from a commit, it first copies it to the index. Whenever it does this, it writes the file to slot zero. Writing to slot zero wipes out slots 1-3, if they are occupied. When git checkout gets a file from the index, it does not have to copy it to the index. (Of course not: it's already there!) This is how git checkout works when you are not in the middle of a merge: you can git checkout -- path/to/file to get the index version back.9

Suppose, though, that you are in the middle of a conflicted merge and are going to git checkout some path, maybe with --ours. (If you are not in the middle of a merge, there's nothing in slots 1-3, and --ours makes no sense.) So you run git checkout --ours -- path/to/file.

This git checkout gets the file from the index—in this case, from index slot 2. Since this is already in the index, Git does not write to the index, just to the work-tree. So the file is not resolved!

The same goes for git checkout --theirs: it gets the file from the index (slot 3), and does not resolve anything.

But: if you git checkout HEAD -- path/to/file, you are telling git checkout to extract from the HEAD commit. Since this is a commit, Git starts by writing the file contents to the index. This writes slot 0 and erases 1-3. And now the file is resolved!

Since, during a conflicted merge, Git records the being-merged commit's ID in MERGE_HEAD, you can also git checkout MERGE_HEAD -- path/to/file to get the file from the other commit. This, too, extracts from a commit, so it writes to the index, resolving the file.


7I often wish Git used a different front-end command for this, since we could then say, unequivocally, that git checkout is safe, that it won't overwrite files without --force. But this kind of git checkout does overwrite files, on purpose!

8This is a bit of a lie, or at least a stretch: commits don't contain files directly. Instead, commits contain a (single) pointer to a tree object. This tree object contains the IDs of additional tree objects and of blob objects. The blob objects contain the actual file contents.

The same is, in fact, true of the index as well. Each index slot contains, not the actual file contents, but rather the hash IDs of blob objects in the repository.

For our purposes, though, this doesn't really matter: we just ask Git to retrieve commit:path and it finds the trees and the blob ID for us. Or, we ask Git to retrieve :n:path and it finds the blob ID in the index entry for path for slot n. Then it gets us the file's contents, and we're good to go.

This colon-and-number syntax works everywhere in Git, while the --ours and --theirs flags only work in git checkout. The funny colon syntax is described in gitrevisions.

9The use-case for git checkout -- path is this: suppose, whether or not you are merging, you made some changes to a file, tested, found those changes worked, then ran git add on the file. Then you decided to make more changes, but have not run git add again. You test the second set of changes and find they are wrong. If only you could get the work-tree version of the file set back to the version you git add-ed just a moment ago.... Aha, you can: you git checkout -- path and Git copies the index version, from slot zero, back to the work-tree.


Subtle behavior warning

Note, though, that using --ours or --theirs has another slight subtle difference besides just the "extract from index and therefore don't resolve" behavior. Suppose that, in our conflicted merge, Git has detected that some file was renamed. That is, in the merge base, we had file doc.txt, but now in HEAD we have Documentation/doc.txt. The path we need for git checkout --ours is Documentation/doc.txt. This is also the path in the HEAD commit, so it's OK to git checkout HEAD -- Documentation/doc.txt.

But what if, in the commit we're merging, doc.txt did not get renamed? In this case, we should10 be able to git checkout --theirs -- Documentation/doc.txt to get their doc.txt from the index. But if we try to git checkout MERGE_HEAD -- Documentation/doc.txt, Git won't be able to find the file: it's not in Documentation, in the MERGE_HEAD commit. We have to git checkout MERGE_HEAD -- doc.txt to get their file ... and that would not resolve Documentation/doc.txt. In fact, it would just create ./doc.txt (if it was renamed there's almost certainly no ./doc.txt, hence "create" is a better guess than "overwrite").

Because merging uses HEAD's names, it's generally safe enough to git checkout HEAD -- path to extract-and-resolve in one step. And if you're working on resolving files and have been running git status, you should know whether they have a renamed file, and therefore whether it's safe to git checkout MERGE_HEAD -- path to extract-and-resolve in one step by discarding your own changes. But you should still be aware of this, and know what to do if there is a rename to be concerned with.


10I say "should" here, not "can", because Git currently forgets the rename a little bit too soon. So if using --theirs to get a file that you renamed in HEAD, you have to use the old name here too, and then rename the file in the work-tree.

这篇关于git checkout --ours不会从未合并的文件列表中删除文件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆