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 从以下四处读取规则,越靠上优先级越高。
- 从命令行接收的模式(
git ls-files --exclude等部分命令) - 与路径位于同一目录或父目录中的
.gitignore— 更深处的文件覆盖上层文件 $GIT_DIR/info/exclude(通常为.git/info/exclude)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
这里如果删除最后一行 *.log,debug.log 就会变为跟踪对象。本站的生成器会检测这种情况,仅在中间没有否定规则时删除后面的重复规则,其余的予以保留。
参考
- gitignore 官方文档:PATTERN FORMAT, NOTES
- git-check-ignore 官方文档
- 核实日期:2026-09-23 (用 git 2.50.1 验证示例)