假设你有一个主分支,你想将你的功能分支合并到一个合并--squash,如果你将拉取请求设置为壁球合并,就会发生。
据我了解,在"高级"解释中,功能分支将通过检查功能分支头部的临时分支来复制。现在,在这个临时功能分支上,所有提交都将以交互方式重新定位,因此与祖先提交不同的所有提交都将压缩到此临时功能分支上。
然后,该临时功能分支将重新定位到master上,因此之后主节点可以快进合并到新的sqaushed提交,并且临时功能分支将被删除。
将包含功能分支上的所有更改的新提交保留为主分支,但与该功能分支上的最后一个提交没有"合并连接/关系"。
通常在拉取请求中,源分支(在本例中为功能分支)也将被删除。如果不这样做,您仍将拥有一个未合并的功能分支,其状态与拉取请求之前的状态相同。
我的"高层次"理解是否正确,还是壁球融合的过程在幕后有所不同?
提前感谢!
你把几个必须分开的概念混合在一起。 特别是,拉取请求是 GitHub(或其他 Web 服务提供商)的功能。 这不是Git做的事情。1同时git merge
是一个 Git 命令行命令,它在您自己的计算机(笔记本电脑或其他计算机)上您自己的存储库中运行。 在使用 GitHub 拉取请求时,您(或至少同意该请求的任何人)可能会操纵某些浏览器按钮来执行"合并"、"变基和合并"或"挤压和合并",这些按钮会在 GitHub 上托管的存储库中进行更改。 这些是相关的,但在几个重要方面有所不同,最大的一个是谁实际拥有存储库。 一旦你拥有多个 Git 存储库,重要的是要记住哪个仓库具有哪些提交指向哪个分支名称,因为你的分支名称是你的,他们的是他们的:它们不需要彼此有任何关系,即使它们都被称为master
或topic
或feature/short
或其他什么。
1Git 有一个git request-pull
命令,该命令生成(但不发送)电子邮件。 这显然与 GitHub 样式的拉取请求不同。
合并这个词,在 Git 中,既是名词又是动词
gitglossary 以这种方式定义合并:
作为谓词:将另一个分支的内容(可能来自外部存储库)引入当前分支。如果合并的分支来自不同的存储库,则首先获取远程分支,然后将结果合并到当前分支中。这种提取和合并操作的组合称为拉取。合并由一个自动过程执行,该过程标识自分支分叉以来所做的更改,然后将所有这些更改一起应用。如果更改发生冲突,可能需要手动干预才能完成合并。
作为名词:除非是快进,否则成功的合并会导致创建一个表示合并结果的新提交,并将合并分支的提示作为父级。此提交称为"合并提交",有时简称为"合并"。
这两段包含了很多内容,我不喜欢将"pull"的定义楔入动词合并的定义中——特别是因为可以告诉git pull
运行git rebase
而不是git merge
作为其第二个 Git 命令——但动词/名词的区别就在这里。 当你执行操作时(动词作为合并部分),你通常会得到名词或形容词,即合并提交,作为结果。
考虑到这一点,让我们看一下实际合并的剖析
忽略git pull
(它只是为你运行git merge
,除非你另有说明),你调用主要的合并为谓词操作的方式是运行:
git merge <thing-to-specify-a-commit>
从命令行。 提交说明符参数可以是分支名称、标记名称、原始提交哈希 ID 或任何允许 Git 查找特定提交的内容。
在 Git 中,提交以图的形式存在,特别是有向无环图或 DAG。 每个提交都由其自己唯一的哈希 ID 标识 - 哈希 ID 本质上是提交的真实名称,并且这个特定名称在任何地方的每个 Git 存储库中都用于表示一个特定的提交,如果该提交在该 Git存储库中。 同时,每个提交都包含其父级的实际哈希 ID,作为其元数据的一部分。
在数学上,图的定义是G = (V,E),其中V是一组顶点(或节点),E是连接这些节点的一组边。 在 Git 中,节点是提交(由其哈希 ID 标识),边缘是单向链接或弧,从子提交指向其父提交。 这些单向链接让 Git 四处移动,从最孩子提交的提交开始。 这为我们提供了一系列绘制图形的方法。 对于 StackOverflow 帖子,我喜欢的方式是像这样写下来,并在右侧添加较新的提交:
... <-F <-G <-H
在这里,每个字母代表一个实际的哈希ID,来自字母的箭头是边/弧:如果H
是提交,它持有其父G
的哈希ID,所以H
指向G
。G
保存F
的哈希 ID,所以G
指向F
,依此类推。 这些箭头都是向后移动的 — 典型的 DAG 具有从父级到子项的箭头,但 Git 更喜欢向后工作(出于各种充分的理由)。阿拉伯数字不过,为了我们自己的方便,我们可以在没有任何箭头方向的情况下开始绘制它们,并且只记住 Git 是倒退的:
...--F--G--H
在向前方向上 - Git 实际上没有这样做 - 提交F
有一个子G
,G
有一个子H
。 从这里开始,让我们H
获得两个孩子,每个孩子都有自己的一个孩子:
I--J
/
...--F--G--H
K--L
为了让 Git找到这些提交,它需要一些分支名称。 分支名称包含分支中最后一个提交的实际哈希 ID——这意味着每次我们向分支添加新提交时,与名称关联的值都会更改——但现在让我们绘制它:
I--J <-- br1
/
...--F--G--H
K--L <-- br2
为了进行合并(运行动词类型的合并),我们将选择一个分支进行签出,例如br1
,然后在另一个分支上运行git merge
:
git checkout br1
git merge br2
这将启动合并为动词的过程。 如果一切顺利,它将进行合并提交,这只是至少有两个父级的提交:3
I--J
/
...--F--G--H M <-- br1 (HEAD)
/
K--L <-- br2
这个新的合并提交M
被添加为当前分支br1
上的最后一次提交,因此名称br1
现在指向新的合并提交M
。
Git 实际上使用三个输入计算合并提交M
的内容(合并文件):两个提示提交(此处、J
和L
)和合并基提交,它被定义为其他两个提交的最佳共同祖先。 在这种特殊情况下,最好的共同祖先很容易看到:它是提交H
。四你可以将 Git 处理此问题的方式视为:
- 运行
git diff --find-renameshash-of-Hhash-of-J
以查看我们更改了哪些内容; - 运行
git diff --find-renameshash-of-Hhash-of-L
以查看它们更改了哪些内容;
合并 - 这两个差异输出,将合并的更改应用于提交
H
中的文件。
忽略冲突解决的所有机制,以及git merge
可以执行的各种特殊非合并情况,这确实是任何合并的关键。Git 找到公共起始点提交,进行两组差异,将它们组合在一起,并将组合的差异应用于公共起点快照。这将形成合并结果的快照。
2特别是,主 Git 对象数据库中的所有内容(提交和文件,以及各种粘附对象)都将永久冻结。 Git 需要这个来使其哈希 ID 工作。 提交在创建时知道其父级或父级:其哈希 ID 已存在。提交还不知道其子项。 承诺刚刚诞生! 它的记忆现在被冻结了。 最终,后来,它有了第一个孩子。 父级无法了解其子项的哈希 ID,因为提交已全部冻结。 如果父母得到第二个孩子、第三个孩子等等,情况也是如此。 孩子认识他们的父母,但父母不了解他们的孩子。
3Git 将具有三个或更多父项的合并提交称为章鱼合并。 章鱼合并没有什么是一系列普通合并不能做的——事实上,情况正好相反:你可以用普通合并做一些 Git 拒绝作为单个章鱼合并做的事情。 章鱼合并比常规合并弱的事实在某种程度上很有用,当你检查一个新的存储库时,你可以合理地保证这个合并不会引入秘密更改,也没有任何冲突解决方案。 但是其他只允许提交对合并的版本控制系统与 Git 一样强大。
4从技术上讲,合并基是两个提交的最低共同祖先,使用 LCA 算法的广义形式。 LCA 在树上得到了很好的定义 — 我上面显示的图形片段实际上是树而不是 DAG——但在 DAG 中可能有多个 LCA。 在这个特定的答案中,我不打算讨论 Git 如何处理多 LCA 情况。
壁球合并现在微不足道
一旦你理解了上述所有内容,git merge --squash
就变得非常简单。 Git 会为真正的合并做所有事情,然后在最后,它进行一个有一个父级而不是两个父级的提交。5
也就是说,我们从:
I--J <-- br1
/
...--F--G--H
K--L <-- br2
并运行git checkout br1; git merge --squash br2
. Git 找到合并基础H
,执行两个差异,合并差异,并将其应用于H
的内容。 然后 Git 制作——好吧,让你制造;请参阅脚注 5 — 新的合并提交,但 Git 不是与父J
和L
M
,而是使用一个父级进行提交,我们可能希望用一个父级调用S
(用于 squash),J
:
I--J--S <-- br1
/
...--F--G--H
K--L <-- br2
如果 Git 进行了合并M
——事实上我们仍然可以进行M
,哈希不会匹配,但M
和S
的快照会匹配。 您可以通过制作M
来确认这一点:
git checkout -b experiment <hash-of-J>
git merge br2
这导致:
I--J--S <-- br1
/
...--F--G--H M <-- experiment (HEAD)
/
K--L <-- br2
M
和S
的内容将匹配:
git diff br1 experiment
什么都不会显示。
5当你运行git merge --squash
时,Git 会让你自己运行git commit
。--squash
标志打开--no-commit
标志。 这是古代版本的 Git 的遗留物,--squash
只是在运行git commit
之前退出git merge
- 真正的合并实际上在最后调用git commit
,除非您使用--no-commit
或合并有冲突。