详解 Git 忽略文件 .gitignore

概述

用户的工作区(也称工作树)内部的内容既可以被 Git 跟踪,也可以不被它跟踪。被跟踪文件,顾名思义,Git 将会跟踪该文件的一系列变更记录。对于 Git 来说,如果一个文件存在于暂存区(也称索引),如果没有特别声明的话,那么该文件将会被系统跟踪,并作为下一个修订记录的一部分。用户添加文件后,系统跟踪这些文件的目的是将它们当作项目历史的一部分。

索引或者暂存区不仅可以用来告知 Git 系统跟踪哪些文件,而且可以作为新建提交的一种暂存器。而且索引或者暂存区可以帮助用户解决合并冲突。

通常对于某些独立文件或者某类文件来说,用户永远都不希望它们作为项目历史记录的一部分而存在,并且也不希望系统跟踪它们。这些文件可能是编辑器的备份文件,也有可能是项目编译系统自动生成的临时文件。

用户不希望 Git 系统自动添加上述文件,例如,在使用 git add :/(添加整个工作区)和 git add .(添加当前目录下的所有文件)命令批量添加文件时,或者使用 git add --all 命令更新工作区状态索引时。

另外一方面,用户又希望 Git 能够有效地防止将不必要的文件添加到系统中。同时用户还希望在执行 git status 命令后不显示这些文件,因为它们的数量可能会非常庞大。有时它们也可能会和一些新增的未知文件混在一起。因此用户希望这类文件特意不被跟踪,即忽略它们。

未跟踪和重新跟踪的文件

如果用户希望忽略某个以前被跟踪的文件,例如从手工生成 HTML 文件迁移到使用类似 Markdown 这样的轻量级标记语言时,用户通常需要在不将它们从工作目录中删除的情况下,将它们添加到忽略文件列表中,对它们取消跟踪。为此,用户可以使用 git rm--cached <文件名> 命令。为了添加(开始跟踪)一个特意不跟踪(即忽略)的文件,用户需要使用命令 git add -f

将文件刻意标记为不跟踪的

这种情况下,用户可以通过 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.oobj/file.o 这样的子目录。
  • 以一个反斜杠结尾的模式只匹配目录,否则它就是递归的(除非它还包含其他斜杠)。
    例如 auto/ 模式将会匹配顶层的 auto 目录以及 src/auto 目录,但是它不会匹配名为 auto 的文件(或者一个标记链接)。
  • 如果希望固定一个模式,并且确保它是非递归的,那么可以在其起始位置添加一个反斜杠进行转义。
    例如 /TODO 文件将会忽略当前层级的 TODO 文件,但是不会忽略子目录中的文件,例如 src/TODO
  • 包含斜杠的模式都是固化并且非递归的,通配符不会匹配作为目录分隔符的斜杠。如果用户希望匹配任意目录,那么可以使用两个连续的星号 ** 替换路径地址的某个部分(例如 **/foofoo/**foo/**/bar)。
    例如 doc/.html 匹配的是 doc/index.html 文件,但是无法匹配地址 doc/api/index.html。为了匹配 doc 目录下的任意 HTML 文件,用户可以使用 doc/**/*.html 模式(或者将 *.html 模式添加到 doc/.gitignore 文件中)。

用户还可以在模式前面加一个感叹号 ! 前缀使之失效,任何根据先前的规则被排除的文件,现在又再次被包含(不再被忽略)进来了。例如已经忽略所有生成的 HTML 文件,但是希望包含某个通过手工生成的文件,用户可以在 gitignore 文件中做如下设置:

# 忽略所有以 .html 结尾的文件
*.html

# welcome.html 文件除外
!welcome.html

注意,Git 基于性能方面的考虑不会排除某个目录,这意味着当父目录被排除后,用户不能将其目录下的某个文件再次包含到跟踪列表中。也就是说,为了将某个子目录作为例外被跟踪,必须做如下配置:

# 除了目录 t0001/bin 之外,将会排除所有文件
/
!/t0001
/t0001/
!/t0001/bin

为了匹配一个用 ! 开始的模式,必须使用一个反斜杠对其进行转义,例如模式 \!important!.md 是为了匹配 !important!.md

确定忽略文件类型

现在我们已经了解了如何将文件状态特意标记为不被跟踪的(忽略),那么接下来的问题是哪些(哪一类)文件应该被标记。另外一个问题是:上述文件的位置是什么?以及我们应该如何在 3 个 .gitignore 文件中声明需要忽略的文件类型?

首先,用户永远不应该跟踪自动生成的文件(通常是由项目的编译系统生成的)。如果用户将它们添加到版本库中了,那么它们很有可能无法和源文件保持同步。此外,它们也不是必须添加的文件,因为系统可以很容易地重新生成它们。唯一的例外是生成这些文件的源文件极少发生变动,并且生成它们需要额外的工具辅助,但是开发人员有可能没有这些工具(如果源代码经常发生变更,用户可以使用一个孤儿分支存放这些生成的文件,只在发布预览版程序时更新该分支)。

这些是所有开发人员都希望忽略的文件,因此它们应该被放到一个被跟踪的 .gitignore 文件中。模式列表将会是经由版本控制的,并且可以通过克隆副本分发给其他开发人员。我们可以在 https://github.com/github/gitignore 上找到和若干编程语言有关的一组非常有用的 .gitignore 模版。

