bigywater.tech
Git · 48

fast-forward 与 --no-ff

什么时候 git merge 只是“挪一下指针”?--no-ff 到底多出了什么?--ff-only 又在什么情况下拒绝?这篇逐种实测,数新增了几个对象,看历史的形状,以及 --squash 为什么会让 git branch -d 说“没合并”。

0 个
快进合并新增的对象数
1 个
--no-ff 新增的对象数:合并提交本身,tree 复用 feature 末端的
not fully merged
--squash 之后 git branch -d feature 的回答——提交图里没有链接

快进

实测 · 本机执行输出
  $ git merge feature 2>&1 | head -4 | sed -E "s/[0-9a-f]{7}\.\.[0-9a-f]{7}/<旧>..<新>/"
  Updating <旧>..<新>
  Fast-forward
   f | 1 +
   1 file changed, 1 insertion(+)
  对象数 8 → 8(新增 0 个);main 现在等于 feature: True
  $ git reflog show main --format='%gd: %gs' | head -2; git log --graph --oneline | sed -E 's/[0-9a-f]{7} //'
  main@{0}: merge feature: Fast-forward
  main@{1}: commit (initial): M1
  * F2
  * F1
  * M1

当前分支是被合并分支的祖先时,合并只是把分支指针挪到对方的位置:没有新提交、没有新对象,历史是一条直线。reflog 里记着 merge feature: Fast-forward。

--no-ff

实测 · 本机执行输出
  $ git merge --no-ff -m "合并 feature" feature 2>&1 | head -3
  Merge made by the 'ort' strategy.
   f | 1 +
   1 file changed, 1 insertion(+)
  对象数 8 → 9(只新增 1 个:合并提交本身,tree 复用)
  合并提交的 tree == feature 末端的 tree: True;parent 数: 2
  $ git log --graph --oneline | sed -E 's/[0-9a-f]{7} //'
  *   合并 feature
  |\
  | * F2
  | * F1
  |/
  * M1

强制创建合并提交,即使可以快进。新提交的 tree 就是 feature 末端的 tree(复用同一个对象),有两个 parent,整个合并只新增 1 个对象。好处是主线上留下功能边界,--first-parent 可以把一个功能折叠成一行。

分叉

实测 · 本机执行输出
  $ git merge --ff-only feature 2>&1 | grep -E "fatal|hint: *git|Merge"; git log --oneline | wc -l
  fatal: Not possible to fast-forward, aborting.
         2
  $ git config merge.ff only; git merge feature 2>&1 | grep -E "fatal|Merge"; git config --unset merge.ff
  fatal: Not possible to fast-forward, aborting.
  $ git merge -m "合并 feature" feature 2>&1 | head -2
  Merge made by the 'ort' strategy.
   f | 1 +

双方都有对方没有的提交时无法快进。默认生成合并提交;--ff-only(或 merge.ff=only、pull --ff-only)改为直接拒绝,提示 Not possible to fast-forward。

历史形状

实测 · 本机执行输出
  [快进]
    $ git log --graph --oneline | sed -E 's/[0-9a-f]{7} //'; echo '  -- --first-parent:'; git log --first-parent --oneline | sed -E 's/[0-9a-f]{7} //'; echo '  -- --merges 的个数:'; git log --merges --oneline | wc -l
    * F2
    * F1
    * M1
      -- --first-parent:
    F2
    F1
    M1
      -- --merges 的个数:
           0
  [--no-ff]
    $ git log --graph --oneline | sed -E 's/[0-9a-f]{7} //'; echo '  -- --first-parent:'; git log --first-parent --oneline | sed -E 's/[0-9a-f]{7} //'; echo '  -- --merges 的个数:'; git log --merges --oneline | wc -l
    *   合并feature
    |\
    | * F2
    | * F1
    |/
    * M1
      -- --first-parent:
    合并feature
    M1
      -- --merges 的个数:
           1

快进是直线,看不出哪些提交属于同一个功能;--no-ff 有菱形,git log --merges 能数出合并次数。

--squash

实测 · 本机执行输出
  $ git merge --squash feature 2>&1 | head -3; git status --short
  Updating 1f97caf..dbed7d4
  Fast-forward
  Squash commit -- not updating HEAD
  A  f
  新提交的 parent 数: 1;tree == feature 末端 tree: True
  $ git branch --merged | tr -d " *" | tr "\n" " "; echo "(--merged 里没有 feature)"; git branch -d feature 2>&1 | head -2 | cut -c1-70
  main (--merged 里没有 feature)
  error: the branch 'feature' is not fully merged
  hint: If you are sure you want to delete it, run 'git branch -D featur

--squash 把对方分支的所有改动放进暂存区,由你提交为一个单 parent 的普通提交。结果的 tree 相同,但提交图里没有指向 feature 的连线,所以 git 不认为它“已合并”,git branch -d 会拒绝。

分步演示

fast-forward 与 --no-ff
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。对象数 = git count-objects -v 里的 count + in-pack;提交使用真实时间,输出里的 hash 做了去除
    • 没有验证:merge.ff=false 配置、git pull 的 fast-forward 行为(上面只用 --ff-only 做了对比);rebase 之后再快进合并的历史形状;--no-ff 对 git bisect、git revert -m 的影响(见后面几篇);GitHub 等托管平台的 “Squash and merge” 按钮
    京ICP备2026067502号-1京公网安备11010502063487号