场景很常见:feature 分支写了一半,测试报了个紧急 bug 要立刻修。顺手 git stash 存一下,切到 hotfix 修完提交,切回 feature,git stash pop——冲突了。

明明改的是完全不同的地方,怎么就冲突了?这篇把原因和 6 个相关的坑一起讲清楚,全部命令在 Git 2.21(Windows PowerShell 5)实测。

冲突的真正原因

git stash 存的不是一个补丁,而是一个 commit 对象,里面记了三样东西:

  • base:创建 stash 那一刻的 HEAD 快照
  • 工作区改动:相对 base 的差异
  • 暂存区改动:相对 base 的差异

pop 的时候执行的是三方合并:以 stash 里的 base 为基准,把「当前工作区」和「stash 内容」合起来。

问题就出在 base 上。如果你 stash 之后,目标分支上的同一个文件也变了(不管是新提交、切分支带过来的内容,还是手动改过),合并基准就和现状对不上了,三方合并发现两边都改了同一条,就是冲突。

实测复现(一条文件走完):

git init stash-demo; cd stash-demo
"v1" | Out-File -Encoding utf8 a.txt; git add a.txt; git commit -m "init"
"v2" | Out-File -Encoding utf8 a.txt
git stash push -m "wip"
"v3" | Out-File -Encoding utf8 a.txt; git commit -am "改同一个文件"
git stash pop

最后一步稳定报冲突。改成两个不同文件的情况就不冲突:

"new" | Out-File -Encoding utf8 b.txt
git stash push -m "wip-newfile"
"other" | Out-File -Encoding utf8 a.txt; git commit -am "只改 a"
git stash pop

这条能干净应用。这就是判断标准:stash 之后同一个文件有没有被改过。

解法一:git stash branch,最干净

如果 pop 已经冲突,别硬解。用 git stash branch 从 stash 创建分支:

git stash branch fix-from-stash "stash@{0}"

它会做三件事:基于 stash 创建那一刻的 base 新建分支、切过去、应用并删除该 stash。因为基准完全匹配,不会冲突。实测会看到提示:

Switched to a new branch "fix-from-stash"
On branch fix-from-stash
Changes not staged for commit:
        modified:   a.txt
Dropped refs/stash@{0}

改完再合回原分支即可。这是处理「stash 放太久、分支已经跑远了」的标准姿势。

解法二:手动解冲突后 drop

冲突已经发生了,git stash pop 停下来留下冲突标记:

git status

会看到 both modified: a.txt。处理流程:

# 手动改完文件,去掉 >>> <<< === 标记
git add a.txt
git stash drop

注意 pop 在冲突时不会删掉 stash,这是设计上的保护。所以必须手动 drop,否则下次 list 里还躺着一份。

解法三:直接放弃这次 pop

想清楚 stash 那份不要了:

git checkout -- .
git stash drop

先还原工作区(会丢掉当前所有未提交改动,操作前确认),再删 stash。

另外 5 个实测踩到的坑

1. 默认不包含未跟踪文件

新建的文件 git stash 之后还在工作区,看起来像「stash 没生效」:

git stash -u   # 含未跟踪文件
git stash -a   # 连 .gitignore 忽略的也一并存

这是最容易理解错的一条:stash 只管 tracked 文件。

2. -m 必须显式写 push

git stash -m "wip"        # 报 usage
git stash push -m "wip"   # 正确

报错原文(实测):

usage: git stash list [<options>]
   or: git stash show [<options>] [<stash>]
   or: git stash drop [-q|--quiet] [<stash>]
   or: git stash ( pop | apply ) [--index] [-q|--quiet] [<stash>]
   or: git stash branch <branchname> [<stash>]
   or: git stash clear
   or: git stash [push [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]
          [-u|--include-untracked] [-a|--all] [-m|--message <message>]
          [--] [<pathspec>...]]

规律:git stash --keep-index 可以省 push,但一旦带 -m 就必须写 push。所以 git stash --keep-index -m "x" 同样报错,要写成 git stash push --keep-index -m "x"。

3. PowerShell 里 stash@{n} 必须加引号

git stash pop stash@{0}     # 被 PowerShell 当成哈希表语法,命令直接变形
git stash pop "stash@{0}"   # 正确

{} 在 PowerShell 里有特殊含义,不加引号参数会被吞掉或拆错。bash 和 zsh 不受影响,但脚本里统一加引号更保险。

4. git stash save 已经不推荐用(而且不会报错)

git stash save "msg"   # 能用,新旧版本都不报错

实测 Git 2.21 上 git stash save "msg" 依然静默成功,连一行提示都没有——正是这种「安静」让它难以被察觉到已经过时。官方从 2.16 起推荐 git stash push -m,较新版本才会打印弃用提示。新脚本别再用 save。

5. show --include-untracked 有版本门槛

git stash show --include-untracked   # Git 2.32+ 才支持

2.21 上实测直接打印 usage: git stash show [<options>] [<stash>],退出码 129(不是 unknown option,它会先拒绝整条命令)。低版本想看未跟踪文件的内容,只能 git stash show -p 看 tracked 部分,未跟踪的另行处理。

一张对照表

操作 命令 是否删除 stash
恢复并删除 git stash pop 是(冲突时否)
恢复但保留 git stash apply 否
恢复指定 git stash apply "stash@{1}" 否
删除指定 git stash drop "stash@{1}" 是
清空全部 git stash clear 全部删除
从 stash 建分支 git stash branch <名> "stash@{n}" 是

apply 和 pop 只差「删不删记录」这一件事,实测 apply 之后 git stash list 仍然有 2 条,pop 之后少一条。

完整的 24 条命令和 5 个场景分类,见 Git stash 速查。其他 Git 日常命令看 Git 速查,回滚和找回误删提交看 Git 回滚速查。