提示:本站也提供了一个 .gitignore 模板生成工具,可以自动生成各类常见项目、开发语言、框架、IDE 所使用的 .gitignore 文件。

其次,临时文件和特定产品相关的用户工具链,这些内容通常都不会与其他开发人员共享。如果模式对版本库和用户都是有效的,例如在版本库中的辅助文件,并且该文件也会影响特定的工作流用户(例如该项目中 IDE 程序),那么它就应该被放到每个克隆的 $GIT_DIR/info/exclude 文件夹中。

在用户采用的通用忽略模式中,不必特意声明版本库(或者项目),一般来说可以通过 core.excludesFile 配置变量中进行声明,同时对于每个用户(全局)还可以在 ~/.gitconfig 或者 ~/.config/git/config 配置做相关设置。一般来说,默认的配置文件路径是 ~/.config/git/ignore

专属于每个用户的忽略文件应该不会是 ~/.gitignore,因为如果用户希望让 ~/directory ($HOME) 主目录保持版本控制,那么该文件有可能就会是用户主目录对应版本库中的 .gitignore 文件。

这里是编辑器或者 IDE 程序生成的备份或者临时文件对应的模式匹配文件所在之处。

已忽略文件通常也是无关紧要的

重要提醒:不要将重要资料添加到忽略列表中,这些资料通常是用户不希望在某个版本库中被跟踪的,但是内容相比忽略文件列表中的内容来说却是非常重要的!被 Git 忽略的这类文件既可以很容易地被重新生成(产品编译系统生成的中间文件),而且对于用户来说又是无关紧要的(临时文件或者备份文件)。

因此 Git 会认为这类已忽略的文件用处不大,当需要做一些清理工作时,即使在没有向用户提示的情况下也可能把它们删除,例如,如果已忽略文件和当前签出的修订内容有冲突时。

忽略文件列表

用户可以在执行 status 命令时使用 --ignored 选项查看被忽略的文件:

$ git status --ignored
On branch master

Ignored files:
  (use "git add -f <file>..." to include in what will be committed)

  .DS_Store

no changes added to commit (use "git add" and/or "git commit -a")

用户还可以使用清理被忽略文件的测试选项:git clean -Xnd 和底层(管道化)的命令 git ls-files:

$ git ls-files --others --ignored --exclude-standard
.DS_Store

后一个命令还可以用来显示匹配忽略模式的被跟踪文件。找到这类文件往往意味着某些文件需要被取消跟踪(也可能是源代码文件临时生成的中间文件),或者也可能是忽略模式覆盖的范围过于宽泛了。因为 Git 是通过暂存区(缓存)已有的文件来识别哪些文件需要被跟踪的,相关的命令如下:

$ git ls-files --cached --ignored --exclude-standard

底层命令(Plumbing)和高层命令(Porcelain)的区别

Git 的命令主要分为两种:一种是方便和用户交互的高层命令,另外一种是方便编写 shell 脚本的管道化底层命令。它们之间的区别在于:高层命令的输出结果可以变更并不断完善,例如在遇到与 HEAD 分离的情况下,执行 git branch 命令后的输出结果显示了其中的分离过程(无分支到与 HEAD 分离)。同时底层命令中也有选项(通常是 --porcelain)来决定是否选择无变更的输出结果。它们的输出结果和行为是可以根据主题配置的。

另外一个比较重要的区别是高层命令会尝试猜测用户的意图,并且会使用默认参数和默认配置。底层命令则没有那么智能,用户必须通过 --exclude-standard 这样的选项和 git ls-files 命令搭配使用,才能让它采用忽略文件的默认设置。

忽略跟踪文件内的变更

也许用户版本库中不少文件发生了变更,但是它们很少被提交。这类文件可以是若干本地配置文件,为了适应用户本地环境经过编辑配置,但是用户永远不希望将它们提交到远程上游分支。这也可以是某个包含新发布的预览版软件名称的文件,只有当它们添加了下一个预览版程序便签后才会被提交。用户可能希望让这类文件大部分时间都保持 “杂乱无章” 的状态,但是又不希望 Git 系统经常向用户提示这些文件发生了变更。为了防止干扰其他变更信息的提示,用户应该将这类信息忽略。

用户可以对 Git 进行配置,让它跳过对工作目录的检查(假定它始终是最新版本),并使用文件的暂存版本替代,可以对某个文件设定相应的 skip-worktree 标记来达到此目的。为此用户将会需要用到底层的 git update-index 命令,它相当于面向用户的高层命令 git add(用户可以使用 git ls-files 查看文件的状态和标记):

$ git update-index --skip-wroktree 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 statusgit diff 命令的输出结果中,与之相关的变更将不会被暂存或提交。

有时这是非常有用的,特别是在检查一个大型项目的文件变更时。不要对被跟踪的文件使用 --assume-unchanged 选项已达到忽略文件的目的。用户务必确保文件没有发生变更,不会发生欺骗 Git 系统的情况,例如 git stash save 命令将会根据你的设置进行保存,这样就可能失去用户本地文件的变更记录。

参考资料:

分享