如何使用 .gitignore 忽略目录中除一个文件以外的所有内容?

How to use .gitignore to ignore everything in a directory except one file?

我在 SO 上找到了几个据称可以解决此问题的解决方案,但出于某些未知原因,其中 none 对我有用。

除了一个特定文件外,我需要忽略给定文件夹中的所有内容。容易,对吧?没那么快。

对于以下每个问题,我都尝试了大多数建议的答案:

...但我没有比开始时更进一步。

这是要包含的文件的路径:

D:\Projects\Website\Website\bin\Settings.json

仓库位于:

D:\Projects\Website

我的 .gitignore 文件是由 Visual Studio 生成的,因此它包含此条目:

[Bb]in/

根据上面的很多问题的答案,我应该可以做这样的事情:

!/Website/[Bb]in/Settings.json

...但这不起作用。该文件仍然被忽略。

None 这些排列可以达到目的:

!*/Settings.json
!**/Settings.json
![Bb]in/Settings.json
![Bb]in/**/Settings.json
![Ww]ebsite/[Bb]in/Settings.json
!Website/bin/Settings.json
!/Website/bin/Settings.json

我也试过在 bin 中放置一个单独的 .gitignore 文件:

# Don't block Settings.json
!Settings.json
!.gitignore

运气不好。

如何阻止 [Bb]in 中除 Settings.json 文件之外的所有内容?

您可以使用 git add -f 指示 git“覆盖”忽略规则并跟踪单个文件:

git add -f Website\bin\Settings.json

即使该文件匹配忽略模式,该文件仍将被跟踪;您需要 运行 git rm --cached 告诉 git 停止跟踪此文件。

添加到 ,很好,我注意到您评论说:

That works. It strikes me as a bit brittle (maybe that's just my imagination, and hopefully I'll be proven spectacularly wrong), but if this is the only way, I can live with it.

这不是 唯一的 方式,我也有同样的痒感,因为它很脆弱,或者在其他方面有微妙的错误。它 确实 工作并且它不会在正常的日常使用中中断,但对我来说,让文件被跟踪并保持跟踪只是因为它们在提交中被跟踪,这对我来说似乎是错误的你提取,当你着手进行新的提交时。

这里的技巧是 Git 路径名 Website/bin/Settings.json 导致文件在提取后位于文件夹中:文件 Settings.json 位于文件夹 bin(它又在文件夹 Website 中,但这只是添加到一堆中;这里一个“文件夹中”层就足够了)。

请注意,对于 Git,Website/bin/Settings.json 只是一个文件名:该文件名以正斜杠的形式存储在 Git 的索引(也称为暂存区)中).1 当 Git 正在扫描您的 工作树 时,稍后 会出现问题。 Git 所做的排除处理——使用 .git/info/exclude 和各种 .gitignore 文件——通过工作树文件工作。它必须:它是关于 未跟踪文件,而 未跟踪文件 的定义是 存在于您的文件中的文件工作树,但不在 Git 的索引中 .

当 Git 将当前 (HEAD) 提交的内容(当前提交中存储的文件集及其所有数据)与索引/暂存中的文件进行比较时-区域,Git 不必,也根本不需要查看您的 工作树 。 Git 需要的一切都在存储库中:当前提交由读取 HEAD 确定,它解析为提交哈希 ID,解析为内部树对象,它获得 Git all文件名和模式及其哈希 ID。建议的 next 提交,在索引/暂存区中,包含文件名和模式及其哈希 ID。散列 ID 让 Git 知道文件是否 100% 匹配,对于大多数目的,这就是我们所关心的:git status 只是打印一个 M 表示已修改,或者 modified,没有弄清楚 实际改变了什么 ,例如。

通读工作树,但是:好吧,那方式更难。 OS 在这里妨碍了。当然,可能有一个 C 库 scandirreaddir 函数,或其他一些枚举文件夹内容的方法。但是 Git 仍然必须在每个名称上调用 lstat,也许。2 无论如何,如果您分析计时结果,为什么 git status 花费更多超过 20 纳秒,您会发现它花费大量时间来读取目录。如果我们能为此找到一些捷径,那不是很好吗?

输入 .gitignore 和其他排除文件:如果我们读取顶级工作树并找到名为 tmpzorg 的目录,但那些 目录 被忽略了——通过 **/tmptmp/ 或其他什么——为什么我们甚至不必打开并阅读它们全部! ./tmp 包含一个文件还是十亿个文件都无关紧要:我们将跳过整个文件!考虑到仅打开和读取目​​录以查找其文件名可能需要几毫秒——并且在每个名称上使用 lstat 可以增加更多——这是一个巨大的节省。

所以,Git 这样做了。如果 Git 正在准备工作树遍历,并且 允许 跳过查看某个文件夹/目录,它 跳过看那个文件夹里面。因此,如果您的 .gitignore 文件显示:

*

那么任何目录名都会匹配,Git 将跳过打开目录,更不用说读取目录了。这发生在您的 Website 文件夹中。

如果您的 .gitignore 显示为:

*
!Website

但是,当 Git 读取顶级目录并找到名称 Website 时,它 不能 忽略它。因此 Git 打开 Website 文件夹并找到 bin,等等。但是:bin 匹配 * 但不匹配 Website,所以它可以忽略。这意味着 Git 可以直接跳过它,从不看里面。您需要添加 Website/bin:

*
!Website
!Website/bin

现在Git必须打开Website/bin阅读。其中的每个文件和目录都可以被忽略,因此要使其中的 Settings.json 成为 而不是 -忽略,我们需要列出该文件:

