我有这样的文件夹结构
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.txt
和2.txt
都没有被跟踪,所以我不清楚在上面的上下文中,/A/*
和/B/*
的正确解释是什么意思。
在其他所有内容都相同的情况下,将以下内容更改为的project/.gitignore
:
!A/
跟踪1.txt
而不跟踪2.txt
。
我想清楚地了解为什么/A/*
不工作,而!A/
在这种情况下工作。
2条答案
按热度按时间c90pui9n1#
您提供的信息不足以重现您的设置:
运行以下脚本:
我完全忽略了目录
B
(在project/A/B
中),这使得1.txt和2.txt都没有被跟踪。如果忽略规则与目录匹配,则
git
将根本不会下降到该目录中,并且没有内部.gitignore
文件可以作用于在其中跟踪的内容。因此,在您的案例中:
/A/*
规则将 * 不 * 忽略目录/A/
:git
将检查其内容,并可能应用/A/.gitignore
中描述的规则,A/B
对/A/*
进行计数,则B/
将被完全忽略,并且B/1.txt
和B/2.txt
都不会被跟踪。这样的规则可以是:
project/A/.gitignore
中的!B/
规则project/.gitignore
中的!A/B
规则你的句子应该调整:
/A/*
模式允许您取消忽略文件和文件夹 * 下一级 *(在A/.gitignore
中),但更深级别的.gitignore
文件不会对它们自己产生影响。igetnqfo2#
请查看LeGEC's answer,找出您所提问题中的一个缺陷。我将忽略这个缺陷,直接研究
.gitignore
规则。但首先,我们需要考虑一些奇怪的事情。在Git和 * 不存储文件夹 * 之间存在某种阻抗不匹配。(仅限文件),以及您的操作系统、这是"Git如何工作"和"你的操作系统如何坚持Git应该工作"之间的根本分歧。这就导致了这个问题。Git必须弥合这种差异,为了做到这一点,Git做出了一定的妥协。背景知识,或者在我们开始之前您需要了解的内容
让我们来看看Git存储的文件和OS存储的文件的区别,假设我们现在在Windows上,文件的路径名是
C:\path\to\file
。我们将在C:\top\mid
中创建一个新的Git仓库,并提交以下两个文件:对于Git,第二个文件是一个名为
sub/file
的 * file *。这会列出这两个文件。从技术上讲,这两个文件在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 rm
或git rm --cached
。这个操作告诉Git在工作树中查找--操作系统要求我们使用的文件夹和文件的东西--Git将从工作树中组装内部格式的、准备提交的"blob"对象,这些对象的哈希ID和路径名Git将隐藏在索引/暂存区中。基本问题
当我们运行任何en-mass
git add
命令时,例如:我们告诉Git * 递归地扫描当前工作目录下的所有文件夹和子文件夹 *。也就是说,Git将打开(使用C库
opendir
函数)路径.
以读取当前目录,它将在其中找到.gitignore
和sub
。如果需要,使用额外的操作系统调用,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。/sub2
或sub2/path
。其中的一部分可以使用glob字符,例如*
或**
(**
名义上表示 * 跨目录/文件夹匹配 *,而不是单个*
,它不会跨越斜杠字符)。!
作为前缀,使其取反。要使!
表示取反,它必须是第一个字符,所以如果你想用!
和/
作为前缀,你必须写!/
,而不是/!
。/
结尾,最后一个斜杠有特殊的含义,不影响“前缀为”或“包含”斜杠的内容。除了最后的斜杠字符之外,关于斜杠的内容有点混乱。我喜欢使用术语 anchored 和 un-anchored 来区分它们:像
sub2
这样的名称或像*
这样的模式是 un-anchored,但是像sub2/path
、/sub2
或/*
这样的名称是 anchored。但是,*/
是 not anchored,因为斜杠是最后一个字符。sub2/
表示“sub 2,但仅当sub 2实际上是一个目录时”,*/
表示“所有内容,但仅当它是一个目录时”。现在我们来看看Git是如何看待这些忽略规则的。记住,当Git扫描某个目录(文件夹),比如
.
或sub
时,它已经读入了相应的.gitignore
文件,并将这些规则转换为内部格式,这样它就知道:/
);/
);!
开始,现在已删除);.gitignore
出现在哪一层(例如,是sub/.gitignore
还是sub2/.gitignore
?--技术上,该信息可以压缩为一个整数,表示递归遍历的深度,但如果这样更容易理解,可以将其视为路径)。Git现在读取目录中的每一个条目,一次一个。每个条目要么命名一个文件(包括一个符号链接,Git将其视为一个文件,其内容就是符号链接目标),要么命名一个文件夹/目录。(在Linux这样的系统上,如果Git遇到“套接字文件”和“设备专用文件”等,它只是跳过它,假装它不存在--Git无法处理这些问题。)
读取了条目的名称后,Git就有了简短的名称(例如
file
或d.ext
)和构造的完整路径(如果我们阅读的是sub
,则为sub/file
;如果我们读的是sub2/b/c
,则为sub2/a/b/c/d.ext
或其他)。如果条目是 not anchored,则只要简单名称(
file
或d.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 * 永远不要忽略任何目录 *,只要遵循一条简单的规则:
这是一个否定的、非锚定的规则。把它放在每个
.gitignore
的 last 项意味着Git将搜索在这一层找到的所有子目录,或者任何没有用自己的.gitignore
覆盖这一规则的更低层。这完全破坏了Git不扫描整个文件子树的优化(有时非常重要)。
一个更有针对性的技巧是,如果存在某条路径:
您可以使用以下前缀:
以确保Git在
keep
中搜索,然后在keep/this/
中搜索,假设keep/.gitignore
不存在或没有覆盖keep/this/
条目。