如何使用 .gitignore 忽略目录中除一个文件以外的所有内容?
How to use .gitignore to ignore everything in a directory except one file?
我在 SO 上找到了几个据称可以解决此问题的解决方案,但出于某些未知原因,其中 none 对我有用。
除了一个特定文件外,我需要忽略给定文件夹中的所有内容。容易,对吧?没那么快。
对于以下每个问题,我都尝试了大多数建议的答案:
- Make .gitignore ignore everything except a few files
- Using .gitignore to ignore everything but specific directories
- Make .gitignore ignore everything except one file
- gitignore everything except specific files
...但我没有比开始时更进一步。
这是要包含的文件的路径:
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
文件之外的所有内容?
预期结果:
Website\bin\Settings.json
不忽略
实际结果:
Website\bin\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 库 scandir
或 readdir
函数,或其他一些枚举文件夹内容的方法。但是 Git 仍然必须在每个名称上调用 lstat
,也许。2 无论如何,如果您分析计时结果,为什么 git status
花费更多超过 20 纳秒,您会发现它花费大量时间来读取目录。如果我们能为此找到一些捷径,那不是很好吗?
输入 .gitignore
和其他排除文件:如果我们读取顶级工作树并找到名为 tmp
和 zorg
的目录,但那些 目录 被忽略了——通过 *
或 */
或 tmp
或 tmp/
或其他什么——为什么我们甚至不必打开并阅读它们全部! ./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 pacman
和 message
,但不匹配 memory
,因为我们正在寻找 m
,然后是任何数字e
s,然后 s
,memory
没有 s
。但我们也会匹配 acmestorage
,因为它有 m
后跟一个 e
后跟 s
,嵌入在 acme
和 storage
中(其中运行一起)。我们可以通过 anchoring 左边的匹配来避免一些这样的情况:^m*s
不会匹配 acmestorage
因为 m
必须是 第一个个字母.
(RE 也让我们用 $
固定在右边。每个 RE 语法都有自己的特点,.gitignore
文件使用 glob 语法而不是 RE 语法,所以我们不要得到在这个兔子洞里走得太远了。只要记住锚定的想法:在左边或右边,或两者都放一根火柴。在 Git 的情况下,锚定路径是精确匹配,粘在两边。那是因为 right 侧 always 锚定。你必须使用 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/
注册到它的排除列表中(如果它们不存在的话)。这看起来很简单。 (具体怎么做否定和锚定要看这里的内部数据结构,我十几年没看过了。。。)
我在 SO 上找到了几个据称可以解决此问题的解决方案,但出于某些未知原因,其中 none 对我有用。
除了一个特定文件外,我需要忽略给定文件夹中的所有内容。容易,对吧?没那么快。
对于以下每个问题,我都尝试了大多数建议的答案:
- Make .gitignore ignore everything except a few files
- Using .gitignore to ignore everything but specific directories
- Make .gitignore ignore everything except one file
- gitignore everything except specific files
...但我没有比开始时更进一步。
这是要包含的文件的路径:
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
文件之外的所有内容?
预期结果:
Website\bin\Settings.json
不忽略实际结果:
Website\bin\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 库 scandir
或 readdir
函数,或其他一些枚举文件夹内容的方法。但是 Git 仍然必须在每个名称上调用 lstat
,也许。2 无论如何,如果您分析计时结果,为什么 git status
花费更多超过 20 纳秒,您会发现它花费大量时间来读取目录。如果我们能为此找到一些捷径,那不是很好吗?
输入 .gitignore
和其他排除文件:如果我们读取顶级工作树并找到名为 tmp
和 zorg
的目录,但那些 目录 被忽略了——通过 *
或 */
或 tmp
或 tmp/
或其他什么——为什么我们甚至不必打开并阅读它们全部! ./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 pacman
和 message
,但不匹配 memory
,因为我们正在寻找 m
,然后是任何数字e
s,然后 s
,memory
没有 s
。但我们也会匹配 acmestorage
,因为它有 m
后跟一个 e
后跟 s
,嵌入在 acme
和 storage
中(其中运行一起)。我们可以通过 anchoring 左边的匹配来避免一些这样的情况:^m*s
不会匹配 acmestorage
因为 m
必须是 第一个个字母.
(RE 也让我们用 $
固定在右边。每个 RE 语法都有自己的特点,.gitignore
文件使用 glob 语法而不是 RE 语法,所以我们不要得到在这个兔子洞里走得太远了。只要记住锚定的想法:在左边或右边,或两者都放一根火柴。在 Git 的情况下,锚定路径是精确匹配,粘在两边。那是因为 right 侧 always 锚定。你必须使用 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 forWebsite/bin/Settings.json
.
这个三行版本的缺点是,在上述处理过程中,Git会打开并读取每个目录,包括每个目录的每个子目录,所以如果有一个顶级 tmp
目录包含十亿个文件(直接或递归后),Git 将花时间检查每个文件。也就是说,!*/
完全击败了在某些情况下节省大量时间的“不要费心看这里”优化。
如果 Git 的排除代码足够聪明,可以意识到如果你写:
*
!Website/bin/Settings.json
它应该自动 将 !/Website/
和 !/Website/bin/
注册到它的排除列表中(如果它们不存在的话)。这看起来很简单。 (具体怎么做否定和锚定要看这里的内部数据结构,我十几年没看过了。。。)