问题描述
我有两个分支 X 和 Y,它们用于包含相同的文件“sample.txt”。我只想要这个文件在分支 Y 中,因此,在分支 X 中删除并做了一个 HEAD 提交。
现在,每当我将分支 Y 合并到 X 时,它都会显示与“sample.txt”[已删除/修改] 的冲突。我想让 GIT 忘记分支 X 中有任何文件“sample.txt”,以便在合并时它永远不会从分支 Y 读取相同的文件并显示冲突。
解决方法
"You can't get there from here." (Boston variant.) 您只需要记住合并时要小心。嗯,有时。有时你会遇到冲突!这是时间和原因。请注意,当您确实遇到冲突时,您仍然需要小心。这才是真正的通用答案:在 Git 中合并时,要小心。Git 是一个工具(或一组工具),而不是一个解决方案。
一旦您正确地完成了一次合并,您可能就不必再次手动仔细地进行合并了。但是合并时要小心!
分支无关
在 Git 中,只有 提交 重要。提交是永久性的(大部分)和不可更改的(完全),并且是 存储库中的历史记录。 分支名称只需选择一个特定的提交即可。
提交同时存在于多个分支
我们来看一个简单的 Git 分支案例。在这里,我们从一个很小的存储库开始,其中包含三个提交。提交 A
——它的真实哈希 ID 是一些看似随机的数字,用 hexadecimal 表示——是第一次提交。 Commit B
,第二个commit,直接指(实际上,字面意思是包含)commit A
的hash ID。我们说 commit B
指向 commit A
。提交 C
,第三次也是当前最后一次提交,指的是提交 B
的哈希 ID。因此我们有:
A <-B <-C
为了定位提交 C
——其哈希 ID 是,记住,看起来是随机的:没有办法先验知道提交 C
是最后一个没有读取所有提交并解决它,并且在大型存储库中,这会非常慢,例如,Git 将提交 C
的哈希 ID 存储在一个分支名称中。这允许 Git 非常快速地找到提交 C
。让我们使用名称 main
,就像 GitHub 现在所做的那样,来存储这个哈希 ID:
A--B--C <-- main
(提交本身总是指向后。他们必须,因为我们不知道当我们创建 B
时 A
的哈希 ID 是什么,也不知道什么是C
将在我们创建 B
时使用。但是为了在 StackOverflow 上绘图,我通常会使用连接线,因为箭头字体在某些系统上效果不佳。)
要在 main
上进行新的提交,我们选择该分支名称,即选择该提交。这也会复制文件——提交存储完整的快照,每次提交都有你或任何人提交时的每个文件的完整副本——这样你就可以看到它们并处理它们。我们进行了一些更改并进行了新的提交,这将获得一个新的、独特的、随机的(但实际上不是随机的)哈希 ID,我们将其称为 D
。吉特:
-
确保
D
包含正确的快照(来自 Git 的暂存区); -
使用您现在配置的姓名和电子邮件地址作为这次新提交的作者和提交者;
-
使用“现在”(日期和时间)作为这次新提交的时间戳;
-
收集要包含在提交中的日志消息;
-
使用当前提交的哈希 ID
C
作为新提交的父哈希 ID,因此新提交D
将指向C
;和 -
写出所有这些,获取新提交
D
的实际哈希 ID:A--B--C
现在是神奇的一步:Git 只是将新提交的哈希 ID 存储到我们现在使用的任何分支名称中。这会导致 name main
指向新提交 D
:
A--B--C--D <-- main
如果我们现在创建几个新分支:
A--B--C--D <-- main,topic1,topic2
所有三个名字将选择相同提交D
。我们选择三个名称之一作为“当前分支”,并通过附加特殊名称 HEAD
来绘制它:
A--B--C--D <-- main,topic1 (HEAD),topic2
一次提交多个新提交,只会导致当前分支名称前进:
E--F <-- topic1 (HEAD)
/
A--B--C--D <-- main,topic2
如果我们现在 git checkout topic2
,Git 删除所有与 commit F
相关的文件——我们刚才删除的那个文件——并安装在它们的放置提交D
的所有文件:
E--F <-- topic1
/
A--B--C--D <-- main,topic2 (HEAD)
现在进行两次新提交会产生:
E--F <-- topic1
/
A--B--C--D <-- main
\
G--H <-- topic2 (HEAD)
现在,这是关于 Git 的奇怪而特别的事情:提交 A-B-C-D
实际上位于所有三个分支。我们现在可以完全删除名称main
:
E--F <-- topic1
/
A--B--C--D
\
G--H <-- topic2 (HEAD)
合并
如果我们合并两个主题,我们会得到一个 merge commit,这是一个指向两个或更多父项的提交。 Git 自己为这次提交构建快照。为此,Git 从两个分支提示提交(此处为 F
和 H
)向后工作,以找到最常见(共享)提交 .在这种情况下,就是提交 D
。
Git 然后找出自 D
中的快照在两个分支提示快照中的每一个中更改。这些是要合并的更改。如果更改之一是删除文件,另一个是对文件不做任何事情,则这两者的组合是删除该文件 .
如果一个更改是删除文件,另一个是修改文件 ...好吧,那个,对于 Git 来说,是一个冲突。它不能对文件做一个小改动又完全删除文件。所以这次你会遇到冲突。
除了自合并基础以来每个文件发生了什么,其他什么都不重要。所以关键问题是:
- 哪个提交是合并基础?
- 从那时起,每个文件发生了什么变化,才能获得两个分支提示提交?
在任何情况下,Git 都可能完全自己构建合并结果,或者它可能会因冲突而停止——这取决于每个文件发生了什么——并让您修复问题。无论哪种方式,您最终都会获得应该进入新提交的最终文件和内容集。如果您必须手动修复合并混乱,请将这些文件存储到 Git 的暂存区中,就像常规的非合并提交一样。1然后您完成合并并Git 进行合并提交
在这里,我假设我们运行了 git checkout topic1; git merge topic2
并且它正常工作:
E--F
/ \
A--B--C--D I <-- topic1 (HEAD)
\ /
G--H <-- topic2
注意 name topic1
是如何更新的。名称 topic2
仍指向现有提交 H
。但是我们现在可以安全地删除name,如果我们用完了它;一旦我们这样做了,我们就只有名字topic1
。 所有提交现在都在 topic1
上,因为从 I
向后移动到 F
和 H
意味着所有提交可以从合并提交访问。 (有关可达性概念的更多信息,请参阅Think Like (a) Git。)
如果愿意,我们现在可以将 topic1
重命名为 main
:
E--F
/ \
A--B--C--D I <-- main (HEAD)
\ /
G--H
现在所有提交都在一个名为 main
的分支上。除了查找提交之外,分支名称从来没有真正重要过。每个名称选择一个特定的提交——通过指向它——并且根据定义,该提交是该分支上的最后提交。从那个点可达的所有早期提交也在那个分支上。
1请注意暂存区,Git 也称其为index 和——现在很少——缓存 em>,与您的工作树分开,您可以在其中查看和处理文件。例如,此暂存区的存在就是为什么您必须如此频繁地运行 git add
。
有些版本控制系统没有暂存区。他们仍然需要一些至少有点相似的东西,但他们隐藏它并且不让你完成它。但 Git 不是其中之一:Git 对它的暂存区大声而自豪,并迫使您了解它。不要试图在不了解它的情况下度过难关。
合并更改未来合并基础
这里最重要的一点不仅仅是通过找到合并基础来了解合并的工作原理,而是了解未来合并可能使用不同的合并基础.让我们画一个更复杂的情况,有一个主分支和一个不断合并的持续开发分支:
...--o--B1-o--L---M1--o---M2--o <-- main
\ / /
o--o--B2--o--B2--o--o <-- develop
此处,B1
是某个时候的合并基础。特别是,它是这个点的合并基础:
...--o--B1-o--L <-- main (HEAD)
\
o--o--B2 <-- develop
我们在 main
上作为 HEAD
运行 git merge develop
和 Git found B1
作为合并基础。 Git 将 B1
中的快照与 L
中的快照进行比较以查看我们在 main
中的更改,并将 B1
中的快照与 B2
中的快照进行了比较查看他们(无论他们是谁)在 develop
上的更改。 Git 然后结合这些更改并进行新的合并提交 M1
:
...--o--B1-o--L---M1 <-- main (HEAD)
\ /
o--o--B2 <-- develop
后来,在更多的开发提交和 main
上的另一个提交之后,我们遇到了这种情况:
...--o--B1-o--L---M1--o <-- main
\ /
o--o--B2--o--B2 <-- develop
我们运行 git checkout main
和 git merge develop
。 Git 从 o
上的最新(最右侧 main
)提交向后搜索,并从 B2
上的最新 (develop
) 提交向后搜索。从 main
往回走,发现无聊提交 o
,然后提交 M1
,然后同时提交 L
和 B2
。从develop
往回走找到B2
,无聊的提交o
(和main
上的不一样),然后提交B2
。
Commit B2
在两个分支上,因此它是合并基础的候选者。此外,它是最佳这样的候选者,因此它实际上是合并基础。
这意味着当 Git 这次进行合并时,重要的是与共享 B2
提交的比较!假设您有一个在 B1
中的文件,在 B2
之前被删除,但 确实 出现在 M1
中,或者至少在随后的 {{1} } 提交(也许您将文件添加回 之后 Git 通过无冲突的合并删除了它;这是您谨慎时的选择之一)。进一步假设 o
中仍然没有缺少 B1
文件。那么比较结果包括:
B2
这两个变化的组合,在Git的合并规则中,就是添加文件。因此,Git 的自动 B1 vs o-on-main => add file
B1 vs B2 => do nothing at all
合并结果将保留文件,其格式与 M2
上当前最后一次提交相同。
一旦合并完成——commit main
存在——commit M2
将成为未来的合并基础:
B2
例如,因为 ...--o--B1-o--L---M1--o---M2 <-- main
\ / /
o--o--B2--o--B2--o <-- develop
位于两个分支(通过 B2
)。