场景很常见: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 回滚速查。