Git reflog 从本地记录到提交找回
先定位当前任务,再对照操作前提、结果与容易混淆的边界。
章节速览8 节 · 展开选择
差异审查回答“内容改了什么”,bisect回答“行为从哪里改变”,revert则用新提交修订旧修改。这次的问题是:刚才明明提交过一段笔记,切回 main 后却在当前日志里看不到,应该先去哪里找?
git reflog 提供一条本地线索:引用曾经指到哪里。把这条记录与提交祖先分开,才能理解“当前分支没有包含”与“对象已经不存在”之间的距离。
提交关系与引用记录是两条线
普通 git log main 沿 main 当前提交的父关系向前走;git reflog show HEAD 查看本地 HEAD 的位置更新。前者是在提交图中追溯祖先,后者是在这份仓库的引用记录中查找曾经的位置。git-reflog 官方手册还特别说明,HEAD 的引用日志会记录分支切换。
因此,同一个提交可以在 HEAD 的引用日志中出现多次:一次是提交产生的位置,一次是切换回来时的位置。条目数量不是新增提交数量,序号也不是提交在祖先链上的深度。
提交对象保存快照和父关系;main 等分支名保存当前指向;reflog 保存本地引用过去的更新。找回提交时,先找出对象身份,再给它一个新的分支名字。
本文使用普通、启用了引用日志的本地仓库。日志是否存在可先用 git reflog exists HEAD 核对其退出状态;git-branch 手册说明,非裸仓库通常默认启用引用日志。本文不讨论多工作树、子模块、服务端日志或已损坏的对象库。
一个独立练习仓里的起点
准备一个与实际项目隔离的练习仓:main 上已经有两个提交 A、B;已跟踪文件 notes.txt 在 A 中写着 version=A,在 B 中改为 version=B。两次修改均已提交,工作区和暂存区干净,分支名 rescue-notes 尚未使用。A、B、D 是本文的教学标签,不是可以输入 Git 的对象 ID。
下面命令采用 PowerShell。先确认当前确实位于练习仓的 main,git status --short 无输出,再脱离分支停在 main 当前的 B:
1 | git status --short |
此时 HEAD 直接指向 B;main 也指向 B,但 HEAD 没有附着在 main 上。git-switch 手册将这种模式用于检查提交与临时实验。现在手工把同一个 notes.txt 编辑为 version=D,然后提交并切回 main:
1 | git add -- notes.txt |
在脱离状态创建 D,D 的父提交仍为 B;main 不会因为这次提交而自动移到 D。切回 main 后,文件显示 B 的版本,普通 git log main 也沿 B、A 这条祖先链。这还不能推出 D 的对象已经被移除。 这里说的“不在 main 的祖先里”仅限定从该分支追溯的范围;仍保留的引用日志也能为对象保留提供线索与保护,不能把它说成已经彻底丢失。
刚切回 main 时,手算两个日志
现在只读查看 HEAD 与 main 的日志。--format 让每行包含完整对象 ID、选择器和说明,避免只凭一段提交标题认人:
1 | git reflog show --format='%H %gD %gs' HEAD |
在严格按上面顺序、没有额外引用更新的练习中,最近条目的对应关系如下。实际输出会使用真实对象 ID;说明文本可能随 Git 版本或操作方式不同。
| 选择器 | 本例指向 | 对应事件 | 为什么不是祖先计数 |
|---|---|---|---|
| HEAD@{0} | B | 从 D 切回 main | 切换产生记录,未创建提交 |
| HEAD@{1} | D | 在脱离状态提交 D | D 是曾经的位置 |
| HEAD@{2} | B | 在 B 脱离 main | 与 HEAD@{0} 可为同一对象 |
| main@{0} | B | main 上的提交 B | 脱离状态没有推进 main |
| main@{1} | A | main 上的初始提交 A | main 与 HEAD 是不同日志 |
gitrevisions 手册定义了序号形式:HEAD@{1} 查 HEAD 的前一个日志值,main@{1} 查 main 的前一个日志值。在本例中,一个得到 D,一个得到 A。它们都不能简单读成“当前提交的父提交”。
自检:HEAD@{1} 和 main@{1} 为什么可能完全不同?
两者查询不同引用的本地日志。HEAD 记录了脱离、提交与切回;main 在脱离期间仍停在 B。本例刚切回时,HEAD@{1} 为 D,main@{1} 为 A,不能用共同的序号推出相同对象。
先核对内容,再固定候选对象
在 PowerShell 中把带花括号的选择器放进引号,避免先被 shell 当作特殊语法处理。Pro Git 的修订选择章节也提醒了这个引用问题。
确认本例候选确实是 D 后,把选择器解析成完整提交对象 ID,再检查提交说明、父关系和目标文件。下面的变量是本次核对结果,不是固定写死的哈希:
1 | $reflogCandidate = git rev-parse --verify 'HEAD@{1}^{commit}' |
逐条确认前一条命令成功,再继续执行;解析失败时不要使用空变量接着操作。^{commit} 要求结果可解析为提交,文件内容则应符合这次要找的 version=D。真实仓库还应核对相关文件与父关系,提交标题相似并不能证明它是同一份工作。
这里没有修改 main、工作区或暂存区。把候选解析成完整 ID,也消除了后续引用更新造成的序号漂移:新的提交或切换会使 HEAD@{n} 的 n 改变,保存的对象身份却不会因为日志新增一行而改变。
flowchart LR A[普通日志中未找到] --> B[查本地HEAD与分支日志] B --> C[识别候选提交] C --> D[解析完整提交ID] D --> E[检查父关系与文件内容] E --> F[确认后创建新分支入口]
给候选创建新的分支名字
工作区干净、候选身份已核对且新名字未占用时,从保存的提交创建分支并切过去:
1 | git status --short |
这一步给 D 建立 rescue-notes 引用;main 仍在 B,D 的父关系也没有改变。notes.txt 切换为 D 的快照。git-switch 官方说明定义了小写 -c 的创建并切换行为,已有分支名应换一个未占用的名字。
创建成功后,HEAD 增加一条切换到救援分支的记录。本例新的 HEAD@{0} 为 D,新的 HEAD@{1} 变为 B。因此,若此时重新把 HEAD@{1} 当作原候选,就会选错。创建名字没有把 D 合并进 main;后续如何整合,应另行审查内容与提交关系。
flowchart TD
A[已核对完整提交ID] --> B{工作区与新名字符合条件}
B -->|是| C[创建rescue-notes并切换]
C --> D[新引用指向候选D]
D --> E[检查快照与分支位置]
B -->|否| F[先处理当前改动或另选名字]
自检:创建救援分支后,再运行原来的 HEAD@{1} 还会得到 D 吗?
本例不会。创建并切换分支又新增一条 HEAD 日志,HEAD@{1} 此时是 B。应使用已经核对并保存的完整提交 ID;救援分支指向 D,但 main 尚未包含 D。
序号和时间选择器都有本地上下文
时间形式如 main@{yesterday} 查询的是本地 main 在那个时刻的记录位置,不是筛选“昨天写成的全部提交”;筛选提交日期属于另一类问题。引用需要有相应日志,记录的时间、时区与保留范围也都影响可用结果。实际定位时,可以先用 git reflog show --date=iso HEAD 查看时间,再核对候选的完整 ID。
还要区分三个长得相近的表达式:HEAD@{1} 是 HEAD 日志的旧位置;main@{1} 是 main 日志的旧位置;在分支上省略引用名的 @{1} 查询当前分支日志,不能默认当成 HEAD 日志的同义写法。@{-1} 则属于之前切换过的分支或提交的选择语法,也不是“前一个提交”。这些区别由 gitrevisions统一定义。
找不到记录时,先问范围
| 现象 | 应先核对的问题 |
|---|---|
| 刚换机器或重新克隆 | 这是另一份本地日志,不会自动继承原机器的操作记录 |
| 提交在日志里,但对象无法读取 | 记录和对象可用性是两件事,不能把一行日志当作完整备份 |
| 查询某个很早的时间 | 该引用日志是否存在、时间是否明确、记录是否仍保留 |
| 文件从未被提交 | reflog 不逐次记录编辑器里的未提交内容 |
| 创建分支失败 | 新名字是否占用,切换是否会影响当前未提交修改 |
| 想继续修改 main | 先保留并审查候选,再决定后续整合,不把找到对象等同于完成整合 |
reflog 的记录保留受配置与维护影响,不能承诺任意旧位置永远可查。它是本地查找线索,而不是替代备份、协作分支或正式保存工作的约定。Pro Git 的数据恢复章节展示了先找到对象、再给对象增加引用的基本思路;本文改用脱离分支的独立练习,不需要制造破坏性操作来学习它。
资料与验证范围
原始资料核对于 2026-10-27:
- git-reflog:本地引用记录、HEAD 的切换记录和只读查看。
- gitrevisions:序号、时间、当前分支与先前切换选择器。
- git-switch与git-branch:脱离状态、创建并切换、新引用与分支查询。
- Pro Git:Revision Selection与Maintenance and Data Recovery:本地性、shell 引用与恢复思路。
tests/verify-git-reflog-article.cjs 在新建独立练习仓执行真实 Git,检查快照、父关系、引用日志、选择器漂移、时间选择和新分支可达性;工作区冲突与重名分支则验证拒绝后的内容和引用保持原状。所有教学仓保留,没有操作博客的部署仓,也没有清理任何目录。验证只说明这些受控历史中的命令语义,不保证任意已经缺失的对象或未提交内容都能找回。
接着可以回到Git 差异审查比较两个快照,或用revert理解为什么“修订已提交内容”与“重新找到一个提交入口”解决的是不同问题。