储藏
在开发中,会遇到很多优先级较高的任务,比如有紧急邮件需要马上处理,要召开临时紧急会议等,而如果每次被打断都需要重新从干净的工作目录重新开始开发,会严重拖慢工作效率,这种情况下,可以使用储藏(stash)。
一般说来,储藏可以捕获工作进度,并允许用户保存工作进度当重新进入开发时再次回到该进度,虽然可以使用分支和提交等操作来实现同等功能,但使用储藏方式更快捷。这有点像是游戏中保存进度的功能,以使用户在下次登陆的时候可以从上次的进度继续游戏。
而Git中的储藏可以通过简单的命令就全面彻底地捕获工作目录和索引,并且该功能能够使版本库是干净的,以执行后续的开发。也就是说使用储藏可以保证没有多余的提交或历史记录。
比如执行下面的命令初始化版本库:
git init
echo abc > file1
git add file1
git commit -m "commit file1"
echo abcd > file2
git add file2
git commit -m "commit file2"
然后修改工作目录和索引,使之变为脏的:
$ echo 1111 > file1
$ git add file1
warning: LF will be replaced by CRLF in file1.
The file will have its original line endings in your working directory
$ echo 2222 > file2
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: file1
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: file2
此时如果要使用储藏保存工作目录和索引:
$ git stash save
warning: LF will be replaced by CRLF in file2.
The file will have its original line endings in your working directory
Saved working directory and index state WIP on master: 92ae186 commit file2
$ cat file1
abc
$ cat file2
abcd
从上面的结果显示可以看出,储藏之后的file1和file2会变成干净的。然后执行某个提交:
echo 3333 > file3
git add file3
git commit -m "commit file3"
此时的版本库状态为:
$ git status
On branch master
nothing to commit, working tree clean
之后再恢复储藏的内容:
$ git stash pop
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: file1
modified: file2
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (3fdc1951b447bfcd51032bbdbc17f5341f049de7)
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: file1
modified: file2
no changes added to commit (use "git add" and/or "git commit -a")
$ cat file1
1111
$ cat file2
2222
可见在恢复储藏后,file1和file2均变为了未暂存的状态,虽然之前file1已经被暂存,但是恢复后的file1却与之前不同。
同时在git stash的保存中,虽然Git会为该操作添加默认的日志信息,但其实用户也是可以自己添加日志信息的:
git stash save "memo message"
在开发中,若使用分支和提交机制模拟中断处理机制,整个过程就是创建临时分支,检出到该分支,然后提交所有未完成的修改,回到主分支,进行紧急处理,然后检出之前的临时分支,继续中断的操作。不过该过程在保存状态时,所有变更都要捕获,同时临时分支的也要正确检出,否则整个还原过程就会出错。
而git stash save命令将保存当前索引和工作命令的状态,并且会将之清楚以匹配当前分支的HEAD。索引和工作命令的内容实际上另存为独立且正常的提交,其可以通过refs/stash来查询:
$ git show-branch stash
[stash] WIP on master: 450aae2 commit file3
而储藏之后的内容提取则是通过git stash pop进行,从命令形式可以看出,pop的意思是出栈,那么便可能存在多个储藏的入栈,出栈:
$ git stash save
Saved working directory and index state WIP on master: 450aae2 commit file3
$ cat file1
abc
$ cat file2
abcd
$ echo 4444 > file1
$ git stash save
warning: LF will be replaced by CRLF in file1.
The file will have its original line endings in your working directory
Saved working directory and index state WIP on master: 450aae2 commit file3
$ cat file1
abc
$ git stash pop
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: file1
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (7da0998acce71b85931a8a10e1353b29153739e6)
wood@DESKTOP-NVKVULV MINGW64 ~/Desktop/GIT/tmp (master)
$ cat file1
4444
$ git stash pop
error: Your local changes to the following files would be overwritten by merge:
file1
Please commit your changes or stash them before you merge.
Aborting
The stash entry is kept in case you need it again.
$ git restore file1
$ git restore file2
$ cat file1
abc
$ cat file2
abcd
$ git stash pop
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: file1
modified: file2
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (3d497bcef240e221c73ba9a7eff8e97dcc159eeb)
$ cat file1
1111
$ cat file2
2222
上面的代码共有几个过程:
- 首先当前的工作目录和索引是脏的,执行git stash save,此时的工作目录和索引变为干净的
- 然后重新将file1变为脏的,同时再次执行git stash save,此时的工作目录和索引有变为干净的
- 中间不做任何操作,以表示中间可能发生的任何有效操作
- 然后执行git stash pop,此时最近的储藏会被提出,file1中的内容也变为脏的
- 而若在工作目录或索引为脏的情况下,是不能使用git stash pop的,这表明该命令需要在工作目录或索引为干净的操作下执行
- 在file1,file2变干净之后,执行git stash pop,此时提出的是第一次git stash save保存的内容,而工作目录和索引也变为了脏的
同时在一个pop操作成功后,Git会自动地将储藏状态栈中保存的状态删除,也就是说,储藏的状态会被丢弃。然而,当需要解决冲突时,Git不会自动丢弃状态,以便用户自己手动处理,而在冲突处理后继续操作则应使用git stash drop来将状态从储藏栈中删除,否则,Git将会维持一个内容不断增加的栈。
而如果想要重新创建一个已经保存在储藏栈中的上下文,又不想把它从栈中删除,那么就可以使用git stash apply,也就是说,pop命令实际是apply和drop的封装。
又因为储藏捕获的实际是工作目录和索引,因此若在save操作后直接执行drop也会删除捕获的工作目录和索引的内容,即此时的工作目录和索引会重新变为干净的。
同时还有个简单的命令git stash list,用来按照时间顺序列举储藏栈:
$ git status
On branch master
nothing to commit, working tree clean
$ echo 1111 > file1
$ git stash save "first stash"
warning: LF will be replaced by CRLF in file1.
The file will have its original line endings in your working directory
Saved working directory and index state On master: first stash
$ echo 2222 > file2
$ git stash save "second stash"
warning: LF will be replaced by CRLF in file2.
The file will have its original line endings in your working directory
Saved working directory and index state On master: second stash
$ git stash list
stash@{0}: On master: second stash
stash@{1}: On master: first stash
从上边的结果可以看出,最近的储藏编号为0,然后依次编号。
git stash show命令则可以显示给定储藏条目相对于它的父提交的索引和文件变更记录:
$ git stash show
file2 | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
上边的结果显示了最近一次储藏内容的文件变更记录,+表示新增了一行,-表示删除了一行,这也就是file2的修改变更。
但是上边打印的内容好像作用不大,此时可以使用-p参数:
$ git stash show -p
diff --git a/file2 b/file2
index acbe86c..c7dc989 100644
--- a/file2
+++ b/file2
@@ -1 +1 @@
-abcd
+2222
上边的打印结果就明确多了,该命令默认打印都是最近的储藏,不过也可以指定打印特定储藏的内容。
而基于git stash的命令特征,比如save后工作目录和索引是干净的,可以应用如下场景:
- 如果某文件当前已经修改,而远程版本库中已发生更新,此时需要拉取该文件以便获取该文件的最新内容,而在拉取操作前需要先储藏,不然可能会丢失已有的修改
- 如果某个修改虽然尚未结束,但已明确该修改不需要了,便可以先save,再drop以获得干净的工作目录和索引
该命令使用的重点就是要明确该命令前后对于工作目录和索引的影响。
此外还有一种情况,若在恢复储藏后,需要执行冲突解决,而该冲突解决太麻烦时,则可以使用git stash branch基于储藏内容生成时的提交将储藏内容转换到一个新分支:
$ echo 1111 > file1
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: file1
no changes added to commit (use "git add" and/or "git commit -a")
$ git stash save
warning: LF will be replaced by CRLF in file1.
The file will have its original line endings in your working directory
Saved working directory and index state WIP on master: 450aae2 commit file3
$ echo 4444 > file4
$ git add file4
warning: LF will be replaced by CRLF in file4.
The file will have its original line endings in your working directory
$ git commit -m "commit file4"
[master 54cf89a] commit file4
1 file changed, 1 insertion(+)
create mode 100644 file4
$ git stash branch other
Switched to a new branch 'other'
On branch other
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: file1
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (ee5e2035584842a880d41d3d97e41edbe5742a85)
$ git branch
master
* other
$ git status
On branch other
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: file1
no changes added to commit (use "git add" and/or "git commit -a")
$ ls
file1 file2 file3
此时确实会将捕获的储藏转换为一个分支,转换到分支并不意味着在出现冲突时可以避免,只是有了一个更稳定的起点进行冲突处理,这种情况应用于较为复杂的场景中,否则便可以直接使用分支和提交机制了。
引用日志
引用日志(reflog)记录非裸版本库中分支头的改变,每次对引用的更新,包括对HEAD的,应用日志都会更新以记录这些应用发生了哪些变化,同样,也可以通过引用日志来进行分支操作的回溯。
更新引用日志的基本操作包括:
- 复制
- 推送
- 提交
- 修改或创建分支
- 变基操作
- 重置操作
从上面内容来看,任何修改引用或变更分支头的Git操作都会记录。
默认情况下,引用日志在非裸版本库中是启用的,在裸版本库中是禁用的。该设置是由配置选项core.logAllRefUpdates控制的。
可以看一下引用日志:
$ git reflog show
450aae2 (HEAD -> other) HEAD@{0}: checkout: moving from master to other
54cf89a (master) HEAD@{1}: commit: commit file4
450aae2 (HEAD -> other) HEAD@{2}: reset: moving to HEAD
450aae2 (HEAD -> other) HEAD@{3}: reset: moving to HEAD
450aae2 (HEAD -> other) HEAD@{4}: reset: moving to HEAD
450aae2 (HEAD -> other) HEAD@{5}: reset: moving to HEAD
450aae2 (HEAD -> other) HEAD@{6}: reset: moving to HEAD
450aae2 (HEAD -> other) HEAD@{7}: reset: moving to HEAD
450aae2 (HEAD -> other) HEAD@{8}: reset: moving to HEAD
450aae2 (HEAD -> other) HEAD@{9}: reset: moving to HEAD
450aae2 (HEAD -> other) HEAD@{10}: reset: moving to HEAD
450aae2 (HEAD -> other) HEAD@{11}: reset: moving to HEAD
450aae2 (HEAD -> other) HEAD@{12}: commit: commit file3
b17a9b7 HEAD@{13}: reset: moving to HEAD
b17a9b7 HEAD@{14}: commit: commit file2
39727b4 HEAD@{15}: commit (initial): commit file1
引用日志记录所有引用的事务处理,但是git reflog show命令一次只显示一个引用的事务,上面的引用日志显示的就是HEAD。而也可以打印其它引用名,比如分支:
$ git reflog other
450aae2 (HEAD -> other) other@{0}: branch: Created from 450aae23b5b1bcc0cb014f552fc1d8e223b43162
引用日志的每一行都记录了引用历史记录中的单次事务,从最近的变更开始倒序显示。从左到右的打印分别是变更提交ID,顺序排列的别名,事务描述。
既然打印了上面的内容,那么就可以通过Git命令来查看该事务的内容:
$ git show HEAD@{1}
commit 54cf89a81437e5b8769b46cd9819cb6e85fc0731 (master)
Author: wood_glb <wood_glb@git.com>
Date: Sun Jul 3 22:38:15 2022 +0800
commit file4
diff --git a/file4 b/file4
new file mode 100644
index 0000000..b0f6d94
--- /dev/null
+++ b/file4
@@ -0,0 +1 @@
+4444
这也就是说,不管是什么事务或者变更,只要是涉及到引用日志中的条目,就可以详细查看,已获知引用的具体变更。
但同时引用日志中的打印并不一定具体提交上的父子关系,因为引用日志记录的只是引用的变更历史,是git reflog,而不是git log。
同时Git还支持多类英语的限定符在花括号中作为引用的一部分,这可能有点像字符串匹配的模糊查询:
$ git log 'HEAD@{last sunday}'
commit 450aae23b5b1bcc0cb014f552fc1d8e223b43162 (HEAD -> other)
Author: wood_glb <wood_glb@git.com>
Date: Sun Jul 3 21:19:13 2022 +0800
commit file3
commit b17a9b72e5bad80fe4da03c191813dded68525d8
Author: wood_glb <wood_glb@git.com>
Date: Sun Jul 3 21:17:34 2022 +0800
commit file2
commit 39727b4522b0273ddfd848cf05faa8f460fb492d
Author: wood_glb <wood_glb@git.com>
Date: Sun Jul 3 21:17:31 2022 +0800
commit file1
或者:
$ git log '@{last sunday}'
commit 450aae23b5b1bcc0cb014f552fc1d8e223b43162 (HEAD -> other)
Author: wood_glb <wood_glb@git.com>
Date: Sun Jul 3 21:19:13 2022 +0800
commit file3
commit b17a9b72e5bad80fe4da03c191813dded68525d8
Author: wood_glb <wood_glb@git.com>
Date: Sun Jul 3 21:17:34 2022 +0800
commit file2
commit 39727b4522b0273ddfd848cf05faa8f460fb492d
Author: wood_glb <wood_glb@git.com>
Date: Sun Jul 3 21:17:31 2022 +0800
commit file1
虽然Git针对引用支持大量的日期限定符,但其并不是万能的,这有点像是字符串的规则匹配,当你的字符串符合其内容的匹配规则时,就可以成功解析,换言之,若想要正常使用而不出错,最好还是有一个固定的书写风格。同时从上面的内容也可以看出@前的引用名如果省略,就默认为当前分支的HEAD。
同时Git还会自动执行垃圾回收机制,以保证引用日志不会因为项目的迭代而变得过于巨大,这个过程中,旧的引用日志会被丢弃。通常情况下,一个提交,如果既不能从某个分支或引用指向,也不可达,就默认在30天后国企,而可达的提交默认在90天后过期。
也可以设置配置gc.reflogExpireUnreachable和gc.reflogExpire来设置垃圾回收机制的时间,同时也可以使用git reflog delete来手动删除条目,使用git reflog expire来让条目过期并被立即删除,或者可以使用下述命令来强制使引用日志过期:
$ git reflog expire --expire=now --all
$ git gc
Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
Delta compression using up to 4 threads
Compressing objects: 100% (7/7), done.
Writing objects: 100% (12/12), done.
Total 12 (delta 2), reused 0 (delta 0), pack-reused 0
同时引用日志都储存在.git/logs目录中,.git/logs/HEAD文件包含HEAD值的历史记录,其子目录.git/logs/refs包含所有引用的历史记录,其中也包括储藏,其子目录.git/logs/refs/heads包含分支头的历史记录。
在引用日志中存储的所有信息,特别是.git/logs目录中的一切内容,终归是临时的,不会对整个版本库存在任何影响,因此删除.git/logs目录或关闭引用日志其实是无所谓的,其后果只是不能解析HEAD@{1}这样的形式而已。
相反,引用日志中可能存在不可达的提交,此时启动引用日志会引入指向提交的引用,而这些提交又是不可达的,而如果需要清理版本库,删除引用日志就会移除这些不可达的提交。