资讯动态

Git 第五章:stash 是一个不挂在分支上的提交

发布时间:2026/10/20 11:20:41 来源:尧图企业网站定制
第二章讲切换分支时列过一张表。手上有没有提交的改动时切换过去会不会把它们弄丢决定了 Git 是否允许切换。但这两种结果其实都不理想被拦下目标分支上的那个文件和当前这边不一样切换过去就必须覆盖掉手上的改动于是 Git 会报出would be overwritten哪里也去不了。被放行没有提交的改动不属于任何一个分支所以会原样跟到目标分支上。本来只想回到master看一眼干净的代码看到的却是一个混着半截改动的版本。这两种情况需要的是同一样东西那就是先把手上的改动收起来让工作区回到 HEAD 那个干净的状态等办完别的事情之后再把改动原样拿回来。这就是git stash的作用。stash 经常被翻译成「暂存」但它和第一章讲的暂存区也就是git add放进去的那个地方并不是一回事。Pro Git 中文版把它翻译成「贮藏」下面就直接写成 stash。「暂存」这个译名和暂存区撞车读中文教程时容易把两者当成一回事。一、收起来再拿回来这里接着使用上一章的那两条分支feature把端口改成了 8080而master把日志改成了 debug。现在位于feature上调整缓存容量改到一半需要先回master一趟gitswitch featuresed-is/容量 256MB/容量 512MB/config.txtgitswitch master# error: Your local changes to the following files would be overwritten by checkout:# config.txt# Please commit your changes or stash them before you switch branches.# Aborting代码 1config.txt 在两条分支上的内容不同切换过去就会覆盖掉这处改动退出码是 1。报错信息的最后一句已经给出了两条路要么提交要么使用 stash。由于这处改动还没有写完不值得为它专门占用一个提交gitstash# Saved working directory and index state WIP on feature: 58e596c feature: 端口换成 8080gitstatus# On branch feature# nothing to commit, working tree cleangitswitch master# Switched to branch master代码 2改动被收走之后工作区回到了 HEAD 的状态切换也就被放行了。WIP是 work in progress 的缩写意思是进行中的工作后面还记录着收起改动时位于哪一条分支以及 HEAD 是哪一个提交。在master上办完事情之后回到feature再把改动拿回来gitswitch featuregitstash pop# On branch feature# 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.txt## no changes added to commit (use git add and/or git commit -a)# Dropped refs/stash{0} (d0a1d536cfeb67ffd014ac358437d1d28ede3f47)代码 3改动回到了工作区。最后一行的意思是收起来的那一份已经被删除了。日常的用法就是这三步先执行stash再去别的地方处理事情最后执行stash pop。麻烦都出在细节上比如收走了哪些东西、拿回来的时候发生了什么、收了不止一份时怎么区分。只要先弄清楚收起来的东西存放在哪里这些问题就都很好回答了。二、收到哪去了一个不挂在分支上的提交再收一次这一回去.git目录里找找它gitstashgitstash list# stash{0}: WIP on feature: 58e596c feature: 端口换成 8080cat.git/refs/stash# d0a1d536cfeb67ffd014ac358437d1d28ede3f47代码 4.git/refs/下面多出了一个名叫 stash 的文件里面是一个 40 位的编号。第二章说过.git/refs/heads/下的每个文件就是一条分支文件里记录着一个提交号。refs/stash也是同一种文件只是它不在heads/下面。它记录的这个编号是什么呢可以问一问 Gitgitcat-file-pstash{0}# tree a3029c405f551ea74d69a4ef13ba6665f842f845# parent 58e596c61979111e271c03cbee27ff82cd121e2d# parent f3883158ec1ae81c6a27a391d191f72177f3b7df# author yourname youexample.com 1790685000 0800# committer yourname youexample.com 1790685000 0800## WIP on feature: 58e596c feature: 端口换成 8080代码 5有 tree、parent、作者和提交信息这就是一个提交。stash 收起来的东西其实就是一个提交。它和第三章讲的合并提交一样有两个父提交。第一个是58e596c也就是收起改动时 HEAD 所在的提交第二个是f388315这是 Git 顺便为暂存区拍下的另一个提交。gitlog--oneline--graph--all# * d0a1d53 WIP on feature: 58e596c feature: 端口换成 8080# |\# | * f388315 index on feature: 58e596c feature: 端口换成 8080# |/# * 58e596c feature: 端口换成 8080# | * 0bcc629 master: 日志级别调成 debug# |/# * d7aecee 初始配置代码 6--all会把refs/stash也算进去所以它出现在了图中。git stash输出的那句Saved working directory and index state说的正是这两个提交。index on feature记录的是暂存区的状态WIP on feature记录的是工作区的状态而且这两个提交都是从58e596c上长出来的。图 1feature和master都没有移动。收起来的两个提交只挂在refs/stash上从任何一条分支往回走都走不到它们。它和普通的提交只差一点那就是它不在任何分支上。git branch --contains stash{0}什么都不会打印不加--all的git log也看不到它。收起改动之后工作区之所以是干净的是因为 Git 把工作区和暂存区都恢复成了 HEAD 的样子而这时改动已经安全地保存在那个提交里了。执行pop取回改动之后这一份就会被删除。当最后一份也被取走时.git/refs/stash这个文件也会随之消失。三、收走了什么没收走什么现在在工作区里同时放上三样东西一处已经执行过add的改动一处还没有执行add的改动以及一个从来没有被 Git 跟踪过的新文件。sed-is/连接池 10/连接池 20/config.txtgitaddconfig.txtsed-is/容量 256MB/容量 512MB/config.txtprintf# 部署说明\n先改 config.txt再重启服务。\nREADME.mdgitstatus-s# MM config.txt# ?? README.md代码 7上一章出现过的那个MM再加上一个未跟踪的文件。gitstashgitstatus-s# ?? README.mdgitswitch mastergitstatus-s# ?? README.md代码 8README.md 没有被收走而且还跟着切换到了 master 上。git stash默认不会收起未跟踪的文件。这和上一章「diff 看不到新文件」是同一个道理这个文件既不在暂存区里也不在任何一个提交里Git 手里并没有它所以收起改动时也就不会处理它。它会继续留在工作区里并且按照第二章讲的规则跟着切换到了master上。如果想把它也一起收起来可以加上-u--include-untracked。这时收起来的提交会多出第三个父提交专门用来保存未跟踪的文件gitstash-ugitstatus-s# 什么都没有gitcat-file-pstash{0}|grepparent# parent 58e596c61979111e271c03cbee27ff82cd121e2d# parent 913bf98d40f555ef88803339649a9ebee82bb49a# parent c897ee15d17d317d464ba1ca11405816ca529df6gitlog-1--format%s stash{0}^3# untracked files on feature: 58e596c feature: 端口换成 8080代码 9^3指的是第三个父提交。如果还想收起被.gitignore忽略的文件就要改用-a。另一个细节出现在把改动拿回来的时候。回到代码 7 的状态只用git stash收起改动然后再执行popgitstash popgitstatus-s# M config.txt# ?? README.md代码 10原来是MM现在只剩下右边的一个M。连接池的那处改动本来已经执行过add但拿回来之后却变成了未暂存的状态。这是因为pop默认只会还原工作区的状态并不处理暂存区。暂存区的那一份明明保存在index on …那个提交里如果想让它也恢复回来就要加上--index坑pop 不还原暂存区所以 add 过的改动会退回未暂存想保留得加 --index。gitstash pop--indexgitstatus-s# MM config.txt# ?? README.md代码 11两列M都恢复回来了。四、pop 和 apply取出来之后留不留取回改动有两条命令手册对apply的说明只有一句话Like pop, but do not remove the state from the stash list.这两条命令做的是同一件事就是把收起来的改动放回当前的工作区区别只在于事后要不要删除那一份。pop会删除apply不会删除pop执行成功时就相当于先执行apply再执行drop。这两条命令都会把改动放到「当前所在的分支」上这一点值得单独测试一下。如果改动是在feature上收起来的拿到master上取出来会发生什么呢# 在 feature 上把容量改成 512MBgit stashgitswitch mastergitstash apply# Auto-merging config.txt# On branch master# 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.txt## no changes added to commit (use git add and/or git commit -a)gitstash list# stash{0}: WIP on feature: 58e596c feature: 端口换成 8080代码 12改动被取出来了而那一份仍然留在列表里。diff --git a/config.txt b/config.txt index 2f3869a..c681876 100644 --- a/config.txt b/config.txt -10,7 10,7 # 缓存 开启 是 -容量 256MB 容量 512MB 过期 600 # 邮件代码 13这时git diff的输出。head-4config.txt# # 服务配置# 端口 3000# 超时 30# 日志 debuggitlog--oneline-1# 0bcc629 master: 日志级别调成 debug代码 14端口仍然是 master 上的 3000也没有产生新的提交。跟过来的只有容量那一行。feature上那次「端口换成 8080」的提交并没有跟过来这是因为取出来的并不是feature分支而是收起改动的那一刻相对于当时 HEAD 的那一段改动。Git 在这里做的是一次和第三章一样的三方合并共同祖先是收起改动时的 HEAD58e596c一边是收起来的那一份另一边是当前的工作区。只有「收起来的那一份相对于祖先改了什么」才会被带过来而端口在祖先里就已经是 8080 了所以并不算作改动。图 2上面一行是收起改动时记录下来的那段改动下面一行是把同一段改动放进 master 的工作区。既然是三方合并就有可能发生冲突。这一回在feature上把日志改成warn并收起来然后到日志已经是debug的master上执行popgitstash pop# Auto-merging config.txt# CONFLICT (content): Merge conflict in config.txt# On branch master# Unmerged paths:# (use git restore --staged file... to unstage)# (use git add file... to mark resolution)# both modified: config.txt## no changes added to commit (use git add and/or git commit -a)# The stash entry is kept in case you need it again.代码 15退出码是 1。最后一句的意思是这一份没有被删除。# 服务配置 端口 3000 超时 30 Updated upstream 日志 debug 日志 warn Stashed changes代码 16两边的标签换成了Updated upstream当前这一边和Stashed changes收起来的那一份。索引里也照样有第三章讲过的那三个槽gitls-files-u# 100644 fc0ae28aef5b7cab0b78d0ed6435199c728d3cea 1 config.txt# 100644 2f3869a319d9a8ac9c458efb5e1d03b9f5c6f1fb 2 config.txt# 100644 843f84e6c91154df3ac49ec0dc2274856074f425 3 config.txt代码 171 号槽里的fc0ae28正是58e596c里的 config.txt也就是上一章代码 15 中 feature 那一边的编号。由此可以印证上面说的那一点共同祖先就是收起改动时的那个 HEAD。pop遇到冲突时并不会删除那一份因为它不知道冲突最终会怎样解决所以留着以防万一。冲突解决之后需要自己执行git stash drop。解决完冲突的文件可以执行git add但这样它就会进入暂存区如果只想让它变回普通的未暂存改动可以使用git restore --staged config.txt也就是git status提示里的那一条命令。如果取出改动时冲突太多而又确实想在收起改动的那个位置上继续工作还有一条路可走那就是git stash branch 新分支名。它会从收起改动时的那个 HEAD58e596c拉出一条新分支在新分支上取出改动成功之后再删除那一份gitstash branch log-level# Switched to a new branch log-level# ……# Dropped refs/stash{0} (a89b078b52c1954d8aef9231516f47f1bd755ab9)gitlog--oneline-1# 58e596c feature: 端口换成 8080代码 18改动回到了它当初产生的地方共同祖先和当前是同一个提交所以不会发生冲突。五、收了不止一份stash 可以一直收下去。连续收两次的结果如下sed-is/容量 256MB/容量 512MB/config.txtgitstashsed-is/连接池 10/连接池 20/config.txtgitstashgitstash list# stash{0}: WIP on feature: 58e596c feature: 端口换成 8080# stash{1}: WIP on feature: 58e596c feature: 端口换成 8080代码 19两份的说明一字不差。在同一条分支、同一个 HEAD 上收起来的改动默认的说明是一样的过几天就分不清哪一份是哪一份了。所以最好在收起改动时写上说明gitstash push-m过期改成 300 试试gitstash list# stash{0}: On feature: 过期改成 300 试试# stash{1}: WIP on feature: 58e596c feature: 端口换成 8080# stash{2}: WIP on feature: 58e596c feature: 端口换成 8080代码 20git stash push是git stash的完整写法。还有一种旧写法git stash save 说明它的效果看起来一样但手册已经把它标记为弃用了原话是This option is deprecated in favour of git stash push。两者的区别在于后面跟着的参数save会把所有参数拼接成说明而push会把参数当成文件路径只收起这几个文件。所以git stash save config.txt收走的是全部改动说明叫作「config.txt」而git stash push config.txt只收起 config.txt 这一个文件。stash{0}永远是最新的那一份越早收起的改动编号就越大。每当新收起一份旧的编号就会全部往后挪一格所以代码 19 里的stash{0}在代码 20 里变成了stash{1}。如果想查看某一份里有什么内容可以用git stash show -p stash{1}打印它的改动不加-p时只会给出文件的统计信息。如果想取出或者丢弃指定的某一份就把编号接在apply、pop或drop的后面gitstash drop stash{1}# Dropped stash{1} (652f449ced33ef83b1db788984a2bcb42bc96226)gitstash list# stash{0}: On feature: 过期改成 300 试试# stash{1}: WIP on feature: 58e596c feature: 端口换成 8080代码 21删除了中间那一份之后原来的stash{2}顶了上来变成了stash{1}。编号之所以会变是因为它从来都不是保存在某个地方的固定编号。Git 会为每个引用记录一本流水账reflog记下它先后指向过哪些提交。stash{n}就是这本账的写法意思是「refs/stash往前数第 n 次的值」。每收起一次改动refs/stash就会改为指向新的提交账本.git/logs/refs/stash里也会多出一行cut-f2.git/logs/refs/stash# WIP on feature: 58e596c feature: 端口换成 8080# On feature: 过期改成 300 试试代码 22最后一行是最新的也就是stash{0}而drop就是从这里删除一行。所以在按照编号进行操作之前最好先执行git stash list看一眼。上次记下的编号只要中间收起或者删除过一次就不再指向同一份内容了。git stash clear会把整个列表清空。它和drop一样执行之前都不会询问确认但drop至少会打印出被删除那一份的提交号。那个提交本身仍然保存在对象库里执行git stash apply 652f449照样能把它取回来。而clear连提交号都不会打印手册的原话是清除掉的东西may be impossible to recover。clear 不打印提交号但对象仍在库里只要记得哈希apply 还是能捞回来。小结命令它做的事git stashgit stash push把暂存区和工作区各拍成一个提交挂在refs/stash上再把两处刷回 HEADgit stash -u/-a连未跟踪的文件一起收 / 连被忽略的文件也收git stash push -m 说明带着说明收save是已弃用的旧写法git stash list列出refs/stash的历次取值stash{0}最新git stash show -p [stash{n}]看某一份改了什么git stash apply [stash{n}]把那一份的改动三方合并进当前工作区那一份留着git stash pop [stash{n}]同上成功后删掉那一份冲突时不删pop/apply加--index连暂存区一起还原git stash drop [stash{n}]删掉一份后面的编号往前补git stash clear全部删掉基本找不回git stash branch 名从收的时候那个提交拉一条新分支在上面取出来这一章的规则同样不需要死记。stash 收起来的就是提交只是它没有挂在任何分支上。所以未跟踪的文件默认不会被收起因为它本来就不在 Git 手里所以取出改动是一次三方合并在哪条分支上取出都可以但带过去的只是那一段改动而不是分支上的历史所以取出时可能会发生冲突而发生冲突时那一份会被保留下来所以编号会发生变化因为它是refs/stash的流水账而不是一个固定的名字。无论是收起改动还是把它拿回来都还是在往前推进。下一章换一个方向讲已经做过的事情比如改坏了的文件、提交错了的提交应该怎样往回退。checkout、restore、reset和revert这四条命令都叫作撤销但它们撤销的并不是同一样东西。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