Skip to content
This page has been auto-translated and may contain errors.View in English

忽略文件与良好习惯 ​

docs.scrimba.com

你克隆了一个叫 weather-app 的项目,运行了 npm install,然后查看新仓库的状态。

bash
$ git status
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	node_modules/
	dist/
	.env

有三个文件夹和文件被标记为未跟踪,而它们都不该出现在提交里。node_modules/ 是几千个文件,npm install 几秒钟就能从 package.json 重新生成出来。dist/ 是构建产物,每次都会从你的源码重新构建出来。而 .env 里存着一个 API 密钥。这些东西永远都不该进入项目的历史记录,可要是不假思索地敲一句 git add .,恰恰就会把它们全带进去。

告诉 Git 该忽略什么 ​

.gitignore 文件列出了 Git 永远不该跟踪的文件和文件夹的匹配模式。你只需要在项目根目录(也就是你在你的第一个仓库中运行 git init 建立仓库的那个位置)创建一次这个文件,从那以后,git status 和 git add . 就会悄悄跳过所有匹配的内容。

bash
# .gitignore
node_modules/
dist/
.env

每一行都是一个匹配模式:像 dist/ 这样的普通文件夹名会忽略项目中任何位置出现的整个同名文件夹。 .gitignore 文件本身要提交进仓库。它体积很小,对每一个克隆这个项目的人都有用,理应像你的源码一样被写进历史记录。

bash
$ git status
On branch main
nothing to commit, working tree clean

这些规则一旦到位,git status 就会安静下来。这正是它的意义所在:那些你永远不想提交的文件夹和文件不再冒出来打扰你,这样一来,真正显示出来的才是值得你关注的东西。

这些匹配模式支持通配符,所以 *.log 会忽略所有以 .log 结尾的文件,不管文件名是什么。在模式末尾加斜杠可以把它限定为只匹配目录,所以 dist/ 只匹配名为 dist 的文件夹,不会碰到恰好同名的文件。在某一行开头加 ! 可以取消忽略某个本会被更宽泛的模式捕获的东西,这在忽略的文件夹里有一个文件需要被跟踪时很有用:

bash
# .gitignore
logs/
!logs/keep-this.log

Git 还会读取一个全局忽略文件,每台机器一份,用来存放你希望在所有项目里都生效的匹配模式:比如编辑器留下的垃圾文件 .DS_Store 或 *.swp。用 git config --global core.excludesfile ~/.gitignore_global 设置一次,你就再也不用一个项目一个项目地重复添加这些模式了。

忽略模式的匹配方式和 shell 的通配符一样:* 匹配任意一串字符,? 匹配一个字符,** 可以跨目录匹配。不带斜杠的模式会在项目的任意深度生效,而带斜杠的模式则被锁定在那条具体的路径上。这些模式的匹配完全是本地行为:Git 只在决定 git status 和 git add 该给你显示什么的时候才会去查 .gitignore。一旦 Git 已经在跟踪某个文件,它并不会因为这个文件而停止跟踪——这一点在下一节里很关键。

Juno告诉 Git 该忽略什么 一个 .gitignore 文件列出了 Git 永远不该跟踪的内容:node_modules/、dist/ 和 .env 几乎是每个项目里的常客。把 .gitignore 文件本身也提交进去,这样每个克隆项目的人拿到的状态都是干净的。我曾经有一次不小心把几千个依赖文件都加进了暂存区,白白浪费了一整个下午才搞明白这个道理。
Juno告诉 Git 该忽略什么 匹配模式支持像 *.log 这样的通配符,末尾的斜杠表示"只匹配目录",开头的 ! 可以取消忽略某个被更宽泛规则捕获的内容。全局忽略文件(用 core.excludesfile 设置)就是编辑器和系统垃圾文件该待的地方,这样你就不用再一个项目一个项目地手动往 .gitignore 里添加 .DS_Store 了。
Juno告诉 Git 该忽略什么 忽略规则是 shell 风格的通配符匹配,在客户端本地执行,它们只会改变 Git 显示给你的"未跟踪"内容。要记住,这条规则只能作用于 Git 还不知道的文件。这个区别在纸面上看起来很小,但实际操作中代价不小。

永远不要提交密钥 ​

