如何管理工作目录,以便用户可以更高效地新建提交。如何在处理工作区和暂存区文件的过程中修复错误,以及如何修复最近一次提交记录中的问题;同时还会了解到如何安全地使用暂存机制和多个工作目录处理工作流中的中断问题。
主要内容有以下几点:
- 忽略文件:特意不让版本控制器管理某些文件。
- 文件属性:特定路径的配置。
- 处理文件和二进制文件。
- 为了提高版本库的可移植性,提供对文件尾行转换的支持。
- git reset命令在不同模式下的应用。
- 为了处理中断而隐藏暂存变更。
- 搜索和查看文件。
- 交互式文件重置和恢复文件变更。
- 通过移除未跟踪文件来保持工作区整洁。
1、忽略文件
用户的工作区(也称工作树)内部的内容既可以被Git跟踪,也可以不被它跟踪。被跟踪文件,顾名思义,Git将会跟踪该文件的一系列变更记录。对于Git来说,如果一个文件存在于暂存区(也称索引),如果没有特别声明的话,那么该文件将会被系统跟踪,并作为下一个修订记录的一部分。用户添加文件后,系统跟踪这些文件的目的是将它们当作项目历史的一部分。
索引或者暂存区不仅可以用来告知Git系统跟踪哪些文件,而且可以作为新建提交的一种暂存器。而且索引或者暂存区可以帮助用户解决合并冲突。
通常对于某些独立文件或者某类文件来说,用户永远都不希望它们作为项目历史记录的一部分而存在,并且也不希望系统跟踪它们。这些文件可能是编辑器的备份文件,也有可能是项目编译系统自动生成的临时文件。
用户不希望Git系统自动添加上述文件,例如,在使用git add :/
(添加整个工作区)和git add.
(添加当前目录下的所有文件)命令批量添加文件时,或者使用git add --all
命令更新工作区状态索引时。
另外一方面,用户又希望Git能够有效地防止将不必要的文件添加到系统中。同时用户还希望在执行git status命令后不显示这些文件,因为它们的数量可能会非常庞大。有时它们也可能会和一些新增的未知文件混在一起。因此用户希望这类文件特意不被跟踪,即忽略它们。
未跟踪和重新跟踪的文件:
如果用户希望忽略某个以前被跟踪的文件,例如从手工生成HTML文件迁移到使用类似Markdown这样的轻量级标记语言时,用户通常需要在不将它们从工作目录中删除的情况下,将它们添加到忽略文件列表中,对它们取消跟踪。为此,用户可以使用git rm--cached <文件名>
命令。为了添加(开始跟踪)一个特意不跟踪(即忽略)的文件,用户需要使用命令git add -f
。
1.1、将文件刻意标记为不跟踪的
这种情况下,用户可以通过Git系统中gitignore文件添加一组shell glob模式来指定希望忽略的文件,文件中每个模式占用一行的位置。
可以通过配置变量core.excludesFile指定每个用户的个性化配置文件,该变量默认的值是$XDG_CONFIG_HOME/git/ignore
。如果环境变量$XDG_ CONFIG_HOME
未设置或者为空的话,那么其默认值为$HOME/.config/git/ignore
。每个本地版本库的$GIT_DIR/info/exclude
文件在本地版本库克隆的管理区中。
.gitignore文件在项目工作区目录下;该文件通常是被系统跟踪记录并且可以和其他开发人员共享的。一些诸如git clean的命令还支持用户通过命令行声明忽略模式。
在判断是否忽略某个路径时,Git系统会根据上述列表中的模式以一定的顺序进行模式匹配,然后根据就近优先原则决定输出结果。.gitignore文件也是按照一定的顺序进行检查的,它会从项目的顶级目录开始,依次遍历项目中的所有文件。
为了增强gitignore文件的可读性,用户可以使用空白行将文件分组(空白行不匹配文件)。用户还可以对模式进行描述或者使用附带注释的模式组,以#开头的代表一行注释(为了实现对一个#开头的哈希字符串进行模式匹配,通常会在第一个哈希字符前面使用\
对它转义,例如\#*#
)。字符尾部的空格会被忽略,除非使用\对它进行转义。
gitignore文件中的每一行都代表一种UNIX的glob模式,即shell通配符。通配符*可以匹配0个或者多个字符(任意字符串),通配符?可以匹配任意单个字符。你还会了解到字符类方括号[…]的使用,例如下面的模式匹配示例:
*.[oa]
*~
这里的第一行内容是告诉Git系统忽略所有后缀名是.a或者.o的文件(例如静态链接库),以及软件编译过程中产生的临时文件。第二行是告诉Git系统忽略所有以~结尾的文件,这类文件常见于很多UNIX文本编辑器采用的临时备份文件。
如果模式中不包含 /
,即文件路径的分隔符,Git会将它视为一个shell glob通配符,并且根据它查找相应的文件名和目录名,例如.gitignore文件的路径或者某个版本库顶级目录。以/
结尾的模式是一个例外,它主要是用来匹配目录的,除非目录级下的反斜杠被移除了。以反斜杠开头的模式是用来匹配路径名称前面位置的,它的含义有以下几种:
- 模式中不包含反斜杠匹配版本库中的任意路径,那么我们可以说该模式是递归的。例如*.o模式匹配任意的文件对象,其中既包含gitignore文件,也包括file.o和obj/file.o这样的子目录。
- 以一个反斜杠结尾的模式只匹配目录,否则它就是递归的(除非它还包含其他斜杠)。例如auto/模式将会匹配顶层的auto目录以及src/auto目录,但是它不会匹配名为auto的文件(或者一个标记链接)。
- 如果希望固定一个模式,并且确保它是非递归的,那么可以在其起始位置添加一个反斜杠进行转义,例如/TODO文件将会忽略当前层级的TODO文件,但是不会忽略子目录中的文件,例如src/TODO。
- 包含斜杠的模式都是固化并且非递归的,通配符不会匹配作为目录分隔符的斜杠。如果用户希望匹配任意目录,那么可以使用两个连续的星号替换路径地址的某个部分(例如/foo,foo/和foo//bar)。例如doc/.html匹配的是doc/index.html文件,但是无法匹配地址doc/api/ index.html。为了匹配doc目录下的任意HTML文件,用户可以使用doc/**/.html模式(或者将.html模式添加到doc/. gitignore文件中)。
- 用户还可以在模式前面加一个感叹号!前缀使之失效,任何根据先前的规则被排除的文件,现在又再次被包含(不再被忽略)进来了。例如已经忽略所有生成的HTML文件,但是希望包含某个通过手工生成的文件,用户可以在gitignore文件中做如下设置:
# 忽略所有通过AsciiDoc源代码工具生成的HTML文件
*.html
# 下列有手工生成的文件将会是个例外
!welcome.html
注意,Git基于性能方面的考虑不会排除某个目录(至少2.7版的程序是如此),这意味着当父目录被排除后,用户不能将其目录下的某个文件再次包含到跟踪列表中。也就是说,为了将某个子目录作为例外被跟踪,必须做如下配置:
#除了目录t0001/bin之外,将会排除所有文件。
/
!/t0001
/t0001/
!/t0001/bin
为了匹配一个用!开始的模式,必须使用一个反斜杠对其进行转义,例如模式!important!.md是为了匹配!important!.md。
1.2、确定忽略文件类型
现在我们已经知道了如何将文件状态特意标记为不被跟踪的(忽略),那么接下来的问题是哪些(哪一类)文件应该被标记。另外一个问题是:上述文件的位置是什么?以及我们应该如何在3个gitignore文件中声明需要忽略的文件类型?
首先,用户永远不应该跟踪自动生成的文件(通常是由项目的编译系统生成的)。如果用户将它们添加到版本库中了,那么它们很有可能无法和源文件保持同步。此外,它们也不是必须添加的文件,因为系统可以很容易地重新生成它们。唯一的例外是生成这些文件的源文件极少发生变动,并且生成它们需要额外的工具辅助,但是开发人员有可能没有这些工具(如果源代码经常发生变更,用户可以使用一个孤儿分支存放这些生成的文件,只在发布预览版程序时更新该分支)。
这些是所有开发人员都希望忽略的文件,因此它们应该被放到一个被跟踪的.gitignore文件中。模式列表将会是经由版本控制的,并且可以通过克隆副本分发给其他开发人员。大家可以在https://github.com/github/gitignore上找到和若干编程语言有关的一组非常有用的.gitignore模版。
其次,临时文件和特定产品相关的用户工具链,这些内容通常都不会与其他开发人员共享。如果模式对版本库和用户都是有效的,例如在版本库中的辅助文件,并且该文件也会影响特定的工作流用户(例如该项目中IDE程序),那么它就应该被放到每个克隆的$GIT_DIR/info/exclude文件夹中。
在用户采用的通用忽略模式中,不必特意声明版本库(或者项目),一般来说可以通过core. excludesFile配置变量中进行声明,同时对于每个用户(全局)还可以在~/.gitconfig(或者~/.config/git/config)配置做相关设置。一般来说,默认的配置文件路径是~/.config/git/ignore
。
专属于每个用户的忽略文件应该不会是~/.gitignore,因为如果用户希望让~/directory ($HOME)主目录保持版本控制,那么该文件有可能就会是用户主目录对应版本库中的.gitignore文件。
这里是编辑器或者IDE程序生成的备份或者临时文件对应的模式匹配文件所在之处。
已忽略文件通常也是无关紧要的:
警告:不要将重要资料添加到忽略列表中,这些资料通常是用户不希望在某个版本库中被跟踪的,但是内容相比忽略文件列表中的内容来说却是非常重要的!被Git忽略的这类文件既可以很容易地被重新生成(产品编译系统生成的中间文件),而且对于用户来说又是无关紧要的(临时文件或者备份文件)。
因此Git会认为这类已忽略的文件用处不大,当需要做一些清理工作时,即使在没有向用户提示的情况下也可能把它们删除,例如,如果已忽略文件和当前签出的修订内容有冲突时。
1.3、忽略文件列表
用户可以在执行status命令时使用–ignored选项查看被忽略的文件:
$ git status --ignored
On branch master
Ignored files:(use "git add -f <file>..." to include in what will be committed)rand.c~no changes added to commit (use "git add" and/or "git commit -a")$ git status --short --branch --ignored
## master
!! rand.c~
用户还可以使用清理被忽略文件的测试选项:git clean -Xnd和底层(管道化)的命令git ls-files:
$ git ls-files --others --ignored --exclude-standard
rand.c~
后一个命令还可以用来显示匹配忽略模式的被跟踪文件。找到这类文件往往意味着某些文件需要被取消跟踪(也可能是源代码文件临时生成的中间文件),或者也可能是忽略模式覆盖的范围过于宽泛了。因为Git是通过暂存区(缓存)已有的文件来识别哪些文件需要被跟踪的,相关的命令如下:
$ git ls-files --cached --ignored --exclude-standard
底层命令(Plumbing)和高层命令(Porcelain)的区别:
- Git的命令主要分为两种:一种是方便和用户交互的高层命令,另外一种是方便编写shell脚本的管道化底层命令。它们之间的区别在于:高层命令的输出结果可以变更并不断完善,例如在遇到与HEAD分离的情况下,执行git branch命令后的输出结果显示了其中的分离过程(无分支到与HEAD分离)。同时底层命令中也有选项(通常是–porcelain)来决定是否选择无变更的输出结果。它们的输出结果和行为是可以根据主题配置的。
- 另外一个比较重要的区别是高层命令会尝试猜测用户的意图,并且会使用默认参数和默认配置。底层命令则没有那么智能,用户必须通过–exclude-standard这样的选项和git ls-files命令搭配使用,才能让它采用忽略文件的默认设置。
1.4、忽略跟踪文件内的变更
也许用户版本库中不少文件发生了变更,但是它们很少被提交。这类文件可以是若干本地配置文件,为了适应用户本地环境经过编辑配置,但是用户永远不希望将它们提交到远程上游分支。这也可以是某个包含新发布的预览版软件名称的文件,只有当它们添加了下一个预览版程序便签后才会被提交。用户可能希望让这类文件大部分时间都保持“杂乱无章”的状态,但是又不希望Git系统经常向用户提示这些文件发生了变更。为了防止干扰其他变更信息的提示,用户应该将这类信息忽略。
用户可以对Git进行配置,让它跳过对工作目录的检查(假定它始终是最新版本),并使用文件的暂存版本替代,可以对某个文件设定相应的skip-worktree标记来达到此目的。为此用户将会需要用到底层的git update-index命令,它相当于面向用户的高层命令git add(用户可以使用’git ls-files’查看文件的状态和标记):
$ git update-index --skip-worktree GIT-VERSION-NAME
$ git ls-files –v
S GIT-VERSION-NAME
H Makefile
不过这种对工作区的省略也会影响到git stash命令;为了暂存用户的变更并且保持工作目的整洁,用户需要禁用该标记(至少是临时措施)。为了让Git再次监测到工作目录中的修订版本,并且开始跟踪文件的变更记录,可以执行下列命令:
$ git update-index --no-assume-unchanged GIT-VERSION-NAME
还有一个类似的选项assume-unchanged,它可以用来让Git系统完全忽略文件的变更,甚至可以假定文件没有发生任何变化。当文件被这个标签标记之后,它们将永远不会出现在git status或git diff命令的输出结果中,与之相关的变更将不会被暂存或提交。
有时这是非常有用的,特别是在检查一个大型项目的文件变更时。不要对被跟踪的文件使用assume- unchanged选项已达到忽略文件的目的。用户务必确保文件没有发生变更,不会发生欺骗Git系统的情况,例如git stash save命令将会根据你的设置进行保存,这样就可能失去用户本地文件的变更记录。
2、文件属性
Git中有一些配置和选项可以用来声明基本路径,它的机制和忽略文件类似(将文件特意标记为不跟踪的)。这些路径声明设置被称为属性。
为了给文件指定一些属性以便匹配给定的模式,用户需要在下列gitattribute文件中添加一行由空格分隔的属性集合作为模式(它的机制和gitignore文件类似)。
单个用户配置文件:对于每个用户来说,文件中的属性会影响与该用户有关的所有版本库,该文件默认路径是~/.config/git/attributes,并且是通过配置变量core.attributesFile声明的。
版本库属性文件:该文件一般在本地版本库克隆的管理区中,文件路径是.git/info/attributes,这些属性只会对声明的单个版本库克隆产生影响(对于单个用户工作流来说)。项目中的.gitattributes文件,其中的属性可以和团队其他成员共享。
模式匹配文件的规则和前面提到的gitignore文件类似,除非存在不兼容的省略模式。
对于给定路径,每个属性可以设置为下列几个状态:设置(设定值为true),未设置(设定值为false),以及给定值或者unspecified:
pattern* set -unset set-to=value !unspecified
注意,用字符串给一个属性赋值时,等号(=)两边是没有空格的!
当存在多个模式匹配路径时,后一行的属性会覆盖前一行的基本属性。Gitattribute文件采用顺序优先原则,即在给定目录中从单个用户文件到.gitattributes文件,这和gitignore文件类似。
2.1、配置Diff和merge
在Git中,用户可以通过属性功能配置如何显示文件不同版本之间的差异,以及如何对文件执行三路合并。该特性可以用来扩展上述操作的功能,使得差异比较结果内容更丰富,合并操作出现冲突的概率更小。它甚至可以用来高效地比较二进制文件。为此我们一般需要设置差异比较(diff)和合并操作(merge)的驱动。
属性文件只能告诉用户使用哪些驱动,其余的信息都在配置文件里面,而且配置信息并不是像.gitattributes文件那样自动就可以和其他开发人员共享的(用户还可以创建一个共享配置片段,然后将它添加到版本库中,方便和其他开发人员使用相对路径引用它)。方便易用的工具配置信息在不同电脑和操作系统上的表现可能不尽相同,不过这也意味着某些信息对跨平台的要求要高一些。
不过,幸好系统内置了不少差异比较和合并操作的驱动供用户选择。
3、使用reset命令修复错误
在开发过程的任意阶段,用户也许会希望撤销一些东西以便达到修复错误的目的,或者希望将当前的工作成果丢弃。在Git核心中不存在git undo这样的命令,也没有任何命令存在–undo选项可供使用。与此同时,倒是很多命令中都有–abort选项,可以让用户丢弃当前的工作成果。没有这类命令或者选项的原因之一就是为了避免在被撤销内容上产生歧义(这对于多步操作尤其重要)。
很多问题都可以通过git reset命令进行修复。它的用途非常广泛,理解它的工作原理会让你在实际应用中如虎添翼。
3.1、回退分支head
git reset
命令在完整的树模式下不仅会影响当前分支首部,而且还会影响索引(暂存区)和工作目录。注意,重置不会影响当前的分支,只会对签出状态下的内容产生影响。为了只重置当前分支首部,不影响所有工作目录,可以使用git reset --soft [<revision>]
命令。
从效率上来看,我们只修改了当前分支的指针(如下图所示的master分支)指向了一个给定的修订节点(HEAD^
—代表本示例中的上一提交记录),暂存区和工作区都没有受到影响。这一操作使得用户丢弃了将要提交状态中所有已变更的文件(分支上与上一版本存在差异文件),使用git status命令后会显示这一变化。
1、移除和修改提交
上述命令的工作方式意味着软性重置可以用来撤销创建提交记录的行为。这也适用于提交的修改,这种方式要比git commit
命令和--amend
选项一起使用更简单一些。事实上,执行下列命令:
$ git commit --amend [<options>]
和执行下列命令是等效的:
$ git reset --soft HEAD^
$ git commit --reedit-message=ORIG_HEAD [<options>]
和使用软性重置不同之处在于,git commit --amend
命令也适用于合并提交。在修改提交时,如果用户只想修改提交的注释信息,那么不需要使用任何命令选项辅助。如果用户想修复一个工作目录中的问题,但是不想修改注释信息,那么可以使用-a --no-edit
选项。如果用户希望修复用于纠正Git配置项目中的作者信息,那么可以使用--reset-author --no-edit
选项。
2、使用reset压缩提交记录
用户并不仅限于将分支的首部移动到上一个提交节点。通过软性重置,用户还可以插入几个更早的提交记录(例如提交和bug修复,或者引入新的功能函数),将一个提交一分为二(或者更多),又或者使用squash指令进行交互式变基操作。对于后者,用户实际上可以插入任意一系列的提交记录,而且不局限于最近的几个提交记录。
3.2、重置分支head和索引
reset命令的默认模式也被称为混合式重置(因为它是介于软性重置和强制重置之间的),修改当前分支的首部指向给定的修订节点,同时重置索引,将相关修订的内容写入暂存区(见下图)。
这一操作使得用户丢弃了处于拟提交状态并且未暂存的所有已变更文件(分支上与上一版本存在差异文件),执行git status
命令后会显示这一变化。git reset --mixed
命令还会使用简要状态格式报告那些未更新的内容。用户还可以访问reset命令的历史版本,例如撤销新增的文件。如果用户没有暂存任何变更的话(或者用户可以直接丢弃这些变更),那么上述操作可以使用git reset
命令实现。如果用户希望撤销添加某个特定文件的操作,那么可以使用git rm --cached <file>
命令。
1、使用reset分割提交
用户还可以使用混合式的reset命令将一个提交一分为二。首先,运行git reset HEAD^
命令,将分支首部和索引指向上一修订节点。然后向第一个提交以交互式添加的方式添加用户希望添加的变更,继而根据索引创建第一个提交(git add -i
和git commit
)。第二个提交可以根据工作目录状态创建(git commit -a
)。
如果交互式移除变更时更简便一些,那么用户也可以这么做。使用git reset–soft HEAD^命令后,可以对每个文件执行reset命令,实现交互式地撤销暂存变更,根据索引的构造状态创建第一个提交记录,然后根据工作目录创建第二个提交记录。
依次循环往复操作,用户就可以替代交互式变基,实现分割历史提交记录的目的。变基操作会切换到适当的提交记录上,实际的分割操作大致和上述操作是一样的。
2、使用WIP提交保存和重排变更状态
假如你正在某个分支上进行功能开发工作,这时有一个紧急的bug修复请求使得你不得不中断目前的工作。你又不想舍弃目前分支上产生的变更记录,但是现在的工作目录又有些凌乱。一个比较可行的解决方案是通过创建一个临时提交(工作进行中的快照,WIP),以便保存当前工作区的状态:
$ git commit -a -m 'snapshot WIP (Work In Progress)'
然后用户就可以停下目前的工作,切换到维护分支,创建一个提交专门修复相关问题。之后用户只需要回到上一个分支(使用签出命令),然后从历史记录中移除WIP提交(使用软性重置),切换到未暂存的起始状态即可(使用混合式重置):
$ git checkout -
$ git reset --soft HEAD^
$ git reset
不过使用git stash 命令处理中断更方便一些。换句话说,这类临时提交(类似的概念验证式的工作)和暂存的不同之处在于,它可以和团队其他成员共享。
3.3、丢弃变更和回退分支
有时看着一团乱麻的工作目录,用户也许非常希望丢弃所有的变更,将工作目录和暂存区(索引)的状态恢复到最近一次提交的状态(最新的稳定版本程序)。又或者用户希望将版本库的状态回退到以往的某个修订版本。如下图所示,强制性重置(reset)操作将会修改当前分支首部的指向,并且重置索引和工作目录树。被跟踪文件产生的所有变更都会被丢弃。
该命令还可以用于撤销(移除)一个提交,就好像该提交从未存在过一样。执行git reset --hard HEAD^
命令后,将会高效地将上一个提交节点移除(不过短时间内还可以通过reflog恢复),除非该节点还可以通过其他分支进行访问。
另外一个常见的用法是通过git reset --hard
命令丢弃工作目录下的变更。
特别重要的一点需要谨记:强制性重置操作是不可恢复地将暂存区和工作目录中的所有变更移除。用户无法撤销这部分操作!变更记录将会永远消失!
1、将提交指向特性分支
假如用户正在master分支上工作,而且已经创建了一系列的提交,同时发现正在研发的功能特性之间联系非常紧密,继而希望将它们整合到一个主题分支上。用户希望将在master分支上的一系列提交(例如最近的3个修订版本)迁移到上述特性分支上。
用户需要创建一个feature分支,保存为提交的变更(如果有的话),将master分支上与主题特性相关的提交记录移除,最后切换到feature分支继续工作(或者可以使用变基操作实现):
$ git branch feature/topic
$ git stash
No local changes to save
$ git reset --hard HEAD~3
HEAD is now at f82887f before
$ git checkout feature/topic
Switched to branch 'feature/topic'
当然,如果有本地的变更需要保存,在上述命令之前需要先执行git stash pop
命令。
2、撤销合并或拉取操作
强制性重置还可以通过git reset --hard HEAD
(HEAD是默认参数,也可以省略)命令取消一个失败的合并操作,例如用户不打算解决合并冲突时(当然在当前的Git系统中,也可以使用git merge --abort
命令达到相同目的)。
用户还可以通过git reset --hard ORIG_HEAD
(这里可以用HEAD@{1}
替代ORIG_HEAD
)命令移除一个成功执行的快进式拉取操作或者变基操作(以及其他移动分支首部的操作)。
3.4、安全模式重置——保留用户变更
强制性重置操作会丢弃用户本地的所有变更,它的效果和git checkout -f命令类似。有时用户也许希望在回退当前分支的同时保留本地的变更,这就是git reset --keep命令能够实现的功能(见下图)。
这种模式会重置暂存区(索引实体),但是仍然保留当前本地工作目录下未暂存的变更。如果遇到异常,该重置操作将会被终止。这意味着工作区中的变更被保存了,并且被移动到了新的提交记录中,其工作原理和git checkout <branch>
命令处理未提交的变更记录类似。执行成功的情况和隐藏变更类似,强制性重置,然后取消隐藏变更。
git reset --keep 命令的工作原理是更新(工作目录下)我们回退的目标版本和分支首部引用版本之间有差异的文件内容。如果在分支首部和目标修订版本之间存在任何差异,并且本地文件存在未提交的变更记录,那么重置操作都会被终止。
1、将变更重定向到较早的修订
假定用户正在忙着做一些事情,不过突然发现自己在工作目录下的工作本应该属于另外一个分支,并且这些工作和当前分支上的上一个提交没有什么关联。例如,用户也许在master分支上开始处理bug修复工作,但是现在发现当前的工作还会影响维护分支maint。
这意味着bug修复的提交记录应该被加入更早的分支,起点可能是上述分支的共同祖先节点(或者是引入bug的某个位置)。这可能会导致master和maint分支合并相同的bug修复提交节点:
$ edit
$ git checkout -b bugfix-127
$ git reset --keep start
另外一种替代性方案是使用更简洁的git stash
命令:
$ edit
$ git stash
$ git checkout -b bugfix-127 start
$ git stash pop
4、隐藏暂存变更
一般来说,计划赶不上变化,当用户参与到一个项目中来时,经常需要临时保存一下当前的工作状态,然后处理其他的工作。git stash
命令是处理这类问题的好帮手。
暂存操作会保存用户凌乱无章的工作目录状态,该状态是指用户工作目录下已经发生变更的被跟踪文件(当然,用户还可以使用–include-untracked选项暂存未跟踪的文件)以及暂存区的状态,系统保存该状态之后,通过运行git reset --hard HEAD
命令,将会重置工作目录和索引,回退到最近一次提交的修订版本(为了匹配首部提交)。用户之后还可以随意地访问已经暂存的变更记录。
暂存记录是保存在堆栈上的:默认情况下,用户读取的是最后一次入栈的暂存变更(stash@{0}
)。当然,用户还可以查看暂存变更列表(使用git stash list
命令),并且可以显式访问某个特定的暂存变更记录。
4.1、使用git stash
如果用户不希望被其他工作打断太久,那么可以简单地将目前的工作暂存起来,处理完别的事务之后,再将暂存的记录恢复即可:
$ git stash
$ ... handle interruption ...
$ git stash pop
默认情况下,git stash pop
命令会恢复最后一次暂存的变更记录,如果恢复成功,那么该记录将会从堆栈中删除。若希望查看用户的暂存记录列表,可以使用git stash list
命令:
$ git stash list
stash@{0}: WIP on master: 049d078 Use strtol(), atoi() is deprecated
stash@{1}: WIP on master: c264051 Error checking for <number>
用户可以声明暂存名称作为参数访问任意历史暂存记录。例如,可以执行通过git stash apply stash@{1}
命令访问第二个记录,而且用户可以使用git stash drop stash@{1}
命令删除该记录(从暂存列表中删除)。git stash pop
命令只是apply+drop的快捷方式。
Git为暂存添加的默认描述信息非常有助于用户回忆是在哪里暂存的该记录(给定分支或者提交),但是无法告知用户当时具体做了些什么,以及暂存的内容是什么。不过用户可以在暂存列表中像使用diff命令那样通过git stash show -p
命令查看暂存列表细节。不过如果用户希望了解暂存变更记录后中断的细节,那么最好在保存当前状态的暂存记录中附加更详细的描述信息:
$ git stash save 'Add <count>'
Saved working directory and index state On master: Add <count>
HEAD is now at 049d078 Use strtol(), atoi() is deprecated
Git将会使用用户提供的信息描述已暂存的变更:
$ git stash list
stash@{0}: On master: Add <count>
stash@{1}: WIP on master: c264051 Error checking for <number>
有时候,当用户在目前工作的分支上执行git stash save
后,因为该分支上新增了太多变更,以致于出现无法顺利执行git stash pop
命令的现象,这主要是因为用户暂存变更后,新增了不少基于该修订的修订版本。如果用户希望在暂存变更之外再新建一个常规的提交,或者只是希望测试一下暂存的变更,那么可以使用git stash branch <分支名>
命令。这会在用户保存变更的那个修订版本的基础上新建一个分支并切换到该分支,恢复用户之前保存的变更,然后将上述暂存的变更在暂存列表中删除。
4.2、隐藏和暂存区
默认情况下,暂存操作会重置工作目录和暂存区状态到HEAD引用的版本。用户可以使用git stash
命令保留索引的状态,然后通过使用--keep-index
选项将工作区重置到暂存状态。
用户还可以使用git stash --patch
命令在将变更暂存之后,指定工作区的表现形式。
在恢复暂存变更时,Git系统一般会尝试只恢复工作区的暂存变更,然后将它们和当前工作目录状态整合(例如和暂存区的内容对应)。如果在整合过程中出现冲突,这些记录会被当作普通的索引存储到暂存区中,即使存在冲突,Git系统也不会丢弃暂存记录。
用户还可以使用–index选项恢复暂存区被隐藏的暂存记录,如果记录之间存在冲突,整合操作将不会成功执行(因为暂存区没有地方为这种冲突记录提供存储空间)。
4.3、暂存探幽
也许用户恢复了某些暂存变更之后,完成了某些工作,由于某些原因又希望撤销恢复的暂存变更。或者用户因为失误将部分暂存内容删除了,又或者清空了所有暂存记录(可以使用git stash clear
命令),现在希望恢复这些记录。又或者用户想看看暂存变更之后工作目录中的文件组织结构。例如,用户需要知道当创建一个暂存记录后,Git系统内部到底执行了哪些操作。
为了暂存用户的变更记录,Git系统会自动创建两个提交对象:一个是和索引有关的(暂存区),另一是和工作目录有关的。通过git stash --include- untracked
命令,Git系统会另外为未跟踪文件自动创建提交对象。
提交对象中包含工作目录下的工作进度,即暂存对象,并且将包含暂存区内容的提交对象作为其第二个父对象。提交对象中存放在了一个特别的引用中:refs/stash
。工作中(WIP)和索引的提交对象中都包含用户保存变更时的修订版本,并且将它当作第一个(仅对于索引提交对象来说)父对象。
我们可以使用git log --graph
命令或者gitk图形化工具查看它们:
$ git stash save --quiet 'Add <count>'
$ git log --oneline --graph --decorate --boundary stash ^HEAD
* 81ef667 (refs/stash) On master: Add <count>
|\
| * ed95050 index on master: 765b095 Added .gitignore
|/
o 765b095 (HEAD, master) Added .gitignore
$ git show-ref --abbrev
765b095 refs/heads/master
81ef667 refs/stash
这里我们不得不使用git show-ref
命令(我们还可以使用git for-each-ref
替代),这是因为git branch -a
命令只显示分支信息,但是不显示相关的引用信息。
当保存未跟踪变更记录时,情况和下列步骤类似:
$ git stash --include-untracked
Saved working directory and index state WIP on master: 765b095 Added\.gitignore
HEAD is now at 765b095 Added .gitignore
$ git log --oneline --graph --decorate --boundary stash ^HEAD
*-. bb76632 (refs/stash) WIP on master: 765b095 Added .gitignore
|\ \
| | * 1ae1716 untracked files on master: 765b095 Added .gitignore
| * d093b52 index on master: 765b095 Added .gitignore
|/
o 765b095 (HEAD, B) Added .gitignore
我们可以看到,未跟踪文件提交是WIP提交对象的第三个父提交,而且它没有父提交。这就是暂存的工作原理,但是Git系统如何维护暂存栈呢?
如果你之前注意过git stash命令的输出结果,其中的stash@{}表达式和reflog的类似,那么你应该已经猜到了,Git在引用日志中查找旧的暂存记录的方式是通过refs/stash引用实现的:
$ git reflog stash
81ef667 stash@{0}: On master: Add <count>
bb76632 stash@{1}: WIP on master: Added .gitignore
1、暂存记录撤销
接下来将会演示本小节的第一个示例:撤销以前执行git stash apply
命令的操作结果。一个可以满足上述需求的备选解决方案是修改暂存中和工作目录变更有关的补丁,然后反向应用它:
$ git stash show -p stash@{0} | git apply -R -
注意,git stash命令的-p选项会显示强制补丁而非变更摘要。我们可以使用git show -m stash@{0}
命令(-m选项是必需的,因为WIP提交在暂存中是以合并提交的形式存在的),或者也可以简单地使用git diff stash@{0}^1 stash@{0}
命令替代git stash show -p
。
2、恢复误删除的暂存
接下来将演示第二个示例:恢复误删除的暂存记录。如果它们仍然在版本库中,用户可以通过引用搜索所有不可达的提交对象和类似的暂存记录(它们使用严格模式并且附带了一个注释信息的合并提交对象)来恢复。
一个简化版的解决方案可能是这样的:
$ git fsck --unreachable |
grep "unreachable commit " | cut -d" " -f3 |
git log --stdin --merges --no-walk --grep="WIP on "
第一行是找到所有不可达的对象,第二行是过滤除了提交和与之对应的SHA-1码标识符之外的所有信息,第三行的意思是进一步过滤,只显示注释中包含"WIP on "的合并提交记录。
不过这个方案并不是完美无缺的,例如查找一个自定义注释信息的暂存记录(该记录是通过git stash save"信息"命令创建的)。