Skip to content

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,未入库改动丢

reset 后 main 回到 C2,C3 仍在对象库,reflog 可找回。

常用写法:

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         # 把分支钉到某个 sha

HEAD~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   # 把一段提交的反向改动先放进暂存区

不要这样用:revertreset 当同义词。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 个可读提交。

rebase 把 D、E 重放成 D'、E',接到更新后的 C 后面;旧 SHA 仍可在 reflog 找到。

常用写法:

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 · 远程同步