为什么从 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/
以及 Documents
、Downloads
等,我不想包含这些目录。
而是添加
!.*/
显示以 .
开头的所有其他目录,如 .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 将:
- 打开目录
.
.
- 打开并阅读此级别的任何
.gitignore
文件,将这些规则添加(附加)到忽略规则中。 (当我们完成这个目录时,这些规则就会被删除。)
- 阅读此目录:它包含文件名和 sub-directories(“文件夹”,如果您喜欢该术语)。
- 在我们阅读每个文件和文件夹名称时,请根据所有现在生效的忽略规则检查它们。请注意,有些规则 仅适用于目录/文件夹 ,而其他规则适用于 文件夹和文件 。 folder-only 规则是以斜杠结尾的规则。此外,有些规则是“积极的”(不要忽略),有些是“消极的”(不要忽略)。 ne以
!
. 开头的规则
Git 在当前规则集中找到 最后适用的规则 ,无论那是什么,然后遵守该规则。所以首先,让我们定义哪些规则适用于哪些 directory-scan 结果,然后是各种规则的作用。
.gitignore
中的规则可以是:
- 带有无斜杠的简单文本字符串,例如
generated.file
;
- 带有尾部斜杠但没有其他斜杠的文本字符串:
somedir/
;
- 带有前导或嵌入式斜杠的文本字符串,有或没有尾部斜杠:
/foo
、a/b
、/foo/
、a/b/
等;或
- 以上任何带有各种 glob 风格的通配符。
这些都可以被否定:如果一个规则以 !
开头,它被否定,我们去掉 !
然后使用剩下的测试。两个关键测试是:
- 条目 是否以 文字
/
结尾?如果是这样,它仅适用于目录/文件夹。回答剩余问题时忽略斜线。
- 条目 是否以 或 包含 斜杠
/
字符? (最后的那个不算在这里。)如果是这样,这个条目是 anchored 或 rooted (我喜欢这个词 anchored 我自己,但我看到这两个术语都被使用了)。
锚定 条目仅匹配在在该级别 找到的文件或文件夹名称。也就是说,/foo
或 foo/bar
不会匹配 sub/foo
或 sub/foo/bar
,只会匹配 ./foo
和 ./foo/bar
,其中 .
是Git 现在正在扫描的目录(文件夹)。这意味着如果条目有多个级别——例如 foo/bar
或 one/two/three
——Git 将不得不记住在扫描 bar
时应用此条目 foo
,或 one
中的 two
和 one/two
中的 three
。所以我们确实必须考虑“更高级别”的规则。但是由于较低级别的规则得到附加,较低级别.gitignore
可以根据需要取消较高级别的规则。
一个 un-anchored 条目 适用于此,并且——除非被覆盖——在 每个 sub-directory 以及 中。也就是说,如果我们确实有 ./one/two/three
,Git 将很可能打开并读取 one
以找到 two
,然后打开并读取 two
以找到 three
,所有 同时仍在当前目录 上工作。同时 any un-anchored 来自这个 .gitignore
的条目 将应用 within one
和 one/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
文件,或添加到索引中。
由于 dir
和 file
被排除在外,此扫描过程 不会 看到它们,也不会尝试 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/.gitignore
到 append 规则,并且将在扫描 [=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,感谢锚定。
我知道这个问题有点神秘,我无法用一句话准确地表达出来(可能需要一些帮助)。
我在我的主目录中初始化了 git(即 ~/
,在 Arch Linux 上)以备份我的点文件(主要是配置)。我想包括其中的所有文件和文件夹,但以 .
开头的文件和文件夹除外(例如 .config/
和 .bashrc
)。
所以我制作了一个 .gitignore
文件,其内容是:
# Ignore everything
*
# Except these files and folders
!.*
但问题是当我列出所有未跟踪的文件 (git status
) 时,出于某种原因它没有列出 .config/
目录。我尝试使用 .gitignore
并添加
!*/
显示所有目录,包括 .config/
以及 Documents
、Downloads
等,我不想包含这些目录。
而是添加
!.*/
显示以 .
开头的所有其他目录,如 .cache/
、.vim/
等。但由于某些原因 .config/
没有显示。
我什至试过了
!.config/
和
!.config
没用。唯一有效的是!*/
(所有目录,这不是我想要的)
任何解决这个问题的方法。真烦人。
[已解决]:
该错误已在 git 版本 2.34.1
中修复勾选
TL;DR
错误修复后,您将需要在“有点解决”部分或类似内容中输入的内容。我想您会想要我在“底线”部分中添加的内容,真的:
/*
!/.*
长
与.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 将:
- 打开目录
.
. - 打开并阅读此级别的任何
.gitignore
文件,将这些规则添加(附加)到忽略规则中。 (当我们完成这个目录时,这些规则就会被删除。) - 阅读此目录:它包含文件名和 sub-directories(“文件夹”,如果您喜欢该术语)。
- 在我们阅读每个文件和文件夹名称时,请根据所有现在生效的忽略规则检查它们。请注意,有些规则 仅适用于目录/文件夹 ,而其他规则适用于 文件夹和文件 。 folder-only 规则是以斜杠结尾的规则。此外,有些规则是“积极的”(不要忽略),有些是“消极的”(不要忽略)。 ne以
!
. 开头的规则
Git 在当前规则集中找到 最后适用的规则 ,无论那是什么,然后遵守该规则。所以首先,让我们定义哪些规则适用于哪些 directory-scan 结果,然后是各种规则的作用。
.gitignore
中的规则可以是:
- 带有无斜杠的简单文本字符串,例如
generated.file
; - 带有尾部斜杠但没有其他斜杠的文本字符串:
somedir/
; - 带有前导或嵌入式斜杠的文本字符串,有或没有尾部斜杠:
/foo
、a/b
、/foo/
、a/b/
等;或 - 以上任何带有各种 glob 风格的通配符。
这些都可以被否定:如果一个规则以 !
开头,它被否定,我们去掉 !
然后使用剩下的测试。两个关键测试是:
- 条目 是否以 文字
/
结尾?如果是这样,它仅适用于目录/文件夹。回答剩余问题时忽略斜线。 - 条目 是否以 或 包含 斜杠
/
字符? (最后的那个不算在这里。)如果是这样,这个条目是 anchored 或 rooted (我喜欢这个词 anchored 我自己,但我看到这两个术语都被使用了)。
锚定 条目仅匹配在在该级别 找到的文件或文件夹名称。也就是说,/foo
或 foo/bar
不会匹配 sub/foo
或 sub/foo/bar
,只会匹配 ./foo
和 ./foo/bar
,其中 .
是Git 现在正在扫描的目录(文件夹)。这意味着如果条目有多个级别——例如 foo/bar
或 one/two/three
——Git 将不得不记住在扫描 bar
时应用此条目 foo
,或 one
中的 two
和 one/two
中的 three
。所以我们确实必须考虑“更高级别”的规则。但是由于较低级别的规则得到附加,较低级别.gitignore
可以根据需要取消较高级别的规则。
一个 un-anchored 条目 适用于此,并且——除非被覆盖——在 每个 sub-directory 以及 中。也就是说,如果我们确实有 ./one/two/three
,Git 将很可能打开并读取 one
以找到 two
,然后打开并读取 two
以找到 three
,所有 同时仍在当前目录 上工作。同时 any un-anchored 来自这个 .gitignore
的条目 将应用 within one
和 one/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
文件,或添加到索引中。
由于 dir
和 file
被排除在外,此扫描过程 不会 看到它们,也不会尝试 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/.gitignore
到 append 规则,并且将在扫描 [=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,感谢锚定。