【笔记】Git Merge 与 Rebase 的方向、参数与 Fast-forward 语义
1. 一句话心智模型
merge 和 rebase 都是在让一个分支包含另一个分支的新提交,但它们移动的对象相反:
merge站在当前分支(HEAD)上,把另一个分支的历史汇入当前分支;已有提交不改,必要时新增一个 merge commit。rebase把当前分支自己的提交摘下,再逐个重放到目标基底之上;被重放的提交会得到新的 commit ID,目标分支本身不动。
flowchart LR
M[当前分支 HEAD] -->|merge 对方分支| MC[新增汇合提交]
F[当前分支自己的提交] -->|rebase 重放| R[新的基底之上]
所以先问“谁站在 HEAD 上、谁的提交要被移动”,再看命令参数;不要把两个命令的“方向”理解成参数左右顺序。
2. Merge:把对方分支合入 HEAD
2.1 目标分支是隐式的
1
git merge <branch>
命令含义是“把 <branch> 合并进当前 HEAD 所在的分支”。git merge 没有一个可指定目标分支的 porcelain 参数;要合并到 main,先切到 main:
1
2
git switch main
git merge feature
若必须不切换工作树而显式构造两个 parent,需要退到 merge-tree、commit-tree、update-ref 等 plumbing 组合。这不是日常 merge 的常规入口,也不会改变“当前 ref 才是被更新者”的基本语义。
2.2 Fast-forward 是是否需要新增汇合点
设当前分支指向 A,待合并分支指向 C:
1
2
3
A---B---C (feature)
^
main
如果当前分支是对方分支的祖先,Git 只需把当前分支 ref 从 A 移到 C,这就是 fast-forward,不创建 merge commit。若两边已经分叉,Git 才需要用双方共同祖先和两条分支的末端生成新的汇合提交。
默认行为相当于 --ff(配置项为 merge.ff = true):
| 参数 | 语义 |
|---|---|
--ff | 能快进就快进;已分叉时创建 merge commit |
--ff-only | 只能快进;已分叉立即报错,不自动产生 merge commit |
--no-ff | 即使能快进也创建 merge commit,保留分支曾经存在的拓扑 |
--squash | 将对方分支相对当前分支的合并结果放入工作树和 index,不自动创建 commit,也不记录 merge parent |
1
git config --global merge.ff only
将默认策略改为 ff-only 后,线性历史仍可直接前进;发生分叉时必须显式选择 rebase、普通 merge 或 --no-ff,避免自动生成未预期的汇合提交。
2.3 其他常用控制项
-m "message":指定 merge commit 的提交信息。--abort/--continue:冲突时放弃或在解决后继续。-X ours/-X theirs:在合并策略处理冲突时优先采用当前分支或对方分支的版本;它们不是“无条件覆盖所有文件”的命令。-s <strategy>:选择合并策略,例如现代 Git 默认使用的ort。
3. Rebase:重新选择当前分支的基底
3.1 三个位置和具名参数
1
git rebase [-i] [<options>] [--onto <newbase> | --keep-base] [<upstream> [<branch>]]
<upstream> 和 <branch> 是按顺序解释的位置参数;--onto 是具名选项。
| 参数 | 回答的问题 | 不写时的默认值 |
|---|---|---|
<branch> | 哪个分支的指针要移动? | 当前 HEAD |
<upstream> | 从哪里划分“该分支自己的提交”? | 当前分支配置的追踪分支;没有追踪分支时,裸跑 git rebase 会报错 |
--onto <newbase> | 重放结果接到哪个提交或分支上? | <upstream> 的字面指向 |
因此不能只写第二个位置参数:<branch> 必须在 <upstream> 已经给出的前提下才能出现。
3.2 branch 参数只是一次自动切换
Rebase 实际操作的对象永远是 HEAD。显式给出 <branch> 时,Git 会先切换到该分支,再执行同样的 rebase:
1
git rebase --onto A B C
可理解为:
1
2
git switch C
git rebase --onto A B
这个切换是命令执行后的真实状态;当前位于另一个分支且有未提交修改时,隐式切换也可能因为工作树冲突而失败。
4. upstream、merge-base 与 --onto 的分工
upstream 同时参与两件事,但它们不是同一个概念:
- 它作为重放范围的下界,用来找出
branch独有的提交。 - 未写
--onto时,它的字面指向也是重放目标。
重放范围可抽象为:
1
merge-base(upstream, branch) 之后,到 branch 末端的提交
merge-base 只负责识别共同祖先,不是 --onto 的默认值。两者分叉时尤其要区分:
1
2
3
4
5
A---B---C (upstream)
/
base--
\
D---E---F (branch)
执行 git rebase upstream branch 会把 D、E、F 重放到 C 后面;A、B、C 不会被当作 branch 自己的提交再次重放。这里共同祖先是 base,但目标基底是 upstream 当前指向的 C。
4.1 为什么需要 --onto
日常场景里,划分范围的分支和新基底通常都是 main,所以只需:
1
git rebase main feature
当“哪些提交要保留”和“接到哪里”不一致时,才拆开写:
1
2
3
main ---X---Y (topic 的基底)
\
D---E---F (feature)
1
git rebase --onto main topic feature
topic 划定了 D、E、F 是要重放的部分,main 指定新的落点;X、Y 不在重放列表中,因此 feature 的新历史会跳过它们。若希望保留 X、Y,应改用 git rebase main feature。
5. 写法组合与常用流程
| 写法 | 被操作的分支 | 重放范围的上游 | 新基底 |
|---|---|---|---|
git rebase | HEAD | 追踪分支 | 追踪分支 |
git rebase --onto develop | HEAD | 追踪分支 | develop |
git rebase main | HEAD | main | main |
git rebase --onto develop main | HEAD | main | develop |
git rebase main feature | feature | main | main |
git rebase --onto develop main feature | feature | main | develop |
常用的历史整理和冲突控制项包括:
-i:交互式重排、合并、改写或删除提交。--autosquash:将fixup!/squash!提交自动放到目标提交附近,通常配合-i。--rebase-merges:尽量保留原有分支与合并结构,而不是全部拍平成一条线。--abort/--continue/--skip:放弃、解决后继续、跳过当前重放提交。--exec "<cmd>":每次重放后执行检查命令,例如测试。
6. 冲突策略的视角陷阱
-X ours / -X theirs 的含义依赖当前操作:
- 在
merge中,ours是当前HEAD分支,theirs是被合入的分支。 - 在
rebase的每次重放中,ours通常是已经重放到的目标基底,theirs是当前正在重放的提交。
这不是两个命令把参数实现成了相反的字符串,而是冲突合并时“当前版本”的参照物变了。使用前应先确认是在汇合两个末端,还是把一个提交应用到另一个基底上;无论哪种模式,策略选项都只影响冲突处理,不等于无条件删除另一侧全部改动。
7. 选择依据与复习索引
- 想保留双方历史并显式记录一次汇合:使用
merge;是否允许 fast-forward 由--ff、--ff-only、--no-ff决定。 - 想让自己的提交基于最新目标分支、接受 commit ID 改写:使用
rebase;先确定重放范围,再确定--onto落点。 - 看到
git rebase A B时,B是被切换并移动的分支,A是范围边界和默认落点;看到git merge A时,A是被合入当前HEAD的分支。 merge-base用于找共同祖先和重放范围,不能替代--onto的目标基底。ours/theirs必须结合 merge 或 rebase 的当前视角解释。