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" # 自动算基线
一条命令,十合一,还不用数数。