1. 项目概述:为什么 stash 不是“临时存档”,而是你工作流里的“时间暂停键”
你正在调试一个数据管道的异常行为,data_pipeline.py里刚加了三处日志打印,config.yaml改了两个参数,tests/test_pipeline.py补了一个边界用例——所有改动都还没 commit,甚至没 run 过测试。这时 Slack 弹出一条高优消息:“生产环境凌晨三点的调度任务失败了,错误日志指向config.yaml的超时配置”。你必须立刻切到main分支,修复这个 bug,然后合入 hotfix。但你的工作区现在是一团半成品,commit?语义不清,历史污染;丢掉?重写一遍至少二十分钟;硬切分支?Git 会直接报错拒绝,因为有未提交变更。
这就是git stash真正存在的意义:它不是把文件塞进抽屉锁起来,而是给你的整个开发状态按下一帧“暂停键”——代码、暂存区、甚至(可选)新创建的文件,全部被原子性地冻结、打包、压入一个栈顶,而你的工作目录瞬间回到上一次 commit 的干净状态。你可以毫无阻碍地 checkout、pull、fix、commit、push,等一切搞定,再一键“恢复播放”,所有暂停时的状态原封不动地回到你眼前。它解决的从来不是“怎么存”,而是“怎么在不打断心流的前提下,安全地切换上下文”。我带过的十几个数据工程团队里,90% 的新人前两周都在用git commit -m "wip"应对这类场景,结果是主干历史里塞满无法 rebase 的中间态提交,reviewer 看得头疼,自己 cherry-pick 时也常漏掉某次 wip。而 stash 是 Git 原生提供的、专为这种“紧急插队”设计的轻量级状态快照机制,它的 LIFO 栈结构、可命名、可选择性保存、可交互式拆分等特性,决定了它比任何手动备份或临时分支都更精准、更可控、更符合人脑的工作节奏。这篇文章不会教你“git stash 是什么”,而是带你亲手拆解它的每一个齿轮:从第一次按下git stash时底层发生了什么,到如何在复杂冲突中毫发无损地取回代码,再到为什么git stash pop和git stash apply的区别,本质是两种完全不同的协作哲学。
2. 核心原理与设计逻辑:stash 背后,Git 在做什么
2.1 stash 的本质:三个 commit 构成的“状态胶囊”
很多人以为git stash就是把文件复制一份存起来,这是最大的误解。实际上,每次git stash push(git stash是它的简写)都会在 Git 对象数据库里创建三个独立的 commit 对象,它们共同构成一个不可分割的“状态胶囊”:
- 第一个 commit(
W):记录你工作目录(Working Directory)的当前快照。它包含所有已修改但未暂存的文件内容(unstaged changes),以及所有新创建但未git add的文件(untracked files,如果你用了-u或-a)。这个 commit 的父节点是当前 HEAD。 - 第二个 commit(
I):记录你暂存区(Index / Staging Area)的当前快照。它只包含所有已git add但尚未 commit 的文件内容(staged changes)。这个 commit 的父节点也是当前 HEAD。 - 第三个 commit(
U):一个特殊的“空”commit,它的树对象(tree object)为空,作用是作为W和I的共同父节点,让整个 stash 结构在 Git 的 DAG(有向无环图)中能被正确引用。你可以把它理解为一个“锚点”。
提示:你可以用
git show stash@{0}查看这个“胶囊”的顶层信息,它会显示W和I两个子 commit 的哈希值。而git stash show -p stash@{0}实际上是在对比W和I两个 commit 的差异,也就是你工作目录和暂存区之间的所有变化。
这个设计解释了所有核心行为:
- 为什么默认不保存 untracked 文件?因为
Wcommit 默认只抓取 Git 已知的 tracked 文件。新文件不在 Git 的索引里,自然不会被纳入W。 - 为什么
git stash pop会自动删除?因为pop的本质是apply+drop。apply操作会将W和I的差异逆向应用到当前工作区和暂存区,而drop则是安全地删除这三个 commit 对象。Git 认为,一旦状态被成功还原,原始快照就完成了使命。 - 为什么
git stash branch能避免冲突?因为它不是简单地apply,而是先基于Ucommit(那个空锚点)创建一个新分支,再在这个纯净的起点上applyW和I。这相当于把你的“暂停状态”当作一个全新的、独立的开发起点,完全避开了当前分支可能存在的任何干扰。
2.2 LIFO 栈:不是列表,而是“活的”操作序列
stash@{0},stash@{1},stash@{2}看起来像数组索引,但它背后是一个动态的、带时间戳的操作栈。每一次git stash push,Git 都会在.git/refs/stash文件里追加一行,记录这次 stash 的 commit 哈希和一个精确到秒的时间戳。stash@{0}永远指向这个文件里最后一行的记录,即最近一次 stash。当你执行git stash drop stash@{2},Git 并不是“删除第 3 个元素”,而是从.git/refs/stash文件里删掉对应时间戳的那一行,然后重新编号剩余的行。所以stash@{3}会变成stash@{2},以此类推。这意味着:
git stash list输出的顺序,就是你实际执行stash命令的时间倒序。- 如果你在不同分支上做了多次 stash,它们会混在一起,按时间排序。
git stash list --oneline后面显示的分支名,只是该 stash 创建时你所在的分支,它本身并不属于那个分支的历史。
2.3 “干净工作区”的严格定义:Git 的底线在哪里
git stash所谓的“clean working directory”,其判定标准极其严格,且与git status的输出逻辑完全一致。它要求同时满足以下三点:
- 没有 modified tracked files:所有已被 Git 跟踪的文件,其工作目录内容必须与暂存区(index)完全一致。
- 没有 staged changes:暂存区必须为空,即没有任何文件处于
git add状态。 - 没有 untracked files(除非显式指定):所有未被 Git 跟踪的文件,必须不存在于工作目录中(
-u选项会放宽此条)。
只要其中任意一条不满足,git stash就会失败并报错。这也是为什么git stash命令本身永远不会失败——它只在上述条件全部满足时才执行,否则直接退出。那些所谓的“错误”,比如No local changes to save,其实是 Git 在告诉你:“兄弟,你现在的工作区已经够干净了,stash 没有东西可存。” 这不是 bug,是 Git 在尽职地守护你的工作流边界。
3. 实操全流程:从第一次 stash 到彻底清理的每一步
3.1 场景复现:一个真实的“数据管道中断”现场
我们用一个具体、可复现的场景来贯穿所有操作。假设你有一个名为>$ git status On branch feature/transform-v2 Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: data_pipeline.py modified: config.yaml Untracked files: (use "git add <file>..." to include in what will be committed) tests/test_edge_case.py no changes added to commit (use "git add" and/or "git commit -a")
data_pipeline.py:你改了数据清洗逻辑,但还没测试。config.yaml:你调大了内存限制,但不确定是否最优。tests/test_edge_case.py:你新建了一个测试文件,专门验证一个极端输入。
此时,git status显示有 2 个 modified 文件和 1 个 untracked 文件。这就是一个典型的、需要git stash救场的“半成品”状态。
3.2 第一次 stash:默认行为与它的局限性
最简单的命令:
$ git stash Saved working directory and index state WIP on feature/transform-v2: 7a3b1c2 Add initial transform logic执行后,再次运行git status:
$ git status On branch feature/transform-v2 nothing to commit, working tree clean工作区确实变干净了。但问题来了:
tests/test_edge_case.py这个新文件依然存在!因为默认的git stash只处理 tracked 文件,test_edge_case.py是 untracked 的,被完全忽略了。git stash list会显示:
这个$ git stash list stash@{0}: WIP on feature/transform-v2: 7a3b1c2 Add initial transform logicWIP on ...的消息毫无信息量。如果你一天内 stash 了 5 次,stash@{0}到stash@{4}全是WIP on ...,你根本分不清哪个是改 pipeline 的,哪个是修 config 的。
注意:
git stash默认只保存 tracked 文件的修改,这是为了安全。Git 认为,untracked 文件可能是编译产物、日志、临时文件,盲目保存它们可能导致 stash 体积膨胀,甚至引入安全隐患。这是一个深思熟虑的设计,而非疏忽。
3.3 进阶 stash:覆盖所有文件与赋予灵魂
3.3.1 包含 untracked 文件:-u选项
要让tests/test_edge_case.py也被存进去,必须显式告诉 Git:
$ git stash push -u -m "WIP: edge case test for transform v2" Saved working directory and index state WIP: edge case test for transform v2 on feature/transform-v2: 7a3b1c2 Add initial transform logic-u(或--include-untracked)的作用,就是强制将所有 untracked 文件也纳入Wcommit 的快照中。执行后,tests/test_edge_case.py会从工作目录消失,git status再次显示working tree clean。
3.3.2 包含 ignored 文件:-a选项(慎用!)
如果tests/test_edge_case.py是被.gitignore忽略的(比如它匹配了*.log规则),那么-u也无效。此时你需要-a(或--all):
$ git stash push -a -m "WIP: full snapshot including ignored logs"-a会无视.gitignore,把所有文件,包括node_modules/、dist/、*.log等,统统打包进Wcommit。这非常危险。我见过最惨的一次,一位同事在项目根目录下执行git stash -a,结果把整个venv/虚拟环境(几百 MB)和build/目录(GB 级)都塞进了 stash,导致后续git stash list卡顿,git gc失败,最终不得不手动清理.git/objects。所以,-a应该是最后的手段,仅在你100% 确认所有 ignored 文件都是你本次开发必需的、且体积可控时才使用。
3.3.3 命名的艺术:-m参数是你的记忆外挂
-m "WIP: edge case test for transform v2"这个消息,是你未来找回这个 stash 的唯一线索。好的 stash 消息应该像 Git commit message 一样,遵循“动词开头 + 具体内容”的原则:
- ✅
Refactor: extract config validation logic from data_pipeline.py - ✅
Fix: handle null values in user_id field in config.yaml - ❌
WIP on feature/transform-v2(Git 默认,毫无价值) - ❌
stuff(过于模糊)
我习惯在消息里加上文件名和关键动作,这样git stash list一眼就能扫出目标。例如:
$ git stash list stash@{0}: On feature/transform-v2: Fix: handle null values in user_id field in config.yaml stash@{1}: On feature/transform-v2: Refactor: extract config validation logic from data_pipeline.py stash@{2}: On main: Hotfix: revert timeout change in config.yaml3.4 安全检查:在 apply 之前,必须做的三件事
在你执行git stash pop或git stash apply之前,请务必完成以下检查。这能帮你避开 80% 的冲突和意外覆盖。
3.4.1 步骤一:git stash list—— 确认目标
$ git stash list --oneline stash@{0}: On feature/transform-v2: Fix: handle null values in user_id field in config.yaml stash@{1}: On feature/transform-v2: Refactor: extract config validation logic from data_pipeline.py stash@{2}: On main: Hotfix: revert timeout change in config.yaml注意--oneline参数,它会让输出更紧凑,方便快速浏览。确认你要操作的是stash@{0}还是stash@{1}。
3.4.2 步骤二:git stash show stash@{0}—— 快速概览
$ git stash show stash@{0} config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)这行输出告诉你,这个 stash 只修改了config.yaml这一个文件,共 1 行增加,1 行删除。如果它显示15 files changed,你就该警惕了,这可能是一个巨大的、跨模块的修改,需要更仔细地审查。
3.4.3 步骤三:git stash show -p stash@{0}—— 逐行审查
$ git stash show -p stash@{0} diff --git a/config.yaml b/config.yaml index abc1234..def5678 100644 --- a/config.yaml +++ b/config.yaml @@ -10,4 +10,4 @@ timeout_ms: 30000 # New field for null handling +null_handling_strategy: "skip" + timeout_ms: 30000这才是最关键的一步。它展示了config.yaml文件里具体的、逐行的修改内容。你在这里可以确认:
- 这确实是你要恢复的修改吗?
- 这个修改是否还适用于当前的代码上下文?(比如,
timeout_ms这行在当前main分支上可能已经被移到了另一个 section 下,直接 apply 就会冲突) - 是否有你不记得的、意外的修改?
实操心得:我给自己定了一条铁律:任何 stash 的
show -p输出超过 20 行,或者涉及核心配置文件(如config.yaml,Dockerfile),就必须在 apply 前,先在一个临时分支上做一次 dry-run。方法是:git stash branch temp-check stash@{0},然后在temp-check分支上git diff HEAD,确认无误后再git checkout回目标分支。
3.5 恢复策略:pop vs apply,一场关于“一次性”与“可复用性”的抉择
3.5.1git stash pop:果断、利落、不留痕迹
$ git stash pop On branch main Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: config.yaml Dropped refs/stash@{0} (a1b2c3d4e5f67890...)pop的行为是原子性的:它先尝试apply,如果成功,立刻drop。输出中的Dropped refs/stash@{0}就是成功的标志。pop的哲学是:“这个状态我已经用完了,它完成了它的使命,可以安全销毁。” 它适合绝大多数场景:你切过去修 bug,修完回来,pop一下,继续干活。简洁、高效、无残留。
3.5.2git stash apply:谨慎、保留、为协作而生
$ git stash apply stash@{1} On branch main Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: data_pipeline.py $ git stash list stash@{0}: On feature/transform-v2: Refactor: extract config validation logic from data_pipeline.py stash@{1}: On main: Hotfix: revert timeout change in config.yamlapply只做一件事:把 stash 的内容应用到当前工作区和暂存区。它绝不删除stash。stash@{1}依然完好地躺在栈里。这带来了巨大的灵活性:
- 跨分支复用:你可以在
main上apply一个在dev分支上创建的 stash,用于快速验证一个 fix。 - 多点部署:一个通用的配置调整(如统一的日志级别),你可以
apply到staging、prod等多个环境分支上。 - A/B 测试:你可以
apply一个 stash,测试效果;如果不满意,git stash pop(注意,这里 pop 的是刚刚 apply 产生的新 stash,不是原来的)就能一键回滚,而原始 stash 依然可用。
注意:
git stash apply默认应用stash@{0}。如果你想应用其他 stash,必须显式指定,如git stash apply stash@{1}。git stash pop stash@{1}也是同理。
3.6 精准控制:当“全有或全无”不再适用
3.6.1 按文件 stash:git stash push <file>
回到最初那个场景,你只想暂时搁置data_pipeline.py的重构,但想立刻把config.yaml的 bug fix 提交出去。这时,git stash push的路径参数就是你的救星:
# 先把 config.yaml 的修改单独 add 进暂存区 $ git add config.yaml # 然后只 stash data_pipeline.py $ git stash push -m "WIP: refactor pipeline logic" data_pipeline.py # 现在,git status 会显示: # staged: config.yaml # unstaged: data_pipeline.py (but it's stashed, so actually clean) # untracked: tests/test_edge_case.py执行后,只有data_pipeline.py的修改被 stash,config.yaml的修改依然在暂存区,你可以直接git commit -m "Fix: null handling in config"。这是一种非常精细的“工作区分区”策略。
3.6.2 按代码块 stash:git stash push -p(交互式)
这是git stash最强大的功能,也是最容易被忽视的。想象data_pipeline.py里,你同时写了两段互不相关的代码:
- 第 10-15 行:一个实验性的、性能优化的算法(WIP)。
- 第 50-55 行:一个修复了某个特定 bug 的补丁(Ready to commit)。
你不想把整个文件都 stash,只想把那 5 行 WIP 代码藏起来。-p(patch)模式就是为此而生:
$ git stash push -p -m "WIP: experimental algo in data_pipeline.py" diff --git a/data_pipeline.py b/data_pipeline.py index 1234567..89abcdef 100644 --- a/data_pipeline.py +++ b/data_pipeline.py @@ -8,7 +8,7 @@ def process_data(data): # Preprocessing cleaned_data = clean(data) - # TODO: Try new algorithm here + # New experimental algorithm result = new_algorithm(cleaned_data) return result Stash this hunk [y,n,q,a,d,s,?]? yGit 会逐个展示每个“hunk”(代码块),并让你选择:
y:把这个 hunk 加入 stash。n:跳过这个 hunk,保留在工作区。q:退出,不 stash 任何东西。a:把当前文件剩下的所有 hunk 都加入 stash。d:把当前文件剩下的所有 hunk 都丢弃(相当于git restore)。?:显示帮助。
这个过程和git add -p完全一致,它让你对代码的“粒度”拥有绝对的控制权。我把它称为“外科手术式 stash”,是处理大型、混合型修改的终极武器。
3.7 清理战场:让 stash 栈保持健康
3.7.1 删除单个 stash:git stash drop
$ git stash drop stash@{2} Dropped refs/stash@{2} (a1b2c3d4e5f67890...)drop是pop的“只删除不应用”版本。当你确认某个 stash 已经过时、错误或不再需要时,用它来清理。执行后,栈会自动重排,stash@{3}变成stash@{2}。
3.7.2 清空所有 stash:git stash clear
$ git stash clear这个命令没有输出,但后果是永久性的。.git/refs/stash文件会被清空,所有相关的 commit 对象会变成“悬空对象”(dangling objects),在下一次git gc(垃圾回收)时被彻底删除。永远不要在没有备份的情况下执行git stash clear。我的做法是,在执行前,先用git stash list > stash-backup.txt把当前所有 stash 的描述导出到一个文本文件,留作万一之需。
3.7.3 从 stash 创建分支:git stash branch
这是处理“高风险 apply”的黄金方案。假设你有一个在feature/old分支上创建的 stash,现在你想把它应用到main分支,但main分支自从feature/old分叉以来,已经经历了大量重构,data_pipeline.py的结构面目全非。直接git stash pop几乎必然产生大量冲突。
git stash branch就是为此而生:
$ git stash branch fix-from-old-stash stash@{0} Switched to a new branch 'fix-from-old-stash' On branch fix-from-old-stash Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: config.yaml Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: data_pipeline.py Dropped refs/stash@{0} (a1b2c3d4e5f67890...)它做了三件事:
- 创建一个新分支
fix-from-old-stash,其起点是stash@{0}创建时的那个 commit(即feature/old分支当时的 HEAD)。 - 在这个纯净的、与 stash “同源”的起点上,
apply该 stash。 - 自动
drop掉这个 stash。
现在,你拥有了一个全新的、独立的分支,里面包含了你想要的所有修改,而且完全没有冲突。你可以在这个分支上从容地git rebase main,把你的修改“嫁接”到最新的main上,解决所有冲突,然后再git push。这是一种将“状态快照”转化为“可协作、可审查、可集成”的正式分支的优雅方式。
4. 常见问题与实战排错:那些让你抓狂的瞬间
4.1 问题一:“No local changes to save” —— Git 在对你微笑
现象:你满怀信心地输入git stash,却得到No local changes to save。你打开编辑器,明明看到文件有修改,Git 却说“没变化”。
原因与排查:
- 文件权限变更:Git 默认只跟踪内容,不跟踪文件权限(如
chmod)。如果你只是改了文件的可执行位,git status不会显示,git stash也无事可做。用git status --ignored确认。 - 行尾符(CRLF/LF)问题:在 Windows 和 macOS/Linux 混合开发环境中,编辑器可能自动转换了行尾符。Git 认为这是“内容变更”,但肉眼难以察觉。用
git diff --word-diff或xxd工具查看二进制差异。 - Git 配置
core.autocrlf设置不当:这是最常见的元凶。在 Windows 上,应设为true;在 Linux/macOS 上,应设为input。检查:git config --global core.autocrlf。
解决方案:
- 首先,永远相信
git status。如果它不显示,那就真的没有 Git 认为的“变化”。 - 如果你确信有变化,用
git diff查看详细差异,定位是内容、权限还是行尾符。 - 修复配置:
git config --global core.autocrlf true(Windows)或git config --global core.autocrlf input(macOS/Linux)。
4.2 问题二:git stash pop后出现冲突 —— 当现实撞上理想
现象:git stash pop执行后,终端没有显示Dropped...,而是列出了几个文件名,并提示Auto-merging <file>,接着在文件里看到了<<<<<<< Updated upstream和>>>>>>> Stashed changes这样的冲突标记。
原因:pop的apply阶段失败了。Git 发现,你当前工作区的某个文件(比如config.yaml),其内容与 stash 里保存的config.yaml的“基线”(即创建 stash 时的 HEAD 版本)相比,已经发生了变化。而 stash 里的修改,又恰好是针对那个“基线”的。这就形成了经典的三路合并冲突:BASE(stash 创建时的版本)、HEAD(你当前的版本)、STASH(你 stash 的修改)。
解决方案(按推荐顺序):
- 手动解决(最常用):
- 用编辑器打开冲突文件,找到
<<<<<<<和>>>>>>>之间的区域。 - 保留你需要的部分,删除冲突标记。
git add <file>标记为已解决。git stash drop stash@{0}(因为pop失败,stash 还在栈里,需要手动清理)。
- 用编辑器打开冲突文件,找到
- 放弃本次 pop,换用
stash branch(最安全):git stash branch recovery-branch stash@{0}- 进入
recovery-branch,git rebase main,在 rebase 过程中逐一解决冲突。这给了你最大的控制权和可追溯性。
- 强制覆盖,丢弃当前更改(仅限你确定当前工作区不重要):
git stash pop --force。这会强制用 stash 的内容覆盖当前工作区,当前工作区的修改将永久丢失。慎用!
实操心得:我遇到冲突的第一反应,永远是
git stash branch。因为它把一个“破坏性”的、可能丢失数据的操作,转化成了一个“建设性”的、完全可控的分支操作。即使你最终发现这个分支也没用,git branch -D recovery-branch也比git reset --hard安全得多。
4.3 问题三:git stash后,untracked 文件还在 —— 你忘了加-u
现象:git stash执行后,git status显示working tree clean,但你新建的test_new.py文件依然躺在目录里。
原因:这是git stash的默认行为,如前所述,它只处理 tracked 文件。test_new.py是 untracked 的,所以被忽略。
解决方案:
- 下次 stash 时,务必加上
-u:git stash push -u -m "message"。 - 如果已经 stash 了,而文件还在,你可以:
git add test_new.py将其变为 tracked。git stash(现在它会被包含)。git restore --staged test_new.py(如果不想让它进入暂存区)。
4.4 问题四:stash 列表太长,找不到想要的那个 —— 用好搜索与过滤
现象:git stash list输出几十行,全是WIP on ...,你花了五分钟才找到那个三天前的、关于“数据库连接池”的 stash。
解决方案:
- 按消息搜索:
git stash list | grep "database"。 - 按分支过滤:
git stash list --grep-ref="refs/heads/main"(只显示在main分支上创建的 stash)。 - 按日期范围:
git stash list --since="2 weeks ago"。 - 终极方案:养成命名习惯。这是成本最低、收益最高的预防措施。把
git stash push -m当成和git commit -m一样重要的仪式。
5. 高级技巧与经验法则:超越基础的生产力提升
5.1 创建自定义 stash 别名:告别冗长命令
Git 的别名(alias)是提升效率的利器。在你的~/.gitconfig文件中添加:
[alias] # 一个安全、带命名、包含 untracked 的 stash s = "!f() { git stash push -u -m \"$(date '+%Y-%m-%d %H:%M') - $*\"; }; f" # 一个快速查看 stash 内容的别名 ss = "!f() { git stash show -p stash@{${1:-0}}; }; f" # 一个安全的 pop,失败时自动创建分支 sp = "!f() { git stash pop || git stash branch auto-recovery-$(date '+%s'); }; f"配置后,你就可以用:
git s "refactor config"代替git stash push -u -m "refactor config"git ss 1查看stash@{1}的完整 diffgit sp安全地 pop,失败时自动创建一个带时间戳的恢复分支
这些别名是我每天必用的,它们把重复的、易错的命令,变成了一个单词,极大地降低了心智负担。
5.2 stash 与 CI/CD 的协同:在自动化流水线中“暂停”状态
在数据管道的 CI/CD 流水线中,git stash也有用武之地。例如,你的build脚本需要生成一个version.json文件,但这个文件不应该被提交。在pre-build阶段,你可以:
# 如果 version.json 存在,先 stash 它,避免干扰构建 if [ -f version.json ]; then git stash push -u -m "ci: stash version.json" version.json 2>/dev/null || true fi # ... 执行构建 ... # 构建完成后,恢复它 git stash pop 2>/dev/null || true这段脚本确保了version.json不会污染你的工作区,也不会被意外 commit。它利用了git stash的原子性和幂等性(|| true保证了即使 stash 不存在,脚本也能继续执行)。
5.3 经验法则:什么时候该用 stash,什么时候该用分支?
这是所有 Git 用户都会困惑的问题。我的答案是基于一个简单的判断矩阵:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 时间 < 1 小时,纯个人临时切换 | git stash | 快速、轻量、无需远程同步,符合“暂停-恢复”的直觉。 |
| 时间 > 1 天,或涉及多人协作 | 创建 WIP 分支(git checkout -b wip/feature-x) | 分支可以git push到远程,成为团队可见的、可讨论的、可 review 的工作单元。stash 是私有的、易丢失的。 |
| 需要在多个分支上反复应用同一组修改 | git stash apply | stash 是“状态”,分支是“历史”。状态可以被多次应用,历史只能被线性继承。 |
| 修改涉及大量重构,且当前分支已落后很多 | git stash branch | 它为你提供了一个完美的、与原始上下文隔离的沙盒,让你可以安全地进行 rebase 或 merge。 |
记住,git stash和git branch不是互斥的,而是互补的。stash branch这个命令,本身就是两者融合的最佳实践。
5.4 最后的忠告:stash 不是保险箱,而是速记本
我见过太多人把 stash 当成“万能保险箱”,里面塞满了几个月前的、早已过时的、连自己都记不清用途的修改。这不仅浪费磁盘空间,更严重的是,它会污染你的决策环境。当你面对一个紧急问题时,一个混乱的 stash 列表会让你犹豫、迟疑,最终可能做出错误的选择。
我的个人实践是:
- 每日清理:每天下班前,执行
git stash list