*
!Website
!Website/bin
!Website/bin/Settings.json

这个相当小的 .gitignore 文件可以工作。然而,它确实有一个缺陷。如果 bin 中有一个名为 Website 的文件或目录,该文件或目录将不会被忽略。如果不忽略,Git 会抱怨它未被跟踪,或者用 git add . 添加它,或其他不良行为。要解决这个问题,我们应该确保只匹配 Website,而不是 bin/Website。这使我们进入 Git 排除规则的 第二 棘手部分。


1索引条目的格式有点乱并且被压缩了,这取决于索引格式版本(其中有几个),但是 git ls-files --stage 会转储找出感兴趣的主要内容,在那里,您会看到以嵌入的正斜杠命名的文件。 Git 当然能够处理和理解 Windows 在这里使用的反斜杠,因此将文件存储在 Website 目录的 bin 文件夹中.

Git 索引中的字符串区分大小写,并以 UTF-8 或等效格式存储,无论文件名如何存储在文件系统中,也无论文件系统的文件是否存在名称不区分大小写。

2一些 readdir 变体包括一个类型字段,例如 DT_DIR,如果你可以依赖它,它可以让你跳过这一步有时;这可以节省大量时间。我不知道 Git 是否尝试这样做:工作树代码已被多次修改,现在具有 fsmonitor 代码的所有复杂性,这是加快速度的不同方式,所以我有最近没看


另一个棘手的部分:锚定名称与非锚定名称

为了正确理解这部分,我想从正则表达式中借用一个概念:锚定 某物在左边或右边。在像 me*s 这样的正则表达式中,我们将匹配 ms pacmanmessage,但不匹配 memory,因为我们正在寻找 m,然后是任何数字es,然后 smemory 没有 s。但我们也会匹配 acmestorage,因为它有 m 后跟一个 e 后跟 s,嵌入在 acmestorage 中(其中运行一起)。我们可以通过 anchoring 左边的匹配来避免一些这样的情况:^m*s 不会匹配 acmestorage 因为 m 必须是 第一个个字母.

(RE 也让我们用 $ 固定在右边。每个 RE 语法都有自己的特点,.gitignore 文件使用 glob 语法而不是 RE 语法,所以我们不要得到在这个兔子洞里走得太远了。只要记住锚定的想法:在左边或右边,或两者都放一根火柴。在 Git 的情况下,锚定路径是精确匹配,粘在两边。那是因为 rightalways 锚定。你必须使用 path/*path/** 来允许任意右-手边部分。)

在我们的示例中,对于 .gitignore,我们希望确保 Website 仅在 顶级 匹配,我们将.gitignore 文件。为此,我们可以使用前导斜杠开始条目:

*
!/Website
!Website/bin
!Website/bin/Settings.json

现在bin/Website不会匹配第二行:第二行锚定在扫描的顶级(根)目录,而bin/Website不在那个级别:它是一级下来。

您可能认为我们应该对所有三个文件名都这样做:

*
!/Website
!/Website/bin
!/Website/bin/Settings.json

这有效,但它不是 必需的,原因是 .gitignore 条目是 自动 锚定如果它里面有一个 embedded 斜线。 Website/bin 中有一个不在两端的斜线,因此它会自动锚定。 Website/bin/Settings.json 有两个这样的斜杠,也被锚定了。

比较棘手的部分

我暗示这里只有两个棘手的部分。我撒了谎。排除文件还有一种使用斜杠的方法,不幸的是,这很棘手,那就是 final 斜杠使条目匹配 only 目录名称.即:

bin/

匹配 bin 目录但不匹配名为 bin.

的文件

此规则独立于其余规则:

  • 前导!否定了整个事情,所以!/Website/意味着不要忽略
  • 前导 /(在任何前导 ! 之后)或任何 不在末尾的嵌入斜杠 表示“锚定,因此 !/Website/ 已锚定。
  • 尾部/表示仅当它是一个目录时,所以!/Website/只匹配一个目录。尾部斜杠不计入锚定目的(你永远不应该使用双尾部斜杠)所以如果你想要锚定,一定要包括一个前导或嵌入的斜杠。

使用 所有 这些规则,我们得出:

*
!/Website
!Website/bin
!Website/bin/Settings.json

完整且正确(前提是我这里的大小写正确:请记住 Git 将区分大小写,无论您的文件系统如何).但是我们可以使用另一种技巧来生成一个稍微短一些的文件。假设我们写:

*
!*/
!Website/bin/Settings.json

Git 将:

  • 打开并阅读顶级工作树目录;
  • 对于每个 文件,忽略它 (*);
  • 对于每个目录忽略它(!*/);
  • 找到Website目录,打开阅读;
  • 对于Website/中的每个文件,忽略它(*);
  • 找到目录bin并且忽略它(!*/);
  • 打开并阅读Website/bin目录;
  • 找到每个文件并忽略它 (*) except for Website/bin/Settings.json.

这个三行版本的缺点是,在上述处理过程中,Git会打开并读取每个目录,包括每个目录的每个子目录,所以如果有一个顶级 tmp 目录包含十亿个文件(直接或递归后),Git 将花时间检查每个文件。也就是说,!*/ 完全击败了在某些情况下节省大量时间的“不要费心看这里”优化。

如果 Git 的排除代码足够聪明,可以意识到如果你写:

*
!Website/bin/Settings.json

它应该自动!/Website/!/Website/bin/ 注册到它的排除列表中(如果它们不存在的话)。这看起来很简单。 (具体怎么做否定和锚定要看这里的内部数据结构,我十几年没看过了。。。)