外观
06 · 变基、拣选与改写历史
这一章是误操作重灾区。共同特点:会换掉 commit 的 SHA。没 push 的提交可以随便整理;已经出现在共享分支上的,默认不要改写,用 revert。
铁律:已推送到别人正在用的分支 → 只追加(revert / 新 commit),不改写(reset / rebase / amend + force)。
git reset 三件套
作用: 把当前分支指针(以及按模式决定的暂存区、工作区)移到某个已有提交。
改动范围: 本地历史一定动;暂存区 / 工作区取决于模式。
典型场景:
- 刚 commit 完发现不该提交,想把改动拿回暂存区或工作区重做。
- 本地玩崩了,回到上一次已知好的提交。
- 把分支尖端挪到 reflog 里救回来的 SHA。
三种模式对照(假设从 C3 回到 C2):
| 模式 | HEAD / 分支 | 暂存区 | 工作区 | 一句话 |
|---|---|---|---|---|
--soft | 到 C2 | 仍是 C3 的内容 | 仍是 C3 的内容 | 提交撤掉,改动全部待 commit |
--mixed(默认) | 到 C2 | 回到 C2 | 仍是 C3 的内容 | 提交和暂存撤掉,代码还在工作区 |
--hard | 到 C2 | 回到 C2 | 回到 C2 | 三区对齐到 C2,未入库改动丢 |

常用写法:
bash
git reset --soft HEAD~1 # 撤销最近一次 commit,改动留在暂存区
git reset HEAD~1 # --mixed:撤销 commit,改动留在工作区
git reset --hard HEAD~1 # 危险:最近一次提交和当时工作区一起丢掉
git reset --hard origin/main # 危险:本地 main 对齐远程(先确认没有独有提交)
git reset --hard ORIG_HEAD # 回到上一次大规模移动 HEAD 之前(merge/reset/rebase)
git reset --hard a1b2c3d # 把分支钉到某个 shaHEAD~1 是「当前提交的第一父提交」。合并提交有两个父,HEAD~1 是被合入的那条当前分支线。
不要这样用:
- 对已 push 的共享分支
reset --hard再 force push。 - 工作区还有没 add 的实验代码时
reset --hard。 - 冲突解决到一半
reset --hard(用--abort)。
做错了怎么办: 立刻:
bash
git reflog
git reset --hard HEAD@{1}
# 或
git reset --hard ORIG_HEAD只要做过 commit,C3 还在。详见 急救手册 · reset --hard。
git revert
作用: 新增一次「与某次提交相反」的提交,不改写旧历史。
改动范围: 本地历史(只追加)。适合已经 push 的情况。
典型场景: 线上某次 commit 有 bug,要撤销它的效果,但保留「曾经存在过、后来被撤销」的记录。
bash
git revert a1b2c3d
git revert HEAD
git revert -m 1 <merge_sha> # 撤销一次 merge:-m 1 表示保留第一父(通常是 main 线)
git revert --no-commit a1b2c3d^..HEAD # 把一段提交的反向改动先放进暂存区不要这样用: 把 revert 和 reset 当同义词。reset 是「分支指针往回走」,revert 是「往前再走一步把效果抵消」。
做错了怎么办: revert 本身也是一次 commit,再 revert 那次 revert,或未 push 时 reset --soft HEAD~1。
git cherry-pick
作用: 把某次提交的补丁复制到当前分支,生成一颗新 SHA 的提交。
改动范围: 本地历史(追加)。
典型场景:
- 提交打到
feature/login了,其实应该在hotfix上。 - 从同事分支只拿走某一个 bugfix,不要整支合并。
- 急救:先把正确提交摘到正确分支,再处理错误分支上的尾巴。
bash
git cherry-pick a1b2c3d
git cherry-pick a1b2c3d..b2c3d4e # 右开区间,注意是否包含端点
git cherry-pick -n a1b2c3d # 只进暂存区,不自动 commit
git cherry-pick --abort
git cherry-pick --continue不要这样用: 在长期分支之间反复互相 cherry-pick 同一批提交,以后 merge 时会「重复补丁」冲突。能 merge / rebase 就不要靠 pick 当常规同步。
做错了怎么办: 进行中 --abort。已经生成新提交且未 push:reset --soft HEAD~1。旧提交还在原分支,pick 只是复制。
git rebase
作用: 把当前分支上「相对上游多出来的提交」一个个摘下来,接到新的基(通常是更新过的 main)后面。历史变成一条直线,SHA 全变。
改动范围: 改写本地历史。
典型场景:
- 功能分支开发几天了,
main已前进,想在 PR 前跟上,并保持线性历史。 - 用交互式 rebase 把 8 个「wip」整理成 2 个可读提交。

常用写法:
bash
git fetch origin
git switch feature/login
git rebase origin/main
git rebase --onto main feature/old feature/login
# 把 feature/login 上「不含 feature/old」的提交接到 main 上
git rebase --abort
git rebase --continue # 解决冲突并 git add 之后
git rebase --skip # 这一颗补丁已经多余,跳过交互式 git rebase -i
bash
git rebase -i HEAD~5
git rebase -i origin/main编辑器里常见指令:
| 指令 | 作用 |
|---|---|
pick | 保留 |
reword | 保留但改说明 |
edit | 停下来让你改文件或 commit --amend |
squash | 并入上一颗,编辑合并后的说明 |
fixup | 并入上一颗,丢掉这颗的说明 |
drop | 丢掉这颗提交 |
不要这样用:
- 对已经 push、且有人基于它开发的分支 rebase,再 force push。只在你独享的功能分支上 rebase。
- rebase 到一半开另一个终端再
reset --hard。 - 在
main上对公共历史做-i整理。
做错了怎么办:
bash
git rebase --abort # 仍在 rebase 中
git reflog # 已经结束:找到 rebase 前的 SHA
git reset --hard ORIG_HEAD冲突怎么解见 09 章。ours/theirs 在 rebase 里和 merge 相反,务必读那一章。
已 push 的功能分支能不能 rebase?
可以,但要用安全的强制推送,并且确认没人(或 PR 上没新 commit)基于旧历史:
bash
git push --force-with-lease origin feature/login不要 --force。--force-with-lease 发现远程比你上次 fetch 时又多了别人的提交,会拒绝覆盖。见 07 章。
把提交从错误分支挪走(未 push)
bash
# 当前误在 main 上提交了 C
git switch -c feature/login # 新分支指向 C,提交被保住
git switch main
git reset --hard HEAD~1 # main 退回;仅当这颗没 push已 push 则不要 reset main,改为在正确分支 cherry-pick,再在 main 上 revert。完整步骤见 急救手册 · 提交到错误分支。
怎么选
| 目标 | 用 |
|---|---|
| 公共历史上撤销某次提交的效果 | revert |
| 本地最新一颗写错 / 漏文件 | commit --amend |
| 丢掉或重做本地若干颗未推送提交 | reset --soft / --mixed |
| 工作区也要回到某次提交 | reset --hard(先看 reflog) |
| 功能分支跟上 main,要直线历史 | rebase |
| 只要别人的某一颗 | cherry-pick |
| 保留完整分叉与合并节点 | merge |
上一章:05 · 分支与合并 · 下一章:07 · 远程同步