你执行了 git stash,切到 main 看了个 PR,再用 git checkout feature/login 切回来,执行 git stash pop——结果 git stash list 空了,没做完的改动也找不着了。
最快的排查方式:先跑 git status 和 git diff。如果能看到修改过的文件,说明 stash 已经应用到工作区,什么都没丢。如果工作区是干净的,那 stash 对应的 commit 还以「悬挂对象」的形式存在——用 git fsck --no-reflog | grep commit 找到它的 SHA,再 git stash apply <SHA> 即可。Git 默认会保留不可达对象至少 30 天(可达的 reflog 条目保留 90 天),所以绝大多数情况下都能找回。
原理:一个 stash 本质上是一个真实的 commit 对象。指针存在 refs/stash 里,更早的条目存在这个引用的 reflog 中。drop、pop 或 clear 删掉的只是这个引用,commit 对象本身会一直留在对象库里,直到垃圾回收(gc)把它清理掉。
先判断你属于哪种情况
先跑下面这几条命令,它们能立刻告诉你该走哪条找回路径。
| 命令 | 如果看到…… | 原因 | 跳到 |
|---|---|---|---|
git status / git diff | 你修改过的文件 | stash 已应用(pop 成功了) | 无需处理 |
git stash list | 有条目,但属于别的分支 | pop 错了 / 有多个 stash | Step 1 |
git reflog stash | 有旧 SHA,但 list 为空 | stash 被 drop 或干净地 pop 掉了 | Step 2 |
git fsck --no-reflog | dangling commit <sha> | 连 reflog 条目也没了 | Step 3 |
| 哪里都查不到 | untracked 文件被 git clean 删了 | 这些文件本来就没进 stash | Step 5 |
常见原因
按命中率从高到低排列。
1. git stash pop 成功应用并自动删除了 stash
git stash pop 等于 git stash apply 加上 git stash drop。如果 apply 没有冲突、成功了,stash 条目会立刻被删除——但你的改动此刻已经在工作区的磁盘上了。它们没丢,只是被应用了。
怎么判断:跑 git status 和 git diff。能看到修改过的文件 = stash 已应用,什么都没丢。
2. 误执行了 git stash drop 或 git stash clear
复制粘贴出错、手快肌肉记忆,或某个 shell 别名把 git stash show 敲成了 git stash drop。git stash clear 则会一次性删掉所有条目。
怎么判断:git stash list 为空。注意:drop 之后,被删的那条通常也会从 stash reflog 里消失——所以如果 git reflog stash 也是空的,直接跳到 Step 3 用 git fsck。
3. pop 遇到冲突,工作区被手动清理掉了
这是大多数人记错的一点。在现代 Git 里(过去十几年的所有版本都是如此),git stash pop 遇到冲突时并不会删除 stash——官方文档明确写道:「Applying the state can fail with conflicts; in this case, it is not removed from the stash list.」也就是说,如果 pop 冲突了,你的 stash 仍然在 git stash list 里。
真正的丢失发生在:面对冲突的工作区,用户跑了 git checkout -- .、git reset --hard 或 git stash drop 去「清理一下」,把半应用的改动抹掉了。但那个 stash commit 仍然以 SHA 的形式存在。
怎么判断:先看 git stash list,条目大概率还在;如果已经被你丢弃了,按 Step 2/3 找回。
4. 你 pop 的 stash 属于另一个分支
你在 feature/login 上 stash 了,忘了,切到 feature/payments 又 stash 了一次。切回来时 git stash list 把 payments 的 stash 排在 stash@{0}、login 的排在 stash@{1},于是一条裸 git stash pop 取错了。stash 是整个仓库共享的,不是按分支隔离的。
怎么判断:git stash list 每条都带分支名:stash@{0}: WIP on feature/payments: ...。指定正确的序号去 pop:git stash pop stash@{1}。
5. git worktree 看不到主工作树的 stash
refs/stash 默认是按工作树(per-worktree)隔离的。在主工作树里建的 stash,在新增的 worktree 里看不到(反之亦然),于是看起来像丢了。
怎么判断:git worktree list 显示多个条目。stash 是在另一个 worktree 的上下文里建的——切到那个 worktree 的目录再跑 git stash list。
6. git clean -fd 删的是 stash 根本没保存的 untracked 文件
裸 git stash 只保存追踪过、被修改的文件。全新的 untracked 文件不会被保存,除非加 -u(--include-untracked)或 -a(--all)。如果你丢的是全新文件,那是 git clean -fd 直接把它们删了,stash 里压根没有它们。
怎么判断:丢的是从未 git add 过的文件。用 git stash show -p <ref> 确认 stash 里到底存了什么(找 new file: 这样的行)。
最短修复路径
Step 1:确认 stash 是否已应用,或者是不是躺在别的分支上
git status
git diff
git stash list
如果 git diff 已经显示出你的改动,那就完事了。如果 git stash list 里还有条目(可能挂在别的分支下),明确指定正确的那条来应用:
git stash apply stash@{1}
Step 2:在 stash reflog 里找被删掉的 stash commit
git reflog stash --date=iso
# 等价写法:git reflog show refs/stash --date=iso
每一行都给出一个 SHA,例如 abc1234 refs/stash@{0}: WIP on feature/login: 3fa1b2c add session token。记下这个 SHA(记作 STASH_SHA)。应用前先看一眼内容:
git stash show -p STASH_SHA # 完整 patch
一条干净的 git stash drop 往往会清空这个 reflog,所以这里要是什么都没有,就走 Step 3。
Step 3:用 git fsck 找悬挂的 stash commit
reflog 也没了的时候,stash commit 就是一个悬挂对象。列出所有悬挂 commit,再 grep 出 stash commit 自带的 WIP on 摘要:
git fsck --no-reflog | awk '/dangling commit/ {print $3}' | while read sha; do
printf '%s ' "$sha"
git show --no-patch --format='%s' "$sha"
done | grep 'WIP on'
stash commit 有 2~3 个父提交(stash 时的 HEAD、暂存区 index,以及用了 -u/-a 时额外的 untracked-files commit),这能帮你确认找对了对象。
Step 4:恢复找回来的 stash
# 直接把找回的 SHA 应用到工作区:
git stash apply STASH_SHA
或者先把它重新登记为正式的 stash 条目(这样它会重新出现在 git stash list 里):
git stash store -m "restored dropped stash" STASH_SHA
git stash pop
(git stash store 是这件事的 porcelain 命令;底层用 git update-ref refs/stash STASH_SHA 效果相同。)
Step 5:处理留下冲突的 pop
如果一次 pop 冲突了、你想继续推进(千万别清理工作区——stash 还在列表里):
git checkout --theirs src/login.ts # 或手动解决,然后:
git add src/login.ts
git stash drop # 一定要等所有冲突都解决完再执行
Step 6:找回被 git clean 删掉的 untracked 文件
被 git clean 删掉的 untracked 文件不在 Git 的对象库里,所以 reflog/fsck 救不回它们。只能靠编辑器的本地历史或文件系统快照:
# VS Code:右键文件(或其所在文件夹)> Open Timeline > 选一条 Local History 记录
# JetBrains:在 Project 视图里右键 > Local History > Show History
# macOS:进入该文件夹的 Time Machine;Linux/ZFS:ls .zfs/snapshot/
如何确认已经修好
git status # 你的文件重新显示为 modified / staged
git diff # 期望的那些改动又回来了
如果你用 git stash store 重新登记了 stash,在 pop 或 drop 它之前,再用 git stash list 确认它确实在列表里。
预防建议
- 优先用
git stash apply而不是git stash pop——apply 即使成功也会保留条目作为安全网,满意之后再手动 drop。 - 给 stash 起名:
git stash push -m "login form WIP: session token handling",让git stash list一眼可读。 - 把 untracked 文件也存进去:
git stash push -u(或用-a连 ignored 文件一起),避免新文件被落下。 - 清理前先空跑:
git clean -nfd只打印会删掉哪些文件,不真删。 - 延长找回窗口:
git config --global gc.reflogExpireUnreachable 90.days(不可达/被 drop 的条目默认保留 30 天;可达 reflog 条目默认 90 天)。 - 用
git worktree add ../login feature/login同时在多个分支上工作,省掉 stash-checkout-stash 的来回折腾。 - 任何丢了会心疼的改动,别 stash,直接提交:
git commit -m "WIP: do not merge"。commit 能从分支可达,用git log也好找。
常见问答 (FAQ)
Q:git stash pop 遇到合并冲突会把我的 stash 删掉吗?
A:不会。官方文档写明,apply 失败出现冲突时,条目「is not removed from the stash list」。去看 git stash list,它还在。解决冲突后再手动 git stash drop。真正导致丢失的,通常是手动清理冲突工作区(比如 git reset --hard)。
Q:被 drop 超过 30 天的 stash 还能找回吗?
A:也许,前提是 git gc 还没把那个对象清掉(松散对象默认有 2 周的 prune 宽限期,但没被引用的 stash 对象常常能活更久,直到某次 gc 真正运行)。试试 git fsck --unreachable --no-reflog,再用 git show 逐个查看 commit <sha>。垃圾回收跑过之后就没法保证了。
Q:我 drop 了 stash,但 git reflog stash 是空的,怎么办?
A:一次成功的 git stash drop 会把条目从 stash reflog 里移除,所以 reflog 帮不上忙。用 git fsck --no-reflog(Step 3)按 WIP on 摘要找到那个悬挂 commit,再 git stash apply <sha>。
Q:怎么在不应用的情况下看一个 stash 的完整 diff?
A:git stash show -p stash@{0} 打印完整 patch。想看文件级别的汇总,把 -p 换成 --stat;找回的 SHA 同样可用 git stash show -p STASH_SHA。
Q:为什么我的 stash 在一个 worktree 里能看到、另一个里看不到?
A:refs/stash 是按 worktree 隔离的。到你创建该 stash 的那个 worktree 目录下跑 git stash list(git worktree list 能列出全部)。
Q:stash 会跨 clone 共享、或被推到远端吗?
A:不会。stash 纯本地——git push/git fetch 都不碰 refs/stash。要跨机器共享 WIP,就提交到一个草稿分支(git switch -c draft/my-feature)再推上去。
相关阅读
- Rebase 之后 commit 消失了
- 我在 detached HEAD 上提交了,怎么办
- 找回 Git 历史里的旧版本文件
- Cherry-pick 解冲突后变成空 commit
- AI 帮你回滚代码改动
- Bisect 在 skipped commit 上卡住
标签: #git #version-control #排查