merge

5 阅读4分钟

简介

学习完了分支之后,合并又是什么呢?

只要你执行:

git merge <远程\或者本地的某个分支>

Git 就会尝试:

把“指定的提交历史”整合进当前分支。

说白了,merge就是两条不同的历史,进行整合,merge 是发生在“两条不同的提交历史线之间”

merge 本质总结(非常关键)

merge 的本质不是:

合并分支

而是:

合并两个 commit 节点的历史

分支只是:

指向 commit 的标签

一个更准确的理解方式:

merge 发生的真正条件是:

当前 HEAD 指向的 commit
和你要 merge 的 commit
不在同一条线性历史上

只要不是直线关系,就需要三方合并。

1.merge做了什么?

看图:

A --- B --- C   (master)
        \
         D --- E   (dev)

现在你在 master 上执行:

git merge dev

Git 会:

  • 找到两条线的共同祖先(B)
  • 计算差异
  • 生成一个新的 commit

结果:

A --- B --- C -------- F   (master)
        \            /
         D --- E ----     (dev)

这个 F 就叫:

merge commit

它有两个 parent:

  • 一个指向 C
  • 一个指向 E

这就是真正意义上的“合并”。

Git merge 的核心步骤是:

1️⃣ 找共同祖先(merge base)
2️⃣ 比较三份内容:

  • 祖先版本(才能计算差异)
  • 当前分支版本
  • 要合并分支版本
    3️⃣ 自动合并
    4️⃣ 如果冲突 → 让你手动解决

这叫:

三方合并(three-way merge

2.为什么 merge 叫“合并历史”?

关键点:

merge commit 会保留两条历史线。

看图:

        D --- E
       /         \
A --- B --- C ----- F

从 F 可以看到:

  • 一条 parent 指向 C
  • 一条 parent 指向 E

历史是完整保留的。

3.三种merge场景

1)场景 1:不同分支之间(最常见)

A --- B --- C   (master)
        \
         D --- E   (dev)

你在 master 上:

git merge dev

这是典型的“两个分支合并”。并产生一个merge commit

必须满足:

1️⃣ 两条分支都各自有新的 commit
2️⃣ 当前分支不是对方(目标分支)的祖先

一定会产生 merge commit。

2)场景 2:合并远程分支

git merge origin/master

这里:

  • 当前分支master
  • origin/master

虽然 origin/master 不是“本地分支”,
但本质仍然是两条提交线。

只要对应的远程分支上,有新的提交,就和场景1是一样的

3)场景 3:merge 一个具体 commit

1)git merge commit-hash

例如:

git merge a1b2c3d

意思是:

把这个 commit(以及它的历史)合并到当前分支。

本质理解(非常关键)

Git merge 的本质是:

把“某个提交节点”整合进当前 HEAD。

它不是“只拿这个 commit 的修改”。

它是:

以这个 commit 为末端的那条历史线。

举例说明

假设现在仓库是:

A --- B --- C --- D   (master)
        \
         E --- F --- G   (feature)

你当前在:

master

现在你执行:

git merge F (注意:F 是 feature 分支上的一个 commit(不是分支名))

找到当前 HEAD(D)
找到目标 commit(F)
找到它们的共同祖先(B) G 是在 F 之后的提交,
merge F 不会包含 G。 因为你 merge 的是“截止到 F 的历史”。

结果:

A --- B --- C ---D--- H   (master)
        \            /
         E -------- F --- G

H 的第二个 parent 指向 F,

但 G 不会被合入。

有可能是到F是测试好的,稳定的功能版本,之后的还没测试,就属于这类场景的合并

2)cherry-pick

如果你只想要:

某个 commit 的改动
而不想引入整条历史

应该用:

git cherry-pick <commit>

一句话定义

git cherry-pick = 把某一个 commit 的“改动内容”复制到当前分支,生成一个新的 commit。

注意:

✔ 是“复制改动”
❌ 不是“合并历史”

例子

当前仓库结构:

A --- B --- C   (master)
        \
         D --- E --- F   (feature)

你现在在:master

你只想要:

feature 上的 E 这个改动

但不想合并整个 feature 分支。

你执行:

git cherry-pick E

结果变成:

A --- B --- C --- E'   (master)
        \
         D --- E --- F   (feature)

注意:

E' 是一个 新的 commit

相当于你在 master 上创建了一个新提交,

它的 parent 是 C,

不是 D。

所以 hash 必然不同。

内部流程是:

1️⃣ 取出 commit E
2️⃣ 计算它相对 parent 的 diff
3️⃣ 把这个 diff 应用到当前 HEAD
4️⃣ 生成新的 commit

本质是:

应用补丁

热修复回移:线上有 bug。

你在 master 上修复:然后你需要把这个修复带回 develop,就需要用到这种场景

一句话总结:

merge 是“合并历史”
cherry-pick 是“复制改动”

一个是结构操作,
一个是补丁操作。

4.一种不产生merge commit的合并

如果当前分支是目标分支的祖先:

A --- B --- C   (master)
                \
                 D --- E   (dev)

在 master 上:

git merge dev

会变成:

A --- B --- C --- D --- E

这种就叫 fast-forward

本质不是创建 merge commit,只是移动指针。所以这种合并不会产生 merge commit

这种合并,后续在rebase一篇中,再继续探讨