为什么从 gitignore using wildcard (*) 和 git 状态中排除时未列出 .config 目录?

Why .config directory is not listed when excluded from gitignore using wildcard (*) and git status?

我知道这个问题有点神秘,我无法用一句话准确地表达出来(可能需要一些帮助)。

我在我的主目录中初始化了 git(即 ~/,在 Arch Linux 上)以备份我的点文件(主要是配置)。我想包括其中的所有文件和文件夹,但以 . 开头的文件和文件夹除外(例如 .config/.bashrc)。

所以我制作了一个 .gitignore 文件,其内容是:

# Ignore everything
*

# Except these files and folders
!.*

但问题是当我列出所有未跟踪的文件 (git status) 时,出于某种原因它没有列出 .config/ 目录。我尝试使用 .gitignore 并添加

!*/

显示所有目录,包括 .config/ 以及 DocumentsDownloads 等,我不想包含这些目录。

而是添加

!.*/

显示以 . 开头的所有其他目录,如 .cache/.vim/ 等。但由于某些原因 .config/ 没有显示。

我什至试过了

!.config/

!.config

没用。唯一有效的是!*/(所有目录,这不是我想要的)

任何解决这个问题的方法。真烦人。

[已解决]:

该错误已在 git 版本 2.34.1

中修复

勾选

TL;DR

错误修复后,您将需要在“有点解决”部分或类似内容中输入的内容。我想您会想要我在“底线”部分中添加的内容,真的:

