reset之后,log里没了刚才的提交,cat-file却能读它;运行gc以后,它可能仍然能读。到底是谁“没了”?
这三个现象不矛盾。分支当前指向哪里、引用曾经指向哪里、对象还能不能读取,是三个问题。我们用两次提交把它们拆开,再故意去掉保护,检查恢复能力的边界。
本文只需要标准Git和PowerShell,不依赖私有源码。实验使用Git for Windows 2.45.2.windows.1、SHA-1仓库。下列代码块须在同一个窗口依次执行。
后半段有立即过期日志和立即清理对象的命令,仅用于本篇新建、没有其他进程访问的废弃实验仓库。不要复制到项目仓库“修复Git”。普通恢复操作不需要先gc,更不应该先清空日志。
1. 先准备能丢弃的两次提交
第一个代码块创建随机临时目录。辅助函数G只是检查每次Git调用的退出码,不修改Git行为;预期失败时另用原始git命令。
$ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $false function G { & git @args; if ($LASTEXITCODE -ne 0) { throw "git failed: $args" } } $lab = Join-Path $env:TEMP ('git-reflog-lab-' + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $lab | Out-Null Set-Location $lab Set-Content .lab-only 'disposable reflog experiment' G init --object-format=sha1 -b master G config user.name 'Blog experiment' G config user.email 'blog@example.invalid' G config core.autocrlf false G config gc.auto 0 Set-Content f.txt v1 G add f.txt G commit -m c1 $c1 = G rev-parse HEAD Set-Content f.txt v2 G add f.txt G commit -m c2 $c2 = G rev-parse HEAD我们提前保存ID,是为了给实验提供判断基准,不代表恢复时总能提前知道它。实际误操作后通常要先从reflog里识别时间和操作,再检查对应提交内容。
2. log里没了,什么仍然存在
这里刻意使用mixed,不用hard:分支和index回到c1,工作区的v2保留。
G reset --mixed $c1 G log --oneline G reflog show HEAD --oneline 'head_is_c1=' + ((G rev-parse HEAD) -eq $c1) 'worktree_still_v2=' + ((Get-Content f.txt -Raw).Trim() -eq 'v2') 'reflog_records_c2=' + (@(G reflog show --format=%H HEAD).Contains($c2)) 'old_type=' + (G cat-file -t $c2) git merge-base --is-ancestor $c2 HEAD $ancestorExit = $LASTEXITCODE if ($ancestorExit -ne 1) { throw 'c2 should not be an ancestor of current HEAD' } 'c2_not_in_current_history=True'本例四项状态分别是True、True、True、commit。最后的退出码1表示“不具有该祖先关系”,不是命令解析错误。普通log从当前入口沿parent向后走,不是整个对象库的目录。
| 工具或状态 | 回答的问题 | 本例观察 |
|---|---|---|
| log | 当前提交历史能走到谁 | c1,不显示c2 |
| reflog | 引用曾经指向哪里 | 留有c2的移动记录 |
| cat-file | 这个对象能否读取 | c2仍是commit |
| 工作区文件 | 当前磁盘上有什么 | f.txt仍为v2 |
reflog记录的是引用移动,不是持续录制编辑器内容。没进入对象库的修改,不能靠一个从未产生过的提交ID恢复。Git reflog手册
3. 普通gc不等于删除最近离开分支的提交
G count-objects -v G gc G count-objects -v 'ordinary_gc_keeps_c2=' + ((G cat-file -t $c2) -eq 'commit') 'gc_does_not_move_HEAD=' + ((G rev-parse HEAD) -eq $c1)本例两项均为True。gc会整理存储、处理保留策略;不能把它简化成“删掉所有分支log看不到的提交”。当前reflog提供保护线索,新近对象也可能受保留期限影响。
这个实验不能单独证明“只有reflog在保护c2”。对象刚创建,年龄也是变量。下面先引入一个确定的引用入口,再用相同的立即清理策略做有无入口的对照。Git gc手册
4. 给旧提交一个名字,比一直赌日志保留更明确
先建救援分支。branch不会切换当前工作区,我们只是重新建立一个入口。检查show的内容以后,再决定是否切换、摘取或导出。
G branch rescued $c2 G show rescued:f.txt if ((G rev-parse --show-toplevel) -ne $lab.Replace('\','/')) { throw 'wrong repository' } if (-not (Test-Path .lab-only)) { throw 'not the disposable lab' } G reflog expire --expire=now --all G gc --prune=now 'rescued_ref_is_c2=' + ((G rev-parse rescued) -eq $c2) 'rescued_survives_expiry=' + ((G cat-file -t $c2) -eq 'commit')两项仍为True:即便实验中立即过期所有reflog,c2仍由rescued引用可达。它的tree/blob也随提交关系得到保留,不是只保存一个提交说明。
路径与标记检查只是防止本篇步骤误跑到其他目录,不是让立即清理在真实项目中变安全。官方手册提醒,取消对象年龄缓冲会增加并发写入时的损坏风险。本篇没有其他访问进程,也没有把这组命令当日常维护建议。
5. 去掉最后入口,旧ID还够用吗
下一块仅用来观察边界,故意删除刚建立的实验分支。若是在真实救援过程中,到上一节就该保留入口并检查内容,不应继续清理。
if ((G rev-parse --show-toplevel) -ne $lab.Replace('\','/')) { throw 'wrong repository' } if (-not (Test-Path .lab-only)) { throw 'not the disposable lab' } G branch -D rescued G reflog expire --expire=now --all G gc --prune=now git cat-file -e "${c2}^{commit}" $missingExit = $LASTEXITCODE if ($missingExit -eq 0) { throw 'expected c2 to be unavailable' } 'old_commit_unavailable=True' 'current_commit_preserved=' + ((G cat-file -t $c1) -eq 'commit') 'worktree_copy_remains=' + ((Get-Content f.txt -Raw).Trim() -eq 'v2') G fsck --full if ((G rev-parse HEAD) -ne $c1) { throw 'HEAD changed unexpectedly' } 'REFLOG_BOUNDARIES_CHECKS_PASSED'本轮c2已不可读,c1仍在,磁盘上的v2却没有被gc删除。这再次说明文件副本、提交对象和引用入口不能混为一谈。有v2文件可以重新提交,但不能据此说旧提交身份、说明和历史关系已恢复。
这里故意保留cat-file的fatal输出,随即检查退出码。不要为隐藏提示而随手加2>$null:在本轮Windows PowerShell 5.1的Stop设置下,重定向的原生错误可能先触发异常,导致预期失败检查根本没执行。开头同时关闭PowerShell 7可选的原生命令错误自动处理,预期失败由正文显式判断。
| 控制条件 | 本轮c2能否读取 | 能推出什么 |
|---|---|---|
| 刚reset,普通gc | 能 | 不能仅凭log判断对象删除 |
| rescued存在,过期日志并立即清理 | 能 | 分支入口足以保留其可达对象 |
| rescued删除,过期日志并立即清理 | 不能 | 知道ID也不能凭空重建已清理对象 |
6. 为什么验证要分三层
图中是本轮实际标准输出节选,排版后截图,不冒充桌面终端窗口。验证程序逐项检查HEAD、对象类型、日志记录、文件内容与预期失败退出码;路径守卫也随正文执行。
如果只检查“f.txt还在”,会把旧提交已清理误当成恢复成功;只检查cat-file成功,又无法判断分支是否已得到可持续的入口。实验刻意控制这些变量,而不是在重要仓库里尝试一长串恢复命令。
这与rebase后旧提交去哪了是同一条主线:入口变化不等于对象消失,对象暂时存在也不等于永久备份。真正误操作时,先停止进一步改写和清理,识别旧状态、建立救援入口,再核对文件与历史。