背景
用 Git 这么多年,"忽略文件"这件事我其实一直在用,但从来没有系统搞清楚过:
.gitignore当然熟,但git status里那个红彤彤的"未跟踪文件列表"里,总有几个我其实不想 commit、也不希望别人看到的东西- 偶尔 clone 同事的仓库,发现他的
.gitignore和我的不一样——比如 IDE 配置文件、个人的调试脚本 - 跟踪之后才发现"这个文件其实不该进版本库",但
git rm又会真的删掉它 - 听说过
--assume-unchanged和--skip-worktree,但一直没分清它们的区别
本文把 Git 里所有"本地忽略"相关的机制一次性理清楚 🎯
🤔 为什么要"本地忽略"
最常见的几个场景:
- 个人配置文件:项目根目录的
config.local.json、.env.local,不该入库 - IDE 配置文件:
.idea/、.vscode/settings.json,但项目级的.vscode/launch.json又想共享 - 构建产物:
build/、dist/、node_modules/,这些肯定不该入库 - 临时调试脚本:
debug_xxx.py、scratch.sql,只想本地跑跑 - 跟踪后又反悔:某个文件已经 commit 了,但后来发现"它在我这的环境下要改一改,不该影响仓库"
这些需求对应的其实是完全不同的几套机制——选错了,要么文件被误推送,要么本地永远摆脱不了红字。
🗂️ 五大机制一览
先放一张总览表,方便后面对照:
| 机制 | 作用对象 | 是否随仓库推送 | 是否全局 | 适用场景 |
|---|---|---|---|---|
.gitignore | 未跟踪文件 | ✅ 是 | ❌ 否(可全局) | 项目级忽略约定 |
.git/info/exclude | 未跟踪文件 | ❌ 否 | ❌ 否 | 个人在某个仓库内的本地忽略 |
core.excludesFile(全局 gitignore) | 未跟踪文件 | ❌ 否 | ✅ 全部仓库 | 本机所有仓库的通用忽略 |
git update-index --skip-worktree | 已跟踪文件 | ❌ 否 | ❌ 否 | 长期"假装我改了但其实没改" |
git update-index --assume-unchanged | 已跟踪文件 | ❌ 否 | ❌ 否 | 短期性能优化,已被 skip-worktree 取代 |
核心区分点:前三种针对"未跟踪文件"(untracked),后两种针对"已跟踪文件"(tracked)。一旦把文件
git add进了仓库,前三种就再也管不了它了。
机制一:.gitignore(项目级约定)
这是大家最熟悉的,但也最容易和"本地忽略"搞混的。
它是什么
放在仓库根目录(或任意子目录)的纯文本文件,每行一条规则。Git 会在提交、扫描时自动读取,符合规则的文件会被排除。
# 注释行以 # 开头
*.log
*.tmp
build/
dist/
.env
.idea/
.vscode/
node_modules/
.DS_Store语法速览
| 写法 | 含义 |
|---|---|
*.log | 所有以 .log 结尾的文件 |
!important.log | 否定,前面规则被覆盖,重新包含 |
build/ | 名为 build 的目录 |
**/temp | 任意层级下的 temp(文件或目录) |
/build | 仅仓库根目录的 build,不递归 |
doc/*.txt | doc/ 下的 .txt,但不包括 doc/sub/x.txt |
doc/**/*.txt | doc/ 下任意层级的 .txt |
? | 单个字符(*.? 匹配 .a、.b 但不匹配 .ab) |
[abc] | 字符集 |
⚠️ 路径默认是相对于
.gitignore所在目录的。例如src/.gitignore里的*.log只对src/下生效。
关键特性
- 会被
git add和git commit跟踪:.gitignore本身就是一个普通文件,它会随仓库被推送 - 适合放**"项目层面的忽略约定"**——比如"这个项目所有人不该提交
build/"
常见陷阱
已经跟踪过的文件,.gitignore 改不了
.gitignore 只能管未跟踪的文件。如果 build/xxx.log 已经被 git add 过,把它加进 .gitignore 不会让它消失——git status 依然会显示它被修改了。
这种情况下要"脱钩",见后文的 git rm --cached 或 skip-worktree。
# 把"已经跟踪但其实不该入仓库"的文件从索引中移除(但保留在工作区)
git rm --cached build/xxx.log
git rm --cached -r build/ # 整个目录--cached 的意思是"只从 Git 索引里删,磁盘上的真实文件保留"。改完之后这个文件变成"未跟踪"状态,从此 .gitignore 就能管住它了。
适用与不适用
- ✅ 团队约定的"所有人都不该 commit 的东西"
- ❌ 个人专属的本地调试脚本(一旦 commit 上去同事也会被影响)
机制二:.git/info/exclude(仓库内个人忽略)
这个是被严重低估的机制——很多人知道 .gitignore,但不知道 .git/exclude。
它是什么
每个 Git 仓库的 .git/ 目录下都有一个 info/exclude 文件。语法和 .gitignore 完全一样,但有两大关键区别:
- 它在
.git/目录里,Git 默认不会跟踪.git/——所以这个文件永远不会被推送 - 它只对当前这一个仓库生效
# 本地调试脚本
debug_*.py
scratch.sql
# 个人 IDE 配置
.idea/workspace.xml
.vscode/launch.local.json
# 我自己用的临时文件
*.local
my-notes.md为什么这是"本地忽略"的真正答案
考虑这样一个场景:
团队项目的
.gitignore里没有写*.local,但我自己的工作流会在仓库里产生config.local。我不想:
- 修改
.gitignore然后 commit 上去(影响其他同事的判断)- 手动
git status时一直被这个红字烦- 哪天手滑
git add .把它推上去
——.git/info/exclude 就是为这种场景设计的。
与 .gitignore 的对比
| 维度 | .gitignore | .git/info/exclude |
|---|---|---|
| 位置 | 工作区(可任意子目录) | .git/info/ |
| 是否入库 | ✅ 是 | ❌ 否(.git/ 整个目录都不入库) |
| 作用域 | 当前仓库所有协作者 | 只有你自己 |
| 优先级 | 同级目录优先 | 一般最后回退到 |
| 适合放 | 团队公共约定 | 个人临时忽略 |
适用与不适用
- ✅ 个人在某个仓库的临时忽略,但又不想影响团队
.gitignore - ✅ clone 下来的开源项目,想加点个人调试用的忽略规则
- ❌ 需要在多个仓库之间共享的忽略规则(用
core.excludesFile)
机制三:core.excludesFile(全局 Git 忽略)
如果说 .git/info/exclude 是"仓库内个人忽略",那 core.excludesFile 就是**"本机所有仓库的全局忽略"**。
它是什么
core.excludesFile 是一个 Git 配置项,指向本机某个文件。该文件语法和 .gitignore 完全一样,但对所有仓库生效。
# 设置全局排除文件(路径自定义)
git config --global core.excludesFile ~/.gitignore_global
# Windows 下
git config --global core.excludesFile "%USERPROFILE%\.gitignore_global"设置完之后,所有仓库都会自动读取这个文件的内容。
常见内容
这个文件里通常放**"永远不会因项目不同而变化"的忽略项**——也就是和"你这个人"绑定的、和项目无关的东西:
# 操作系统产生的垃圾
.DS_Store
Thumbs.db
desktop.ini
# 编辑器 / IDE 个人配置
.idea/workspace.xml
*.swp
*.swo
*~
.vscode/launch.local.json
# 各种语言的临时文件
*.pyc
__pycache__/
*.class
*.o
*.obj关键特性
- 不和任何仓库绑定——
git config --global写在你用户的~/.gitconfig里,不属于任何项目 - 适合放"所有项目我都不想看到 X"这种跨项目偏好
- 可以和
.gitignore同时存在,规则会叠加
适用与不适用
- ✅ 操作系统垃圾文件(
.DS_Store、Thumbs.db)——你绝不可能希望任何项目跟踪它 - ✅ 个人 IDE 的私有配置(不打算 commit 的那部分)
- ❌ 特定项目特有的构建产物(如某个项目用
output/,另一个用dist/)——这种应放项目.gitignore
机制四:git update-index --skip-worktree(跟踪文件的本地忽略)
从这一节开始,进入了针对"已跟踪文件"的领域——这是 .gitignore 系列完全管不到的。
场景
某个文件已经 commit 进仓库了,但在你本地需要临时改一改,改了之后既不想 commit、也不想
git status一直提示你。
典型场景:
config.json:项目里有个"默认配置",但你本地要改成你自己的密钥/路径package-lock.json:团队用的,但你本地跑npm i会改它- 某个 CI 配置文件:你本地跑一个测试版本,但 push 上去的应该是原始版本
命令
# 让 Git 假装"工作区里这个文件没动过"
git update-index --skip-worktree path/to/file
# 一次性跳过多个
git update-index --skip-worktree config.local.json之后你随便改这个文件,git status 都不会再报告它被修改,直到你撤销这个标志。
# 查看当前哪些文件被标记为 skip-worktree
git ls-files -v | grep '^S'
# 撤销标记
git update-index --no-skip-worktree path/to/file关键特性
- 不会修改仓库的 HEAD 状态——远程和其他协作者完全感知不到
- 是 Git 专门为"已跟踪文件的本地修改" 设计的功能
git stash、git checkout行为会更复杂(见下文陷阱)
适用与不适用
- ✅ 已跟踪文件,但你本地的修改纯属个人偏好、不应该影响仓库
- ✅ 想长期"假装"某个文件没改(例如我每次开机都会改
config.local.json) - ❌ 仅为了性能(让
git status变快)——这是--assume-unchanged的旧用途,且现在已经基本不需要
陷阱
慎用 git stash / git checkout
被 skip-worktree 标记的文件,行为会变得反直觉:
git stash可能不会把它的修改存起来git checkout时,Git 不知道该用工作区版本还是 HEAD 版本
实际工程中建议:如果你只是想"短期屏蔽"自己手抖的修改,用 stash + 忽略;如果是"长期想让它完全消失",用 skip-worktree。
机制五:git update-index --assume-unchanged(已基本被取代)
这是 Git 早期提供的一个机制,原本目的是让 git status 跑得更快。
它是什么
# 告诉 Git:"假设这个文件没改,别去检查"
git update-index --assume-unchanged path/to/file
# 查看当前哪些文件被标记
git ls-files -v | grep '^[[:lower:]]'
# 撤销
git update-index --no-assume-unchanged path/to/file它和 skip-worktree 的区别
两者表面上看几乎一样——都是"让 Git 假装文件没动"。但核心区别在于:
| 维度 | --assume-unchanged | --skip-worktree |
|---|---|---|
| 设计目的 | 性能(让 git status 跳过 stat) | 语义("工作区改动不该入库") |
| 适用对象 | 适合"Git 自己别去管它" | 适合"我故意改了但不入库" |
| 行为差异 | 一些 Git 命令会强制重新检查它 | Git 承诺继续"假装没改" |
| 现代建议 | ⚠️ 有缺陷,已不推荐 | ✅ 推荐 |
Git 官方文档(
git update-index --help)原话:"This option is meant for performance reasons. ...--skip-worktreeis a better alternative"。
实际踩坑
- 很多 Git 命令会忽略 assume-unchanged 标志,导致它"突然失效"——比如某些
git pull流程会重新对比文件 - 多人协作时容易混乱:你假设它没改,别人 push 时它就可能覆盖你的工作
- 在分支切换时行为不稳定
结论
⚠️ 新项目里不要用
--assume-unchanged。如果你看到老脚本或老教程推荐它,统一改成--skip-worktree。它们的命令几乎一样,但--skip-worktree在所有 Git 命令下都表现一致。
🔀 五种机制如何选择
把上面的内容浓缩成一张决策图:
你的文件"该不该进仓库"?
│
├─ 不该进,且没进 → 未跟踪文件
│ │
│ ├─ 这条规则属于"整个团队" → 写 .gitignore(commit 上去)
│ ├─ 这条规则只属于"我,在当前仓库" → 写 .git/info/exclude
│ └─ 这条规则属于"我,在所有仓库" → 写 core.excludesFile
│
└─ 不该进,但已经进了 → 已跟踪文件
│
├─ 想长期"假装没改" → git update-index --skip-worktree
└─ 想要"脱钩"(彻底从仓库移除) → 先 git rm --cached,然后加进 .gitignore真实场景对照
| 场景 | 推荐机制 |
|---|---|
项目里 node_modules/ 不该入库 | .gitignore |
我本地有 debug_xxx.py 不该入库 | .git/info/exclude |
永远不想看到 .DS_Store | core.excludesFile |
团队项目的 config.example.json 跟踪了,但我本地改成 config.local.json 想保留本地版 | --skip-worktree |
我提交时误把 secret.json 加了进去 | git rm --cached + 立刻轮换密钥 + 加进 .gitignore |
🛠️ 一些实用小技巧
1. 调试"为什么没被忽略"
# 查看某个文件被哪条规则命中了
git check-ignore -v path/to/file
# 同时检查全局和本地规则
git check-ignore -v --global path/to/file这个命令会打印出"忽略它的是哪一行规则"——配 .gitignore 的时候特别有用。
2. 一次性把"已跟踪但不该跟踪"的文件清出索引
# 先看看会处理哪些文件
git rm -r --cached --dry-run build/
# 确认无误后真正执行
git rm -r --cached build/3. 找出仓库里"我标记为 skip-worktree 的所有文件"
git ls-files -v | grep '^S '输出会类似:
S config.local.json
S docs/notes.md4. 给"已被忽略"的目录加例外
# 忽略所有 .log
*.log
# 但不忽略 important.log
!important.log! 前缀的规则优先级低于之前的同名规则——所以要让例外生效,必须先写"忽略",再写"不忽略"。
📌 容易忽略的细节
.gitignore的"否定规则"!必须显式覆盖前面规则——不能跨段落影响.gitignore是只对未跟踪文件生效;已跟踪文件改了,.gitignore写一万行也没用.git/info/exclude的语法完全等价于.gitignore,可以无缝切换core.excludesFile路径最好用绝对路径(~在 Windows 上 Git 解析不一定正确)--skip-worktree标记在 克隆新仓库时不会自动带上——它是本地的、随.git/index的- 被
skip-worktree标记的文件,commit 前请确认:否则你 commit 的就是"假装没改的版本",推送上去别人看到的是错的
一句话总结
Git 提供了 5 套独立的"忽略"机制,按"未跟踪 / 已跟踪"和"个人 / 团队 / 全局"两个维度划分——选错机制不会报错,但会让你每天被
git status的红字骚扰,或者把不该推送的文件推上去 ✅
理解这 5 套机制的适用对象和作用域,基本就能应对工程里 99% 的"忽略"需求了。