2

.gitignore考虑文件中的以下两种模式

foo/*
foo/**

模式格式规范指出:

星号*匹配除斜线之外的任何内容。[...]

尾随/**匹配内部的所有内容。例如,abc/**匹配目录中的所有文件abc,相对于 .gitignore 文件的位置,具有无限深度。

在斜线之后直接在模式末尾使用时,这对我来说听起来是一样的。我确实测试了一些案例 - 有和没有下面的子目录foo以及各种否定模式 - 并且没有观察到任何差异。

/**有没有什么情况会选择/*呢?


起初,我希望看到一个具有如下模式的用例,但没有,因为这两种模式都会忽略内部的所有内容,并且规范还指出“[...] 无法重新包含如果该文件的父目录被排除在外,则该文件 [...]"

foo/*
!foo/a/b/c/file.txt

foo/**
!foo/a/b/c/file.txt
4

1 回答 1

1

技术差异很明显。如果您正在使用一些处理**, 1并作为模式和字符串对传入的 fnmatch 函数:

fnmatch(pattern="foo/**", string="foo/bar/baz")

匹配。但是,使用 pattern foo/*,它不会匹配。

然而,由于处理 s 的方式,这里对于纯正模式.gitignore没有意义。那是由于您用斜体字标注的那句话。Git在深度优先搜索工作树之前或期间读取排除文件(,和您的全局排除文件)。这种深度优先搜索使用这种一般形式的代码。我在这里使用 Python 作为语法,但并没有真正尝试让它全部工作(也没有尝试提高效率,与 Git 相比,从内部来说,效率低下)。.gitignore.git/info/exclude

# 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 字段包含我们的全局和每个 repo 模式。请注意,excludes.more()必须使用自动清除的数据结构递归search调用返回时排除子目录,并且需要处理排除器文件优先级,因为更深.gitignore的覆盖了外层.gitignore。)

这种处理排除目录的方式是它根本不会费心去查看它的内部。这就是事实的根源,即仅给定积极的排除(没有任何!foo/**东西),这里没有必要**:如果我们确定某个目录将被排除,则它已经与其中的所有内容一起被排除。

但我们不仅有积极的模式:我们也有消极的模式。例如,考虑这个非常简单的.gitignore文件:

# ignore things named skip unless they're directories
*skip
!*skip/

否定, !*skip/,覆盖,*skip但仅当文件名为fooskiporbarskip或实际上是目录时。所以我们确实往里面看fooskip/,当我们在那里时,我们跳过了另一个名为quuxskip但不是名为的子目录的文件plughskip

这意味着打败 Git 优化的一个简单方法是:

!*/

这样的行放置在.gitignore文件的适当位置(靠近或末尾)会导致搜索所有目录,即使它们会被忽略规则忽略。也就是说,我们的excludes.is_excluded()调用将接收本地文件名——不管它是什么——和一个True用于 is-a-directory 测试的标志,以便*/匹配它;前缀!意味着该目录不会被忽略,因此我们将递归搜索它。

这一行完全放弃了 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 文件可以被第二条规则匹配。这里我们需要双*精度,因为它们以foo/;为前缀。如果我们把这个.gitignore文件放进去foo/.gitignore,我们可以使用更简单的单一*形式:

*
!one/is/important/this-file
!*/

无论如何,这是一般原则,也是一个**有用的理由。

(请注意,您也可以在进行第一次保存它的提交之前将一个重要文件强制添加到 Git 的索引中,或者在创建.gitignore将忽略它的规则之前添加它。我自己不喜欢这个特殊的技巧,因为它意味着你在 Git 的索引中有一个文件,如果它被意外地从Git 的索引中删除,将不会被重新添加。)


1请注意,POSIX 和 Pythonfnmatch一开始都不会处理这些问题。在 Python 中,你会想要glob.glob. 当然,Git 一开始并没有将这些公开为函数调用。

于 2021-05-15T23:20:39.293 回答