Git之 git stash

我有时会遇到这样的情况:正在 dev 分支上开发某个项目,这时有人反馈了一个 bug,需要紧急修复,但是正在开发的内容又不想现在提交,因为只完成了一半(强迫症不想增加一个脏的提交,然后惦记着日后做 rebase,麻烦)。或者是很嗨地开发完了一个功能,快要提交的时候才发现当前所在分支竟然是 master,而这本应是在 dev 分支开发的内容(两条分支并不同步,可能因冲突而不能直接切换分支)。

这些时候,我 prefer 使用 git stash(git存储)来快速处理。

一、git stash 的作用

  • git stash 可以获取你工作目录的中间状态,并将它保存到一个未完结变更的本地堆栈中,让当前工作区变干净。
  • stash 中的内容随时可以重新应用,而且不仅可以恢复到原先开发的分支,还可以应用到其他任意指定的分支上。

所以你就可以在 git stash 后,顺利地切换到另一个分支去修改 bug,修改完提交后,再切回 dev 分支。然后使用 git stash pop 来恢复之前的进度继续开发新功能。也可以在 git stash 后,从 master 切换到 dev 分支,然后使用 git stash pop 把在 master 上 stash 的内容恢复到 dev 上,然后在 dev 分支上提交。

二、git stash 的作用范围

git stash 默认会存储以下文件:

  • unstaged changes:未添加到暂存区的 git track 文件(曾经 add 过,然后修改了)
  • staged changes:添加到暂存区的文件(add 了但还没 commit

默认不会存储以下文件:

  • untracked files:在工作目录中的新文件(从未 add 过)
  • ignored files:被忽略的文件(符合 .gitigore 文件里的规则的文件)

总结起来,也就是默认只针对你修改过的被追踪的文件和暂存的变更,其他的不管。

三、stash 存到哪里去了

git 会在 .git/refs/ 目录下建立一个 stash 文件,存放最后一个 stash 的节点指针。
然后在 .git/logs/refs/ 目录下建立一个 stash 文件,存放所有的 stash 记录(每条记录包括下一个节点指针、当前节点指针、作者、邮箱、hash、stash message 六个字段 )。

四、git stash 的用法

stash 当前修改

1
git stash

这个命令把当前工作区所有未提交的修改(不包括untrack 和 ignore 的)都保存起来,同时会给每次 stash 添加默认的 message(来自于最后一次commit的message),所以如果你在未commit的期间进行了多次stash,会产生看上去一样的stash记录(其实不一样),因为message完全相同。

所以,一般我会使用git stash save命令取代git stash,给每次stash填写一个message作为标识。

1
git stash save <message>

查看 stash

1
git stash list

输出如下:

1
2
3
4
$ git stash list
stash@{0}: On feature/fix: conflict2 # 自己添加的 message
stash@{1}: On feature/fix: change c
stash@{2}: WIP on feature/fix: 6488467 update # 未加message的stash,获取的最后一次提交日志

重新应用缓存的 stash

1
git stash pop

输出如下:

1
2
3
4
5
6
7
8
9
10
$ git stash pop
On branch feature/fix
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: readme.txt

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (aed4ca7d79520beebd22af25bc2b21964fa3a0f2)

这个指令将缓存栈中的第一个stash中对应的修改应用到当前的工作目录下,并将这个stash删除。

你也可以指定 stash list 中的任意一个 stash 来恢复:

1
2
git stash pop stash@{num}
git stash pop stash@{2} # 恢复 stash list 中的第三个到当前工作目录

你也可以只应用某个 stash,而不自动删除它,在需要多次应用某个 stash 的时候适用。

1
git stash apply stash@{num}

移除 stash

1
git stash drop stash@{num}

删除掉某个 stash 缓存。git stash pop 其实就相当于 git stash apply stash@{0} + git stash drop stash@{0}

你也可以一次删除所有的 stash 缓存:

1
git stash clear

查看指定 stash 的 diff

1
git stash show

查看 stash 中第一个缓存与当前目录的修改数量。也可以指定stash的名字。例如:

1
2
3
$ git stash show stash@{0}
readme.txt | 1 +
1 file changed, 1 insertion(+)

也可以在命令后添加 -p 或者 -patch 查看具体的修改diff。输出结果如下:

1
2
3
4
5
6
7
8
9
10
$ git stash show stash@{0} -p
diff --git a/readme.txt b/readme.txt
index 1fd5ae1..08106f6 100644
--- a/readme.txt
+++ b/readme.txt
@@ -4,3 +4,4 @@ test dev

add feature

+change c

冲突的处理

从 stash 里应用缓存的时候,可能会跟当前工作目录上的内容有冲突。

1
2
3
4
5
6
7
8
9
10
11
$ git stash apply stash@{0}
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt

$ git status
On branch feature/fix
Unmerged paths:
(use "git reset HEAD <file>..." to unstage)
(use "git add <file>..." to mark resolution)

both modified: readme.txt

这个时候,stash 会在自动合并文件内容后,标出冲突所在。

1
2
3
4
5
<<<<<<< Updated upstream
change b
=======
>>>>>>> Stashed changes
change c

Git用 <<<<<<<=======>>>>>>> 标记出冲突中不同来源的内容。其中:
<<<<<<< Updated upstream 指当前工作区的内容。
======= 是分割线。
>>>>>>> Stashed changes 指 stash 缓存的修改内容。

只需跟解决其他冲突一样,处理完冲突(选择保留哪段代码或者都不要重新写)之后,add 和 commit 这个文件即可。

从 stash 创建分支

有时候,在当前分支对某次 idea 进行了 stash 之后,又在这个分支做了大量 commit、pull、merge 等操作,再想进行 stash pop 已经很明显会造成冲突,但是你不想处理冲突又很想继续之前的进度完成自己的 idea,有个更方便的方法让你无冲突地恢复你储藏的变更:

1
git stash branch <new-branch-name>

它会创建一个新的分支,检出你在stash时所处的提交,重新应用你的工作,如果成功,将会丢弃储藏。

暂存未跟踪或忽略的文件

刚才有提到,git stash 的默认作用范围不包括未跟踪和忽略的文件,其实你也可以强行暂存他们:

1
2
git stash -u #(或者 --include-untracked)stash untracked 文件 
git stash -a #(或者 --a)stash 当前目录下的所有修改

总结

工具千千万,使用工具的方法千千万,条条大路通罗马,如果没有git stash命令,我们确实照样可以使用 git 做日常代码管理(只是有时不那么便利罢了),但是如果有这么一个高效工具却不去使用那就是给自己添堵了。

最后我想说:I love git!


git官方文档