我们不会从未合并的文件列表中删除文件
嗨,我需要合并这样的两个分支。
这仅仅是一个例子,我正在处理数百个需要解析的文件。
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
,只有'我们'分支有正确的内容,当我保存它时,文件从未合并列表中消失。 但由于我有这样的数百个文件,这不是一个选项。
我认为这种方法会将我带到我想要的地方 - 轻松地说出我想保留哪个分支的哪个文件。
但我想我误解了合并后git checkout --ours/theirs
的概念git checkout --ours/theirs
命令。
你能否给我提供一些信息,如何处理这种情况? 我使用git 1.7.1
这主要是git checkout
在内部工作的一个怪癖。 Git人员倾向于让实现指定接口。
最终的结果是,在用--ours
或者 - --theirs
git checkout
之后,如果你想解决冲突,你还必须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的路径名和一个选项或分支机构名称区分举例来说,如果你有一个文件命名。 --theirs
,它看起来像一个选项,但--
会告诉不要,这真是一个路径名。)
要了解这一切在内部是如何工作的,以及为什么你需要单独的git add
除非你不需要,请继续阅读。 首先,让我们快速回顾合并过程。
合并,第1部分:合并如何开始
当你运行:
$ git merge commit-or-branch
Git所做的第一件事是找到指定的提交和当前( HEAD
)提交之间的合并基础。 (注意,如果你在这里提供了一个分支名称,就像在git merge otherbranch
,Git将它转换为一个commit-ID,即分支的顶端。它保存最终合并日志消息的分支名称参数,但需要提交ID来查找合并基础。)
找到合适的合并基础后,1 Git会生成两个git diff
列表:一个从合并基础到HEAD
,另一个从合并基础到您确定的提交。 这会得到“你改变了什么”和“他们改变了什么”,Git现在必须结合。
对于你做出改变但没有改变的文件,Git可以只取你的版本。
对于他们做出改变而你没有改变的文件,Git可以采用他们的版本。
对于你们都做过修改的文件,Git必须做一些真正的合并工作。 它逐行比较变化,看它是否可以合并它们。 如果可以将它们结合起来,就是这样。 如果合并似乎是基于纯粹的逐行比较再次发生冲突,那么Git会为该文件声明一个“合并冲突”(并继续尝试合并,但留下冲突标记)。
一旦Git合并了它所能做的所有事情,它就会完成合并 - 因为没有冲突 - 或者因合并冲突而停止。
1如果您绘制提交图,则合并基础很明显。 没有绘制图表,这有点神秘。 这就是为什么我总是告诉人们绘制图表,或者至少要根据需要绘制图表来理解。
技术定义是合并基础是提交图中的“最低共同祖先”(LCA)节点。 用不太专业的术语来说,这是当前分支与您正在合并的分支加入的最新提交。 也就是说,通过记录每个合并的父提交ID,Git能够找到两个分支最后一次在一起,因此弄清楚你做了什么以及他们做了什么。 尽管如此,Git必须记录每个合并。 具体而言,它必须将两个(或全部,用于所谓的“章鱼”合并)父ID写入新的合并提交。
在某些情况下,有多个合适的合并基础。 该流程取决于您的合并策略。 默认的递归策略将合并多个合并基础以产生“虚拟合并基础”。 这很罕见,您现在可以忽略它。
合并,第2部分:停止冲突,Git的“索引”
当Git停止这种方式时,它需要给你一个解决冲突的机会。 但是这也意味着它需要记录冲突,这就是Git的“索引”(也称为“暂存区域”),有时甚至是“缓存” - 实际上是它的存在。
对于工作树中的每个阶段文件,索引最多有四个条目,而不仅仅是一个条目。 其中至多三个实际上正在使用,但有四个插槽,编号为0
到3
。
插槽零用于解析的文件。 当你使用Git而不是合并时,只有零槽被使用。 当你在工作树中编辑一个文件时,它有“非持续更改”,然后你git add
文件,并将更改写入存储库,更新插槽零; 您的更改现在已“上演”。
插槽1-3用于未解析的文件。 当git merge
必须以合并冲突结束时,它会使插槽零为空,并将所有内容写入插槽1,插槽2和插槽3.合并基本版本的文件记录在插槽1中,-- --ours
版本记录在插槽2,并且 - --theirs
版本记录在插槽3中。这些非零插槽条目是Git如何知道文件未解析。
当你解析文件时,你可以git add
它们,这会删除所有的插槽1-3项,并写入一个零插槽的提交阶段提交条目。 这就是Git知道文件已被解析并准备好进行新的提交。 (或者,在某些情况下,您将文件git rm
到文件中,在这种情况下,Git会向槽0写入一个特殊的“已删除”值,并再次删除槽1-3)。
2有些情况下,这三个插槽中的一个也是空的。 假设合并库中不存在new
文件,并且在我们和他们的文件中都添加了new
文件。 然后:1:new
留空, :2:new
和:3:new
记录添加/添加冲突。 或者,假设文件f
确实存在于基础中,在我们的HEAD分支中被修改,并且在其分支中被删除。 然后:1:f
记录基本文件, :2:f
记录我们的文件版本,以及:3:f
为空,记录修改/删除冲突。
对于修改/修改冲突,所有三个插槽都被占用; 仅当缺少一个文件时,其中一个插槽为空。 逻辑上不可能有两个空插槽:不存在删除/删除冲突,以及创建/添加冲突。 但是重命名冲突有一些奇怪之处,我在这里省略了,因为这个答案足够长! 在任何情况下,在第1,2和/或3号插槽中存在的某些值会将文件标记为未解决。
合并,第3部分:完成合并
一旦所有文件解析完毕 - 所有条目都只在零编号的位置上 - 你可以git commit
合并结果。 如果git merge
能够在没有帮助的情况下执行合并,它通常git commit
为您运行git commit
,但实际提交仍然通过运行git commit
。
commit命令的作用与以往一样:它将索引内容转换为树对象并写入新的提交。 合并提交的唯一特别之处在于它有多个父提交ID.3额外的父项来自git merge
留下的文件。 默认的合并信息也来自一个文件(实际上是一个单独的文件,尽管原则上它们可能已经合并)。
请注意,在所有情况下,新提交的内容都由索引的内容决定。 而且,一旦新的提交完成,索引仍然是满的:它仍然包含相同的内容。 默认情况下, git commit
在此时不会进行另一个新的提交,因为它会发现索引与HEAD
提交匹配。 它将此称为“空”,并要求--allow-empty
进行额外的提交,但索引不是空的。 它仍然非常完整 - 它与HEAD
提交完全相同。
3这假定你正在做一个真正的合并,而不是一个壁球合并。 在进行壁球合并时, git merge
有意不会将额外的父ID写入额外的文件,这样新的合并提交只有一个父节点。 (出于某种原因, git merge --squash
也会禁止自动提交,就好像它也包含--no-commit
标志一样。不清楚为什么,因为你可以运行git merge --squash --no-commit
如果你想禁止自动提交。)
壁球合并不记录其他父母。 这意味着如果我们再次合并,一段时间后,Git将不知道从哪里开始差异。 这意味着如果您打算放弃其他分支,通常应该只进行压缩合并。 (有一些棘手的方法可以将压缩合并和真正的合并结合起来,但它们远远超出了这个答案的范围。)
git checkout branch
如何使用索引
所有这一切,我们都必须看看git checkout
如何使用Git的索引。 请记住,在正常使用情况下,只有槽0被占用,并且索引对每个分阶段文件都有一个条目。 此外,该条目与当前( HEAD
)提交匹配,除非您修改了文件,并且git add
了结果。 它也与工作树中的文件相匹配,除非您修改了文件
如果您在某个分支上,并且您正在git checkout
某个其他分支,Git会尝试切换到另一个分支。 为了成功,Git必须用每个文件的索引条目替换其他分支的条目。
比方说,只是为了具体,你是master
,你在做git checkout branch
。 Git会比较每个当前索引条目和索引条目,它将需要在分支branch
的提示最多的提交上。 也就是说,对于文件README.txt
, master
内容是否与branch
相同?还是它们不同?
如果内容相同,Git可以简单地转到下一个文件。 如果内容不同,Git必须对索引条目做些什么。 (Git检查工作树文件是否与索引条目不同也是在这一点上。)
具体来说,在branch
文件与master
文件不同的情况下, git checkout
必须将索引条目替换为来自branch
的版本 - 或者,如果README.txt
不存在于branch
的提示提交中,Git必须删除索引条目。 此外,如果git checkout
要修改或删除索引条目,它还需要修改或删除工作树文件。 Git确保这是一件安全的事情,即在它允许您切换分支之前,工作树文件与master
提交文件相匹配。
换句话说,这是Git如何(以及为什么)发现是否可以更改分支 - 无论您是否有修改,都会通过从master
分区切换到branch
。 如果您在工作树中进行了修改, 但两个分支中的修改后的文件都是相同的,Git可以将修改保留在索引和工作树中。 它可以也会提醒你这些修改后的文件“结转”到新的分支中:容易,因为它必须检查这个。
一旦所有的测试都通过了,Git认为可以从master
切换到branch
- 或者如果你指定了--force
- git checkout
实际上用所有更改(或删除)的文件更新索引,并更新工作树匹配。
请注意,所有这些操作都使用了插槽零。 根本没有插槽1-3条目,所以git checkout
不必删除任何这样的事情。 你并不处于冲突的合并中,你运行git checkout branch
不仅仅检出一个文件,而是git checkout branch
一整套文件和交换分支。
还要注意,你可以不检出分支,而是检查一个特定的提交。 例如,你可能会看到以前的提交:
$ git log
... peruse log output ...
$ git checkout f17c393 # let's see what's in this commit
这里的动作与检出分支相同,不同之处在于不使用分支的提示提交,Git检出任意提交。 现在不用“在”新的分支,你现在不在分支上:5 Git给你一个“分离的HEAD”。 要重新连接你的头,你必须将git checkout master
或git checkout branch
到分支上。
4如果Git正在执行特殊的CR-LF结尾修改或应用涂抹过滤器,索引条目可能与工作树版本不匹配。 这非常先进,现在最好的办法是忽略这种情况。 :-)
5更准确地说,这会让您置身于一个匿名(未命名)分支,该分支将从当前提交开始增长。 如果你提交了新的提交,你将会保持独立的HEAD模式,只要你git checkout
一些其他的提交或分支,你就会在那里切换,Git将“放弃”你提交的提交。 这种分离的HEAD模式的重点在于让您环顾四周,并让您做出新的提交,如果您不采取特殊措施来保存它们,它们就会消失。 然而,对于Git相对较新的人来说,提交“刚刚离开”并不是那么好 - 所以确保你知道你在这个“独立HEAD”模式下,无论你何时进入。
git status
命令会告诉你你是否处于分离的HEAD模式。 经常使用它。如果你的Git比较旧(OP的版本是1.7.1,现在已经很老了),那么git status
并不像现代版本的Git那样有用,但它还是比没有好。
6某些程序员喜欢将密钥git status
信息编码到每个命令提示符中。 我个人不会走这么远,但可以是一个好主意。
检出特定的文件,以及为什么它有时会解决合并冲突
尽管如此, git checkout
命令还有其他的操作模式。 特别是,你可以运行git checkout [flags etc] -- path [path ...]
来检出特定的文件。 这是奇怪的地方。 请注意,当你使用这种形式的命令时,Git不检查以确保你没有覆盖你的文件
现在,不是改变分支,而是告诉Git从某个地方获取一些特定的文件,然后将它们放到工作树中,如果有的话覆盖任何地方。 棘手的问题是:Git在哪里获取这些文件?
一般来说,Git有三个地方保存文件:
checkout命令可以从前两个位置读取,并始终将结果写入工作树。
当git checkout
从提交中获取文件时,它首先将其复制到索引。 每当它做到这一点,它将文件写入零槽。 写入插槽零将抹掉插槽1-3,如果它们被占用。 当git checkout
从索引获取文件时,它不必将其复制到索引。 (当然不是:它已经在那里了!)当你不在合并中时,这就是git checkout
工作方式:你可以通过git checkout -- path/to/file
来获得索引版本。
不过,假设你处于一个冲突的合并中,并且正在git checkout
一些路径,可能还有--ours
。 (如果你不在合并的中间,那么slot 1-3中没有任何内容,-- --ours
是没有意义的。)所以你运行git checkout --ours -- path/to/file
。
这个git checkout
从索引获取文件 - 在这种情况下,从索引插槽2获取文件。由于这已经在索引中,因此Git不会写入索引,而是写入工作树。 所以文件没有解决!
git checkout --theirs
:它从索引(插槽3)获取文件,并且不解决任何问题。
但是 :如果你git checkout HEAD -- path/to/file
,你告诉git checkout
从HEAD
提交中提取。 由于这是一个提交,所以Git通过将文件内容写入索引开始。 这写入插槽0并擦除1-3。 现在该文件已解决!
因为在冲突的合并过程中,Git会在MERGE_HEAD
记录正在合并的提交的ID,您也可以通过git checkout MERGE_HEAD -- path/to/file
来从另一个提交中获取文件。 这也从提交中提取,所以它写入索引,解析文件。
7我经常希望Git为此使用了一个不同的前端命令,因为我们可以毫不含糊地说,git checkout是安全的,它不会在没有--force
情况下覆盖文件。 但这种git checkout
确实会覆盖文件!
8这是一个谎言,或者至少是一段延伸:提交不直接包含文件。 相反,提交包含一个指向树对象的(单个)指针。 该树对象包含附加树对象和Blob对象的ID。 blob对象包含实际的文件内容。
事实上,索引也是如此。 每个索引槽都包含,而不是实际的文件内容,而是存储库中blob对象的哈希ID。
然而,就我们的目的而言,这并不重要:我们只要求Git检索commit:path
并找到树和我们的blob ID。 或者,我们要求Git检索:n:path
,它在槽n
path
的索引条目中找到blob ID。 然后它会得到我们文件的内容,我们很好去。
这个冒号和数字的语法在Git中无处不在,而--ours
和--theirs
标志只能在git checkout
工作。 有趣的冒号语法在gitrevisions
描述。
9 git checkout -- path
用例git checkout -- path
是这样的:假设你是否合并,对文件做了一些修改,测试,发现这些修改有效,然后在文件上运行git add
。 然后你决定做更多的改变,但还没有再次运行git add
。 您测试第二组更改并发现它们是错误的。 如果只有你可以将文件的工作树版本设置回git add
刚git add
的版本.... aha,你可以:你git checkout -- path
和Git复制索引版本,从插槽零,回到工作树。
微妙的行为警告
但请注意,除了“从索引提取并因此不解决”行为之外,使用--ours
或 - --theirs
还有另一个细微差别。 假设在我们冲突的合并中,Git检测到某个文件已被重命名。 也就是说,在合并基础中,我们有文件doc.txt
,但是现在在HEAD
我们有Documentation/doc.txt
。 我们需要的git checkout --ours
是Documentation/doc.txt
。 这也是HEAD
提交中的路径,所以可以通过git checkout HEAD -- Documentation/doc.txt
。
但是如果在我们合并的提交中, doc.txt
没有被重命名? 在这种情况下,我们应该能够通过git checkout --theirs -- Documentation/doc.txt
他们的git checkout --theirs -- Documentation/doc.txt
doc.txt
从索引中获取他们的doc.txt
。 但是,如果我们试图git checkout MERGE_HEAD -- Documentation/doc.txt
,那么Git将无法找到该文件:它不在MERGE_HEAD
提交中的Documentation
中。 我们必须git checkout MERGE_HEAD -- doc.txt
以获取他们的文件...并且不能解决Documentation/doc.txt
。 实际上,它只会创建./doc.txt
(如果它被重命名,几乎肯定没有./doc.txt
,因此“创建”比“覆盖”更好)。
因为合并使用HEAD
的名字,所以它通常足够安全,可以通过git checkout HEAD -- path
一步完成提取和解析的git checkout HEAD -- path
。 如果您正在解析文件并且正在运行git status
,则应该知道它们是否有重命名的文件,因此是否可以安全地进行git checkout MERGE_HEAD -- path
通过放弃您的操作,一步完成提取和解析的git checkout MERGE_HEAD -- path
自己的变化。 但是你仍然应该意识到这一点,并且知道如果需要重新命名,该怎么办。
10我在这里说“应该”,而不是“可以”,因为Git目前很快就忘记了重命名。 所以如果使用--theirs
来获取在HEAD
重命名的文件,则必须在这里使用旧名称,然后在工作树中重命名该文件。
上一篇: ours does not remove files from unmerged files list
下一篇: Resolve conflicts using remote changes when pulling from Git remote