文章

【笔记】Git Merge 与 Rebase 的方向、参数与 Fast-forward 语义

【笔记】Git Merge 与 Rebase 的方向、参数与 Fast-forward 语义

1. 一句话心智模型

mergerebase 都是在让一个分支包含另一个分支的新提交,但它们移动的对象相反:

  • 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-treecommit-treeupdate-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. upstreammerge-base--onto 的分工

upstream 同时参与两件事,但它们不是同一个概念:

  1. 它作为重放范围的下界,用来找出 branch 独有的提交。
  2. 未写 --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 rebaseHEAD追踪分支追踪分支
git rebase --onto developHEAD追踪分支develop
git rebase mainHEADmainmain
git rebase --onto develop mainHEADmaindevelop
git rebase main featurefeaturemainmain
git rebase --onto develop main featurefeaturemaindevelop

常用的历史整理和冲突控制项包括:

  • -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 的当前视角解释。

8. 参考资料

本文由作者按照 CC BY 4.0 进行授权