经常性的,我们的开发工作专注于项目的某一部分,而各种事情还处于混乱状态,这时你想切换个分支干点其他事。问题是,现在的工作还没做完,还不能提交到本地或者远程仓库,一会还得切回这个状态继续干。这种问题的解决方法就是使用“git stash”命令。
Stashing带走工作目录的dirty state,就是那些modified tracked files and staged changes。
这些未完成的修改被保存到一个stack中,你可以在随时恢复,不限于特定branch。
使用这个命令,把当前的working directory修改和index的内容保存下来,回到HEAD commit。
所以stash命令和已经commit的内容是无关的,无论是本地还是远程仓库。
stash命令将当前工作状态(Working directory and index state WIP)保存,不会产生conflict。
可以使用help选项查看git stash的帮助信息:
$git stash --help
Stashing Your Work
让我们来演示一下stashing操作,现在你要进入一个项目开始工作,修改一些文件,然后缓存某一个修改。
如果执行git status,就会看到当前dirty state (反之为clean state):
$ git status
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: index.html
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: lib/simplegit.rb
现在你想切换分支,但不想提交代码,就可以stash这些变更。在stack推送一个新的stash,执行git stash 或者git stash push:
$ git stash
Saved working directory and index state \
"WIP on master: 049d078 Create index file"
HEAD is now at 049d078 Create index file
(To restore them type "git stash apply")
现在看到你的工作目录是无变化的:
$ git status
# On branch master
nothing to commit, working directory clean
现在就能切换分支去干其他的,你的那些更改存到stack里了。想看你都存了那些更改,使用git stash list:
$ git stash list
stash@{0}: WIP on master: 049d078 Create index file
stash@{1}: WIP on master: c264051 Revert "Add file_size"
stash@{2}: WIP on master: 21d80a5 Add number to log
在本例中,前面已经存了2个stashes,你可以操作这三个不同的保存下来的变更。
最新的stash存在index 0,每进行一次stash操作,stash列表顺序后移。
你可以使用git stash apply,来直接恢复你最新stash的变更。
如果想恢复早先的变更,可以通过名字指定某一个,比如:git stash apply stash@{2}。
如果你不指定,git默认就是用最新的stash直接恢复:
$ git stash apply
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: index.html
modified: lib/simplegit.rb
no changes added to commit (use "git add" and/or "git commit -a")
在Git 2.11版本以后,可以直接使用数字来表示stash的索引。
$ git stash drop 1
$ git stash pop 1
$ git stash apply 1
$ git stash show 1
在数字前面加上--index也可以:
$ git stash pop --index 1
git stash apply后,你会发现你保存下来的变更被恢复。
这种情况,你在你的干净的工作目录上执行恢复操作,而且这个branch就是你之前保存变更的那个。
但这两个条件不是必须的,分支可以切换,工作目录也可以有已修改的和未提交的文件,不影响执行apply a stash。
注意,git stash操作本质上也是一次临时commit,有commit id号,自然也记录下了每个文件的新旧File index变化。
在Git stash apply时,如果stash里某个文件有变更,而工作目录里这个文件也有变更,是不能成功stash apply或pop的。
要把工作目录里的文件先git add一下,加到暂存区里,或者commit也可以。
在git stash apply时,针对文件的不同状态,会产生不同的行为。
拿某一个文件来说,修改后执行stash缓存,本地无更改,然后stash apply,就会直接merge。因为stash这个临时commit里,此文件有两个File Index,一个是旧的,一个是新的。而本地工作目录中此文件的最新提交,也有两个File Index,一个旧的,一个新的。Stash里的旧的File Index和本地的新的File Index是一致的,所以文件的变化状态是连续的,文件的File Index也是连续的,直接执行了更新操作。
如果此文件被修改并执行stash缓存操作,本地文件也进行了修改。那此文件工作目录下的最新提交的旧File Index和Stash中的旧File Index一致,但新的File Index不同。这是stash apply,会执行auto merge操作,相当于基于同一个File Index进行了两次修改,然后合并两个修改,如果出现冲突,就要手动处理。
如果此文件被修改并执行stash缓存操作,而本地文件进行了多次修改或者切换了工作分支,这时stash缓存中的此文件的旧File Index和此文件本地工作目录中的最新提交的新旧File Index都不同,这是就会出现Merge Conflict,就需要手动merge。因为同一个文件,无法merge一个没有关联的修改。这时git就会在文件里把两个版本的文件内容都显示出来,并告诉用户出现conflict,需要用户自己手动处理。
恢复修改时,如果是已经执行过git add,进入了staged状态的修改会回退,如果要保留staged的修改,需要加上--index选项。这样执行git stash apply之后,就会和初始状态一致。
就是说,工作目录的改动和index内的改动(git add后)在被stash之后,再apply或者pop,就都会被还原成工作目录中的改动,如果要保留index的改动不被回退到index中,就要加上这个选项。
$ git stash apply --index
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: index.html
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: lib/simplegit.rb
使用apply命令恢复stack上保存的工作,这个保存的修改还继续存在,要把这个修改从stack上删除,要执行git stash drop:
$ git stash list
stash@{0}: WIP on master: 049d078 Create index file
stash@{1}: WIP on master: c264051 Revert "Add file_size"
stash@{2}: WIP on master: 21d80a5 Add number to log
$ git stash drop stash@{0}
Dropped stash@{0} (364e91f3f268f0900bc3ee613f9f733e82aaed43)
还有一种方法,执行git stash pop,会恢复变更,然后立刻将这个变更删除。
如果在git stash pop出现conflict时,不会自动删除这个stash,要再自己手动drop stash。
出现的conflict自己修改,conflict解决后,再执行相关的git操作,add、commit、push等。
Creative Stashing (多种stash用法)
还有一些其他有用的stash使用方式。
第一个常用的选项是 --keep-index,在使用git stash命令时使用。
这个选项的作用是告诉git,不要把index里的内容加到stash里。
也就是如果你git add(stage)了一个修改的文件后,再执行git stash --keep-index,则这个修改不会被stash。
$ git status -s
M index.html
M lib/simplegit.rb
$ git stash --keep-index
Saved working directory and index state WIP on master: 1b65b17 added the index file
HEAD is now at 1b65b17 added the index file
$ git status -s
M index.html
另外一个常用的功能是stash时也包含未跟踪的文件。默认情况下,git stash只暂存修改的和staged的跟踪(tracked)文件。如果加上选项--include-untracked或者-u,git stash时会将未跟踪文件也暂存起来。但是,这时仍是未包含ignored(忽略)文件。若要包含ingnored文件,需要使用选项 --all (或者就用-a)。
$ git status -s
M index.html
M lib/simplegit.rb
?? new-file.txt
$ git stash -u
Saved working directory and index state WIP on master: 1b65b17 added the index file
HEAD is now at 1b65b17 added the index file
$ git status -s
$
最后,如果你使用了--patch标记,git stash就什么内容都不会暂存,而是进入一种交互模式,让你选择哪些变更要暂存,哪些变更继续保留在工作目录。
$ git stash --patch
diff --git a/lib/simplegit.rb b/lib/simplegit.rb
index 66d332e..8bb5674 100644
--- a/lib/simplegit.rb
+++ b/lib/simplegit.rb
@@ -16,6 +16,10 @@ class SimpleGit
return `#{git_cmd} 2>&1`.chomp
end
end
+
+ def show(treeish = 'master')
+ command("git show #{treeish}")
+ end
end
test
Stash this hunk [y,n,q,a,d,/,e,?]? y
Saved working directory and index state WIP on master: 1b65b17 added the index file
Creating a Branch from a Stash (根据stash来创建分支)
将本地修改stash后,有时pop时会出现merge conflict,这是一个坏消息,为了避免这种情况,我们最好能恢复到当时创建stash时的环境。
可以使用git stash branch <new branchname>, 直接根据当时的状态创建出一个新的branch,然后再自动执行git stash pop。
$ git stash branch <branchname> [<stash>]
Create并checkout一个新的branch,是基于指定stash创建的commit ID。如果不指定stash,则apply最新的stash,然后创建一个分支,并drop最新的stash。
此操作会保留当前的stash reflog,只会删除创建branch所使用的stash,不会删除其他的stash。
$ git stash branch testchanges
M index.html
M lib/simplegit.rb
Switched to a new branch 'testchanges'
On branch testchanges
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: index.html
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: lib/simplegit.rb
Dropped refs/stash@{0} (29d385a81d163dfd45a452a2ce816487a6b8b014)
其他
Stash Push详细说明
把本地的修改(包含了working tree和index里的)存储到一个新的stash entry,然后回滚当前状态到HEAD。
$ git stash push [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [-m|--message <message>] [--pathspec-from-file=<file> [--pathspec-file-nul]] [--] [<pathspec>…]
-p 是挨个hunk确认(Interactively / 交互式操作)是否stage。各个字母的提示输入“?”可以显示帮助说明。类似git add --patch。
-a 是包含了ignore的file
-- 此符号接路径或文件参数,指定需要stash的文件或路径。The new stash entry records the modified states only for the files that match the pathspec.
-q 静默模式,不显示提示信息。
quiet,suppress feedback messages。
--pathspec-from-file=<file>
指定文件作为文件或路径(pathspec)输入源,文件内容使用LF或CR/LF分隔,里面每行内容是要stash的文件或文件夹。
修改描述信息
如果直接使用git stash命令,存储的stash的描述信息就是当前最新的commit的commit ID加上log信息。
这样阅读起来很不方便,最好使用-m来加入定制的描述信息。
$ git stash push -m "My description."
如果已经加入stash了,要改名就比较麻烦,可以先drop,然后再push -m加入。
Stash的存储:
git stash操作,本质是一次commit操作,有commit id,存放在 refs/stash。
$ cat .git/refs/stash
dbda2ec3a585c5be99500c021d8a0d564a1f3dce
上面命令显示的就是最新一次stash push操作的commit ID。
如果做了多次stash操作,则旧的stash存在logs里:
$ cat .git/logs/refs/stash
0000000000000000000000000000000000000000 456a5762e3e30dba7604d7bf49ea7e36c88a6575 username <user@abc.com> 1616053132 +0800 WIP on lark_tmp: 7838a408c Change illumination to less bright
456a5762e3e30dba7604d7bf49ea7e36c88a6575 ce59c24b9fa358e5fc885333e7af64eed7f10177 username <user@abc.com> 1616053150 +0800 WIP on lark_tmp: 7838a408c Change illumination to less bright
一个stash entry表示一个commit 提交,记录了工作目录的状态( records the state of the working tree)。
查看
可以使用git stash show查看具体内容:
$ git stash show
CMakeLists.txt | 5 +-
libs/BT_Stack/CMakeLists.txt | 14 -
libs/BT_Stack/ReleaseNote.txt | 96 --
libs/BT_Stack/include/addflags.h | 493 -------
libs/BT_Stack/include/blueapi.h | 1383 --------------------
$ git stash show [<diff-options>] [<stash>]
git stash show -p stash@{1}
默认显示diffstat,几个文件修改,改了几处(包含插入和删除各几处)。
加了-p显示具体内容。
option就是git diff命令可以使用的。
查看有几条stash entry:
$ git stash list [<log-options>]
可以使用:
git stash list -2, 显示最近的两条stash的entry。
Stash操作的意义
第一种情况,向仓库拉取最新状态到本地工作目录(Pulling into a dirty tree).
Git的pull规则,如果远程仓库的最新提交修改了某个文件,而你本地工作目录也修改了这个文件,那是没法pull成功的。
这时有两个选择:
a. 将本地改动加入到staging area或index中,然后commit提交。这时再pull,就会进行merge,如果有冲突或其他问题会提示,可能需要手动操作。
b.将本地改动使用stash暂存下来,把这些改动放到一边,然后git pull远程仓库代码,完成后执行stash pop操作,如果有用冲突或其他问题会提示,可能需要手动操作。
第二种情况,被中断的工作状态(Interrupted workflow)。
事情做到一半存下来即可。这样可以不用提交就能直接切换分支。
另一个分支事情做完了再切回来,然后stash pop就回到最初状态了。
删除stash
$ git stash pop [--index] [-q|--quiet] [<stash>]
删除某个单独的暂存状态,并将其应用到当前的工作树状态上。
是git stash push的反操作,inverse operation。
--index表示index里面的文件状态保留。
stash参数不指定默认就是stash@{0}, 使用stash@{<revision>}
如果恢复时出现冲突,需要手动处理冲突,然后手动 git stash drop删除stash。
$ git stash clear
删除所有的stash。
$ git stash drop [-q|--quiet] [<stash>]
Remove a single stash entry from the list of stash entries.
删除某个stash。如果不指定就删除最新的。
注:
1. git stash save已经不推荐使用,建议使用git stash push。
因为push命令后面能跟路径/文件参数,而save命令不支持。
2. git stash不加参数相当于git stash push
3 . 恢复(restore) 一个stash:
#git stash apply // 应用但不删除
#git stash pop // 应用并删除
但pop命令只能使用git stash暂存的stash,但apply可以使用各种commit,比如使用git stash push和git stash create的。
4. Stash的名字: WIP on branchname
WIP: work-in-progress
5. stash@{0}是最新创建的,1是前一个。
所以引用stash可以使用序号,比如序号n,就是stash@{n}
====================================
Cleaning your Working Directory
上面讲解了git stash命令,用来保存那些你想保留的修改。但另外一种情况,有种可能你想清除所有改动,回到HEAD状态。
怎么办?使用git clean。
在Git仓库中,可能有各种操作,比如代码merge,外部工具对文件进行需改,还有编译时产生的中间文件。
使用Git clean就可以把这些不属于Git仓库,没有跟踪的文件删除掉。
$ git clean -f -d
-f选项表示“force”,“really do this”。 如果git配置里,clean.requireForce,这个是true的话,git clean必须带-f才能操作。
-d选项表示对文件夹遍历。
如果你对clean操作不放心,也可以先尝试下看有哪些文件会被删除,可以加上选项--dry-run或者-n。
$ git clean -d -n
Would remove test.o
Would remove tmp/
默认情况下git clean 只会删除未跟踪的文件,而不会删除忽略的文件。
如果你要删除这些没加入git的文件,比如一些编译过程中的.o文件,需要加上-x选项。
$ git status -s
M lib/simplegit.rb
?? build.TMP
?? tmp/
$ git clean -n -d
Would remove build.TMP
Would remove tmp/
$ git clean -n -d -x
Would remove build.TMP
Would remove test.o
Would remove tmp/
除了-f直接删除,-n显示要删除,还有-i选项,是个interactive flag。
$ git clean -x -i
Would remove the following items:
build.TMP test.o
*** Commands ***
1: clean 2: filter by pattern 3: select by numbers 4: ask each 5: quit
6: help
What now>
这种方式会每个文件单独确认,由用户来确认选择何种操作。
参考: