背景
git相对svn有许多好的设计,其中一个就是git stash功能。许多教程在介绍git stash的使用场景时,经常举例是:当你开发着新功能时,写到一半时(既没办法提交,又舍不得撤销),突然报告了一个BUG,你必须立马FIX这个BUG。
如何理解这个应用场景?
基础知识
首先得对git有基础知识,如下图所示:
git 是分3个区的,分别是:
- 工作区(working dir): 简单说就是我们看得见摸得着的目录和文件。
- 索引区(index/stage): 又叫
暂存区
,是被git管理(暂存)了,但尚未提交的。 - 版本区(repository): 就是我们常说的版本,仓库。
状态变迁是这样的:
-
Untracked
:当我们新建一个文件a.txt,并且编辑内容时,这个a.txt仅仅处于工作区
,状态是untracked
或叫unstaged
,这个状态是不被git管理的,只被OS的文件系统管理。 -
Staged
: 我们执行git add a.txt
后,文件才进入索引区
,状态是staged
,开始被git管理。 -
Committed
: 再执行git commit a.txt -m 'add a.txt'
,状态是committed
,纳入仓库。
可见git跟svn不同,git的提交,是要经历两个阶段的,它不会直接从工作区跳跃到版本区,中间隔着索引区/暂存区。
git stash 内涵
有了上述分区概念后,我们用个示意图来表达下 git stash 具体做了什么:
重点关注左半部分用橙色标注的部分,注意编号1、2、…、n和n+1。
这些步骤只有两个操作:
- 一个是
git stash
,表示把索引区的内容转存到stash栈里面,同时工作区跟索引区保持一致(实际上工作区中的untracked的内容依然存在,不会被清除)。 - 另一个是
git stash pop
,表示把转存到stash的弹回索引区。
实验实战
构建实验场景
笔者在做代码教程的时候,常希望利用git,把代码演变过程记录下来。比如我写了代码a.txt,演示了功能点a,并打tag为demo-a
;接着写了代码b.txt,演示了功能点b,并打tag为demo-b
。以便读者可以git checkout demo-a
或git checkout demo-b
来重现笔者当时的代码场景,而不用一上来就看到太多无关的代码。
但是,笔者时常一不小心就写完了a.txt和b.txt,但是却迟迟没有提交,现在笔者想分开提交,先提交a.txt,再提交b.txt。同时,每次提交后,我都必须跑一边单元测试,以便验证正确,如果不正确应该回退这个提交,调整代码后,再提交。
问题来了:在已经提交a.txt,尚未提交b.txt的时候,为了排除b.txt是否对a.txt单元测试有干扰,必须把b.txt从工作区删了,但之后提交b.txt的时候,又得拿回来。
如何用 git stash 解决?
实验操作过程
- 编写文件 a.txt和b.txt
➜ GitTutorial echo "aaa" > a.txt
➜ GitTutorial echo "bbb" > b.txt
➜ GitTutorial ls
a.txt b.txt
➜ GitTutorial
- 借助git管理
➜ GitTutorial git init
Initialized empty Git repository in workspace/GitTutorial/.git/
- 提交 a.txt
➜ GitTutorial git:(master) ✗ git add a.txt
➜ GitTutorial git:(master) ✗ git status
On branch master
Initial commit
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: a.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
b.txt
➜ GitTutorial git:(master) ✗ git commit a.txt -m 'add a.txt'
[master (root-commit) 9264345] add a.txt
1 file changed, 1 insertion(+)
create mode 100644 a.txt
➜ GitTutorial git:(master) ✗ git status
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)
b.txt
nothing added to commit but untracked files present (use "git add" to track)
➜ GitTutorial git:(master) ✗
上述代码,提交a.txt,需要两个步骤,git add a.txt
和git commit a.txt -m 'add a.txt'
,每次操作后,用git status
命令,验证下a.txt经历了从untracked
状态到staged (to be committed)
状态,再到纳入版本库的变迁过程。
a.txt提交完后,当前情况是:a.txt已经纳入版本仓库,b.txt依然是untracked状态。
用Eclipse Git 可视化看下各区的情况:(Project -> Team -> Commit ... -> Open Git Staging view)
- 给a.txt提交打tag,标记为demo-a
➜ GitTutorial git:(master) ✗ git tag demo-a
➜ GitTutorial git:(master) ✗ git tag
demo-a
➜ GitTutorial git:(master) ✗
- 暂时“删除”b.txt
为了对a.txt进行单元测试,排除b.txt对它可能产生的干扰,需要暂时“删除”b.txt,让工作区只剩下a.txt。执行 git stash
,结果:
➜ GitTutorial git:(master) ✗ git stash
No local changes to save
➜ GitTutorial git:(master) ✗
提示的是
No local changes to save
OMG 怎么回事?
因为b.txt是untracked状态,并没进入索引区,git stash
是把索引区转存起来。所以我们需要:
- 把工作区所有内容先纳入索引区;
- 然后把索引区转存到stash里面。
➜ GitTutorial git:(master) ✗ git add *
执行git stash
➜ GitTutorial git:(master) ✗ git stash
Saved working directory and index state WIP on master: 9264345 add a.txt
HEAD is now at 9264345 add a.txt
➜ GitTutorial git:(master) ls
a.txt
此时工作区只有 a.txt了,b.txt暂时消失了。可以跑只有a.txt的单测了。
可以用命令 git stash list
查看stash栈的情况。
- 恢复b.txt到工作区
➜ GitTutorial git:(master) git stash pop
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: b.txt
Dropped refs/stash@{0} (a6325932f00d22c978617102d9fe6b10b9605e79)
➜ GitTutorial git:(master) ✗ ls
a.txt b.txt
➜ GitTutorial git:(master) ✗
执行 git stash pop
后,b.txt回到工作目录了。
- 提交b.txt,并打标签demo-b
➜ GitTutorial git:(master) ✗ git commit * -m 'add b.txt'
[master ec78d66] add b.txt
1 file changed, 1 insertion(+)
create mode 100644 b.txt
➜ GitTutorial git:(master) git tag demo-b
➜ GitTutorial git:(master) git tag
demo-a
demo-b
➜ GitTutorial git:(master) git log
- 读者如果想看demo-a怎么办?
执行 git checkout demo-a
➜ GitTutorial git:(master) ls
a.txt b.txt
➜ GitTutorial git:(master) git checkout demo-a
Note: checking out 'demo-a'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b new_branch_name
HEAD is now at 9264345... add a.txt
➜ GitTutorial git:(9264345) ls
a.txt
➜ GitTutorial git:(9264345)
注意:git报了一个警告,叫detached HEAD
这是什么意思呢? 请听下回讲解。