Skip to content

背景

用 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.pyscratch.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 会在提交、扫描时自动读取,符合规则的文件会被排除

text
# 注释行以 # 开头
*.log
*.tmp
build/
dist/
.env
.idea/
.vscode/
node_modules/
.DS_Store

语法速览

写法含义
*.log所有以 .log 结尾的文件
!important.log否定,前面规则被覆盖,重新包含
build/名为 build 的目录
**/temp任意层级下的 temp(文件或目录)
/build仅仓库根目录的 build,不递归
doc/*.txtdoc/ 下的 .txt但不包括 doc/sub/x.txt
doc/**/*.txtdoc/ 下任意层级的 .txt
?单个字符(*.? 匹配 .a.b 但不匹配 .ab
[abc]字符集

⚠️ 路径默认是相对于 .gitignore 所在目录的。例如 src/.gitignore 里的 *.log 只对 src/ 下生效。

关键特性

  • 会被 git addgit commit 跟踪.gitignore 本身就是一个普通文件,它会随仓库被推送
  • 适合放**"项目层面的忽略约定"**——比如"这个项目所有人不该提交 build/"

常见陷阱

已经跟踪过的文件,.gitignore 改不了

.gitignore 只能管未跟踪的文件。如果 build/xxx.log 已经被 git add 过,把它加进 .gitignore 不会让它消失——git status 依然会显示它被修改了。

这种情况下要"脱钩",见后文的 git rm --cachedskip-worktree

bash
# 把"已经跟踪但其实不该入仓库"的文件从索引中移除(但保留在工作区)
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/——所以这个文件永远不会被推送
  • 它只对当前这一个仓库生效
text
# 本地调试脚本
debug_*.py
scratch.sql

# 个人 IDE 配置
.idea/workspace.xml
.vscode/launch.local.json

# 我自己用的临时文件
*.local
my-notes.md

为什么这是"本地忽略"的真正答案

考虑这样一个场景:

团队项目的 .gitignore没有*.local,但我自己的工作流会在仓库里产生 config.local。我不想:

  1. 修改 .gitignore 然后 commit 上去(影响其他同事的判断)
  2. 手动 git status 时一直被这个红字烦
  3. 哪天手滑 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 完全一样,但对所有仓库生效

bash
# 设置全局排除文件(路径自定义)
git config --global core.excludesFile ~/.gitignore_global

# Windows 下
git config --global core.excludesFile "%USERPROFILE%\.gitignore_global"

设置完之后,所有仓库都会自动读取这个文件的内容。

常见内容

这个文件里通常放**"永远不会因项目不同而变化"的忽略项**——也就是和"你这个人"绑定的、和项目无关的东西:

text
# 操作系统产生的垃圾
.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_StoreThumbs.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 上去的应该是原始版本

命令

bash
# 让 Git 假装"工作区里这个文件没动过"
git update-index --skip-worktree path/to/file

# 一次性跳过多个
git update-index --skip-worktree config.local.json

之后你随便改这个文件,git status不会再报告它被修改,直到你撤销这个标志。

bash
# 查看当前哪些文件被标记为 skip-worktree
git ls-files -v | grep '^S'

# 撤销标记
git update-index --no-skip-worktree path/to/file

关键特性

  • 不会修改仓库的 HEAD 状态——远程和其他协作者完全感知不到
  • 是 Git 专门为"已跟踪文件的本地修改" 设计的功能
  • git stashgit 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 跑得更快

它是什么

bash
# 告诉 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-worktree is a better alternative"

实际踩坑

  • 很多 Git 命令会忽略 assume-unchanged 标志,导致它"突然失效"——比如某些 git pull 流程会重新对比文件
  • 多人协作时容易混乱:你假设它没改,别人 push 时它就可能覆盖你的工作
  • 在分支切换时行为不稳定

结论

⚠️ 新项目里不要用 --assume-unchanged。如果你看到老脚本或老教程推荐它,统一改成 --skip-worktree。它们的命令几乎一样,但 --skip-worktree 在所有 Git 命令下都表现一致。


🔀 五种机制如何选择

把上面的内容浓缩成一张决策图:

text
你的文件"该不该进仓库"?

├─ 不该进,且没进 → 未跟踪文件
│  │
│  ├─ 这条规则属于"整个团队" → 写 .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_Storecore.excludesFile
团队项目的 config.example.json 跟踪了,但我本地改成 config.local.json 想保留本地版--skip-worktree
我提交时误把 secret.json 加了进去git rm --cached + 立刻轮换密钥 + 加进 .gitignore

🛠️ 一些实用小技巧

1. 调试"为什么没被忽略"

bash
# 查看某个文件被哪条规则命中了
git check-ignore -v path/to/file

# 同时检查全局和本地规则
git check-ignore -v --global path/to/file

这个命令会打印出"忽略它的是哪一行规则"——配 .gitignore 的时候特别有用。

2. 一次性把"已跟踪但不该跟踪"的文件清出索引

bash
# 先看看会处理哪些文件
git rm -r --cached --dry-run build/

# 确认无误后真正执行
git rm -r --cached build/

3. 找出仓库里"我标记为 skip-worktree 的所有文件"

bash
git ls-files -v | grep '^S '

输出会类似:

text
S config.local.json
S docs/notes.md

4. 给"已被忽略"的目录加例外

text
# 忽略所有 .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% 的"忽略"需求了。