API 密钥、数据库密码或签名证书永远不应该出现在提交记录里。 无论是私有仓库还是公开仓库,这条规则同样适用,而且从第一次提交开始就该遵守。把密钥放进像 .env 这样的文件里,第一天就把这个文件加进 .gitignore,然后让应用从那里读取这些值,而不是直接写进源码文件里。

bash
# .env
WEATHER_API_KEY=sk_live_9f8e7d6c5b4a
bash
# .gitignore
.env

每次都要自动遵守这条规则,没有例外。一旦密钥出现在提交记录里,任何能访问这个仓库的人都能拿到它;如果是公开仓库,那就是任何人都能拿到。

在真实项目里,密钥通常不止存在一个地方:本地机器上用一个 .env 文件,部署环境用密钥管理工具或者宿主平台的环境变量配置。两者在实际用法上是一样的:值都存在源码文件之外、Git 之外,你的代码在运行时从环境中读取它。改成提供一份 .env.example 文件,只写变量名而不写真实值,这样团队成员知道该配置什么,却永远看不到你真正的密钥。

这里有一个让这条规则毫无商量余地的事实:从某个**提交**里删掉一个密钥,并不会把它从历史记录里抹掉。之前每一个包含这个密钥的提交依然保留着它,永久保存在仓库的历史里,而且任何人克隆过这个仓库,都会把那些旧提交一并带走。在新提交里删除文件,只意味着最新的快照里不再有它了;保存旧值的那个对象仍然待在 .git 里,任何人只要检出某个更早的提交,或者翻一下日志,就能拿到它。

"删掉文件再提交一次"永远修不好一个已经泄露的密钥。如果真的有密钥泄露进了某次提交,就去服务提供商那边把它作废或重新生成(发一个新密钥,把旧的废掉),这样泄露出去的值才会失去价值,不管你之后对仓库做了什么。重写历史来清除密钥是可行的,但这并不能撤销密钥暴露期间已经发生的任何使用。把"作废/重置密钥"当作真正管用的解决办法,把清理历史当作附加的、可选的补充手段。

Juno永远不要提交密钥 永远不要把真实的 API 密钥、密码或证书放进提交里。把密钥存放在像 .env 这样的文件里,并在你第一次运行 git add 之前,就把 .env 加进 .gitignore。这是整章里唯一一条值得当作铁律来遵守的规则。
Juno永远不要提交密钥 本地密钥放在 .env 里,部署环境的密钥放在宿主平台的环境变量或密钥管理工具里,两者都绝不能被提交。发布一份只写变量名的 .env.example,这样团队成员知道该配置什么,却看不到真实的值。
Juno永远不要提交密钥 在新提交里删掉密钥,并不会把它从历史记录里移除。之前的每一个提交、每一份已经存在的克隆,仍然保留着它。密钥一旦泄露,立刻去服务提供商那边重置它:这个重置动作才是真正管用的修复办法。

文件已被跟踪后再加进 .gitignore ​

这是几乎每个人都至少会踩一次的坑。你不小心提交了一个文件,发现了这个失误,把它加进 .gitignore(下面这行 echo 命令会把路径追加到那个文件里),然后指望 Git 从此忘掉它。但事实并非如此。

bash
$ git add config/settings.json
$ git commit -m "Add app settings"

# later, you realize this was a mistake
$ echo "config/settings.json" >> .gitignore
$ git status
On branch main
nothing to commit, working tree clean

git status 显示工作区是干净的,但 config/settings.json 依然被跟踪着,未来每一次 git log 和每一次克隆里它都还在。忽略规则只对 Git 还不知道的文件起作用。 一个文件一旦被添加并提交,它就成了历史记录的一部分,而 .gitignore 对已经进入历史的文件没有任何话语权。

要真正让它从此不再被跟踪,你需要告诉 Git 把这个文件从跟踪列表里剔除,同时保留它在磁盘上的实际内容:

bash
$ git rm --cached config/settings.json
$ git commit -m "Stop tracking config/settings.json"

git rm --cached <file> 会把这个文件从 Git 的跟踪范围里移除,但不会把它从你的工作目录里删掉。提交这次移除操作之后,从这时起,你的 .gitignore 规则才终于起到了它一开始就该起的作用。

同样的修复方法对整个文件夹也适用,用 git rm -r --cached <folder> 即可,这通常是在发现 node_modules/ 或 dist/ 在项目早期(还没人加 .gitignore 之前)就被提交进去之后要用的招。运行一次,提交这次移除操作,这个文件夹就会从此后的提交里消失,而每个团队成员本地磁盘上的文件都毫发无损。

