有没有遇过这种情境,某个系统开发写到一半,结果被老板或客户「插单」,被要求紧急修正一个现有系统的 Bug 或添加一个功能,眼前的程式即将完成,老板的「急件」又不能拖,一个未完成的软体开发状态外加紧急调整的需求,这简直是软体品质的一大考验。如果你有这种困扰,那麽 Git 可以漂亮的帮你完成任务。
我们知道使用 Git 版控的时候,有区分「工作目录」与「索引」。工作目录裡面会有你改到一半还没改完的档案(尚未加入索引),也有新增档案但还没加入的档案(尚未加入索引)。而放在索引的资料,则是你打算透过 git commit 建立版本 (建立 commit 物件) 的内容。
git commit
当你功能开发到一半,被紧急插单一定手忙脚乱,尤其是手边正改写到一半的那些程式码不知该如何是好。在 Git 裡有个 git stash 指令,可以自动帮你把改写到一半的那些档案建立一个「特殊的版本」(也是一个 commit 物件),我们称这些版本为 stash 版本,或你可以直接称他为「暂存版」。
git stash
我们手边改到一半的档案,可能会有以下状态:
若要将这些开发到一半的档案建立一个「暂存版」,你有两个选择:
git stash -u
注: git stash 也可以写成 git stash save,两个指令的结果是一样的,只是 save 参数可以忽略不打而已。
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 status
这时,我们利用 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 物件的「绝对名称」:
.git\refs\stash
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 物件:
git cat-file -p stash
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
由于「工作目录」已经被重置,所以变更都储存到 stash 这裡,哪天如果你想要把这个暂存档案取回,就可以透过 git stash pop 重新「合併」回来。如下所示:
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 代表显示精简版的状态):
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 清单,目前仅一份暂存版:
git stash list
C:\git-stash-demo>git stash list stash@{0}: WIP on master: 95eff6b a.txt: set 1 as content
而且你可以看到建立暂存版之后,工作目录是乾淨的。此时我们在建立另一个 new.txt 档案,并且再次建立暂存版:
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> 指令,就可自订暂存版的注解:
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@{0}
git stash apply
如果你想取回「特定一个暂存版」,你就必须在最后指名 stash id,例如 stash@{1} 这样的格式。例如如下范例,我使用 git stash apply "stash@{1}" 取回前一个暂存版,但保留这版在 stash 清单裡:
stash@{1}
git stash apply "stash@{1}"
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}" 将特定暂存版删除。
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 即可全部删除。
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 指令与参数:
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8