有没有遇过这种情境,某个系统开发写到一半,结果被老板或客户「插单」,被要求紧急修正一个现有系统的 Bug 或添加一个功能,眼前的程式即将完成,老板的「急件」又不能拖,一个未完成的软体开发状态外加紧急调整的需求,这简直是软体品质的一大考验。如果你有这种困扰,那麽 Git 可以漂亮的帮你完成任务。
我们知道使用 Git 版控的时候,有区分「工作目录」与「索引」。工作目录裡面会有你改到一半还没改完的档案(尚未加入索引),也有新增档案但还没加入的档案(尚未加入索引)。而放在索引的资料,则是你打算透过 git commit
建立版本 (建立 commit 物件) 的内容。
当你功能开发到一半,被紧急插单一定手忙脚乱,尤其是手边正改写到一半的那些程式码不知该如何是好。在 Git 裡有个 git stash
指令,可以自动帮你把改写到一半的那些档案建立一个「特殊的版本」(也是一个 commit 物件),我们称这些版本为 stash 版本,或你可以直接称他为「暂存版」。
我们手边改到一半的档案,可能会有以下状态:
若要将这些开发到一半的档案建立一个「暂存版」,你有两个选择:
git stash
会将所有已列入追踪(tracked)的档案建立暂存版git stash -u
会包括所有已追踪或未追踪的档案,全部都建立成暂存版注: git stash
也可以写成 git stash save
,两个指令的结果是一样的,只是 save
参数可以忽略不打而已。
我们来看看一个简单的例子。我们先透过以下指令快速建立一个拥有两个版本的 Git 储存库与工作目录:
mkdir git-stash-demo
cd git-stash-demo
git init
echo. > a.txt
git add .
git commit -m "Initial commit"
echo 1 > a.txt
git add .
git commit -m "a.txt: set 1 as content"
目前的「工作目录」是乾淨的,没有任何更新到一半的档案:
C:\git-stash-demo>git log
commit 95eff6b19a9494667985ed5da37427bb08b8cdd7
Author: Will <doggy.huang@gmail.com>
Date: Fri Oct 11 08:17:15 2013 +0800
a.txt: set 1 as content
commit 346fadefdd6ed2c562201b5ca37d1e4d26b26d54
Author: Will <doggy.huang@gmail.com>
Date: Fri Oct 11 08:17:14 2013 +0800
Initial commit
C:\git-stash-demo>git status
# On branch master
nothing to commit, working directory clean
C:\git-stash-demo>dir
磁碟区 C 中的磁碟是 System
磁碟区序号: 361C-6BD6
C:\git-stash-demo 的目录
2013/10/11 上午 08:17 <DIR> .
2013/10/11 上午 08:17 <DIR> ..
2013/10/11 上午 08:17 4 a.txt
1 个档案 4 位元组
2 个目录 9,800,470,528 位元组可用
接著我新增一个 b.txt,再将 a.txt 的内容改成 2,如下:
C:\git-stash-demo>type a.txt
1
C:\git-stash-demo>echo 2 > a.txt
C:\git-stash-demo>type a.txt
2
C:\git-stash-demo>echo TEST > b.txt
C:\git-stash-demo>dir
磁碟区 C 中的磁碟是 System
磁碟区序号: 361C-6BD6
C:\git-stash-demo 的目录
2013/10/11 上午 08:55 <DIR> .
2013/10/11 上午 08:55 <DIR> ..
2013/10/11 上午 08:54 4 a.txt
2013/10/11 上午 08:55 7 b.txt
2 个档案 11 位元组
2 个目录 9,704,288,256 位元组可用
C:\git-stash-demo>git status
# On branch master
# 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: a.txt
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# b.txt
no changes added to commit (use "git add" and/or "git commit -a")
现在我们用 git status
得出我们有两个档案有变更,一个是 a.txt 处于 "not staged" 状态,而 b.txt 则是 "untracked" 状态。
这时,我们利用 git stash -u
即可将目前这些变更全部储存起来 (包含 untracked 档案),储存完毕后,这些变更全部都会被重置,新增的档案会被删除、修改的档案会被还原、删除的档案会被加回去,让我们目前在工作目录中所做的变更全部回复到 HEAD 状态。这就是 Stash 帮我们做的事。如下所示:
C:\git-stash-demo>git stash -u
Saved working directory and index state WIP on master: 95eff6b a.txt: set 1 as c
ontent
HEAD is now at 95eff6b a.txt: set 1 as content
C:\git-stash-demo>git status
# On branch master
nothing to commit, working directory clean
在建立完成「暂存版」之后,Git 会顺便帮我们建立一个暂存版的「参考名称」,而且是「一般参考」,在 .git\refs\stash
储存的是一个 commit 物件的「绝对名称」:
C:\git-stash-demo>dir .git\refs\
磁碟区 C 中的磁碟是 System
磁碟区序号: 361C-6BD6
C:\git-stash-demo\.git\refs 的目录
2013/10/11 上午 08:57 <DIR> .
2013/10/11 上午 08:57 <DIR> ..
2013/10/11 上午 08:57 <DIR> heads
2013/10/11 上午 08:57 41 stash
2013/10/11 上午 08:17 <DIR> tags
1 个档案 41 位元组
4 个目录 9,701,650,432 位元组可用
我们用 git cat-file -p stash
即可查出该物件的内容,这时你可以发现它其实就是个具有三个 parent (上层 commit 物件) 的 commit 物件:
C:\git-stash-demo>git cat-file -p stash
tree 86cf41ab650d8d0ce5fdd003bb7b722a917438a2
parent 95eff6b19a9494667985ed5da37427bb08b8cdd7
parent b79c4650e72ad4627d691a2d6cfb192626e24e94
parent 9b4e4a100776783dc76d16c3872235e6314d15e3
author Will <doggy.huang@gmail.com> 1381453062 +0800
committer Will <doggy.huang@gmail.com> 1381453062 +0800
WIP on master: 95eff6b a.txt: set 1 as content
有三个 parent commit 物件的意义就在于,这个特殊的暂存版是从另外三个版本合併进来的,然而这三个版本的内容,我们一样可以透过相同的指令显示其内容:
C:\git-stash-demo>git cat-file -p 95ef
tree eba2ef4205738a5015fc47d9cfe634d7d5eae466
parent 346fadefdd6ed2c562201b5ca37d1e4d26b26d54
author Will <doggy.huang@gmail.com> 1381450635 +0800
committer Will <doggy.huang@gmail.com> 1381450635 +0800
a.txt: set 1 as content
C:\git-stash-demo>git cat-file -p b79c
tree eba2ef4205738a5015fc47d9cfe634d7d5eae466
parent 95eff6b19a9494667985ed5da37427bb08b8cdd7
author Will <doggy.huang@gmail.com> 1381453061 +0800
committer Will <doggy.huang@gmail.com> 1381453061 +0800
index on master: 95eff6b a.txt: set 1 as content
C:\git-stash-demo>git cat-file -p 9b4e
tree b583bfe854b66756dd0f8ee96cab0c898193b5fd
author Will <doggy.huang@gmail.com> 1381453062 +0800
committer Will <doggy.huang@gmail.com> 1381453062 +0800
untracked files on master: 95eff6b a.txt: set 1 as content
从上述执行结果你应该可以从「讯息纪录」的地方清楚看出这三个版本分别代表那些内容:
也就是说,他把「原本工作目录的 HEAD 版本」先建立两个暂时的分支,这两个分支分别就是「原本工作目录裡所有追踪中的内容」与「原本工作目录裡所有未追踪的内容」之用,并在个别分支建立了一个版本以产生 commit 物件并且给予预设的 log 内容。最后把这三个分支,合併到一个「参照名称」为 stash
的版本 (这也是个 commit 物件)。不仅如此,他还把整个「工作目录」强迫重置为 HEAD 版本,把这些变更与新增的档案都给还原,多的档案也会被移除。
由于「工作目录」已经被重置,所以变更都储存到 stash
这裡,哪天如果你想要把这个暂存档案取回,就可以透过 git stash pop
重新「合併」回来。如下所示:
C:\git-stash-demo>git status
# On branch master
nothing to commit, working directory clean
C:\git-stash-demo>git stash pop
# On branch master
# 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: a.txt
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# b.txt
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (0e5b72c96fcf693e0402c40cd58f980bb3ff7efd)
执行完毕后,所有当初的工作目录状态与索引状态都会被还原。事实上 Git 骨子裡是透过「合併」的功能把这个名为 stash
的版本给合併回目前分支而已。最后,它还会自动将这个 stash
分支给删除,所以称它为【暂存版】非常贴切!
Git 的 stash 暂存版可以不只一份,你也可以建立多份暂存档,以供后续使用。不过,在正常的开发情境下,通常不会有太多暂存版才对,会有这种情况发生,主要有两种可能:
我们延续上一个例子,目前工作目录的状态应该是有两个档案有变化,我们用 git status -s
取得工作目录的状态(其中 -s
代表显示精简版的状态):
C:\git-stash-demo>git status -s
M a.txt
?? b.txt
现在,我们先建立第一个 stash 暂存版:
C:\git-stash-demo>git stash save -u
Saved working directory and index state WIP on master: 95eff6b a.txt: set 1 as content
HEAD is now at 95eff6b a.txt: set 1 as content
然后透过 git stash list
列出目前所有的 stash 清单,目前仅一份暂存版:
C:\git-stash-demo>git stash list
stash@{0}: WIP on master: 95eff6b a.txt: set 1 as content
而且你可以看到建立暂存版之后,工作目录是乾淨的。此时我们在建立另一个 new.txt
档案,并且再次建立暂存版:
C:\git-stash-demo>git status -s
C:\git-stash-demo>echo 1 > new.txt
C:\git-stash-demo>git status -s
?? new.txt
C:\git-stash-demo>git stash save -u
Saved working directory and index state WIP on master: 95eff6b a.txt: set 1 as c
ontent
HEAD is now at 95eff6b a.txt: set 1 as content
我们在再一次 git stash list
就可以看到目前有两个版本:
C:\git-stash-demo>git stash list
stash@{0}: WIP on master: 95eff6b a.txt: set 1 as content
stash@{1}: WIP on master: 95eff6b a.txt: set 1 as content
你应该会很纳闷,都没有自订的注解,过了几天不就忘记这两个暂存档各自的修改项目吗?没错,所以你可以自订「暂存版」的纪录讯息。我们透过 git stash save -u <message>
指令,就可自订暂存版的注解:
C:\git-stash-demo>git stash -h
usage: git core\git-stash list [<options>]
or: git core\git-stash show [<stash>]
or: git core\git-stash drop [-q|--quiet] [<stash>]
or: git core\git-stash ( pop | apply ) [--index] [-q|--quiet] [<stash>]
or: git core\git-stash branch <branchname> [<stash>]
or: git core\git-stash [save [--patch] [-k|--[no-]keep-index] [-q|--quiet]
[-u|--include-untracked] [-a|--all] [<message>]]
or: git core\git-stash clear
C:\git-stash-demo>git stash pop
Already up-to-date!
# On branch master
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# new.txt
nothing added to commit but untracked files present (use "git add" to track)
Dropped refs/stash@{0} (5800f37937aea5fb6a1aba0d5a1598a940e70c96)
C:\git-stash-demo>git stash save -u "新增 new.txt 档案"
Saved working directory and index state On master: 新增 new.txt 档案
HEAD is now at 95eff6b a.txt: set 1 as content
C:\git-stash-demo>git stash list
stash@{0}: On master: 新增 new.txt 档案
stash@{1}: WIP on master: 95eff6b a.txt: set 1 as content
这时,如果你直接执行 git stash pop
的话,他会取回最近的一笔暂存版,也就是上述例子的 stash@{0}
这一项,并且把这一笔删除。另一种取回暂存版的方法是透过 git stash apply
指令,唯一差别则是取回该版本 (其实是执行合併动作) 后,该暂存版还会留在 stash 清单上。
如果你想取回「特定一个暂存版」,你就必须在最后指名 stash id,例如 stash@{1}
这样的格式。例如如下范例,我使用 git stash apply "stash@{1}"
取回前一个暂存版,但保留这版在 stash 清单裡:
C:\git-stash-demo>git stash list
stash@{0}: On master: 新增 new.txt 档案
stash@{1}: WIP on master: 95eff6b a.txt: set 1 as content
C:\git-stash-demo>git stash apply "stash@{1}"
# On branch master
# 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: a.txt
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# b.txt
no changes added to commit (use "git add" and/or "git commit -a")
C:\git-stash-demo>git stash list
stash@{0}: On master: 新增 new.txt 档案
stash@{1}: WIP on master: 95eff6b a.txt: set 1 as content
如果确定合併正确,你想删除 stash@{1}
的话,可以透过 git stash drop "stash@{1}"
将特定暂存版删除。
C:\git-stash-demo>git stash drop "stash@{1}"
Dropped stash@{1} (118cb8a7c0b763c1343599027d79f7b20df57ebf)
C:\git-stash-demo>git stash list
stash@{0}: On master: 新增 new.txt 档案
如果想清理掉所有的暂存版,直接下达 git stash clear
即可全部删除。
C:\git-stash-demo>git stash list
stash@{0}: On master: 新增 new.txt 档案
C:\git-stash-demo>git stash clear
C:\git-stash-demo>git stash list
Git 的 stash (暂存版) 机制非常非常的实用,尤其是在 IT 业界插单严重的工作环境下 (不只台湾这样,世界各地的 IT 业界应该也差不多),这功能完全为我们量身打造,非常的贴心。在 Subversion 裡就没有像 Git 这麽简单,一个指令就可以把工作目录与索引的状态全部存起来。
本篇文章也试图透过指令了解 stash 的核心机制,其实就是简单的「分支」与「合併」而已,由此可知,整套 Git 版本控管机制,大多是以「分支」与「合併」的架构在运作。
我重新整理一下本日学到的 Git 指令与参数: