【Git】误删分支、reset --hard 后提交丢了怎么办?------reflog、fsck 与安全恢复实战

执行 git reset --hard HEAD~3 后才发现最近三个提交没有推送;清理分支时删错了那条尚未合并的功能分支;交互式 rebase 结束后,某个重要提交不见了。这些事故看起来像"代码已经被 Git 删除",实际往往只是 分支或 HEAD 的指针被移动,提交对象暂时失去了显眼的名字。
Git 的 reflog 会在本地记录引用指针的变化。只要目标对象仍存在,就可以从旧位置找到提交哈希,再创建救援分支使它重新可达。恢复时最危险的不是命令不会写,而是慌乱中继续执行 gc、prune、重复 reset,或者直接在公共分支强推,导致可恢复范围进一步缩小。
本文从 Git 对象和引用模型出发,复现一次 hard reset,再分别讲解误删分支、错误 rebase、错误 amend、force push 等场景;同时说明 reflog 的本地性、过期策略和 git fsck 的兜底边界。所有恢复都遵循一个安全原则:先验证提交,再建立救援分支,最后才整理历史。
1. 为什么提交"丢了"却可能仍能找回
Git 提交是对象,包含目录树、父提交、作者、时间和提交说明;分支则是 refs/heads/* 下指向某个提交的可移动引用。git reset --hard 会移动当前分支与 HEAD,并把索引和工作区同步到目标提交;删除分支主要删除引用。两者通常不会在执行瞬间逐字节销毁旧提交对象。
假设历史为 C1 -> C2 -> C3,main 指向 C3。执行 git reset --hard C1 后,main 改为指向 C1,C2 和 C3 不再能通过 main 的正常历史看到。但 HEAD 的 reflog 通常仍保存"从 C3 移到 C1"的记录。找到 C3 的哈希并创建 rescue 分支后,C3 又获得长期引用,C2 也因父子关系重新可达。

这也解释了恢复的时间窗口:没有分支、标签或 reflog 引用的对象属于不可达对象,后续可能被过期和垃圾回收清理。reflog 不是云端备份,也不是永久保险箱;它只是非常有价值的本地操作日志。
2. 可复现实验:hard reset 后用 reflog 恢复
下面的 PowerShell 脚本在系统临时目录创建仓库,提交两个版本,记录第二个提交哈希,执行 hard reset,然后验证旧提交并建立救援分支。finally 只删除脚本自己创建的随机临时目录,不会触碰当前项目。
powershell
$ErrorActionPreference = "Stop"
$demo = Join-Path ([System.IO.Path]::GetTempPath()) ("git-reflog-demo-" + [guid]::NewGuid())
New-Item -ItemType Directory -Path $demo | Out-Null
try {
git -C $demo init | Out-Null
git -C $demo config user.name "Demo User"
git -C $demo config user.email "demo@example.com"
"v1" | Set-Content (Join-Path $demo app.txt)
git -C $demo add app.txt
git -C $demo commit -m "base" | Out-Null
"important" | Set-Content (Join-Path $demo feature.txt)
git -C $demo add feature.txt
git -C $demo commit -m "important feature" | Out-Null
$lost = (git -C $demo rev-parse HEAD).Trim()
git -C $demo reset --hard HEAD~1 | Out-Null
git -C $demo show --quiet --oneline $lost
git -C $demo branch rescue/demo $lost
git -C $demo log --oneline --decorate --all
} finally {
Remove-Item -LiteralPath $demo -Recurse -Force
}
在真实事故中,不要事先依赖 $lost 变量,而是用 reflog 找到它:
bash
git status
git reflog --date=iso --decorate
git show --stat <candidate-hash>
git show <candidate-hash> --
git branch rescue/2026-08-14 <candidate-hash>
git log --graph --oneline --decorate --all
git show 是恢复前的验证关口。核对作者、时间、提交说明、文件列表和关键差异,避免仅凭"看起来像"的提交说明选错对象。确认后先创建救援分支,目标提交获得稳定名字,后续即使继续分析也不容易再次丢失。

3. reflog 应该怎么看:HEAD@{n}、时间与哈希
git reflog 常见输出类似:
text
4a12c8e HEAD@{2026-08-14 13:20:10 +0800}: reset: moving to HEAD~2
91bd77f HEAD@{2026-08-14 13:18:42 +0800}: commit: important feature
2cc5a60 HEAD@{2026-08-14 13:10:03 +0800}: commit: base
左侧是操作完成后指向的提交,右侧是动作说明。若 reset 把 HEAD 从重要提交移到旧位置,那么目标通常位于 reset 之前那条记录。可以使用 HEAD@{1}、HEAD@{2} 这样的选择器,也可以使用带日期的表达式;但序号会随着新操作增加而变化,因此恢复工单和脚本中应尽快保存完整哈希。
不同引用有各自的 reflog。git reflog show HEAD 查看 HEAD 移动,git reflog show main 查看本地 main 的引用变化,git reflog --all 扩大搜索范围。误删分支后,如果分支自己的 reflog 已不可见,HEAD reflog 仍可能包含曾经 checkout 或 commit 到该分支的记录。
bash
git reflog show HEAD --date=iso
git reflog show main --date=iso
git reflog --all --date=iso
git log -g --all --oneline --decorate
不要在没有验证的情况下直接执行 git reset --hard HEAD@{5}。一旦选错,又会覆盖当前索引和工作区,制造第二起事故。更安全的方式永远是:
bash
git branch rescue/checkpoint HEAD
git branch rescue/candidate <candidate-hash>
git switch rescue/candidate
第一条先保护事故发生后的当前位置,第二条保护候选提交。两条线都留下后,再比较与合并。
4. 四类事故的具体恢复方案
4.1 reset --hard 后丢失本地提交
先停止提交和清理,查看 HEAD reflog,找到 reset 前的提交,执行 git show 验证,然后创建救援分支。若只需取回其中一个提交,可以切回目标分支后 git cherry-pick <hash>;若需要整条提交链,可 merge 救援分支,或在确认没有协作风险后把本地分支移动回去。
bash
git reflog --date=iso
git branch rescue/before-reset <hash>
git switch main
git cherry-pick <hash>
如果工作区中还有未提交修改,hard reset 可能已经覆盖它们。reflog 主要记录引用移动,不会自动保存从未提交到对象数据库的普通文件内容。此时要检查编辑器本地历史、文件系统快照、IDE Local History 或备份,不能承诺仅靠 Git 恢复。
4.2 误删尚未合并的分支
执行 git branch -D feature/pay 后,若最近曾在该分支提交或切换,HEAD reflog 往往能看到分支尖端。找到最后一个正确提交后重新创建分支:
bash
git reflog --all --date=iso
git show --stat <hash>
git branch feature/pay-recovered <hash>
若分支曾推到远端,还可以先 git fetch --all --prune,检查远端引用、合并请求页面或同事仓库。注意 --prune 会清理已经不存在的远端跟踪引用;事故初期不应盲目执行大量清理命令,先记录当前引用更稳妥。
4.3 rebase、squash 或 amend 后提交不见
rebase 会创建新提交并移动分支,旧提交短期内仍可能在 reflog 中。先找到 rebase (start) 前的分支尖端,把它保存为救援分支,然后用 git range-diff old-base..rescue old-base..current 或普通 diff 比较前后差异。不要看到提交哈希变化就断言内容丢失,因为 rebase 后内容相同的提交也会获得新哈希。
git commit --amend 也会生成新提交并替换分支尖端。要找回 amend 前版本,同样从 reflog 中定位上一位置。验证两个提交的 tree 与 message 后,选择保留其中一个或 cherry-pick 必要改动。
4.4 force push 覆盖了远端历史
先通知团队停止继续推送,避免更多引用移动。执行强推的那台机器往往还有本地 reflog;其他同事尚未 fetch 的仓库也可能保留旧远端分支或本地分支。找到旧尖端后创建明确命名的救援分支并推送,而不是立刻再次强推 main。
bash
git branch rescue/remote-main-before-force <hash>
git push origin rescue/remote-main-before-force
随后通过代码评审决定使用 merge、revert 还是在维护窗口恢复目标分支。长期预防应启用受保护分支、禁止直接强推、要求评审和状态检查。--force-with-lease 比裸 --force 更能检测远端是否被别人更新,但它也不是备份机制。
5. 找到提交后,branch、cherry-pick、reset、revert 怎么选

创建救援分支风险最低,只增加引用,不改写当前工作区和公共历史,适合所有事故的第一步。分支名应包含日期、事故或来源,避免几天后无人知道它为何存在。
cherry-pick把一个或几个提交的变更复制到当前分支,会产生新提交,适合只取回有限修改。若提交依赖前置提交,应按顺序选择一段范围,并认真解决冲突与运行测试。
reset 移动分支。--soft 保留索引和工作区,--mixed 重置索引但保留工作区,--hard 同时覆盖索引与工作区。它适合明确的本地历史整理,不适合未经沟通改写公共分支。
revert添加一个反向提交,不删除原历史,适合已经共享的分支。恢复被误 revert 的变更时,可以 revert 那次 revert,但仍需理解冲突和后续提交依赖。
#mermaid-svg-7VF0XsJQh4fOiuqR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7VF0XsJQh4fOiuqR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7VF0XsJQh4fOiuqR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7VF0XsJQh4fOiuqR .error-icon{fill:#552222;}#mermaid-svg-7VF0XsJQh4fOiuqR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7VF0XsJQh4fOiuqR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7VF0XsJQh4fOiuqR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7VF0XsJQh4fOiuqR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7VF0XsJQh4fOiuqR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7VF0XsJQh4fOiuqR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7VF0XsJQh4fOiuqR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7VF0XsJQh4fOiuqR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7VF0XsJQh4fOiuqR .marker.cross{stroke:#333333;}#mermaid-svg-7VF0XsJQh4fOiuqR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7VF0XsJQh4fOiuqR p{margin:0;}#mermaid-svg-7VF0XsJQh4fOiuqR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7VF0XsJQh4fOiuqR .cluster-label text{fill:#333;}#mermaid-svg-7VF0XsJQh4fOiuqR .cluster-label span{color:#333;}#mermaid-svg-7VF0XsJQh4fOiuqR .cluster-label span p{background-color:transparent;}#mermaid-svg-7VF0XsJQh4fOiuqR .label text,#mermaid-svg-7VF0XsJQh4fOiuqR span{fill:#333;color:#333;}#mermaid-svg-7VF0XsJQh4fOiuqR .node rect,#mermaid-svg-7VF0XsJQh4fOiuqR .node circle,#mermaid-svg-7VF0XsJQh4fOiuqR .node ellipse,#mermaid-svg-7VF0XsJQh4fOiuqR .node polygon,#mermaid-svg-7VF0XsJQh4fOiuqR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7VF0XsJQh4fOiuqR .rough-node .label text,#mermaid-svg-7VF0XsJQh4fOiuqR .node .label text,#mermaid-svg-7VF0XsJQh4fOiuqR .image-shape .label,#mermaid-svg-7VF0XsJQh4fOiuqR .icon-shape .label{text-anchor:middle;}#mermaid-svg-7VF0XsJQh4fOiuqR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7VF0XsJQh4fOiuqR .rough-node .label,#mermaid-svg-7VF0XsJQh4fOiuqR .node .label,#mermaid-svg-7VF0XsJQh4fOiuqR .image-shape .label,#mermaid-svg-7VF0XsJQh4fOiuqR .icon-shape .label{text-align:center;}#mermaid-svg-7VF0XsJQh4fOiuqR .node.clickable{cursor:pointer;}#mermaid-svg-7VF0XsJQh4fOiuqR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7VF0XsJQh4fOiuqR .arrowheadPath{fill:#333333;}#mermaid-svg-7VF0XsJQh4fOiuqR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7VF0XsJQh4fOiuqR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7VF0XsJQh4fOiuqR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7VF0XsJQh4fOiuqR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7VF0XsJQh4fOiuqR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7VF0XsJQh4fOiuqR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7VF0XsJQh4fOiuqR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7VF0XsJQh4fOiuqR .cluster text{fill:#333;}#mermaid-svg-7VF0XsJQh4fOiuqR .cluster span{color:#333;}#mermaid-svg-7VF0XsJQh4fOiuqR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7VF0XsJQh4fOiuqR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7VF0XsJQh4fOiuqR rect.text{fill:none;stroke-width:0;}#mermaid-svg-7VF0XsJQh4fOiuqR .icon-shape,#mermaid-svg-7VF0XsJQh4fOiuqR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7VF0XsJQh4fOiuqR .icon-shape p,#mermaid-svg-7VF0XsJQh4fOiuqR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7VF0XsJQh4fOiuqR .icon-shape .label rect,#mermaid-svg-7VF0XsJQh4fOiuqR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7VF0XsJQh4fOiuqR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7VF0XsJQh4fOiuqR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7VF0XsJQh4fOiuqR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
发现提交或分支丢失
停止 gc、prune 和破坏性操作
git reflog --date=iso
找到目标提交?
git show 验证内容
git branch rescue/
git fsck --lost-found
发现 dangling commit?
检查远端、同事仓库与备份
选择 cherry-pick / merge / reset
6. reflog 找不到时,使用 fsck 兜底
如果 reflog 已过期、分支从未在当前工作副本检出,或记录被清理,可尝试扫描对象数据库:
bash
git fsck --full --no-reflogs --unreachable
git fsck --lost-found
输出可能包含 dangling commit <hash>。这只说明对象暂时没有正常引用,不代表它就是需要的版本。用 git show --stat <hash>、git show <hash> 逐个检查,再为正确对象创建分支。大型仓库的 dangling 对象很多,可结合作者、日期、提交说明和变更文件过滤。

必须明确限制:fsck 只能发现仍存在的对象。如果对象已经被过期与垃圾回收删除,它无法凭空恢复。此时应检查远端托管平台、流水线工作目录、部署制品、同事克隆、IDE 历史和备份。已部署二进制可能帮助重建逻辑,但不能等价还原原始源码、提交元数据和完整历史。
事故期间不要执行下列命令:
bash
git gc --prune=now
git prune
git reflog expire --expire=now --all
这些命令在日常维护中各有用途,但会显著缩短不可达对象的可恢复窗口。也不要使用来源不明的"清理仓库脚本"。
7. reflog 的边界:本地、过期与多工作树
reflog 默认保存在当前仓库的 .git/logs 中,不会随普通 push 自动上传到远端。换电脑、重新 clone 后看到的是新仓库的记录;远端服务器是否保存、保存多久,由托管平台实现和权限决定,不能假设可以查询。
Git 当前文档中,普通可达 reflog 记录的默认过期时间通常为 90 天,不可达记录通常为 30 天,但仓库配置、维护任务和托管环境都可能不同。可用以下命令查看显式配置:
bash
git config --show-origin --get gc.reflogExpire
git config --show-origin --get gc.reflogExpireUnreachable
git config --show-origin --get gc.pruneExpire
未输出不等于永不过期,而是使用默认行为。修改期限会增加对象库占用,也仍无法覆盖磁盘损坏、仓库被删和设备丢失。高价值仓库应有远端备份、受保护分支与恢复演练。
多工作树环境还要注意 reflog 和 HEAD 的作用范围。执行维护命令前先列出工作树和所有引用,确认目标提交是否在其他工作树中仍可达:
bash
git worktree list --porcelain
git show-ref
git reflog --all --date=iso

#mermaid-svg-ILqT3gqS7qukAiY4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ILqT3gqS7qukAiY4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ILqT3gqS7qukAiY4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ILqT3gqS7qukAiY4 .error-icon{fill:#552222;}#mermaid-svg-ILqT3gqS7qukAiY4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ILqT3gqS7qukAiY4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ILqT3gqS7qukAiY4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ILqT3gqS7qukAiY4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ILqT3gqS7qukAiY4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ILqT3gqS7qukAiY4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ILqT3gqS7qukAiY4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ILqT3gqS7qukAiY4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ILqT3gqS7qukAiY4 .marker.cross{stroke:#333333;}#mermaid-svg-ILqT3gqS7qukAiY4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ILqT3gqS7qukAiY4 p{margin:0;}#mermaid-svg-ILqT3gqS7qukAiY4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ILqT3gqS7qukAiY4 .cluster-label text{fill:#333;}#mermaid-svg-ILqT3gqS7qukAiY4 .cluster-label span{color:#333;}#mermaid-svg-ILqT3gqS7qukAiY4 .cluster-label span p{background-color:transparent;}#mermaid-svg-ILqT3gqS7qukAiY4 .label text,#mermaid-svg-ILqT3gqS7qukAiY4 span{fill:#333;color:#333;}#mermaid-svg-ILqT3gqS7qukAiY4 .node rect,#mermaid-svg-ILqT3gqS7qukAiY4 .node circle,#mermaid-svg-ILqT3gqS7qukAiY4 .node ellipse,#mermaid-svg-ILqT3gqS7qukAiY4 .node polygon,#mermaid-svg-ILqT3gqS7qukAiY4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ILqT3gqS7qukAiY4 .rough-node .label text,#mermaid-svg-ILqT3gqS7qukAiY4 .node .label text,#mermaid-svg-ILqT3gqS7qukAiY4 .image-shape .label,#mermaid-svg-ILqT3gqS7qukAiY4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-ILqT3gqS7qukAiY4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ILqT3gqS7qukAiY4 .rough-node .label,#mermaid-svg-ILqT3gqS7qukAiY4 .node .label,#mermaid-svg-ILqT3gqS7qukAiY4 .image-shape .label,#mermaid-svg-ILqT3gqS7qukAiY4 .icon-shape .label{text-align:center;}#mermaid-svg-ILqT3gqS7qukAiY4 .node.clickable{cursor:pointer;}#mermaid-svg-ILqT3gqS7qukAiY4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ILqT3gqS7qukAiY4 .arrowheadPath{fill:#333333;}#mermaid-svg-ILqT3gqS7qukAiY4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ILqT3gqS7qukAiY4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ILqT3gqS7qukAiY4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ILqT3gqS7qukAiY4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ILqT3gqS7qukAiY4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ILqT3gqS7qukAiY4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ILqT3gqS7qukAiY4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ILqT3gqS7qukAiY4 .cluster text{fill:#333;}#mermaid-svg-ILqT3gqS7qukAiY4 .cluster span{color:#333;}#mermaid-svg-ILqT3gqS7qukAiY4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ILqT3gqS7qukAiY4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ILqT3gqS7qukAiY4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-ILqT3gqS7qukAiY4 .icon-shape,#mermaid-svg-ILqT3gqS7qukAiY4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ILqT3gqS7qukAiY4 .icon-shape p,#mermaid-svg-ILqT3gqS7qukAiY4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ILqT3gqS7qukAiY4 .icon-shape .label rect,#mermaid-svg-ILqT3gqS7qukAiY4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ILqT3gqS7qukAiY4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ILqT3gqS7qukAiY4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ILqT3gqS7qukAiY4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 临时保持可达
可能被 gc 清理
分支 refs/heads/main
提交 C3
提交 C2
提交 C1
reset 后 main
reflog 旧记录
rescue 分支
无引用且记录过期
8. 七个常见错误,为什么会让恢复更难
错误一:发现问题后继续 commit、rebase、reset
每次引用移动都会产生新记录,HEAD@{n} 序号随之变化,事故时间线更难读。应先暂停写操作,导出 reflog 和 show-ref。
错误二:直接 reset 到猜测的位置
猜错会覆盖当前工作区,制造第二个待恢复版本。先 branch,不要先 reset;创建引用几乎没有破坏性。
错误三:只看提交说明,不检查 diff
重复说明很常见,例如多个提交都叫"fix bug"。必须检查作者、时间、父提交、文件列表和关键内容。
错误四:把 reflog 当成远端历史
同一个仓库在不同机器上的 reflog 不同。执行事故操作的机器、长期工作的同事机器和 CI 缓存可能分别拥有不同线索。
错误五:立刻运行 gc 或仓库瘦身工具
清理不可达对象会直接损害恢复概率。空间不足时也应先复制整个仓库目录或对象库,再在副本上分析。
错误六:恢复后直接强推公共分支
找到提交只代表技术上可恢复,不代表有权改写团队历史。先推救援分支,通过评审确认整合方案,并通知依赖该分支的成员。
错误七:忽略未提交文件的边界
普通未暂存修改没有形成 Git 对象,reflog 不负责保存。stash、已 add 的 blob、IDE 历史和文件系统快照可能提供线索,但恢复能力不同,必须如实说明。
9. 一套适合团队的事故处置流程
- 暂停自动维护、强推、rebase、reset 与清理任务,记录事故机器和时间。
- 复制仓库或至少备份
.git,在副本中分析可降低二次操作风险。 - 保存
git status、git show-ref、git reflog --all --date=iso输出。 - 根据时间、作者和文件定位候选哈希,使用
git show验证。 - 为事故后当前位置和目标提交分别创建救援分支。
- reflog 无结果时再运行
git fsck,同时查询远端、同事仓库和备份。 - 通过测试和代码评审选择 cherry-pick、merge、revert 或受控 reset。
- 若需更新远端,先推救援分支;任何历史改写都要通知团队并设置维护窗口。
- 复盘受保护分支、备份、权限、强推策略和恢复演练是否缺失。
9.1 高价值仓库需要哪些预防措施
首先是保护默认分支:禁止直接删除和裸强推,要求合并请求、状态检查与最少评审人数。紧急维护账号应采用临时授权并留下审计,而不是让所有开发者长期拥有绕过权限。需要改写历史的仓库,也应先推送带时间戳的备份引用,再经过明确维护窗口执行。
其次是备份可恢复性。只把代码推到一个远端,并不等于完整备份;远端账号误删、供应商故障和凭据泄露仍可能影响同一份数据。可以定期制作镜像仓库或 bundle,存放在权限隔离的位置,并对恢复过程做抽样演练。备份要覆盖 refs、标签和需要保留的 LFS 对象,还要记录恢复所需的访问方式。没有演练过的备份,发生事故时可能才发现缺少对象或权限。
再次是降低误操作概率。团队脚本应把 --force 替换为受控的 --force-with-lease,在 reset、rebase、分支删除前显示目标引用和工作区状态。对重要分支,可以在本地 hook 或命令包装器中要求二次确认。不过本地提示不是安全边界,服务端分支保护才是最终约束。
9.2 恢复后如何证明内容完整
"分支重新出现"不等于恢复完成。先比较救援分支与预期基线的提交图,确认父提交关系没有断裂;再检查关键文件的 tree、构建结果和测试。若使用 cherry-pick,哈希变化是正常现象,但补丁内容、提交顺序和依赖必须核对。若使用 merge,注意是否带入了事故后不应保留的修改。
对于已经部署的系统,还要把源码提交、制品摘要和环境版本对齐。容器镜像标签可能会被覆盖,可靠证据应使用不可变摘要;发布系统最好记录源提交、构建流水线和制品摘要。事故记录中写明原始丢失哈希、救援分支、最终整合提交、执行人和验证结果,后续才能审计,也能在相似事故中快速复用流程。