Git stash 在 checkout 之后不见了——如何找回

切换分支后 git stash list 变空,未完成的改动找不到了。本文教你用 stash reflog 或 git fsck 找回丢失的 stash 并恢复到工作区。

你执行了 git stash,切到 main 看了个 PR,再用 git checkout feature/login 切回来,执行 git stash pop——结果 git stash list 空了,没做完的改动也找不着了。

最快的排查方式:先跑 git statusgit 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 错了 / 有多个 stashStep 1
git reflog stash有旧 SHA,但 list 为空stash 被 drop 或干净地 pop 掉了Step 2
git fsck --no-reflogdangling commit <sha>连 reflog 条目也没了Step 3
哪里都查不到untracked 文件被 git clean 删了这些文件本来就没进 stashStep 5

常见原因

按命中率从高到低排列。

1. git stash pop 成功应用并自动删除了 stash

git stash pop 等于 git stash apply 加上 git stash drop。如果 apply 没有冲突、成功了,stash 条目会立刻被删除——但你的改动此刻已经在工作区的磁盘上了。它们没丢,只是被应用了。

怎么判断:跑 git statusgit diff。能看到修改过的文件 = stash 已应用,什么都没丢。

2. 误执行了 git stash dropgit stash clear

复制粘贴出错、手快肌肉记忆,或某个 shell 别名把 git stash show 敲成了 git stash dropgit 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 --hardgit 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,在 popdrop 它之前,再用 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 listgit worktree list 能列出全部)。

Q:stash 会跨 clone 共享、或被推到远端吗? A:不会。stash 纯本地——git push/git fetch 都不碰 refs/stash。要跨机器共享 WIP,就提交到一个草稿分支(git switch -c draft/my-feature)再推上去。

相关阅读

标签: #git #version-control #排查