跳到主要内容

Git 把多个提交合并成一个

写功能时习惯随手提交,等要发 PR 时分支上已经堆了十个 fix typo、update config。评审人看着累,自己也心虚。

这篇讲怎么在发 PR 前把这些提交压成一个。

核心有两个办法:

要可控、想挑着保留 message,用 git rebase -i;只想最快搞定,用 git reset --soft——后面接 hash 或分支名都行,不用数 HEAD~n。

一、rebase -i:一条命令进交互界面​

假设分支叫 aaa,想把最近 10 个提交合并成 1 个:

git rebase -i HEAD~10

更稳的写法是指定切出来的基线分支,不用数数量:

git rebase -i develop

会弹出一个编辑界面,长这样:

pick a1b2c3 add login api
pick d4e5f6 add validation
pick g7h8i9 fix typo
pick j1k2l3 add unit test
pick m4n5o6 update config
...

改成只留第一个 pick,后面全换成 s:

pick a1b2c3 add login api
s d4e5f6 add validation
s g7h8i9 fix typo
s j1k2l3 add unit test
s m4n5o6 update config
...

规则三条:

  • 第一行必须是 pick,作为基础提交
  • 后面全部写 s 或 squash
  • s 的含义是「把这条合并进上一条」

保存退出(vim 里 Esc → :wq → 回车)。

接着 Git 会让你编辑合并后的 message,把所有旧 message 删掉,写一条新的:

feat: add user login feature

- add login api
- add validation
- add unit test

再保存退出,合并完成。

二、reset --soft:最短路径​

git reset --soft HEAD~10
git commit -m "feat: add user login feature"
git push -f origin aaa

原理很直白:--soft 把 10 个提交的改动全部退回暂存区,工作区不动,然后重新提交一次。

代价是旧 message 全部丢失——不过本来也要重写,多数时候无所谓。

有 hash 就别数 HEAD~n​

HEAD~10 要人肉数提交,数错一次就得去翻 reflog。其实 reset --soft 后面接什么都可以,只要是指向某个提交的东西:

git reset --soft <commit-hash> # 最省事:回退到这个提交之后(不含它)
git reset --soft develop # 回退到 develop 的位置
git reset --soft origin/develop # 用远程分支当基线,不受本地 develop 影响
git reset --soft HEAD@{5} # reflog 里的历史位置

拿 hash 的方式:

git log --oneline -20 # 找到「合并起点」那条,复制左侧 hash
git merge-base HEAD develop # 或者让 Git 自己算和 develop 的分叉点

git reset --soft <hash> 的含义是:HEAD 移到这个提交上,之后的所有改动全部进暂存区。所以 hash 要选你要保留的最后一条提交——通常就是分支切出来时的基线提交。

不放心就先留个保险分支,再动手:

git branch backup # 当前状态打一份备份,出问题随时回来
git reset --soft $(git merge-base HEAD develop)

merge-base 省掉了数数和猜 hash 两件事,比 HEAD~10 可靠得多。真要数错了也不用慌:--soft 只挪指针、不碰工作区,git reflog 里能看到移动前的 HEAD,git reset --soft HEAD@{1} 就退回去了。

顺带区分一下三个参数,选错才会真的丢东西:

参数HEAD暂存区工作区用来做 squash
--soft移动保留保留✅ 就是这个
--mixed(默认)移动清空保留也行,但要重新 git add .
--hard移动清空清空❌ 改动直接没了

三、两种方式怎么选​

方式操作复杂度保留原 message适用场景
rebase -i中,要手改 s可选择保留想挑几条合、要中途删改提交
reset --soft HEAD~n极低,三行命令全部重写提交数量少、数得准
reset --soft <hash>极低,还不用数全部重写日常首选,提交多也不会数错

日常推荐 reset --soft,最快最干净。

四、完整流程速查​

git status # 确认工作区干净
git log --oneline -10 # 看一眼这 10 个提交

# 方式一:rebase,可控
git rebase -i develop

# 方式二:reset --soft,最快(有 hash 就用 hash,不用数 HEAD~n)
git reset --soft develop # 或 git reset --soft <commit-hash>
git commit -m "feat: add user login feature"

git push -f origin aaa

五、几个容易踩的坑​

  • 只在自己的功能分支上做,不要对 develop / main 操作
  • push -f 前确认没人基于你的分支开发
  • PR 已经被人 review 过的话,force push 会让已有 review 变成 outdated(记录还在,但不再对应最新代码)
  • 10 个提交里有 merge commit 也不用担心,rebase -i 会把它展开成普通提交
  • git reset --soft 用 hash 时,选的是要保留的最后一条提交,别把想留的那条也退进去
  • 想压缩的是已推送的提交时,后面必须跟 --force-with-lease 强推,别用裸 -f(见 Git 安全强推)
  • 数错了 HEAD~n 也不用紧张:git reflog 看一眼前一个位置,git reset --soft HEAD@{1} 就回来了,暂存区不丢

六、嫌麻烦就设个别名​

# ~/.gitconfig
[alias]
squash-since = "!f() { git reset --soft ${1:-$(git merge-base HEAD develop)} && git commit -m \"$2\"; }; f"

用法:

# 参数是 hash 或分支名,省略则自动用和 develop 的分叉点
git squash-since <commit-hash> "feat: add user login feature"
git squash-since develop "feat: add user login feature"
git squash-since "" "feat: add user login feature" # 自动算基线

一条命令,十合一,还不用数数。