我有两个分支,master和feature。 如果我这样做:
git diff --name-only master..feature
我得到了一长串文件,其中一些是源代码,所以没有被 .gitignore 排除
但是,当我尝试将功能合并到主控中时:
git checkout master
git merge feature
在合并过程中,我只在 master 中更改了一个文件。
为什么会这样?
另一个有趣的事情是,如果我尝试反向并将母版合并到功能中,则会删除在功能分支中创建的文件。
我该如何解决此问题并避免将来出现此问题?
这不是错误。
请考虑以下简单示例。 假设有一个名为example.txt
的文件。 在分支X中,它写道:
This is
quite
a file.
在分支 Y 中,它写道:
This is
not
a file.
合并分支 X 和 Y 的结果应该是什么? 具体来说,您希望在名为example.txt
的文件中显示哪些内容?
我未能向您提供哪些信息(如果有)? 在回答这个问题之前,您还需要了解什么?
(在继续阅读之前,请尝试提出答案。
Git 是关于提交,而不是文件
在我们继续之前,让我们注意你在 Git 中处理的存储单位是提交,而不是文件。 确实,提交包含文件,但这里的一般想法是它是一个包交易:提交具有所有文件的完整快照。 如果我们进行一些起始提交:
git checkout somebranch
并将一个大文件bigfile.py
拆分为两个较小的文件,small1.py
和small2.py
并完全删除bigfile.py
然后提交,与旧提交相比,新提交缺少bigfile.py
并添加了两个较小的文件。 当我们签出旧提交时,我们只有三个文件中的一个 - 大文件 - 当我们签出新提交时,我们只有三个文件中的两个。 这是一个一揽子交易:你可以选择一个文件的提交,或者两个文件的提交,但你永远不会同时得到大文件和一个小文件,或者所有三个文件,或者其他组合。
尽管如此,提交包含文件,这在以后我们开始合并时会很重要。 但除了包含文件(这是它们的主要数据:每个文件的快照(就像您提交时它的外观一样)之外,每个提交都包含一些元数据或有关提交的信息。 这包括您在输出中看到的内容git log
例如,提交人员的姓名和电子邮件地址,以及日期和时间戳。1
在所有这些元数据中,Git 在每次提交中存储一些早期提交的原始哈希 ID。 大多数提交只存储一个较早的提交哈希 ID。 这些哈希 ID 也是提交的"真实名称":它们是 Git 实际查找每个提交的方式。 提交存储在一个大的键值数据库中,提交的哈希 ID 是键,提交的内容是值。
每次提交都存储了上一个提交的哈希 ID,我们最终会得到一个很好的简单线性提交链。 如果我们使用大写字母来代表每个哈希 ID,我们会得到如下所示的绘图:
... <-F <-G <-H
其中H
是链中最后一个提交的哈希 ID。 在提交H
中,Git 存储了之前提交G
的实际哈希 ID。 在提交G
中,Git 存储了更早的提交F
的哈希 ID,等等。
这些链允许 Git 向后工作,从最新的提交返回到早期的提交。 这些是Git 存储库中的历史记录,因此这些链对于使用 Git 至关重要。 而且,由于每个提交都存储一个完整快照,我们必须让 Git比较两个提交以查看更改的内容。 如果我们让 Git 将G
中的快照与H
中的快照进行比较 ,例如,这告诉我们当我们从G
进行H
时我们更改了什么。
所以,这就是git log
所做的:它从最新的提交(例如H
)开始,打印出哈希 ID 和元数据,如果我们使用-p
来获取补丁,提取G
和H
(到临时内存区域)并比较两个提交的快照以找出更改的内容, 并告诉我们这一点。 然后,在显示提交H
后,Git 向后移动一步以提交G
:它打印出哈希 ID 和元数据,如果我们使用-p
,则比较F
-vs-G
。 打印出G
后,git log
再往后退一步,F
,依此类推。
(换句话说,Git是向后工作的。 我不会在这里强调这一点,但它解释了很多关于 Git 的信息,一旦你意识到这一点。
1如果你使用git log --pretty=fuller
,你会看到每个提交实际上有两个:作者和提交者。 每个都由三元组组成:姓名,电子邮件,时间戳。 如今,通常两者都是相同的,除了挑选提交,其中保留了原始提交的作者,提交者是执行挑选的人,提交者时间戳是挑选操作的时间。
提交为了使上述工作,我们必须以某种方式知道链中最后一个提交的哈希ID。 我们需要将该哈希 ID 提供给 Git,因为 Git 最终只能通过其哈希 ID找到提交。 我们可以写下这些哈希ID,把它们记在纸上,或者白板上,或者其他东西。 但它们真的很大很丑,很难正确输入。 另外,我们有一台电脑。 为什么不让计算机为我们记住哈希 ID? 我们可以将第二个数据库添加到我们的 Git 存储库中:它将保存名称,例如master
或develop
或feature
,并使用这些名称记住上次(最近、最有用、无论如何)提交的哈希 ID。
这就是分支名称:它是名称数据库中的一个条目。 实际名称扩展了一点:master
真的很refs/heads/master
,feature
真的很refs/heads/feature
。 这为其他类型的名称留出了空间,例如标签名称:v2.1
真的很refs/tags/v2.1
。 但特别是对于分支名称,它们都持有提交哈希 ID(每个一个),并且该哈希 ID 是我们将考虑为"在分支上"的最后一次提交的 ID。
如果我们只有一个分支,一切都很容易:
...--F--G--H <-- master
在这里,分支名称master
是唯一的名称,它保存着我们最近提交的哈希 ID,提交H
。 因此,名称master
指向链末尾的提交。 这让我们(和 Git)访问提交H
. 向后提交H
点以提交G
,这让我们(和 Git)可以访问它;再次向后提交G
点,依此类推。
如果我们现在创建一个新的分支名称,例如feature
,我们可以选择任何现有的提交来指向这个新名称。 但是,大多数情况下,我们会选择正在使用的提交:H
,通过master
。 所以我们会得到:
...--F--G--H <-- feature, master
现在我们遇到了一个问题。 我们使用哪个分支名称? 请记住,我们将添加一个特殊名称HEAD
,并将其附加到这两个分支名称之一。 让我们将HEAD
附加到feature
— 如有必要,通过运行git checkout feature
— 并绘制它:
...--F--G--H <-- feature (HEAD), master
我们仍在使用提交H
,但现在我们使用它是因为名称feature
。
现在让我们以通常的方式创建一个新的提交:修改一些文件,甚至可能创建新文件和/或删除现有文件,并根据需要使用git add
和/或git rm
来更新它们,并git commit
结果。 无需过多担心所有细节,这让 Git 保存了一个新的快照,添加了一些元数据,并将集合写出为新的提交。 新提交获得一个新的、唯一的哈希 ID——看起来随机且不可预测,因为它取决于我们提交提交的确切时间——但我们只称其为提交I
。 新提交将向后指向现有提交H
:
I
/
...--F--G--H
一旦新提交存在,甚至在我们回到能够运行更多命令之前,Git 现在会做它最后一个特殊技巧:它将新提交的哈希 ID 写入当前分支名称,即HEAD
附加到的分支名称中。 由于这是feature
,我们得到:
I <-- feature (HEAD)
/
...--F--G--H <-- master
提交H
在提交I
之前出现,但它仍然是master
分支上的最后一次提交。 提交I
是feature
上的最后一次提交,但通过H
提交的提交也在feature
。
现在让我们继续对feature
再做一个承诺:
I--J <-- feature (HEAD)
/
...--F--G--H <-- master
然后运行git checkout master
. 这将使我们的HEAD
远离feature
,并将其附加到master
。 它还将更新我们的工作区,以便我们使用提交H
的内容,而不是提交J
的内容:我们所有的文件现在都匹配H
,而不是J
。 我们所做的任何更新和快照到I
和J
中都安全地存储在那里,I
和J
,但现在它们已经从我们的视野中消失了,因为我们已经承诺H
:
I--J <-- feature
/
...--F--G--H <-- master (HEAD)
我们现在可以创建另一个新的分支名称,例如feature2
,并附加HEAD
:
I--J <-- feature
/
...--F--G--H <-- feature2 (HEAD), master
然后在feature2
上进行两个新提交:
I--J <-- feature
/
...--F--G--H <-- master
K--L <-- feature2 (HEAD)
或者,我们可以继续直接在master
上进行这些提交:
I--J <-- feature
/
...--F--G--H
K--L <-- master (HEAD)
就图形本身而言——提交集之间有向后箭头(这里画成线条,因为文本中可用的箭头图形很差)——没关系:我们不能更改任何现有的提交(永远),但我们总是可以添加新的提交,无论哪种方式,我们最终都会得到这组提交。 这只是哪些名称找到这些提交的问题。 但是 Git 允许我们随时创建、销毁或移动分支名称。 提交不会更改;只是我们用来查找它们的名称可能不同。
合并
是时候回答上面的问题了:缺少什么?
当我们在 Git 中合并一些提交时,这都是关于组合工作。 这个想法是,在某些系列的提交(也许I-J
)中,有人做了一些工作,而某人(可能是其他人)在其他一系列的提交(K-L
)中做了一些工作。 这给了我们这个:
I--J <-- br1
/
...--G--H
K--L <-- br2
由于提交的性质——它们永远不会改变——我们可以从这张图中看出,这两行工作从一个共同的起点开始,即提交H
。 从视觉上看,很容易看出,J
中的所有东西都是H
的后裔,L
也是如此。 它们也是G
的后裔,但H
"更好",因为它"更接近"端点提交。
现在,我们已经知道 Git 可以比较两个快照,例如G
和H
,或I
和J
. 如果 Git 可以轻松地将H
直接与J
进行比较怎么办? 好吧,它可以;如果我们让 Git 这样做,我们就会发现从H
到J
有什么不同。 这就是某人在顶线上所做的工作。 所以这些是br1
的变化.
同样,如果我们让 Git 将H
中的内容与L
中的内容进行比较,我们将找出某人在底线上做了哪些工作。 无论文件是什么不同,以及我们使用什么规则来更改H
中的文件内容到L
中的内容,这就是某人在br2
上所做的。
这也告诉我们缺少什么。 为了合并example.txt
,我们不仅需要两个端点文件(例如,一个在第 2 行说quite
,另一个在第 2 行说not
——还需要文件的基本副本。example.txt
的基本副本是提交H
中文件的副本。 提交H
是两个提示提交的合并基础,每个文件的副本是我们找出更改内容的方式。
如果基本副本显示:
This is
quite
a file.
然后我们知道仍然说quite
的那一行没有任何变化,而在说not
的那一行中改变了。
如果基本副本显示:
This is
not
a file.
然后我们知道仍然说not
的那一行没有任何变化,而在说quite
的那一行中改变了。
如果基本副本没有第 2 行 — 如果它读取,则完整:
This is
a file.
然后我们有一个合并冲突,因为两个人都做了一个改变:都添加了第 2 行,但他们添加了不同的第 2 行。
这对您的案件意味着什么
如果两个分支提示提交(以名称master
找到的提交和以名称feature
找到的分支提示提交)不同,那只是告诉我们它们是不同的。 Git 提出的配方,将更改一个提交以使其与另一个提交匹配,只是告诉我们如何将一个提示提交更改为另一个提示提交。
如果这两个分支提示提交之间的合并基提交是第三次提交,2我们需要知道第三次提交中的内容,因为这就是git merge
如何找出master
中更改的内容以及feature
中更改的内容。然后,merge 命令将尝试合并这两组更改,将合并的更改应用于合并基中的任何内容。
正如博士所评论的那样,您可以将三点表示法与git diff
命令一起使用:
git diff master...feature
例如。 这有 Git:
- 找到两个提示提交之间的合并基(我们称之为
$B
); - 运行相当于
git diff $B feature
它告诉你feature
上发生了什么变化,关于这个合并基础。 如果随后运行相同的命令,并交换两个名称:
git diff feature...master
Git 将找到相同两个提示提交的合并基础,3然后 diff$B
vsmaster
:这显示了master
上发生了哪些变化。
同样,git merge
对这些情况所做的是:4
- 运行两个差异,将输出保存在临时区域中;
- 获取每个文件的合并基础版本;
- 如果可能的话,合并差异;和
- 将组合的差异应用于文件的合并基本版本。
如果一切顺利,git merge
将根据结果进行合并提交。 合并提交与常规非合并提交没有太大区别:它仍然具有所有文件的快照(由上面的合并过程构建)和一些元数据。 合并提交的特殊之处在于它将两个分支提示提交都列为其父级,以便 Git 可以沿着两个分支返回(现在通过合并提交将它们合并为一个"分支":这暴露了单词"branch"中的一个缺陷;请参阅">分支"到底是什么意思?)。
2这里有一些堕落的案例。 特别是,如果合并基是两个分支提示提交之一,我们要么有一个简单的"可快进"的情况,要么就没有什么可以合并的了。 但是,鉴于您发布的内容,您一定不能遇到这些情况之一。
3如果只有一个合并基提交(通常是这种情况),则两个分支提示提交的列出顺序并不重要。 但是,对于某些复杂的提交图,可能有两个或多个合并基提交。 在这里,情况变得相当模糊。 直到最近,git diff
命令才很好地处理这个问题;git merge
处理得更好,但它仍然很棘手。
4这个描述对你如何进行合并、图形的形状等做出了很多假设,并且在其他方面与git merge
内部实际做的事情相比大大简化。 这个想法是捕捉总体目标,而不涉及一些更棘手的机制。 例如,这忽略了合并如何处理重命名文件的情况。