嵌套.gitignore文件--/folder/* 和!folder/之间的区别

ecbunoof  于 2022-11-27  发布在  Git
关注(0)|答案(2)|浏览(193)

我有这样的文件夹结构

project/
       ----A/
            ----B/
                 -1.txt
                 -2.txt
                 -.gitignore [ content is: (Line1) * (Line2) !1.txt ]
            -.gitignore [ content is: (Line1) /B/* ]
       -.gitignore [ content is: (Line1) /A/*
.git/
-.gitignore [content is: (Line1) /project/*]

上面的语句既不跟踪1.txt,也不跟踪2.txt
我对project/.gitignore的理解其中包含:

/A/*

洗涤:
忽略文件夹A/ * 下的所有内容,除了 * 在子文件夹中更深的.gitignore中可能遇到的例外,例如,由于project/A/B/.gitignore,这是:

*
!1.txt

这也是我对project/A/.gitignore的解释,它是:

/B/*

也就是说,忽略文件夹B/ * 下的所有内容,除了 * 在子文件夹中更深的.gitignore中可能遇到的例外,例如,由于project/A/B/.gitignore
因为在上面的例子中,1.txt2.txt都没有被跟踪,所以我不清楚在上面的上下文中,/A/*/B/*的正确解释是什么意思。
在其他所有内容都相同的情况下,将以下内容更改为的project/.gitignore

!A/

跟踪1.txt而不跟踪2.txt
我想清楚地了解为什么/A/*不工作,而!A/在这种情况下工作。

c90pui9n

c90pui9n1#

您提供的信息不足以重现您的设置:
运行以下脚本:

#!/bin/bash

rm -rf /tmp/testrepo
mkdir -p /tmp/testrepo
cd /tmp/testrepo

git init

mkdir -p project/A/B

touch project/A/B/1.txt project/A/B/2.txt

check_ignore () {
        local path=$1
        echo "--- checking $path:"
        git check-ignore -v "$path"
}

echo "# with initial .gitignore files:"

check_ignore project/A
check_ignore project/A/B
check_ignore project/A/B/1.txt
check_ignore project/A/B/2.txt

echo "!A/" >> project/.gitignore

echo
echo "# after adding '!A/' in project/.gitignore:"

check_ignore project/A
check_ignore project/A/B       # that directory is still gitignored
                               # by the '/A/*' gitignore rule
check_ignore project/A/B/1.txt # so its content is not inspected
check_ignore project/A/B/2.txt

我完全忽略了目录B(在project/A/B中),这使得1.txt和2.txt都没有被跟踪。
如果忽略规则与目录匹配,则git将根本不会下降到该目录中,并且没有内部.gitignore文件可以作用于在其中跟踪的内容。
因此,在您的案例中:

  • /A/*规则将 * 不 * 忽略目录/A/git将检查其内容,并可能应用/A/.gitignore中描述的规则,
  • 然而,如果没有规则针对A/B/A/*进行计数,则B/将被完全忽略,并且B/1.txtB/2.txt都不会被跟踪。

这样的规则可以是:

  • project/A/.gitignore中的!B/规则
  • project/.gitignore中的!A/B规则

你的句子应该调整:
/A/*模式允许您取消忽略文件和文件夹 * 下一级 *(在A/.gitignore中),但更深级别的.gitignore文件不会对它们自己产生影响。

igetnqfo

igetnqfo2#

请查看LeGEC's answer,找出您所提问题中的一个缺陷。我将忽略这个缺陷,直接研究.gitignore规则。但首先,我们需要考虑一些奇怪的事情。在Git和 * 不存储文件夹 * 之间存在某种阻抗不匹配。(仅限文件),以及您的操作系统、这是"Git如何工作"和"你的操作系统如何坚持Git应该工作"之间的根本分歧。这就导致了这个问题。Git必须弥合这种差异,为了做到这一点,Git做出了一定的妥协。

背景知识,或者在我们开始之前您需要了解的内容

让我们来看看Git存储的文件和OS存储的文件的区别,假设我们现在在Windows上,文件的路径名是C:\path\to\file。我们将在C:\top\mid中创建一个新的Git仓库,并提交以下两个文件:

.gitignore
sub/file

对于Git,第二个文件是一个名为sub/file的 * file *。

git ls-files --stage

这会列出这两个文件。从技术上讲,这两个文件在Git的 * index * 或 * staging area * 中,但Git是从索引中构建提交的,而不是从工作树中构建提交的。(术语 * index * 和 * staging area * 几乎可以互换。在讨论Git的技术方面时,出于各种原因,我倾向于使用更短、更没有意义的一个。)
相比之下,你的Windows机器上没有一个名为sub/file的文件,而是在C:\top\mid中有一个名为sub的文件夹,在sub文件夹中有一个名为file的文件,所以这个文件的完整路径是C:\top\mid\sub\file,Git知道仓库本身是C:\top\mid,并把这部分去掉,和 * 构造 * 名称sub/file,并带有 * 正斜杠 *,以便在您适当地运行git add时更新其文件的索引副本。
所以Git有一种平面文件系统,文件名中嵌入了"文件夹名"和正斜杠。但 * 计算机的 * 文件系统有文件夹和文件。即使我们迁移到macOS或Linux或其他系统,我们仍然有文件夹和文件的安排;我们现在使用/top/mid/sub/file来代替愚蠢的驱动器号和烦人的反斜杠。
因为Git实际上是通过向仓库写出一个包含所有文件的提交来创建 * 新 * 的提交(名称和内容)就像索引/暂存区中列出的那样,我们的工作--每当我们做新的工作时--包括更新,也许添加和/或删除 * 工作树 * 中的操作系统风格的文件,但之后我们必须 * 告诉Git更新它的索引 *。我现在有了新的东西 * step--使用git add,有时使用git rmgit rm --cached。这个操作告诉Git在工作树中查找--操作系统要求我们使用的文件夹和文件的东西--Git将从工作树中组装内部格式的、准备提交的"blob"对象,这些对象的哈希ID和路径名Git将隐藏在索引/暂存区中。

基本问题

当我们运行任何en-mass git add命令时,例如:

git add .

我们告诉Git * 递归地扫描当前工作目录下的所有文件夹和子文件夹 *。也就是说,Git将打开(使用C库opendir函数)路径.以读取当前目录,它将在其中找到.gitignoresub。如果需要,使用额外的操作系统调用,Git会发现.gitignore * 是 * 一个文件,而sub * 是 * 一个文件夹,并且会得到关于 * 这个文件和文件夹的lstat数据。
Git的索引--其具有第三项 * cache *--包含先前获得的lstat数据,并且Git有时可以使用该索引来非常快速地确定例如.gitignore文件未被修改,因此不需要用一个新的压缩和Git化文件替换.gitignore的索引副本。(随着时间的推移,Git软件变得越来越复杂,除了某些例外),索引中没有 * folder * 的条目,所以一般来说,Git会被强制打开并读取sub文件夹,递归地,与打开和读取.文件夹的方式相同。
打开并读取完sub后,Git会找到file,然后Git将两个名字组合起来得到sub/file(即使在Windows上,操作系统也希望将其命名为sub\file).像往常一样,缓存的lstat数据不一定能让Git快速跳过打开、读取、压缩如果没有,Git会打开并读取和压缩它,然后检查该内容是否已经存在于仓库中的任何提交位置。

所有这些扫描、打开和阅读都是 * 非常慢的 *。所以对于不应该添加的文件,我们通过在.gitignore中列出它们的名称来避免Git的麻烦。这对 * 文件 * 来说很好--但是对于mid中的每个文件夹,Git都必须打开和读取它,对于该文件夹中的每个子文件夹,Git都必须打开和读取它,因为Git已经做了很好的优化,所以递归扫描目录通常是git add中最慢的部分。
为了让这个过程 * 更 * 快,Git试图变聪明。假设我们最终会 * 忽略 * sub2中的所有内容,因为有一行像sub2/**sub2/*sub2/。那么Git可以直接跳过sub2文件夹,而不是 * 打开并阅读 * 它!
所以,Git会这样做:如果我们告诉Git某个 directory aka folder 应该被忽略,Git会跳过打开和阅读它。这意味着该目录中的任何文件--甚至是其中的.gitignore文件--都不会被 * 看到 ,因此不能被服从。
这意味着如果你想让Git进入某个目录(文件夹)来 * 扫描 * 它,路径的那部分,从.开始(在我们的例子中对应于top/mid
一定不能被忽略 *。一旦它 * 不 * 被忽略,Git就会打开并阅读它,包括它所包含的任何.gitignore文件。然后,将.gitignore文件 * 中 * 的规则临时添加到顶层.gitignore和/或.git/info/exclude和/或core.excludesFile忽略规则(优先级较高,但强制限制在此子目录)。

更多详细信息

记住上面的内容-这些规则涵盖了Git * 看到 * 的内容,如果Git没有 * 看到 * 某些内容,它就不可能git add它-我们现在来看看单独的.gitignore-file规则:

  • 条目可以是简单的名称,也可以是像sub2*这样的glob。
  • 一个条目可以 * 以斜杠作为前缀 *,或者 * 包含 * 斜杠,例如/sub2sub2/path。其中的一部分可以使用glob字符,例如*****名义上表示 * 跨目录/文件夹匹配 *,而不是单个*,它不会跨越斜杠字符)。
  • 一个条目可以用!作为前缀,使其取反。要使!表示取反,它必须是第一个字符,所以如果你想用!/作为前缀,你必须写!/,而不是/!
  • 一个条目可以以/结尾,最后一个斜杠有特殊的含义,不影响“前缀为”或“包含”斜杠的内容。

除了最后的斜杠字符之外,关于斜杠的内容有点混乱。我喜欢使用术语 anchoredun-anchored 来区分它们:像sub2这样的名称或像*这样的模式是 un-anchored,但是像sub2/path/sub2/*这样的名称是 anchored。但是,*/not anchored,因为斜杠是最后一个字符。

  • final* 斜杠(如果存在)表示“仅当这是一个目录时”,因此sub2/表示“sub 2,但仅当sub 2实际上是一个目录时”,*/表示“所有内容,但仅当它是一个目录时”。

