历史整理、撤销与恢复都可能改变仓库状态,但它们的目标不同:整理改善记录方式,撤销抵消不需要的变化,恢复重新保护仍有价值的内容。选择命令前,需要明确想保留什么,以及其他人是否已经依赖当前历史。

本文是 Git 系列第四篇,建议先掌握基础操作和远程协作。所有改写和恢复实验应在独立练习仓库中进行。文中的 A、B、C 是提交图示意,<commit> 等占位符必须替换为实际引用;各小节是独立场景,不是一份连续执行的脚本。

1. 操作分类与影响范围

操作选择

目标 优先考虑 主要影响
取消暂存,保留编辑结果 restore --staged 索引
丢弃未暂存的已跟踪文件修改 restore 工作区文件
修正最近一次本地提交 commit --amend 当前分支历史
整理多次本地提交 交互式 rebase 当前分支历史
撤回本地提交并重新组织 reset HEAD、索引或工作区
抵消已共享的修改 revert 新增反向提交
找回旧提交位置 reflog 与新分支 本地引用

“是否推送”是判断共享状态的线索,但不是唯一条件:同事可能通过其他远程或 bundle 取得提交。关键在于是否有人基于旧历史继续工作。

现场保护

git status
git diff
git diff --cached
git log --graph --oneline --decorate -n 10
git branch backup/before-history-edit

备份分支只保存当前提交位置,不保存未提交内容。未提交修改应先形成可保留的提交、使用合适范围的 stash,或复制到仓库外的可靠位置。被忽略文件、未跟踪文件与子模块中的修改也要分别考虑,不能只看到一个备份分支就认为整个目录已被保护。

2. 最近提交修正

说明与内容

假设刚提交任务功能,但说明不准确:

git commit --amend -m "feat: add task completion summary"

如果遗漏了 README 更新,先编辑文件,再暂存并修正:

git add README.md
git diff --cached
git commit --amend --no-edit
git show --stat HEAD

--no-edit 保留原说明,但提交内容和标识仍可能变化。amend 会把当前索引用于替换最近提交,因此暂存区里若还混入其他修改,也会一起进入新提交。修正前的复查不能省略。

修正前:A ── B
修正后:A ── B'  ← 当前分支

旧 B 不再是当前分支顶端。尚未共享时,替换最近记录通常容易理解;已共享时,新增一个修正提交往往更适合协作者继续工作。

3. 提交重组与迁移

变基机制

假设功能分支从 A 创建,主分支后来新增 B、C,功能分支产生 D、E:

A ── B ── C  ← main
 \
  D ── E     ← feature/summary

在功能分支且工作区干净时执行 git rebase main,Git 会尝试把功能分支的提交重放到新起点:

A ── B ── C ── D' ── E'  ← feature/summary
          ↑
         main

图示采用普通线性提交。重放可能遇到冲突、已存在的补丁或空提交,不能理解成无条件复制每个原提交。新历史需要重新检查内容与测试;提交图看起来更整齐不等于整合一定正确。

交互式整理

在练习分支保留至少四个提交,用最近三条记录演示:

git log --oneline -n 4
git rebase -i HEAD~3

编辑器中的提交通常从旧到新排列,例如:

pick a1b2c3d add summary
pick b2c3d4e fix summary wording
pick c3d4e5f explain summary usage

这些哈希只作展示,应修改 Git 实际打开的列表。把第二行 pick 改为 fixup,第三行改为 reword,保存后 Git 会把第二条合入前一条,再要求编辑第三条说明。

动作 作用
pick 保留并应用提交
reword 修改说明
edit 暂停以调整内容
squash 合入前一条并整理说明
fixup 合入前一条,通常丢弃当前说明
drop 不再应用该提交

需要拆分某个提交时可以选择 edit,暂停后重新组织索引和提交;初次练习不必同时学习全部动作。整理后用 log 查看记录数,再将最终文件树与备份分支比较,确认没有意外丢失功能。

后续修正也可先建立 fixup 提交,再统一整理:

git add README.md
git commit --fixup=<target-commit>
git rebase -i --autosquash <base>

<base> 应位于待整理目标之前;如果目标是根提交,应另行使用适当的 --root 范围。autosquash 帮助安排修正的位置,最终仍需检查编辑列表。

冲突控制

rebase 暂停后,阅读 status,编辑冲突文件并暂存,再继续:

git status
git add <resolved-file>
git rebase --continue

--abort 返回变基前状态;--skip 跳过当前补丁,可能丢失它的变化;--quit 停止变基流程,却不主动恢复原 HEAD、索引和工作区。它们不是等价的“退出”。变基过程中 ours 通常指已重建的一侧,theirs 指正在重放的提交,不能沿用普通合并时对分支名称的直觉。

提交迁移

若一个修复提交需要进入另一条分支,可切换到目标分支并应用:

git switch release/task-list
git cherry-pick <fix-commit>

目标分支必须已经存在,且应先确认工作区干净。cherry-pick 将变化应用到目标并通常创建新提交,不会自动删除来源分支上的原记录,也不建立两条分支之间的合并父子关系。

遇到冲突时解决后 git add,再 git cherry-pick --continue;放弃则使用 --abort。若修复依赖前置改动,仅选择一条提交可能无法工作,应检查依赖并测试目标版本。

远程更新边界

改写已经推送的个人分支前,需要确认团队允许且无人依赖旧历史。--force-with-lease 比无条件强推增加预期引用检查,但后台 fetch 可能更新默认检查依据,因此它不是协作确认的替代品。Git 推送手册说明了这一限制。

