git stash:只收起一个文件,不惊动其余

只把一个文件放进 stash,其余进行中的改动原地不动:git stash push -- <路径>、交互式 -p,以及取回改动的三种方式。

HUMAN作者 @mrsaynothing35d2 分钟读完

代码评审来了,三个文件改到一半,只能动其中一个。把全部收进 stash 再把另外两个重新敲一遍——这是没人怀念的仪式。

TL;DR:git stash push -m "备注" -- <路径> 只 stash 指定的路径,脏树的其他部分原地不动。 用 git stash pop 取回(或者用 git restore --source stash@{0} -- <路径> 从任意一个 stash 里单独拎出一个文件)。pathspec 形式随 git 2.13 发布(2017 年 5 月)——最近八年的任何工具链都有它。

git stash push -m "app.conf prod tweak" -- app.conf
git stash list
# stash@{0}: On main: app.conf prod tweak
git stash pop

stash 的是文件,不是整棵树。

git 里怎么只 stash 一个文件?

光秃秃的 git stash 会扫走整个工作区——所有已跟踪的修改进 stash,一切回到 HEAD。当脏树是混合状态时,这就错了:一个文件可以交付了,其余是真的没写完。答案是带 pathspec 的 push,依据 git-stash 文档:

# 之前:三个脏文件
$ git status --short
 M app.conf
 M notes.md
 M main.py

# 只 stash app.conf
$ git stash push -m "app.conf prod tweak" -- app.conf
Saved working directory and index state On main: app.conf prod tweak

$ git status --short
 M notes.md
 M main.py

app.conf 回到已提交的状态,进了 stash@{0};notes.md 和 main.py 纹丝没动。-m 的备注可选,但等 git stash list 出现第二条时,那三秒钟就值回票价——名叫「WIP」的 stash 老得很快。

两个有用的细节:

  • pathspec 放在 -- 之后。 双横线后面的都是路径,不是开关。和 git checkout -- <路径> 同一约定,而且不是装饰:在旧版本里 git stash push -- main.py 和 git stash push main.py 行为不同,裸路径可能被误读。
  • 未跟踪文件需要 -u。 崭新的文件没有可 stash 的已跟踪历史,普通 push 会跳过它。git stash push -u -- newfile.py 会带上它;-a 更进一步,连被忽略的也扫进去。

怎么只 stash 一个文件的一部分?

文件本身就是半成品——三块好的 hunk,一块见不得人的——交互式流程会把它切开:

git stash -p            # 或者: git stash push -p

Git 逐个 hunk 询问 Stash this hunk [y,n,q,a,d,j,g,/,e,p,?]?。进 stash 的答 y,留下的答 n。结果是一个只含你批准内容的 stash,文件的其余部分照旧脏在工作区。同一套逐 hunk 的机制也驱动着 git add -p,肌肉记忆可以直接平移。

git stash push -- <路径> git stash -p
粒度 整个文件 单个 hunk
速度 一条命令,可脚本化 交互式,逐 hunk
CI 可复现 是(路径作为参数) 否——提示符前得有人
适合 「这个文件,其余不要」 「这处改动,那处不要」
自 git 2.13(2017 年 5 月) 早已有之

从文档自身的模型得出的经验法则:边界是文件就用 pathspec,边界在文件内部就用 -p。

怎么从 stash 里 pop 出单个文件?

git stash pop 恢复 stash@{0} 并删除该条目。这是常规路径——但 pop 对 stash 而言是全有或全无,遇到冲突会中止,条目保留。两个更细的选择:

# 1. 只应用,不删除(可以安全地重复)
git stash apply stash@{0}

# 2. 从 stash 里取出一个文件,条目原样保留
git restore --source stash@{0} -- app.conf
# 同一操作的 2.23 之前写法:
git checkout stash@{0} -- app.conf

restore/checkout 形式回答的是「三个改动一起 stash 了,现在只要其中一个」——它把该路径的 stash 内容复制进工作区,stash@{0} 原样不动。注意:它会覆盖工作区里的那份;如果这条路径上的现有修改还要紧,先 diff:

git diff stash@{0} -- app.conf

stash 以真实提交的形式存在于一根类似 reflog 的栈上,所以 stash@{0} 接受任何 commit-ish 语法;误删的 stash 也能从 reflog 里捞回来——直到垃圾回收把它吃掉。

什么时候 stash 是错的工具?

stash 是草稿纸,不是分支:在 git branch 里没有名字,没有评审,默认不显示 diff,条目无声堆积直到被遗忘。如果进行中的工作需要扛过跨机器、跨天数的上下文切换,就提交到分支上——带着历史比无名堆栈强。等这些停好的 commit 要落到别处,git cherry-pick 负责搬运。如果目标是撤销而不是停放,git undo last commit 的决策树适用。

从文档化行为里整理的诚实故障账本:

症状 原因 修法
stash 之后文件不见了 未跟踪路径被跳过 git stash push -u -- <路径>
pop 时 error: Your local changes ... would be overwritten 该路径在 stash 之后又变了 先提交或 stash 新状态,再 pop
取回的改动消失了 pop 撞上冲突并中止 解决冲突后 git stash apply
「刚才是哪个 stash?」 一堆没有名字的 stash 每次都写 -m 备注

给 git stash push 加上 pathspec 的那页 2017 年 5 月的发布说明已经八岁,而大多数 git stash 的肌肉记忆比它还老。值得重学一次:如今全树扫荡才是特例,不是默认。