/*
!/.*

一样,Git 2.34.0 中的.gitignore 通配符处理存在错误,将在2.34.1 中修复。不过,在这种情况下,我认为该错误使您的通配符工作更好

第一行:

# Ignore everything
*

做他们声称的事情:忽略一切。所有文件和文件夹(目录)都将被忽略。后续行插入异常。但是等一下,ignored 到底是什么意思?要到达那里,我们必须注意 Git 的 index(或 staging area)是什么以及 Git 如何使来自索引 / staging-area.

的新提交

Git 中的索引或暂存区是一个核心且关键的概念。在不了解索引的作用的情况下尝试使用 Git 有点像在不了解机翼和引擎的作用的情况下尝试驾驶飞机。1 所以:索引是关于您计划进行的 下一次提交 。如果您从不进行任何新提交,那么您真的不需要知道它,但是如果您确实想要进行新提交,您需要知道这个。2

当你第一次提取一些提交时,为了使用和处理它,Git 填写它的索引来自那个提交,这样索引包含所有来自该提交的文件。从这一点开始,您在工作树中所做的一切,为了进行新的提交,都与Git无关。也就是说,在您告诉 Git 您希望 Git 将更新的 and/or 新文件复制到 Git 的索引之前,它是不相关的

git add 命令是关于更新 Git 的索引。您命名为 git add 的文件,例如 git add file1 file2,将被复制到 Git 的索引中。如果已经存在这两个文件的副本,则这些副本将从索引中删除,并替换为更新的文件。如果没有,这些文件会新添加 索引。

一旦文件在索引中,您可以随时替换它:任何.gitignore条目都是无关紧要的在此刻。您还可以 将其从索引 中删除,使用 git rm,或在删除工作树副本后使用 git add:两者都将删除索引副本。现在它 不再在索引中 并且 .gitignore 条目重新播放。

您可以使用 en-masse git add,如 git add .git add *3 来获得 Git 扫描 目录和文件并为您添加它们。当您这样做时,Git 将 跳过 某些目录 and/or 文件(如果可以的话),这是 .gitignore 真正发挥作用的地方。


1“我为什么要关心那些?我只关心把我的乘客和货物从A点送到B点,而且那些在飞机里面,而不是在外面翅膀。

2再扩展一下飞机的类比:如果你只是打算把机身当房子用,那么确实,你不需要关心引擎和机翼。

3请注意,在 Unix-like shell 中,git add *git add . 完全不同,因为 shell 将为 Git 扩展 *:Git 永远不会看到文字星号。当 shell 扩展 * 时,它会排除 dot-files ,至少默认情况下(bash 特别有一个控制旋钮来改变这种行为)。在某些 CLI 中,文字星号 * 会变成 Git,然后 Git 会展开 *,现在它可以如果 Git 想要的话,就像 git add . 那样。但是输入 git add . 更容易(不需要 SHIFT 键)所以这就是我一直做的事情,首先消除了差异。


如何Git扫描工作树

如果您 运行 git add . 或同等学历(再次参见脚注 3),Git 将:

  1. 打开目录..
  2. 打开并阅读此级别的任何 .gitignore 文件,将这些规则添加(附加)到忽略规则中。 (当我们完成这个目录时,这些规则就会被删除。)
  3. 阅读此目录:它包含文件名和 sub-directories(“文件夹”,如果您喜欢该术语)。
  4. 在我们阅读每个文件和文件夹名称时,请根据所有现在生效的忽略规则检查它们。请注意,有些规则 仅适用于目录/文件夹 ,而其他规则适用于 文件夹和文件 。 folder-only 规则是以斜杠结尾的规则。此外,有些规则是“积极的”(不要忽略),有些是“消极的”(不要忽略)。 ne以 !.
  5. 开头的规则

Git 在当前规则集中找到 最后适用的规则 ,无论那是什么,然后遵守该规则。所以首先,让我们定义哪些规则适用于哪些 directory-scan 结果,然后是各种规则的作用。

.gitignore 中的规则可以是:

  • 带有斜杠的简单文本字符串,例如generated.file
  • 带有尾部斜杠但没有其他斜杠的文本字符串:somedir/
  • 带有前导或嵌入式斜杠的文本字符串,有或没有尾部斜杠:/fooa/b/foo/a/b/ 等;或
  • 以上任何带有各种 glob 风格的通配符。

这些都可以被否定:如果一个规则以 ! 开头,它被否定,我们去掉 ! 然后使用剩下的测试。两个关键测试是:

  • 条目 是否以 文字 / 结尾?如果是这样,它仅适用于目录/文件夹。回答剩余问题时忽略斜线。
  • 条目 是否以 包含 斜杠 / 字符? (最后的那个不算在这里。)如果是这样,这个条目是 anchoredrooted (我喜欢这个词 anchored 我自己,但我看到这两个术语都被使用了)。

锚定 条目仅匹配在在该级别 找到的文件或文件夹名称。也就是说,/foofoo/bar 不会匹配 sub/foosub/foo/bar,只会匹配 ./foo./foo/bar,其中 . 是Git 现在正在扫描的目录(文件夹)。这意味着如果条目有多个级别——例如 foo/barone/two/three——Git 将不得不记住在扫描 bar 时应用此条目 foo,或 one 中的 twoone/two 中的 three。所以我们确实必须考虑“更高级别”的规则。但是由于较低级别的规则得到附加,较低级别.gitignore可以根据需要取消较高级别的规则。

一个 un-anchored 条目 适用于此,并且——除非被覆盖——在 每个 sub-directory 以及 中。也就是说,如果我们确实有 ./one/two/three,Git 将很可能打开并读取 one 以找到 two,然后打开并读取 two 以找到 three,所有 同时仍在当前目录 上工作。同时 any un-anchored 来自这个 .gitignore 的条目 将应用 within oneone/two 目录,如果是目录,则在 one/two/three 内,依此类推。

所以,已经有很多事情要考虑了。现在我们加入全局匹配。

通常的 glob 是 *:人们写 foo*bar*.pyc 或其他什么。 Git 也允许 **,其含义类似于 bash:零个或多个目录。 (我发现 Git 中的 ** 很奇怪,而且在我看来有点错误,它有时似乎意味着“一个或多个”而不是“零个或多个”,所以我建议避免 ** 如果可能的话。这很难推理,所以这通常不是一个好主意,而且 Git 的忽略规则基本上消除了对 ** 的任何需求。所以如果你 会使用它,仔细测试它并准备好在将来 Git 转移到你身上,以防 one-or-more ?bug? 得到修复,或影响您的用例,或其他。)

那么,假设我们有这两个条目:

*
!.*

Git 打开并读取 . 并找到以下名称:

dir
file
.dir
.file

其中 dir.dir 是目录(文件夹),file.file 是 non-directories(文件)。

* 规则匹配所有四个名称。 !.* 规则匹配最后两个名字。 !.* 规则稍后出现在 .gitignore 文件中,因此它会覆盖 * 规则。 Git 因此“看到”.dir.file.

因为.file一个文件,这意味着git add .“看到”了它。它将检查 .file 是否需要 git add 编辑以替换现有的 .file 文件,或添加到索引中。

由于 dirfile 被排除在外,此扫描过程 不会 看到它们,也不会尝试 git add一。由于 dir 本身是一个 目录 (不是文件),它永远不会在索引本身中。索引 named dir/thing 中可能有一个文件,Git 将检查是否应由 git add . 更新该文件,但是 Git不会扫描dir查看dir.[=162中是否还有其他文件=]

由于file是被排除的文件,所以扫描过程看不到它。但是如果 file 已经存在于索引中,Git 将检查它是否应该被这个 git add . 更新,即使它没有被 扫描 这里。换句话说,这些“索引中已存在的文件”检查发生在 outsid(之前或之后)“扫描目录”通过。

同时,由于 .dir 未被 排除,Git 现在打开并读取 .dir,递归地:

  • Git 检查 .dir/.gitignore(适用于在 .dir 中找到的条目的 .gitignore)。如果存在,Git 附加这些规则。
  • Git 使用所有相同的方法递归扫描 .dir。然后完成扫描 .dir 所以 Git 删除附加规则。

现在让我们看看 Git 在扫描 .dir 时生效的规则。

appended-to 规则

如果有 .dir/.gitignore,Git 打开并读取它并附加到现有规则。如果不是,我们仍然有相同的规则集:

*     (positive wildcard: ignore every name)
!.*   (negative wildcard: don't ignore dot-names)

.dir 里有什么?假设我们有:

file1
dir1
.file2
.dir2

名字 file1 匹配 * 所以 它被忽略了 。 Git 不会 git add 它到索引,如果它还不存在的话。同样,dir1 匹配 *,因此 它会被忽略 。 Git 甚至不会 扫描 它以查看那里是否有任何文件。

名称 .file2 匹配 *,但也匹配 .*,因此覆盖否定条目是适用的规则:Git 将 git add .dir/.file2 .名称 .dir2 具有相同的功能,因此覆盖适用并且 Git 将 打开并读取 .dir/.dir2。这通过与以前相同的递归:Git 查找 .dir/.dir2/.gitignoreappend 规则,并且将在扫描 [=135] 时使用 appended-to 规则=],然后回到我们自己的 .dir/.gitignore 附加规则集,同时继续扫描 .dir,然后从这个递归级别 return 并删除 .dir/.gitignore 规则。

底线

最后,这里的技巧是我们希望 * 规则应用 仅在顶层 。一旦进入 .foo/,我们就不想忽略 .foo/main_config.foo/secondary_config。所以我们希望 * 仅在顶层应用

使用:

# Ignore everything
*

# Except these files and folders
!.*
!.*/*

让我们更接近:我们忽略了一切,但是通过否定规则 !.*!.*/* - 我们小心地 不要 忽略 .foo 之类的。一旦我们进入.foo,我们小心不要忽略.foo/main_config.

错误或可能的错误,取决于您真正想要什么,这里是……好吧,假设我们有 .foo/thing1/config.foo/thing2/config.*/* 模式 包含嵌入的斜杠 ,这意味着它是 锚定的 。它匹配 .foo/thing1,以便扫描该目录。但它 匹配 .foo/thing1/config.

我们可以尝试类似的方法:

!.*/*
!.*/**/

我特别讨厌这个,因为 ** 太难推理了。我们也可以这样写:

!.*/*
!.*/*/
!.*/**/

以防 **“一个或多个”bug 困扰我们(我认为不会,但这是一个考虑因素)。但最简单的方法是 锚定原始 globs,写成:

/*
!/.*

这使得顶层.gitignore规则适用到top-levelwork-tree条目。 Sub-level .gitignore 文件,如果存在,可以建立 sub-level 规则,不需要覆盖任何 top-level 规则,因为 top-level 规则 已经不适用任何 sub-level,感谢锚定。