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 statusgit 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 statusgit add 显示什么时查询 .gitignore。它不会阻止文件在 Git 已经追踪它后停止被追踪,这在下一节很重要。

Juno告诉 Git 哪些文件要忽略.gitignore 文件列出了 Git 应该永远不追踪的内容:node_modules/dist/.env 在几乎任何项目中都是常见的。提交 .gitignore 文件本身,这样克隆项目的每个人都能获得相同的干净状态。我曾经在不知道这一点的情况下浪费了整个下午来暂存数千个依赖文件。
Juno告诉 Git 哪些文件要忽略 模式支持像 *.log 这样的通配符,尾部斜杠表示"仅目录",开头的 ! 会反向忽略更宽泛匹配中的内容。全局忽略文件使用 core.excludesfile 设置,是放置编辑器和操作系统垃圾的地方,这样你就不用手动将 .DS_Store 添加到每个项目的 .gitignore 中了。
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"

# 稍后,你意识到这是个错误
$ 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 通过**索引**追踪文件,这是对下一个提交中会包含什么的记录。添加和提交文件会写入索引和提交快照中的条目;.gitignore 仅当 Git 决定对一个还没有索引条目的文件做什么时才被查询。

一旦条目存在,忽略模式就与其无关。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 索引为每个被追踪的文件保存一个条目,.gitignore 仅在还没有条目时被查询。git rm --cached 删除索引条目,而不接触磁盘上的文件,这才是这里的实际修复。为好措施起见添加忽略模式,以便文件不能滑回去。

好习惯:小提交和干净的仓库

整洁的 .gitignore 是保持仓库值得工作的一半。另一半是你如何塑造你的提交。使每个提交成为一个专注的变化:一个错误修复、一个小功能、一个单一重构。如果一个下午的工作涉及多个不相关的东西,将它分成多个提交,而不是在一天结束时全部放进一个。

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

小提交更容易审查,出现问题时更容易还原,六个月后更容易回读,当你试图记住一行代码为什么存在时。提交循环涵盖了什么使提交信息有用;这个习惯是关于保持变化本身足够小,一个好信息甚至可能写出来。

将该习惯与干净的 .gitignore 结合,收益会复利增长:一个由小的、有目的的提交组成的历史,没有生成的文件夹或杂乱的配置文件混乱差异。审查一个充满 dist/ 杂乱的变化,以及某人编辑的两行真实代码,对所有相关人员来说都很痛苦,而且完全可以通过在第一天设置 .gitignore 来避免。

大规模地这会以超越可读性的方式付出代价。一个没有生成文件和依赖文件夹的仓库保持足够小以快速克隆和干净地搜索,而专注提交的历史是使 git log --onelinegit diff 在两个时间点之间实际有用于理解发生了什么和为什么的原因。所有这些都无法应对一个历史,其中一半的提交是 node_modules/ 杂乱,另一半混合了五个不相关的变化。

Juno好习惯:小提交和干净的仓库 保持每个提交为一个专注的变化,保持你的 .gitignore 覆盖永远不应该被追踪的文件夹和密钥。这两个习惯一起是使仓库值得工作的大部分原因。小习惯,在早期形成,可以为你节省许多后续清理。
Juno好习惯:小提交和干净的仓库 小的、专注的提交加上干净的 .gitignore 使审查和差异实际上可读,而不是被生成的噪音埋没。在第一次提交前设置 .gitignore。等到 node_modules/ 在拉取请求中第五次出现要花费的清理成本会更多。
Juno好习惯:小提交和干净的仓库 精益的仓库和专注提交的历史是当你查看它时保持克隆快速和历史可读的原因。每个偷偷进入历史的生成文件或依赖文件夹都是每个未来克隆和搜索要付出的小代价。提前设置忽略规则,它就不再是值得再想一次的问题。