这个行为直接源于暂存区到底是什么。Git 通过**索引(index)**来跟踪文件,索引就是记录下一次提交究竟会包含哪些内容的地方。添加并提交一个文件,会往索引里写入一条记录,也会写进那次提交的快照里;只有当 Git 要决定如何处理一个在索引里还没有记录的文件时,才会去查 .gitignore。

一旦某个文件在索引里已经有了记录,忽略模式对它就毫无意义。git rm --cached 会删掉索引里的记录,同时把文件留在磁盘上,这正是为什么它才是真正能解决这个问题的命令,而单单编辑忽略规则什么用都没有。

Juno文件已被跟踪后再加进 .gitignore 如果一个文件已经被 Git 跟踪了,之后再把它加进 .gitignore 也没用。忽略规则只对 Git 从来没添加过的文件起作用。要让一个不小心混进去的文件不再被跟踪,运行 git rm --cached <file> 然后提交这次改动。
Juno文件已被跟踪后再加进 .gitignore.gitignore 只对未跟踪的文件起作用。对于已经提交过的东西,用 git rm --cached <file> 移除它,整个目录就用 git rm -r --cached <folder>,然后提交。每个人本地的文件都不受影响,改变的只是 Git 对它们的跟踪状态。
Juno文件已被跟踪后再加进 .gitignore 索引里为每个被跟踪的文件都保存着一条记录,只有当还没有记录时,Git 才会去查 .gitignore。git rm --cached 会删掉索引里的记录,而不动磁盘上的文件,这才是真正管用的修复方式。顺便也把忽略规则加上,这样这个文件就不会再溜回来了。

良好习惯:小提交与干净的仓库 ​

一份整洁的 .gitignore 只是让仓库好用的一半,另一半在于你怎么组织提交。让每次提交只做一件专注的事:一个 bug 修复、一个小功能、一次单独的重构。如果一个下午的工作涉及好几件互不相关的事,就把它拆成好几次提交,而不是到了一天结束时一股脑塞进一次提交里。

bash
$ git add src/weather-widget.js
$ git commit -m "Fix temperature rounding in weather widget"

小提交更容易review,出问题时也更容易回退,六个月后你想搞清楚某一行代码为什么存在时也更容易读懂。提交循环讲了怎样写出有用的提交信息;而这个习惯讲的是把改动本身控制得足够小,让写一条好的提交信息成为可能。

把这个习惯和一份干净的 .gitignore 结合起来,好处会叠加:一段由小而目的明确的提交组成的历史记录,没有生成出来的文件夹或杂乱的配置文件搅乱 diff。review 一个改动,如果里面混着一堆 dist/ 的变动和某人真正编辑的那两行代码,对所有相关的人来说都很痛苦,而这完全可以靠第一天就设置好的 .gitignore 来避免。

在更大规模下,这样做的好处不只是可读性。一个没有生成文件和依赖文件夹的仓库,克隆起来足够快,搜索起来也足够干净;而一段由专注的提交组成的历史,才能让 git log --oneline 和两个时间点之间的 git diff 真正有助于理解发生了什么、为什么会发生。如果历史记录里一半是 node_modules/ 的变动,另一半把五件互不相关的改动混在一起,这些都不会好用。

Juno良好习惯:小提交与干净的仓库 让每次提交只做一件专注的事,同时让你的 .gitignore 覆盖那些永远不该被跟踪的文件夹和密钥。这两个习惯加在一起,基本上就决定了一个仓库用起来是不是舒服。养成的小习惯越早,之后省下的清理工作就越多。
Juno良好习惯:小提交与干净的仓库 小而专注的提交加上一份干净的 .gitignore,能让review和diff真正可读,而不是埋没在生成出来的噪音里。在第一次提交之前就把 .gitignore 设置好。等到 node_modules/ 第五次出现在 pull request 里才想起来处理,花的清理成本远比事先设置要高。
Juno良好习惯:小提交与干净的仓库 一个精简的仓库和一段专注的提交历史,是保证克隆速度快、日后翻查历史时能看得清楚的关键。每一个悄悄混进历史记录的生成文件或依赖文件夹,都是未来每一次克隆和搜索都要付出的一点点代价。提前把忽略规则设置好,这件事以后就不再值得费心了。