Git stash恢复实战:保留暂存状态,隔离冲突现场

临时切换任务时,文件内容保存了,不代表原来的暂存选择也能恢复。另一个常见误会是:新分支上应用stash冲突,就只能在冲突现场不断试命令。本文用四个一次性仓库做完整演练:核对保存范围、比较是否恢复索引,再把发生冲突的改动恢复到另一个工作目录。

适合已经使用Git、需要保留“部分已暂存、部分仍在修改”状态的开发者。本次实测macOS、Python 3.14.4、Git 2.50.1(Apple Git-155)。全部内容为合成文本;实验不读取你的项目,不联网,不推送,也不修改全局Git配置。

下载完整恢复实验:四个场景、18项具名检查与观察结果(ZIP)

一个文件同时保留三个版本

解压进入stash-recovery-lab-v1后运行python3 run_lab.py。需要Python 3.11+与Git 2.28+;脚本在临时目录中创建仓库,结束后清理这些实验材料。它没有要求你在当前项目中执行清理或恢复命令。

位置 notes.txt内容
HEAD提交 title=base
索引(已暂存) title=staged
工作区 title=staged,另有body=unstaged一行

另有未跟踪的scratch.txt和被*.tmp忽略的cache.tmp。故意区分这四类状态,是为了让“恢复成功”能够逐项核对,而不只是看文件重新出现。

输出的checks_passed为18,包含保存范围、索引与工作区内容、未跟踪文件、冲突状态和stash记录保留检查。这18项是四个场景内的具名校验,不是18种复杂仓库结构或跨平台兼容测试。

默认stash与-u,实际留下什么

第一组仓库执行默认git stash push,已跟踪文件的暂存与工作区修改被保存;scratch.txt和cache.tmp仍在原目录。第二组使用git stash push -u,scratch.txt被收入条目并从工作区移走,cache.tmp仍留下。

文件状态 默认push push -u
已跟踪修改 保存 保存
未跟踪scratch.txt 留在目录 保存并移走
忽略的cache.tmp 留在目录 留在目录

脚本还读取stash保存的未跟踪文件对象,确认scratch.txt确实存在,而不是只凭原目录里不见了就判断保存成功。忽略文件不在这个恢复集合里;如果里面是你需要保留的配置或资料,应另做受控备份,不能把-u当作整个目录快照。

真实项目在保存前先查看:

git status --short --untracked-files=all
git diff
git diff --cached
git status --short --ignored

先识别哪些文件可进入仓库对象,尤其是尚未跟踪的配置。本文不使用收集全部忽略文件的选项;“保存更多”也可能意味着把秘密一起收入Git对象。演示里的缓存文本不含任何真实数据。

恢复文本和恢复暂存选择,是两项验收

在与保存时相同的干净基线上,实验分别执行普通apply与带–index的apply。结果如下:

恢复方式 工作区内容 索引内容
apply staged标题和unstaged正文均恢复 仍是base;此前暂存选择没有恢复
apply –index 相同的完整工作区内容 恢复为staged标题

两种方式看起来都把文本找回来了,但下一次提交的内容不同。脚本比较的不只是notes.txt,还比较git diff --cached、git diff及scratch.txt;带–index的结果与原始选定状态一致。

准备恢复时先核对条目:

git stash list --format='%gd %H %s'
git stash show -u --stat 'stash@{0}'

确认目标后,记录该条目的完整对象ID。不要把stash@{0}当作永远不变的标识;后续保存会改变序号。若刚才push提示没有本地修改,必须停止检查,不能把已有的旧条目误当作本次保存。

以下用变量保存已核对的对象ID;占位文本必须替换,不能原样执行:

stash_oid='替换为已核对的完整对象ID'
git stash apply --index "$stash_oid"
git diff --cached
git diff
git status --short --untracked-files=all

本例选择apply,因此恢复后stash条目仍在。–index也不是冲突免疫选项;它尝试恢复索引状态,遇到不兼容的当前内容仍可能失败。此时应保留条目和现场,再决定怎样处理。

新分支发生冲突后,把原状态放到另一目录核对

第四组实验先保存上述状态,再创建other分支,把同一标题改为title=other-branch并提交。此时执行普通git stash apply,本次退出码为1,notes.txt出现在未合并路径清单中,stash仍然存在。

我们没有在这个目录反复apply,也没有用强制重置抹掉冲突。实验从stash保存时的基线创建一个新的工作目录,再在那里应用原条目和索引。下面对应相同的恢复思路;目标目录必须尚不存在:

base_oid=$(git rev-parse "$stash_oid^1")
git worktree add --detach ../stash-recovery-check "$base_oid"
git -C ../stash-recovery-check stash apply --index "$stash_oid"
git -C ../stash-recovery-check diff --cached
git -C ../stash-recovery-check diff
git -C ../stash-recovery-check status --short --untracked-files=all

stash的第一父提交是保存时的HEAD基线。把恢复放回这个基线,可以先重建“当时未提交的工作”,再单独处理它与新分支的差异。实验中,新目录的索引、工作区、两个diff和未跟踪文件都与保存前一致;原冲突目录的文件保持原状。

cache.tmp没有在新目录出现,因为它从未被-u保存。这个结果不是恢复失败,而是保存范围的边界终于变得可见。linked worktree也共享仓库对象与引用,它不是另外一份独立备份。

这里没有解决新分支的业务冲突,只是把原始工作完整拿到一个容易检查的位置。新目录处于detached HEAD;若要在那里继续开发,先创建一个命名分支,再按需要整理提交。原冲突现场如何解决、改动怎样合入目标分支,仍须由代码含义决定,不能只凭冲突标记自动选一边。

最后确认的不是“命令退出0”

实际恢复时,至少应核对四件事:文件正文是否完整、暂存内容是否还是预期子集、未跟踪文件是否齐全、忽略文件是否另有安排。确认之前保留原stash,且不要强制删除含未提交内容的恢复工作目录。

本实验覆盖单个普通文本文件的索引与工作区差异、未跟踪文本、忽略缓存,以及同一行的真实合并冲突;没有测试子模块、Git LFS、稀疏检出、符号链接、二进制冲突、stash被删除或磁盘损坏。脚本清理的是自己新建的临时练习仓库,不应把这种清理方式移植到你的工作项目。

包中保留了观察结果与完整脚本。可以先在临时实验里改变一个条件,例如让未跟踪文件重名或增加另一处文本修改,再观察Git返回什么;确认自己的场景后,再处理真实仓库。不要把一次文本演练扩大成“所有未提交文件都能找回”的承诺。

源码包SHA-256:75458a05a4270a145dd11a36fac2d9399dd85b9c817bb16060c0f4f736b195b8,MIT许可。封面为AI生成的恢复概念示意,非终端操作截图;正文结果来自本次运行。

资料与旧稿对照

资料查阅与编辑日期:2026年10月25日。作者:玖柒。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享