9.3 探秘git stash

了解一下git stash的机理会有几个好处:当保存了多个进度的时候知道从哪个进度恢复;综合运用前面介绍的Git知识点;了解Git的源码,Git将不再神秘。

在执行git stash命令时,Git实际调用了一个脚本文件实现相关的功能,这个脚本的文件名就是git-stash。看看git-stash安装在哪里了。


$git—exec-path

/usr/lib/git-core


如果查看一下这个目录,您会震惊的。


$ls/usr/lib/git-core/

git git-help git-reflog

git-add git-http-backend git-relink

git-add—interactive git-http-fetch git-remote

git-am git-http-push git-remote-ftp

git-annotate git-imap-send git-remote-ftps

git-apply git-index-pack git-remote-http

…省略40余行…


实际上在1.5.4之前的版本中,Git会将这样的一百多个以git-<cmd>格式命名的程序安装到可执行路径中,而这样做的唯一好处就是不用借助任何扩展机制就可以实现命令行补齐:即键入git-后,连续两次键入<Tab>键,就可以把这一百多个命令显示出来。这种方式随着Git子命令的增加显得越来越混乱,因此从1.5.4版本开始,不再提供git-<cmd>格式的命令,而是用唯一的git命令。而之前的名为git-<cmd>的子命令则保存在非可执行目录下,由Git负责加载。

在后面的章节中偶尔会看到形如git-<cmd>字样的名称,以及同时存在的git<cmd>命令。可以这样理解:git-<cmd>作为软件本身的名称,而其命令行为git<cmd>。

最初很多Git命令都是用Shell或Perl脚本语言开发的,在Git的发展中一些对运行效率要求高的命令用C语言改写。而git-stash(至少在Git 1.7.4版本)还是使用Shell脚本开发的,研究它比研究用C写的命令要简单得多。


$file/usr/lib/git-core/git-stash

/usr/lib/git-core/git-stash:POSIX shell script text executable


解析git-stash脚本会比较枯燥,还是通过运行一些示例来理解更好一些。

当前的进度保存列表是空的。


$git stash list


下面在工作区中做一些改动。


$echo Bye-Bye.>>welcome.txt

$echo hello.>hack-1.txt

$git add hack-1.txt

$git status-s

A hack-1.txt

M welcome.txt


可见暂存区中已经添加了新增的hack-1.txt,修改过的welcome.txt并未添加到暂存区。执行git stash保存一下工作进度。


$git stash save "hack-1:hacked welcome.txt,newfile hack-1.txt"

Saved working directory and index state On master:hack-1:hacked welcome.txt,newfile hack-1.txt

HEAD is now at 2b31c19 Merge commit 'acc2f69'


工作区恢复了修改前的原貌(实际上用了git reset—hard HEAD命令),文件welcome.txt的修改不见了,文件hack-1.txt整个都不见了。


$git status-s

$ls

detached-commit.txt new-commit.txt welcome.txt


再做一个修改,并尝试保存进度。


$echo fix.>hack-2.txt

$git stash

No local changes to save


进度保存失败!可见本地没有被版本控制系统跟踪的文件并不能保存进度。因此本地新文件需要先执行添加操作,然后再执行git stash命令。


$git add hack-2.txt

$git stash

Saved working directory and index state WIP on master:2b31c19 Merge commit

'acc2f69'

HEAD is now at 2b31c19 Merge commit 'acc2f69'


不用看就知道工作区再次恢复原状。如果这时执行git stash list会看到有两次进度保存。


$git stash list

stash@{0}:WIP on master:2b31c19 Merge commit 'acc2f69'

stash@{1}:On master:hack-1:hacked welcome.txt,newfile hack-1.txt


从上面的输出中可以得出两个结论:

在用git stash命令保存进度时,如果提供说明则更容易通过进度列表找到保存的进度。

每个进度的标识都是stash@{<n>}格式,像极了前面介绍的reflog的格式。

实际上,git stash就是用前面介绍的引用和引用变更日志(reflog)来实现的。


$ls-l.git/refs/stash.git/logs/refs/stash

-rw-r—r—1 jiangxin jiangxin 364 Dec 6 16:11.git/logs/refs/stash

-rw-r—r—1 jiangxin jiangxin 41 Dec 6 16:11.git/refs/stash


那么在“第7章Git重置”一章中学习的reflog可以派上用场了。


$git reflog show refs/stash

e5c0cdc refs/stash@{0}:WIP on master:2b31c19 Merge commit 'acc2f69'

6cec9db refs/stash@{1}:On master:hack-1:hacked welcome.txt,newfile hack-1.txt


对照git reflog的结果和前面git stash list的结果,可以肯定用git stash保存进度,实际上会将进度保存在引用refs/stash所指向的提交中。多次的进度保存,实际上相当于引用refs/stash一次又一次的变化,而refs/stash引用的变化由reflog(即.git/logs/refs/stash)所记录下来。这个实现是多么简单而巧妙啊。

一个新的疑问又出现了,如何在引用refs/stash中同时保存暂存区的进度和工作区中的进度呢?查看一下引用refs/stash的提交历史能够看出端倪。


$git log—graph—pretty=raw refs/stash-2

*commit e5c0cdc2dedc3e50e6b72a683d928e19a1d9de48

|\tree 780c22449b7ff67e2820e09a6332c360ddc80578

||parent 2b31c199d5b81099d2ecd91619027ab63e8974ef

||parent c5edbdcc90addb06577ff60f644acd1542369194