现在我们来看看Git是如何看待这些忽略规则的。记住,当Git扫描某个目录(文件夹),比如.sub时,它已经读入了相应的.gitignore文件,并将这些规则转换为内部格式,这样它就知道:

  • 此规则仅适用于目录,或者不适用(具有现在已删除的尾随/);
  • 此规则是锚定的,或者不是(有或没有另一个/);
  • 是否定的,或者不是(did或didn 't以!开始,现在已删除);
  • .gitignore出现在哪一层(例如,是sub/.gitignore还是sub2/.gitignore?--技术上,该信息可以压缩为一个整数,表示递归遍历的深度,但如果这样更容易理解,可以将其视为路径)。

Git现在读取目录中的每一个条目,一次一个。每个条目要么命名一个文件(包括一个符号链接,Git将其视为一个文件,其内容就是符号链接目标),要么命名一个文件夹/目录。(在Linux这样的系统上,如果Git遇到“套接字文件”和“设备专用文件”等,它只是跳过它,假装它不存在--Git无法处理这些问题。)
读取了条目的名称后,Git就有了简短的名称(例如filed.ext)和构造的完整路径(如果我们阅读的是sub,则为sub/file;如果我们读的是sub2/b/c,则为sub2/a/b/c/d.ext或其他)。

  • 如果条目是 not anchored,则只要简单名称(filed.ext)与此非锚定规则匹配,只要任何“must be a directory”的内容匹配,它就匹配。

  • 如果条目 * 是 * anchored,那么完整的路径名必须符合anchored规则,不包括任何基于深度被排除的部分。例如,如果我们在sub2/b/c中查找,并且有一个sub2/b/.gitignore表示c/d.ext,那么如果这是d.ext,我们在这里匹配,但是如果条目表示x/d.ext,我们就不匹配:我们从完整路径中去掉的部分是sub2/b/,因为这是.gitignore规则的来源)。

