重新认识 .gitignore:目录级规则与 .git/info/exclude
我以前理解的 .gitignore 比较简单:用户级忽略文件处理个人习惯,项目根目录里的 .gitignore 处理团队共用的规则。
直到最近排查一个项目中的本地运行目录时,我才注意到,项目的子目录里也可以放 .gitignore。例如,.trellis/.gitignore 里有这样一条规则:
.runtime/它实际忽略的是 .trellis/.runtime/,不是仓库根目录下的 .runtime/。.gitignore 并不只有根目录那一份,而是可以随着目录层级分开维护。
.gitignore 到底在解决什么问题
.gitignore 告诉 Git,哪些尚未被跟踪的文件或目录不必作为提交候选。它会影响这些操作:
git statusgit add .git ls-files --others --exclude-standard它不会删除文件,也不会阻止程序生成文件,更不是安全保护。文件被忽略后,Git 默认不会把它作为待提交内容展示或加入索引。
这里最容易踩坑:已经被 Git 跟踪的文件,不会因为后来新增 .gitignore 规则就自动消失。
如果要停止跟踪一个已经提交过的文件,需要先把它从索引中移除,同时保留工作区文件:
git rm --cached path/to/file然后再把对应路径写入 .gitignore,后续它才不会重新进入提交候选。
目录级 .gitignore 的作用域
一个仓库可以有这样的结构:
repo/├── .gitignore├── src/│ └── .gitignore└── tools/ └── .gitignore判断 src/generated/cache.json 是否被忽略时,Git 会考虑仓库根目录、src/ 以及更靠近目标文件的目录中的 .gitignore。每份文件的规则都从它所在的目录开始,向下覆盖子目录。
例如,仓库根目录的 .gitignore:
# 只匹配仓库根目录下的 dist/dist/
# 没有斜杠,匹配当前目录及所有子目录中的日志文件*.logsrc/.gitignore:
# 只匹配 src/generated,不匹配仓库根目录的 generated/generated/这里的 / 是相对于当前 .gitignore 所在目录的。于是,src/.gitignore 中的 /generated/ 对应的就是 src/generated/。
这也是我看到 .trellis/.gitignore 后才真正意识到的地方。子目录里的忽略文件可以把该目录自己的构建产物、缓存目录或工具运行时文件放在离它们更近的地方,不必把所有细节都堆进根目录的 .gitignore。
Git 会从哪些地方读取忽略规则
Git 通常按下面的优先级读取排除规则,前面的来源优先级更高:
| 优先级 | 来源 | 适合放什么 |
|---|---|---|
| 1 | 命令行传入的排除参数 | 某一次命令的临时过滤条件 |
| 2 | 各级目录中的 .gitignore | 应随项目共享的规则,以及某个子目录自己的规则 |
| 3 | $GIT_COMMON_DIR/info/exclude | 当前本地仓库使用、但不想提交给团队的规则 |
| 4 | core.excludesFile 指向的文件 | 当前用户在所有仓库中都适用的规则 |
对于目录中的 .gitignore,Git 会同时考虑目标文件所在目录和父目录中的规则;更靠近目标文件的规则可以覆盖父目录中的规则。同一层级中,如果多个模式都匹配,后出现的模式决定最终结果。
所以,“全局忽略”“根目录忽略”和“子目录忽略”虽然都提供排除模式,但作用域、共享方式和优先级并不一样。
常见模式怎么解释
不带斜杠的模式
*.log只要文件名匹配,就可以命中当前 .gitignore 目录及其子目录中的日志文件,例如:
app.loglogs/app.logsrc/test/app.log以斜杠开头的模式
/tmp/它只匹配当前 .gitignore 所在目录下的 tmp 目录。
在仓库根目录的 .gitignore 中,它匹配 repo/tmp/;在 tools/.gitignore 中,它匹配 repo/tools/tmp/。
以斜杠结尾的模式
cache/末尾的斜杠表示只匹配目录,不匹配同名普通文件。
使用 ** 跨越目录
**/generated/这类模式可以匹配不同层级中的 generated 目录。比如,**/foo/bar 可以匹配 foo/bar、a/foo/bar 或更深层级的路径。
用 ! 取消忽略
*.log!keep.log这表示先忽略日志文件,再把 keep.log 重新纳入 Git 的候选范围。
但这里有一个限制:如果 keep.log 的父目录本身已经被忽略,Git 不会继续进入这个目录寻找可以恢复的文件。例如:
build/!build/keep.log通常不能达到预期,因为 build/ 已经让 Git 跳过了整个目录。可以先保留目录的可访问性,再排除其中不需要的内容:
build/*!build/keep.log复杂的 ! 规则最好用 git check-ignore 验证,不要只凭肉眼判断。
.git/info/exclude 是什么
.git/info/exclude 也是 Git 的忽略规则文件。它位于仓库的 .git 目录中,不会被提交,也不会随着普通的 git clone 复制给其他人。
它适合存放当前仓库的本地忽略配置:
- 规则只对当前仓库生效;
- 规则不会出现在提交记录中;
- 规则不会影响其他协作者;
- 它比全局忽略更具体,又不会污染项目公共的
.gitignore。
比如,只想在当前仓库忽略本地笔记和某个临时目录,可以编辑:
$EDITOR "$(git rev-parse --git-path info/exclude)"加入:
# 只属于我在这个仓库中的本地文件notes-local/review-draft.md如果当前仓库使用了 worktree,直接写死 .git/info/exclude 不一定稳妥。git rev-parse --git-path info/exclude 会返回当前仓库实际使用的 exclude 文件路径。
规则放在哪里,可以按下面的边界判断:
| 文件 | 是否提交 | 典型用途 |
|---|---|---|
项目根目录或子目录的 .gitignore | 是 | 团队都应该遵守的项目规则 |
.git/info/exclude | 否 | 仅当前仓库、仅本机工作流需要的规则 |
core.excludesFile 指向的文件 | 否 | 所有仓库都适用的个人规则,如 .DS_Store |
所有开发者都会生成的构建目录,应该写进项目 .gitignore;个人使用的编辑器临时文件,放进 .git/info/exclude;所有仓库都要忽略的 macOS .DS_Store,再考虑全局忽略文件。
如何查出究竟是哪条规则生效
遇到“为什么这个文件没有出现在 git status 中”时,先运行:
git check-ignore -v -- path/to/file输出通常会包含规则来源、行号、具体模式和目标路径,例如:
.trellis/.gitignore:8:.runtime/ .trellis/.runtime这比只盯着根目录 .gitignore 猜测可靠得多。
如果目标文件已经被跟踪,默认情况下 git check-ignore 可能不会把它当作普通的 ignored 文件展示。这时可以加上 --no-index,只按工作区的忽略规则检查:
git check-ignore -v --no-index -- path/to/file其他有用的排查命令:
# 查看被忽略的文件git status --ignored
# 查看全局忽略文件配置及其来源git config --show-origin --get core.excludesFile
# 查看当前仓库实际的 info/exclude 路径git rev-parse --git-path info/exclude几个容易误判的地方
忽略不是删除
.gitignore 不会删除已经存在的文件,也不会清理工作区。它只是改变 Git 对未跟踪内容的判断。
忽略不是安全措施
把 .env 写进 .gitignore,只能降低它被误提交的概率。如果密钥已经提交过,应该立即轮换密钥,并按仓库历史清理方案处理;不能把“已经写入 .gitignore”当成泄露风险已经解决。
规则写对了,文件也可能仍然显示
常见原因包括:文件已经被跟踪、模式相对于错误的 .gitignore 目录、父目录被排除导致 ! 无法生效,或者后面又有一条规则覆盖了前面的规则。
.git/info/exclude 不是团队配置
它很适合处理我的本地文件,但新同事重新克隆仓库后不会自动拥有这些规则。如果规则对所有人都重要,就应该提交到项目中的 .gitignore。
排查时按这个顺序看
处理 Git 忽略问题时,我通常按下面的顺序判断:
- 这个文件是否已经被跟踪?
- 目标文件所在目录及父目录中,是否存在
.gitignore? - 规则中的
/是相对于哪一份.gitignore? - 是否有更靠近目标文件的规则或后出现的
!规则覆盖它? - 是否还有
.git/info/exclude或全局core.excludesFile在生效? - 用
git check-ignore -v查看 Git 给出的证据。
这样排查,通常很快就能定位规则的来源:它可能来自某一级目录的 .gitignore,也可能来自当前仓库的 .git/info/exclude 或用户级配置。
顺带一提,目录级忽略文件并不是 Git 2.x 才增加的能力。Git 的早期实现中,v0.99.2 已经出现了按目录读取 exclude 文件的支持,早于 Git 1.0.0。只是我最近才真正把它用起来。
参考:
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!