明确审查旧远程顶端后,可以指定期望值;以下是语法示意,不应原样执行:

git push --force-with-lease=refs/heads/<branch>:<expected-oid> origin <branch>:<branch>

它要求远程目标仍等于已经核对的提交。若被拒绝,应重新检查他人的更新,不能机械地更换期望值后重试。受保护分支也可能完全禁止此类更新。

4. 文件恢复与暂存撤销

索引恢复

git restore --staged README.md

在已有提交的仓库中,这通常从 HEAD 恢复索引中的该文件,工作区编辑结果保留。新加入的文件取消暂存后,可能重新成为未跟踪文件。

工作区恢复

git diff -- README.md
git restore README.md

默认从索引恢复工作区文件,因此丢弃的是未暂存部分。假设 HEAD 内容为 A、索引为 B、工作区为 C,执行后工作区变为 B,并非 A。

指定来源可以恢复历史版本:

git restore --source=<commit> -- README.md
git diff -- README.md

默认只修改工作区;要将结果提交,仍需检查、暂存和提交。git restore -p 可以按块选择恢复内容,但被丢弃的未暂存文本未必有 Git 恢复入口。

5. 提交重置与反向提交

重置模式比较

为隔离实验,新建 git-reset-lab 仓库并配置练习身份。在其中创建 value.txt,内容为 one,提交后打标签 lab-base;再改成 two,提交并打标签 lab-tip。两次提交均只涉及这个文件。

git init -b main
git config user.name "Practice User"
git config user.email "practice@example.com"

第一次填写文件后执行 git add value.txt、git commit -m "test: base value"、git tag lab-base;第二次填写后同样提交,说明为 test: updated value,然后 git tag lab-tip。确认工作区干净。

从 lab-tip 出发重置到 lab-base,三种模式分别得到:

模式 HEAD 文件版本 索引内容 工作区内容 简短状态
--soft one two two M value.txt
--mixed one one two ` M value.txt`
--hard one one one 干净

以下只在这个独立实验仓库执行。每次测试之间用 lab-tip 还原起点;还原命令本身也会覆盖文件,所以不要在中间添加其他有价值的修改。

git reset --soft lab-base
git status --short
git diff --cached
git reset --hard lab-tip

git reset --mixed lab-base
git status --short
git diff
git reset --hard lab-tip

git reset --hard lab-base
git status --short
git reset --hard lab-tip

这里讨论的是不带路径的提交重置形式。带路径的 reset 可以只更新索引,不移动分支。--hard 不仅覆盖已跟踪文件,还可能覆盖或移除妨碍检出目标文件的未跟踪路径,因此不能把未跟踪文件视为必然安全。Git 重置手册区分了这些形式与影响。

反向提交

已共享的错误通常通过新提交抵消:

git revert <bad-commit>
git show HEAD

它反向应用目标提交的变化,不把整个仓库简单恢复到目标之前。例如目标之后有其他独立功能,新反向提交可以保留这些功能;若后续修改依赖目标变化,则可能冲突或引入逻辑问题。解决冲突后 git add 并 git revert --continue,放弃使用 --abort。

在上面的实验中,回到 lab-tip 后执行 git revert --no-edit lab-tip,文件应恢复为 one,但日志增加第三条记录,第二条不会消失。这正是与 reset 的核心区别。

合并提交撤销

合并提交有多个父提交,撤销时需要指定作为主线的父序号:

git show --no-patch --format="%H %P" <merge-commit>
git revert -m 1 <merge-commit>

只有确认第一父线就是要保留的主线时才使用 -m 1。撤销合并不会删除原合并关系,今后再次合并相同祖先不一定重新引入被撤销的变化;可能需要撤销那次 revert 或生成新的修正,应结合提交图判断。

6. 引用日志与历史恢复

提交定位

误重置、误删分支或离开 detached HEAD 后,应尽量减少继续改写与清理操作,先检查本地引用日志:

git reflog
git show "HEAD@{2}"

序号会随操作变化,HEAD@{2} 只是语法示例,不能假设它一定是丢失的提交。应按操作说明、时间和实际内容选择候选哈希,再建立恢复分支:

git show <candidate-commit>
git branch recovery/task-work <candidate-commit>
git log --oneline recovery/task-work -n 5

建立分支不会立即覆盖当前工作区。确认候选正确后,再决定切换、合并或 cherry-pick,通常比直接再次 hard reset 更容易检查。

恢复条件与限制

reflog 是本地记录,通常不会随 clone 或 push 传输,且会按配置过期。删除分支后,其专属引用日志可能已不可用,但 HEAD 的日志仍可能保留曾经切换或提交的位置。不可达对象也可能最终被清理,因此不能保证任意旧提交永久可找回。

未提交且未被 Git 保存过的内容、被 clean 删除的普通未跟踪文件,不能依赖 reflog 恢复。编辑器历史、系统备份或其他副本可能有帮助,但属于 Git 之外的恢复来源。

命令速查

目标 命令
最近提交修正 git commit --amend
交互式整理 git rebase -i <base>
取消暂存 git restore --staged <file>
撤销共享修改 git revert <commit>
引用日志 git reflog
保护候选提交 git branch recovery/<name> <commit>

实践练习

在独立实验仓库中先创建一条临时分支和一个提交,记录其文件内容,再切走并删除该练习分支。通过 reflog 找到候选提交并建立恢复分支。验收时检查哈希、文件内容与提交说明,而不是只确认新分支名称存在。

参考资料

系列导航:第四篇 / 共五篇 · 上一篇:远程协作与版本管理 · 下一篇:历史分析与仓库维护 · 系列导读 · 命令速查