guide

gitignore 的优先级与否定(!)模式

说明最后一条规则获胜的原理、.gitignore、info/exclude 与全局设置之间的优先级,以及父目录被排除后无法用 ! 恢复的原因和解决方法。

当 gitignore 规则没有按预期工作时,原因大多在于顺序父目录。本文整理 git 在多条规则中究竟遵循哪一条。

同一文件内:最后匹配的规则获胜

在一个 .gitignore 中,git 自上而下读取,由最后匹配的模式决定结果。如果该模式以 ! 开头则不忽略,否则忽略。

*.log
!important.log

important.log 同时匹配这两行,但最后一行是否定,因此会被跟踪。调换顺序,结果也随之反转。

!important.log
*.log

现在 important.log 最后匹配的是 *.log,因此被忽略。这就是用生成器合并多个模板时,选择顺序就是规则顺序的原因。例如同时使用 Java 模板的 *.jar 和 Gradle 模板的 !gradle-wrapper.jar 时,Gradle 必须排在后面。

多个文件之间的优先级

git 从以下四处读取规则,越靠上优先级越高。

  1. 从命令行接收的模式(git ls-files --exclude 等部分命令)
  2. 与路径位于同一目录或父目录中的 .gitignore — 更深处的文件覆盖上层文件
  3. $GIT_DIR/info/exclude (通常为 .git/info/exclude)
  4. core.excludesFile 设置所指向的文件(全局 gitignore)

例如根目录 .gitignore 中有 *.tmp,而 sub/.gitignore 中有 !*.tmp,则 sub/y.tmp 会按照下层文件的否定规则被跟踪。反之,即使在 .git/info/exclude 中写了 !x.gitignore 中的 x 优先,因此 x 仍被忽略。

$ git check-ignore -v x y.tmp sub/y.tmp
.gitignore:1:x	x
.gitignore:2:*.tmp	y.tmp
sub/.gitignore:1:!*.tmp	sub/y.tmp

最重要的例外:父目录被排除后无法恢复

git 文档是这样写的:如果文件的父目录被排除,就无法再重新包含该文件。 出于性能考虑,git 根本不会查看被排除目录的内部,因此针对其中文件的 ! 规则连读都不会被读取。

logs/
!logs/keep.txt

在这组规则下,logs/keep.txt 仍被忽略。运行 git check-ignore -v,显示的决定规则是 logs/

解决方法是排除目录的内容,而不是目录本身。

logs/*
!logs/keep.txt

logs/* 不匹配 logs 目录本身,因此 git 会进入目录,对 keep.txt 应用否定规则。Rails 模板的 /log/* + !/log/.keep、VS Code 模板的 .vscode/* + !.vscode/settings.json 都利用了这一原理。

要恢复更深的路径,需要把中间的目录也逐一恢复。

config/*
!config/app/
config/app/*
!config/app/defaults.yml

允许列表(allowlist)模式

如果不想列举要忽略的内容,而是只列举要跟踪的内容,也使用同样的原理。

# 忽略根目录下的一切
/*
# 只重新包含需要的内容
!/.gitignore
!/src/
!/README.md

/* 只匹配根目录中的每一项,并不排除根目录本身,因此 !/src/ 可以生效。在这种方式下,.gitignore 自身也会被 /* 匹配,所以不要忘记 !/.gitignore。实际上,如果没有这一行就运行 git status --ignored.gitignore 会出现在忽略列表中。

重复规则与顺序

同一条规则出现多次时,前面的那条不会影响结果,因为后面相同的规则总是更晚匹配。反之,删除后面的重复规则,如果中间有否定规则,就可能改变结果。

*.log
!debug.log
*.log

这里如果删除最后一行 *.logdebug.log 就会变为跟踪对象。本站的生成器会检测这种情况,仅在中间没有否定规则时删除后面的重复规则,其余的予以保留。

参考

← 上一篇.gitignore 模式语法全解