TL;DR:它实际上并不是快进本身
您的问题归结为:“为什么 Git 不遵守我的自定义合并方向?”事实上,这个问题可能会发生在any合并,并且any自定义合并驱动程序。事实上,这次合并can仅作为快进操作来完成保证您(根据您的具体情况)会遇到问题。
原因归结为以下事实:any custom .gitattributes
合并驱动程序,包括merge=ours
, 被调用only当 Git 认为有“需要合并的东西”时。在您意识到 Git 需要什么才能拥有这样的信念之前,这似乎并没有那么糟糕。
侧边栏:合并策略
这里值得一提的是,作为侧边栏,Git 的-s strategy
论证git merge
。这些策略接管整个过程,包括“找到合并基础”步骤 - 以及之后的所有内容 - 因此可以做自己的事情,其中包括ignoring .gitattributes
完全。显然,如果策略忽略了你的.gitattributes
,在那里设置自定义合并驱动程序或模式不会有帮助。
因此,我们只关注-s
策略do使用合并基础和 Git 调用的两个heads(我们将标记为“我们的”和“他们的”),以及do use .gitattributes
。 Git 内置了三个——recursive
, resolve
, and subtree
- 但它们在这里的工作方式都是相同的,关于合并的内容以及自定义合并驱动程序会发生什么。 (另外两个内置合并策略,ours
and octopus
,要么根本不理会合并基础和“他们的”,要么——对于octopus
——有两个以上的头,所以没有“我们的”和“他们的”的明确概念。)
一个合并底座和两个头部
所以,现在我们已经确定了具有以下功能的内置合并:合并基地提交和两个head提交,我们可以看看 Git 以它预编程的 Gitty 方式来思考意味着什么要合并的东西.
The two heads更容易定义。其中之一,我们称之为“我们的”,只是HEAD
本身。另一个是我们传递给任何参数git merge
:
git merge A
意思是“我们的”是HEAD
“他们的”是由A
.
这是你的git log --all --decorate --oneline --graph
再次输出(顺便说一句,感谢您将其包括在内 - 这对于大多数合并至关重要!):
* da6a750 (A) Further in A, okay for merging back into master
* bf27b58 Merge branch 'master' into A
|\
| * 86294d1 (HEAD -> master) Development on master
* | abe6b8a Welcome to branch A
|/
* 589517c First commit
所以我们可以说两个头都承诺了86294d1
(HEAD
or master
或者只是“我们的”)并承诺da6a750
(A
或者只是“他们的”)。
The 合并基地是他们在图表历史中首先共享的任何提交,即从两个头开始,如果需要的话在历史中向后工作,直到找到他们共同的提交,您可以reach从两个头。所以我们从da6a750
,向后退一步得到bf27b58
,然后向后退一步,得到两者86294d1
and abe6b8a
。同时,我们从86294d1
而且...哦,看我们已经达成了共同承诺! :-)
自合并基地以来is两个头之一,通常我们要么快进,要么抱怨没有什么可以合并。由于合并基础是“我们的”头,因此在这两个选项中,Git 会选择快进操作。使用--no-ff
告诉 Git:不要选择那个,毕竟继续进行完整的合并。
现在,事实是合并基础is“我们的”承诺保证我们会遇到您的问题,但事实上,即使合并基础不是“我们的”提交,我们也可能会遇到您的问题。让我们看看一次提交里面有什么,在下一个层次上,Git 在两者上都工作时需要什么和做什么git diff
and git merge
——但首先,让我们想想什么git merge
is supposed to do.
合并的目标是合并工作
一般来说,跑步时的想法git merge
是我们想要做两组工作——事情we did on our分支进入our承诺,以及“他们”所做的事情,无论他们是谁their分支进入their提交——并产生一个两全其美的新提交:这需要我们所做的任何好东西,plus他们做过的任何好事。
如果我们水平绘制图表而不是垂直绘制图表,旧的提交在左侧,新的提交在右侧,我们可以这样画:
o--o--o--...--H <-- ours
/
...--o--B
\
o-----...-----T <-- theirs
其中每个o
是一个提交,也是B
, H
, and T
。犯罪B
is the 合并基地,该图中的两个叉子在“过去”(向左)方向重新汇合。H
是我们的(HEAD)承诺并且T
是他们分支的头/尖端提交。那么,我们如何才能将我们的工作与他们的工作结合起来呢?
Git 的答案是运行两个git diff
s:
git diff B H # find out what we did
git diff B T # find out what they did
那么就可以combine这两个差异:
-
无论我们在何处添加某些内容(某些文本行)到某些文件,Git 都应该使最终结果在这些文件中包含这些添加的行。无论我们在某些文件中删除了某些文本行,它都应该使最终结果删除这些行。
Because git diff
将差异表示为“删除这个并添加那个”(即使对于以下差异)change这个到那个),涵盖了一切git diff
says.
-
同样,无论他们在哪里添加行,Git 都应该使最终结果包含添加的行。无论他们在哪里删除行,Git 都应该使最终结果具有相同的删除。
-
处理一个非常常见的情况,如果我们和他们做了完全一样更改 - 删除相同的原始行,和/或添加相同的替换 - Git 仅需要one copy这个的。
-
当然,如果有一个地方我们都以不同的方式触及了相同的界限,Git 就会举起它的隐喻之手,感叹道“OY合租!”,并声明合并冲突。
(正是这些合并冲突让我们最头疼,所以大部分扭曲的旋钮Git 为我们提供了旨在以某种方式处理这些冲突的方法。这大多是正确的.gitattributes
也合并属性——尽管这与我们这里的问题没有直接关系。)
现在,所有这些组合需要大量工作,因此要让 Git 运行起来fast,有一个捷径。
原始提交里面有什么git merge
to git diff
我们可以查看任何提交对象,或者实际上任何 Git 对象,git cat-file -p
:
$ git cat-file -p HEAD
tree 5bc304073b94505cd3f6716829c4cec5a7474762
parent 29257c2c82dca881c4cc65765392a32e46264fbe
author Chris Torek <[email protected]> 1490287144 -0700
committer Chris Torek <[email protected]> 1490297185 -0700
insert early footnote on Git branch creation
In the "about version control" chapter section that introduces
(我在这里剪掉了其余的部分)。
这里更有趣的部分实际上是tree
,让我们看看其中的一些:
$ git cat-file -p 5bc304073b94505cd3f6716829c4cec5a7474762
100644 blob 8d1519c435c4da5a65228785fa7ba7033fe011ff .gitignore
100644 blob 66c9d22a735ee9d8da7f7ed49599583aa642842f Makefile
100644 blob c9c824fa6668e45976c4fe8a10e4d5c25e272f0c about.tex
100644 blob 1757109f5aa921ecf9a8051180c25f09e1496c07 aboutvc.tex
(我再次在这里剪掉了东西)。
每个的原始哈希 IDblob
对象(即存储的文件版本)告诉 Git哪个版本与此提交一起进行。 (更准确地说,这是这个文件的版本tree
对象,但是这个tree
与此提交一致,因此它相当于同一件事。)
Git can, and in fact has to, extract these blob hash IDs for each of the three commits—the merge base, "ours", and "theirs". The hash IDs are how it will be able to diff the old and new versions of files like aboutvc.tex
(in my case) or specific
(in yours). But there is an interesting thing about these hash IDs: they're based entirely on the contents of the object.1 If two files in two different commits are exactly, completely, 100% bit-for-bit identical, they have the same hash and are stored in the repository just once. This means that no matter how many commits have a copy of that particular version of that file, there's only one copy stored in the database.
1In fact, they are cryptographic hashes of the object contents, including the little type-and-size header Git sticks on the front of each object. That header is why the now-famous SHA-1 hash collision is not an immediate problem for Git.
相同的哈希 => 问题
这种快速哈希比较——事实是same散列的意思是“该文件的相同版本”——意味着git diff
and git merge
可以立即轻松地知道有不用找了到某些文件,从基地到我们的,或基地到他们的......而这正是merge=ours
出错。 Git 会比较基础与我们的、基础与他们的。一对有same哈希。一对有一个不同的 hash.
此时,Git 简单地assumes无论合并策略或旋钮设置如何,正确的答案都是.gitattributes
,就是从有一个头的地方取出文件不同的哈希。对于大多数文件,在大多数情况下,这就是正确答案。但是如果我们定义了一个自定义合并驱动程序,或者设置merge=ours
,这可能是wrong answer.
当不同的一个头是“他们的”,并且自定义合并方向是“保留我们的”时,这是错误的答案。无论选择哪个提交作为合并基础都是如此,但是当合并基础是HEAD
——是我们的承诺——那么all与我们的基础的差异中的哈希值是相同的,结果是always“他们的文件版本”。
那其实就是why首先,快进是可能的:最终的合并树始终只是他们的树。实际上,Gitignores所有自定义方向.gitattributes
。即使您强制进行真正的合并而不是快进非合并“合并”,这仍然是正确的。
也许 Git 应该检查自定义合并驱动程序或merge=ours
指令,并禁用此快捷方式,至少对于真正的(非快进)合并。但事实并非如此,因此你会遇到这个问题。你会also对于其他情况也存在此问题,其中需要进行真正的合并,但文件仅在基础与它们的比较中被修改。
最后一个侧边栏:不要对配置文件执行此操作
人们经常想用这个merge=ours
以确保配置文件存储在分支上的内容保持在该分支上的方式。这几乎总是错误的总体策略:相反,配置文件应该是完全省略来自版本控制,或者至少来自这个特定存储库的版本控制。而不是承诺,例如config.ini
or config.php
,提交一个config.ini.sample
or config.default.php
或一些这样的。将此配置复制到“真实配置”,或者如果“真实”配置丢失或不完整,则将其读取为辅助策略。
这为您提供了一种一般版本配置(示例和/或默认配置)的方法,无需版本控制具体运行时间某人使用的配置this存储库作为他们运行软件/应用程序本身的地方。如果用户希望对她进行版本控制特别的配置,她可以将其存储在单独的存储库中,并替换config.ini
带有(例如)到的符号链接../myconfigs/fooapp.ini
,这是她对配置进行版本控制的地方。
(类似的技巧是从$HOME/.gitconfig
or /usr/local/etc/fooapp.ini
。也就是说,首先单独存储配置。再说一次,如果你想要或需要某种default配置,您可以将该版本与软件一起保留,但是用户自己的配置是独立的,根本不在您自己的版本控制之下。)