.gitignore 文件中的单个 /* 和双 /** 尾随星号有什么区别?
What is the difference between a single /* and double /** trailing asterisk in a .gitignore file?
考虑 .gitignore
文件中的以下两种模式
foo/*
foo/**
pattern format specification 状态:
An asterisk *
matches anything except a slash. [...]
A trailing /**
matches everything inside. For example, abc/**
matches all files inside directory abc
, relative to the location of the .gitignore file, with infinite depth.
在模式末尾直接用在斜线之后,这对我来说听起来是一样的。我确实测试了一些情况 - foo
下面有和没有子目录以及各种否定模式 - 没有观察到任何差异。
有没有人会选择 /**
而不是 /*
的情况?
起初,我希望看到一个具有如下所示模式的用例,但并没有,因为这两种模式都会忽略内部的所有内容,而且规范还表示 ".. .] 如果文件的父目录被排除在外,则无法重新包含该文件 [...]""
foo/*
!foo/a/b/c/file.txt
foo/**
!foo/a/b/c/file.txt
技术上的 差异非常明显。如果您正在使用一些 fnmatch 函数来处理 **
、1 并作为您的模式和字符串对传入:
fnmatch(pattern="foo/**", string="foo/bar/baz")
它将匹配。但是,使用模式 foo/*
,它 不会 匹配。
但是,由于 .gitignore
的处理方式,纯正 模式在这里没有任何意义。那是因为你用斜体标注的句子。 Git 在通过工作树进行深度优先搜索之前或期间读取排除文件(.gitignore
、.git/info/exclude
和您的全局排除文件)。这种深度优先搜索使用这种一般形式的代码。我在这里使用 Python 作为语法,但并没有真正尝试使其全部工作(也没有尝试提高效率,与 Git 相比,从内部来说,效率低下)。
# call the given function fn on each file in the directory
# (note that we have already committed to reading the directory).
def search(dir, excludes, fn):
try:
with open(os.path.join(dir, ".gitignore")) as stream:
excludes = excludes.more(dir, stream)
except FileNotFoundError:
pass # ignore the lack of a .gitignore
all_files = os.listdir(dir)
for name in all_files:
full_path = os.path.join(dir, name)
is_dir = os.path.isdir(full_path)
if excludes.is_excluded(name, path, is_dir):
continue # don't add this file or search this directory
if is_dir:
search(full_path, excludes, fn)
else:
fn(full_path)
(我们将通过 cd
-ing 到工作树的顶部并使用 search(".", repo.top_excluder, add_file)
或类似的东西来启动整个事情。这里的 top_excluder 字段包含我们的全局和每个回购模式。请注意,excludes.more()
必须使用一种数据结构,该数据结构在递归 search
调用 returns 时自动清除子目录排除项,并且需要处理排除文件优先级,因为更深的 .gitignore
覆盖了外层 .gitignore
。)
这种处理排除目录的方式是,它根本不会费心去查看它的内部。这就是事实的来源,即仅给出肯定排除(没有 !foo/**
之类的东西),这里不需要 **
:如果我们确定某些目录将被排除,它已经被排除以及其中的所有内容。
但我们不仅有积极的模式:我们也有消极的模式。例如,考虑这个非常简单的 .gitignore
文件:
# ignore things named skip unless they're directories
*skip
!*skip/
否定,!*skip/
,覆盖 *skip
,但仅当文件名为fooskip
或barskip
或实际上 是一个目录 。所以我们确实查看了 fooskip/
内部,当我们在那里时,我们跳过了另一个名为 quuxskip
的文件,但没有跳过名为 plughskip
.
的子目录
这意味着击败Git优化的一个简单方法是:
!*/
这样一行,放置在 .gitignore
文件的适当位置(靠近或末尾),导致所有 目录 被搜索,即使它们否则将被忽略规则忽略。也就是说,我们的 excludes.is_excluded()
调用将接收本地文件名——无论它是什么——以及用于目录测试的 True
标志,以便 */
匹配它;前缀 !
将意味着此目录 而不是 被忽略,因此我们将递归搜索它。
这一行完全放弃了Git 试图在此处 进行的优化,因此如果您有应该忽略的目录,它会相对昂贵。但如果您不想使用更冗长的方法,这是使 .gitignore
表现良好的一种非常快速和肮脏的方法。也就是说,而不是:
foo/*
!foo/one/
foo/one/*
!foo/one/is/
foo/one/is/*
!foo/one/is/important/
foo/one/is/important/*
!foo/one/is/important/this-file
你可以简单地写:
foo/**
!foo/one/is/important/this-file
!foo/**/
这将迫使 Git 费力地搜索整个 foo
目录 及其所有子目录 以便 foo/one/is/important/this-file
file可以通过第二条规则匹配。这里我们需要双 *
因为它们以 foo/
为前缀;如果我们将这个 .gitignore
文件放入 foo/.gitignore
中,我们可以使用更简单的单一 *
形式:
*
!one/is/important/this-file
!*/
无论如何,这是一般原则,也是 **
有用的原因。
(请注意,您也可以在第一次提交之前将一个重要文件强制添加到 Git 的索引中,或者在创建 .gitignore
规则之前添加它那会忽略它。我自己不喜欢这个特殊的技巧,因为它意味着你在 Git 的索引中携带了一个文件,如果它不小心 从 中删除 Git的索引,不会重新添加。)
1请注意,POSIX 和 Python fnmatch
首先都不处理这些。在 Python 中,您需要 glob.glob
。 Git,当然,首先不会将这些公开为函数调用。
考虑 .gitignore
文件中的以下两种模式
foo/*
foo/**
pattern format specification 状态:
An asterisk
*
matches anything except a slash. [...]
A trailing
/**
matches everything inside. For example,abc/**
matches all files inside directoryabc
, relative to the location of the .gitignore file, with infinite depth.
在模式末尾直接用在斜线之后,这对我来说听起来是一样的。我确实测试了一些情况 - foo
下面有和没有子目录以及各种否定模式 - 没有观察到任何差异。
有没有人会选择 /**
而不是 /*
的情况?
起初,我希望看到一个具有如下所示模式的用例,但并没有,因为这两种模式都会忽略内部的所有内容,而且规范还表示 ".. .] 如果文件的父目录被排除在外,则无法重新包含该文件 [...]""
foo/*
!foo/a/b/c/file.txt
foo/**
!foo/a/b/c/file.txt
技术上的 差异非常明显。如果您正在使用一些 fnmatch 函数来处理 **
、1 并作为您的模式和字符串对传入:
fnmatch(pattern="foo/**", string="foo/bar/baz")
它将匹配。但是,使用模式 foo/*
,它 不会 匹配。
但是,由于 .gitignore
的处理方式,纯正 模式在这里没有任何意义。那是因为你用斜体标注的句子。 Git 在通过工作树进行深度优先搜索之前或期间读取排除文件(.gitignore
、.git/info/exclude
和您的全局排除文件)。这种深度优先搜索使用这种一般形式的代码。我在这里使用 Python 作为语法,但并没有真正尝试使其全部工作(也没有尝试提高效率,与 Git 相比,从内部来说,效率低下)。
# call the given function fn on each file in the directory
# (note that we have already committed to reading the directory).
def search(dir, excludes, fn):
try:
with open(os.path.join(dir, ".gitignore")) as stream:
excludes = excludes.more(dir, stream)
except FileNotFoundError:
pass # ignore the lack of a .gitignore
all_files = os.listdir(dir)
for name in all_files:
full_path = os.path.join(dir, name)
is_dir = os.path.isdir(full_path)
if excludes.is_excluded(name, path, is_dir):
continue # don't add this file or search this directory
if is_dir:
search(full_path, excludes, fn)
else:
fn(full_path)
(我们将通过 cd
-ing 到工作树的顶部并使用 search(".", repo.top_excluder, add_file)
或类似的东西来启动整个事情。这里的 top_excluder 字段包含我们的全局和每个回购模式。请注意,excludes.more()
必须使用一种数据结构,该数据结构在递归 search
调用 returns 时自动清除子目录排除项,并且需要处理排除文件优先级,因为更深的 .gitignore
覆盖了外层 .gitignore
。)
这种处理排除目录的方式是,它根本不会费心去查看它的内部。这就是事实的来源,即仅给出肯定排除(没有 !foo/**
之类的东西),这里不需要 **
:如果我们确定某些目录将被排除,它已经被排除以及其中的所有内容。
但我们不仅有积极的模式:我们也有消极的模式。例如,考虑这个非常简单的 .gitignore
文件:
# ignore things named skip unless they're directories
*skip
!*skip/
否定,!*skip/
,覆盖 *skip
,但仅当文件名为fooskip
或barskip
或实际上 是一个目录 。所以我们确实查看了 fooskip/
内部,当我们在那里时,我们跳过了另一个名为 quuxskip
的文件,但没有跳过名为 plughskip
.
这意味着击败Git优化的一个简单方法是:
!*/
这样一行,放置在 .gitignore
文件的适当位置(靠近或末尾),导致所有 目录 被搜索,即使它们否则将被忽略规则忽略。也就是说,我们的 excludes.is_excluded()
调用将接收本地文件名——无论它是什么——以及用于目录测试的 True
标志,以便 */
匹配它;前缀 !
将意味着此目录 而不是 被忽略,因此我们将递归搜索它。
这一行完全放弃了Git 试图在此处 进行的优化,因此如果您有应该忽略的目录,它会相对昂贵。但如果您不想使用更冗长的方法,这是使 .gitignore
表现良好的一种非常快速和肮脏的方法。也就是说,而不是:
foo/*
!foo/one/
foo/one/*
!foo/one/is/
foo/one/is/*
!foo/one/is/important/
foo/one/is/important/*
!foo/one/is/important/this-file
你可以简单地写:
foo/**
!foo/one/is/important/this-file
!foo/**/
这将迫使 Git 费力地搜索整个 foo
目录 及其所有子目录 以便 foo/one/is/important/this-file
file可以通过第二条规则匹配。这里我们需要双 *
因为它们以 foo/
为前缀;如果我们将这个 .gitignore
文件放入 foo/.gitignore
中,我们可以使用更简单的单一 *
形式:
*
!one/is/important/this-file
!*/
无论如何,这是一般原则,也是 **
有用的原因。
(请注意,您也可以在第一次提交之前将一个重要文件强制添加到 Git 的索引中,或者在创建 .gitignore
规则之前添加它那会忽略它。我自己不喜欢这个特殊的技巧,因为它意味着你在 Git 的索引中携带了一个文件,如果它不小心 从 中删除 Git的索引,不会重新添加。)
1请注意,POSIX 和 Python fnmatch
首先都不处理这些。在 Python 中,您需要 glob.glob
。 Git,当然,首先不会将这些公开为函数调用。