[Note **的匹配在这里变得有点复杂,有时候,试图加快这一过程的.gitignore代码在测试版本中也会出错。Git的内部测试套件为了捕捉这样的bug变得很复杂。]
如果条目 * 不 * 匹配,我们继续;如果它 * 匹配 *,它被记住,我们继续。我们对每个.gitignore条目都这样做,并取 * 最后一个 * 匹配,不管它是什么,否则我们没有匹配。
如果没有匹配项,则不忽略该文件或目录。如果它是一个文件,我们将考虑git add-ing它,或者递归扫描它。
如果有匹配项,则忽略该文件或目录,除非它是否定规则:对于否定规则,我们假装没有匹配。
这是一整套规则,注意没有例外,例如,“有一条规则说不要费心去阅读sub2,即使有一条附加的否定规则说要保留sub2/important.file“。我认为Git应该自动为你做这件事,至少对于常量字符串是这样(像***这样的glob匹配器可能会使它变得太难)。

一些一般性的有用提示

常见的问题是Git忽略了我们希望它搜索的目录。我们可以(付出代价)告诉Git * 永远不要忽略任何目录 *,只要遵循一条简单的规则:

!*/

这是一个否定的、非锚定的规则。把它放在每个.gitignorelast 项意味着Git将搜索在这一层找到的所有子目录,或者任何没有用自己的.gitignore覆盖这一规则的更低层。
这完全破坏了Git不扫描整个文件子树的优化(有时非常重要)。
一个更有针对性的技巧是,如果存在某条路径:

!keep/this/important.file

您可以使用以下前缀:

!keep/
!keep/this/

以确保Git在keep中搜索,然后在keep/this/中搜索,假设keep/.gitignore不存在或没有覆盖keep/this/条目。

相关问题