“撤销上一次提交”和“把分支整理干净”看似都是 Git 的日常操作,但 git reset 与 git rebase 改动的对象并不相同。把它们当作互相替代的命令,尤其是在已经推送的共享分支上操作,很容易给协作带来麻烦。

本文不提供“一键后悔药”,而是建立一套判断方式:要改的是提交指针、暂存区、工作区,还是提交历史本身?这些提交是否已经被其他人依赖?

先看 Git 的三个区域

可以把本地状态拆成三个层次:

  • HEAD:当前分支指向的提交,也就是下一次提交的父节点。
  • 暂存区(index):准备写入下一次提交的文件快照。
  • 工作区(working tree):磁盘上正在编辑的文件。

git reset 的核心是把 HEAD 或暂存区对齐到指定提交;不同模式决定工作区是否也被修改。官方文档对其定义是“将 HEAD 或 index 设为已知状态”,具体行为取决于模式和参数。git-reset 文档

git rebase 则会把一串提交重新应用到新的基点上。它产生新的提交对象,因此提交 ID 会变化;官方将这一过程描述为把提交重新应用到新的 base tip。git-rebase 文档

reset:根据要保留的内容选择模式

假设当前历史为:

1
A -- B -- C  (HEAD, feature)

你希望“撤销 C”,但不同场景要保留的内容不同。

只撤销提交,保留暂存状态:—soft

1
git reset --soft HEAD~1

HEAD 回到 B,但 C 中的变更仍留在暂存区。适合刚提交后发现提交说明需要调整,或想把 C 与后续已暂存的改动合并成一次提交。

撤销提交并取消暂存,保留工作区:—mixed

1
2
git reset HEAD~1
# 等价于 git reset --mixed HEAD~1

这是不带模式时的默认行为。HEAD 回到 B,暂存区回到 B,C 的文件修改仍留在工作区。适合希望重新挑选要提交的文件,或拆分刚才的提交。

同时丢弃工作区修改:—hard

1
git reset --hard HEAD~1

这会让 HEAD、暂存区和工作区都对齐到 B。它适合明确确认目标提交之后的本地变更都不需要保留的情形。执行前应先检查:

1
2
3
4
git status
git log --oneline -5
git diff
git diff --staged

不要把 --hard 当作清理工作区的常规手段;未提交且未被其他引用保存的修改可能难以恢复。若只是取消某个文件的暂存,优先使用更精确的命令:

1
git restore --staged path/to/file

rebase:用于整理自己的未共享提交

设远端主线已经前进:

1
2
3
A -- B -- D  (origin/main)
\
C -- E (feature)

在自己的功能分支上执行:

1
2
git fetch origin
git rebase origin/main

概念上,Git 会把 C、E 的补丁依次应用到 D 之后,得到新的 C’、E’:

1
A -- B -- D -- C' -- E'  (feature)

这便于在合并前消除不必要的分叉,也能更早发现与主线的冲突。但 C’ 和 E’ 不再是原来的提交,所以已被其他协作者拉取、基于其继续开发的分支,不应随意 rebase 后再强推。

交互式 rebase 可用于合并、重排或编辑近期提交:

1
git rebase -i HEAD~3

它适合整理仍只属于自己的提交。编辑 todo 列表时,squash 会合并提交,reword 会改说明,edit 会暂停让你修改内容。操作前先确认范围,避免把已合并或已共享的提交也纳入。

冲突不是“随便选一边”

rebase 中出现冲突时,Git 会暂停。可按下面的闭环处理:

1
2
3
4
git status                 # 确认冲突文件与当前状态
# 手工解决冲突,并运行必要的构建或测试
git add <已解决的文件>
git rebase --continue

如果发现方向不对,优先退出:

1
git rebase --abort

这会终止当前 rebase 并尝试回到开始时的状态;具体语义以当前 Git 版本的官方文档为准。不要为了“尽快结束冲突”直接使用 --skip,除非你已经确认被跳过的提交不应保留。

已推送提交,先判断是否共享

对于已经推送到远端的提交,最安全的默认策略是新增一个反向提交,而不是改写公开历史:

1
git revert <commit>

revert 会生成一个新提交来抵消指定提交的改动,因此通常更适合共享主分支。

如果团队明确允许改写某个个人远端分支,完成 rebase 后也应使用带租约的推送:

1
git push --force-with-lease origin feature

--force-with-lease 会在远端引用不符合预期时拒绝推送,能避免无意覆盖后来提交;它仍是重写远端历史的操作,不应对保护分支或他人共同使用的分支轻率执行。

一份实用决策表

目标 优先选择 前提
修改刚提交的内容或说明 reset --soft / commit --amend 提交尚未共享
取消暂存并重新拆分提交 reset 或 restore --staged 保留工作区修改
放弃明确不需要的本地改动 reset --hard 已确认目标与影响范围
让个人功能分支跟上主线 fetch 后 rebase 提交未被他人依赖
撤销已发布的共享提交 revert 保留可追溯历史

命令本身没有“万能安全模式”。真正可靠的做法是在执行前先用 git status、git log --oneline --graph 和 git diff 看清状态,确认提交是否共享,并在改写历史前保留可回退的引用。理解 HEAD、暂存区和工作区分别会怎样变化后,reset 与 rebase 就不再是容易误触的命令,而是可以精确使用的工具。