||author Jiang Xin<jiangxin@ossxp.com>1291623066+0800

||committer Jiang Xin<jiangxin@ossxp.com>1291623066+0800

||

||WIP on master:2b31c19 Merge commit 'acc2f69'

||

|*commit c5edbdcc90addb06577ff60f644acd1542369194

|/tree 780c22449b7ff67e2820e09a6332c360ddc80578

|parent 2b31c199d5b81099d2ecd91619027ab63e8974ef

|author Jiang Xin<jiangxin@ossxp.com>1291623066+0800

|committer Jiang Xin<jiangxin@ossxp.com>1291623066+0800

|

|index on master:2b31c19 Merge commit 'acc2f69'


从提交历史中可以看到进度保存的最新提交是一个合并提交。最新的提交说明中有WIP字样(是Work In Progess的简称),代表了工作区进度。而最新提交的第二个父提交(提交历史中显示为第二个提交)有index on master字样,说明这个提交代表着暂存区的进度。

但是提交历史中的两个提交都指向了同一个树——tree 780c224……,这是因为最后一次做进度保存时工作区相对暂存区没有改变,这使得工作区和暂存区在引用refs/stash中的存储变得有些扑朔迷离。别忘了第一次进度保存工作区、暂存区和版本库都是不同的,可以用于验证关于refs/stash实现机制的判断。

第一次进度保存可以使用reflog中的语法,即用refs/stash@{1}来访问,也可以用简称stash@{1}。下面就来研究一下第一次的进度保存。


$git log—graph—pretty=raw stash@{1}-3

*commit 6cec9db44af38d01abe7b5025a5190c56fd0cf49

|\tree 7250f186c6aa3e2d1456d7fa915e529601f21d71

||parent 2b31c199d5b81099d2ecd91619027ab63e8974ef

||parent 4560d76c19112868a6a5692bf9379de09c0452b7

||author Jiang Xin<jiangxin@ossxp.com>1291622767+0800

||committer Jiang Xin<jiangxin@ossxp.com>1291622767+0800

||

||On master:hack-1:hacked welcome.txt,newfile hack-1.txt

||

|*commit 4560d76c19112868a6a5692bf9379de09c0452b7

|/tree 5d4dd328187e119448c9171f99cf2e507e91a6c6

|parent 2b31c199d5b81099d2ecd91619027ab63e8974ef

|author Jiang Xin<jiangxin@ossxp.com>1291622767+0800

|committer Jiang Xin<jiangxin@ossxp.com>1291622767+0800

|

|index on master:2b31c19 Merge commit 'acc2f69'

|

*commit 2b31c199d5b81099d2ecd91619027ab63e8974ef

|\tree ab676f92936000457b01507e04f4058e855d4df0

||parent 4902dc375672fbf52a226e0354100b75d4fe31e3

||parent acc2f69cf6f0ae346732382c819080df75bb2191

||author Jiang Xin<jiangxin@ossxp.com>1291535485+0800

||committer Jiang Xin<jiangxin@ossxp.com>1291535485+0800

||

||Merge commit 'acc2f69'


果然上面显示的三个提交对应的三棵树各不相同。查看一下差异。用“原基线”代表进度保存时版本库的状态,即提交2b31c199;用“原暂存区”代表进度保存时暂存区的状态,即提交4560d76;用“原工作区”代表进度保存时工作区的状态,即提交6cec9db。

原基线和原暂存区的差异比较。


$git diff stash@{1}^2^stash@{1}^2

diff—git a/hack-1.txt b/hack-1.txt

new file mode 100644

index 0000000..25735f5

—-/dev/null

+++b/hack-1.txt

@@-0,0+1@@

+hello.


原暂存区和原工作区的差异比较。


$git diff stash@{1}^2 stash@{1}

diff—git a/welcome.txt b/welcome.txt

index fd3c069..51dbfd2 100644

—-a/welcome.txt

+++b/welcome.txt

@@-1,2+1,3@@

Hello.

Nice to meet you.

+Bye-Bye.


原基线和原工作区的差异比较。


$git diff stash@{1}^1 stash@{1}

diff—git a/hack-1.txt b/hack-1.txt

new file mode 100644

index 0000000..25735f5

—-/dev/null

+++b/hack-1.txt

@@-0,0+1@@

+hello.

diff—git a/welcome.txt b/welcome.txt

index fd3c069..51dbfd2 100644

—-a/welcome.txt

+++b/welcome.txt

@@-1,2+1,3@@

Hello.

Nice to meet you.

+Bye-Bye.


用stash@{1}来恢复进度。


$git stash apply stash@{1}

On branch master

Changes to be committed:

(use "git reset HEAD<file>…"to unstage)

#

new file:hack-1.txt

#

Changes not staged for commit:

(use "git add<file>…"to update what will be committed)

(use "git checkout—<file>…"to discard changes in working directory)

#

modified:welcome.txt

#


显示进度列表,然后删除进度列表。


$git stash list

stash@{0}:WIP on master:2b31c19 Merge commit 'acc2f69'

stash@{1}:On master:hack-1:hacked welcome.txt,newfile hack-1.txt

$git stash clear


删除进度列表之后,会发现stash相关的引用和reflog都不见了。


$ls-l.git/refs/stash.git/logs/refs/stash

ls:cannot access.git/refs/stash:No such file or directory

ls:cannot access.git/logs/refs/stash:No such file or directory


通过上面的这些分析,有一定Shell编程基础的读者就可以尝试研究git-stash的代码了,在研究过程中你可能会有新的发现。