语言:简体中文
开发工具

Gitignore Debugger

准确找出 Git 没有忽略你的文件的原因。

粘贴文件路径和你的 .gitignore 规则。我们会告诉你哪条规则匹配,以及为什么。

全部在你的浏览器中运行。你的文件永远不会被上传。

什么是 .gitignore 文件?

.gitignore 文件是一个文本文件,用来告诉 Git 哪些文件不纳入版本控制。每一行都是一个模式 —— *.lognode_modules/.env —— 任何匹配到某个模式的未跟踪路径都会从 git status 中消失,被 git add . 跳过,也永远不会被提交。

这句话的分量几乎全落在 未跟踪 这个词上;单凭它就能解释比所有模式写错加起来还多的“gitignore 不起作用”的情况。Git 只会为它还没有跟踪的文件去查 .gitignore。一个文件一旦被提交过,之后无论你往 .gitignore 里加什么,Git 都会一直继续跟踪它。

Gitignore Debugger 能做什么

粘贴文件路径和你的 .gitignore 内容:这个 .gitignore 调试器会给出与 Git 相同的判定 —— 忽略或未忽略 —— 并指出做出这一判定的确切规则和行号。接着它会按 Git 求值的顺序列出所有匹配该路径的规则,并标出真正起决定作用的那一条。

最后这一点,正是普通 gitignore 检查工具所缺少的。知道一个文件被忽略了,很少是难点。难的是知道你那四十条规则里究竟是 哪一条 做的,以及你原本以为会生效的那条为什么输了。这在两个方向上都有用:既能看出你想提交的文件为什么被 Git 忽略,也能看出你想清掉的文件为什么没被忽略。

引擎重新实现了 Git 的模式匹配规则:按基名匹配的模式与锚定模式的区别、* 绝不跨越 /** 的三种形式、只匹配目录的结尾 /、字符类,以及经过转义的行尾空格。它的行为是对照 git check-ignore 的真实输出验证过的,而不是凭猜测。

使用方法

  1. 按路径在仓库中的实际写法输入,相对于仓库根目录 —— 写 src/config/local.json,而不是 C:\projects\app\src\config\local.json。请使用正斜杠;Windows 的反斜杠会自动为你转换。
  2. .gitignore 整个粘贴进来。不要只留下你怀疑的那一条规则 —— 真正的问题通常出在规则之间的优先级上,而删减文件恰恰会丢掉这部分信息。
  3. 先看判定,再看下面的规则链条。不起作用的 gitignore 规则,就是在这里不再是个谜的。

如果要针对目录而不是文件来测试 gitignore 规则,请让路径以斜杠结尾 —— 比如 build/。这个区别很重要,因为以 / 结尾的规则只匹配目录。

为什么 gitignore 规则有时不起作用

大致按出现频率排列,元凶如下:

gitignore 规则的优先级:最后一次匹配胜出

这就是那个让所有人都栽跟头的地方。在同一个 .gitignore 里,决定一个路径命运的是 最后 一个匹配它的模式,而不是第一个。下面这两个文件包含的规则完全相同,对 important.log 却给出相反的结果:

*.log
!important.log        → important.log 会被提交
!important.log
*.log                 → important.log 会被忽略

所以把宽泛的规则放在前面,例外放在后面。在多个文件之间,位于子目录中的 .gitignore 对其下的路径会胜过上层的那一个。Git 还会读取仅属于本仓库、且永远不会被提交的 .git/info/exclude,以及你的全局 core.excludesFile。一条你在任何 .gitignore 里都找不到的规则,往往就藏在这两处之一 —— git check-ignore -v 会显示是哪个文件的哪一行做出的决定。

否定规则,以及 ! 为什么经常失效

在模式前加上 !,可以把先前某条规则忽略掉的内容重新包含回来。这件事有两个限制,而第二个几乎解释了所有“gitignore 的否定不起作用”的情况。

第一,必须先有东西忽略过这个路径。单独写一条 !important.log 什么用也没有,因为压根就没有需要覆盖的对象。

第二点则意外得多:你无法把一个位于被忽略目录中的文件重新包含回来。下面这种写法不起作用,而且调换顺序也救不了:

build/
!build/keep.txt

build/ 被忽略时,Git 根本不会进去。它不会去查看 build/keep.txt,因此永远走不到第 2 行。改成忽略目录的内容,这样目录本身仍然可以被遍历:

build/*
!build/keep.txt

如果要救回更深处的东西,中间的每一层目录也都必须保持可访问 —— 先 build/*,再 !build/assets/,再 !build/assets/logo.svg

被忽略的上层目录

Git 的目录遍历会在遇到的第一个被忽略的目录处停止,其下的一切都会直接继承这个判定,而不会被逐一检查。这就是为什么一个被忽略的上层目录,会胜过你为文件本身写的任何规则 —— 不管那条规则看起来多么具体。

遇到这种情况时,调试器不会只说文件被忽略了,而会指出是哪个上层目录挡住了遍历;同时它仍会显示你为该文件写的否定规则,并标记为不可达,因为 Git 从来没有走到那么深、根本不会考虑它们。先把目录修好,下面的规则自然就又能生效了。

常见的 .gitignore 错误

几个实际例子

在这些规则下,路径 src/logs/debug.log 没有被忽略。否定是最后一次匹配,而且没有任何上层目录挡住遍历,所以它被走到了,并且胜出:

*.log
!src/logs/debug.log

在这些规则下,路径 build/keep.txt 被忽略了,原因是第 1 行的 build/ 匹配了它的上层目录。第 2 行永远不会触发:

build/
!build/keep.txt

在这条规则下,路径 docs/api/notes.md 没有被忽略,因为这个模式被它的斜杠锚定了,而 * 不会跨过 api/notes.md 里的那个斜杠。本意要写的是 docs/**/*.md

docs/*.md

把上面任意一个例子粘进页面顶部的 gitignore 测试器,就能看到完整的规则链条。回到终端里,git check-ignore -v path/to/file 就是内置的对应命令 —— 它会指出做出决定的来源文件、行号和模式,值得记住。

常见问题

Git 为什么忽略了我的文件?

因为有某个模式匹配到了它,而且往往不是你正在看的那一个。常见的元凶是子目录里的 .gitignore、被忽略的上层目录,或者你的全局排除文件。运行 git check-ignore -v path/to/file:它会准确打印出做出决定的来源文件、行号和模式。把路径和规则粘进上面的调试器能看到同样的结果,还会额外显示那些匹配到了却输掉的规则。

我的 .gitignore 规则为什么不起作用?

大致按频率排列,四个原因几乎覆盖了所有情况:文件已经被跟踪,所以规则被完全跳过;后面的规则把它覆盖了,因为最后一次匹配胜出;某个上层目录被忽略,所以 Git 根本没走到这条规则;或者模式的锚定方式和你想的不一样 —— 只要斜杠出现在结尾以外的位置,模式就会被锚定到包含该 .gitignore 的目录。

! 为什么没有把我的文件重新包含回来?

要么在它之前没有任何东西忽略过这个路径,于是这条否定没有可覆盖的对象;要么 —— 可能性大得多 —— 这个文件位于一个被忽略的目录里。Git 不会进入被忽略的目录,所以它从未见到这个文件,也从未对你的 ! 规则求值。

请忽略目录的内容,而不是目录本身:把 build/ 换成 build/*,把 !build/keep.txt 留在它后面,否定就能被走到了。

.gitignore 是最后一条规则胜出吗?

是的。在一个文件内部,决定路径命运的是最后一个匹配它的模式 —— 既不是第一个,也不是最具体的那个。*.log 后面接 !important.log 会保住 important.log;把这两行调换,它就被忽略了。请把宽泛的规则放在前面,例外放在后面。

为什么一个被忽略的文件夹会让否定规则失效?

因为 Git 的目录遍历会在第一个被忽略的目录处停止。当 build/ 匹配时,Git 会把整棵子树标记为忽略,而不去列出里面有什么,所以 !build/keep.txt 并不是被覆盖了,而是根本没被走到。调换顺序毫无用处;唯一有用的是让这个目录重新变得可遍历,而这正是 build/* 所做的。

怎么测试一条 .gitignore 规则?

把文件路径和你的规则粘进本页顶部的调试器。它会给出判定、起决定作用的规则和行号,以及一路上所有匹配到的规则 —— 包括那些原本就不可能生效的否定规则。

在真实仓库里,git check-ignore -v path/to/file 会指出负责的来源文件、行号和模式,而 git status --ignored 会列出当前被忽略的所有内容。

.gitignore 对已经被跟踪的文件有效吗?

无效。.gitignore 只作用于未跟踪的路径。一个文件一旦被提交过,Git 就会继续跟踪它,而规则会被跳过 —— 这是一条完全正确的规则看起来毫无作用的最常见原因。要在不删除文件的前提下停止跟踪它,请运行 git rm --cached path/to/file 并提交这次改动。

有一点很容易误导人:即使文件正被跟踪,git check-ignore 仍会报告匹配,因为它只测试模式,并不查看索引。这条命令回答“是的,被忽略”,并不意味着 Git 已经停止跟踪该文件。

.gitignore 的规则里可以有空格吗?

中间可以。my logs/ 能匹配名字里带空格的目录,完全不需要转义。行尾的空格是另一回事:Git 会把它们裁掉,所以 *.log 后面跟一个空格,行为和 *.log 完全一样。如果要匹配真的以空格结尾的文件名,请转义它 —— 写成 foo\

而行首的空格是有意义的,永远不会被裁掉:一条不小心缩进了的规则,只会匹配以那个空格开头的路径。如果某条规则莫名其妙毫无反应,检查一下是不是多了缩进。

除了 .gitignore,Git 还会去哪里找排除规则?

还有另外三处,按以下优先级依次查阅:首先是 .gitignore 文件本身,越深的目录优先级越高;然后是仅属于本仓库、永不提交的 .git/info/exclude;再然后是 core.excludesFile 所指向的位置,也就是你的全局排除列表。

所以一条你在仓库里到处都找不到的规则,通常就在后两者之一。git config --get core.excludesFile 能查出全局文件的路径,而 git check-ignore -v 会指出真正做出决定的那个文件。

*** 有什么区别?

* 匹配除 / 以外的任意字符序列,所以它永远不会离开单个路径片段。src/*/test.js 恰好只往下一层。

** 可以跨越分隔符,但只在 Git 认可的三个位置上有效 —— 开头的 **/ 表示任意深度,结尾的 /** 表示某个目录里的全部内容,中间的 /**/ 表示零个或多个目录。在其他位置,比如 a**b 里,多出来的星号就和单个 * 表现一样。