在存储库的嵌套文件夹中设置多个 .gitignore 文件的正确方法

Proper way to setup multiple .gitignore files in nested folders of a repository

我有以下文件夹结构:

Project/
    .git/
    .gitignore #1
    a/
        a1/
             a2.txt
             a3.txt
        .gitignore #2
    b/
        b1.txt
    c.txt

我希望 git 不会忽略 a2.txt,并且不会忽略整个 b/。其他一切都应该被忽略。

根据suggestions/comments/answers提供.gitignore #1的内容为:

a/
c.txt

这基本上会忽略 c.txt 和文件夹 a/ 中的所有内容,后者不会被更深的嵌套 .gitignore.

覆盖

.gitignore #2的内容是:

!a1/a2.txt

我希望这个嵌套更深的 gitignore 文件不会忽略文件 a2.txt.

然而,运行 git status --ignored 导致:

On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        .gitignore
        b/

Ignored files:
  (use "git add -f <file>..." to include in what will be committed)
        a/
        c.txt

nothing added to commit but untracked files present (use "git add" to track)

也就是说,整个 a/ 似乎都被忽略了,尽管我希望 .gitignore #2 会提供例外情况。

如何正确使用嵌套 .gitignore 来实现上述要求?

(注意:我只将上面描述中的 .gitignore 文件命名为 #1#2 以明确区分两者。在我的实际计算机中,这些文件正确命名为 .gitignore.)

这里的一般规则是这样的:

  • Git 将使用 OS 的工具来读取目录。
  • 要扫描目录,Git 调用 opendir 和关联的 readdir(最终 closedir)函数。
  • readdir 然后 returns 目录条目 ,一次一个。每个条目都包含一个 name component,定义如下。条目 可能 还包含其他信息——特别是目录与文件的区别——但这就是 Git 在这里真正可以指望的了。如果 OS 用 DT_DIRDT_FILE 等填充 d_type 字段,Git 将尝试使用它,否则 Git 可能不得不退回到调用 lstat(这很昂贵)。

阅读整个目录后,Git 现在有了一组名称组件。 name component 基本上是 path-name 中斜杠之间的部分:例如,对于 path/to/file.ext 我们有三个组件,pathto,以及 file.ext。请注意,/path/to/file.ext 也是如此:前导斜杠仅表示“从顶部”而不是“从我们在树中的任何位置”。 Git 做了一些(相当奇特的)使用相同的想法——以斜杠开头的路径是“根相对”,其余的是“当前位置相对”——当在 .gitignore 中使用“锚定”条目时文件(见下文)。因此,如果 path/to/file 存在于工作树的顶层,则 Git 在扫描顶层目录时将只看到 path 部分。

(旁注:POSIX 也包括 scandir,但人们发现这个接口很难正确使用。在某些系统上它在各种意义上也“更高效”,尽管并非总是如此或者可以预见,使用较低级别的 readdir 例程,而 Git 使用 readdir。)

现在 Git 有了名称组件,Git 可以根据此特定级别的 .gitignore 检查它们(如果存在)。它还可以将每个组件与首先在此处获得 Git 的任何前导路径名组合起来。 对于初始扫描,没有这样的前导组件,也没有合并发生,但让我们在下面观察如果允许我们继续进入 path/(这是一个目录)会发生什么.

组件现在可能需要 类型 检查:文件与目录。 .Real-world 文件系统可能有其他类型,包括 symbolic link,但为了我们这里的目的 symbolic link 暂时被当作一个文件来对待。我们只想知道组件是否代表一个目录。

现在,我们目前阅读的任何 .gitignore 文件中的条目——包括我们正在阅读的 this 目录中的条目——被标记为三个独立方式:

  • 有些是 锚定的 ,例如 /path/toa/b,有些不是,例如 [=34] =] 例如。 anchored 条目是包含任何斜杠的条目 删除单个尾部斜杠(如果存在)之后。

  • 一些仅用于 目录,一些用于 所有名称。如果条目以尾部斜杠结尾,则条目被标记为 directory-only。 (由于尾部斜杠是“仅目录”标志,因此在决定是否设置“锚定”标志时必须忽略它。)

  • 有些是肯定的(“忽略”)条目,有些是否定的(“不要忽略”)条目。否定条目是以 ! 作为第一个字符开头的条目。 (/path 的锚定否定条目必须阅读 !/path/!path 在这里不起作用。)

所以假设我们正在读取顶层,或者我们正在读取顶层内的目录 path。假设我们在此级别遇到两个名称组件:pathto。我们现在或多或少同时检查所有这些东西(按顺序,以便“最后输入”覆盖):

  • 根据所有 non-anchored 忽略表达式检查 目录条目 本身。 path 是其中任何一个的匹配项吗?如果是这样,根据 positive/negative 标志,此名称为 ignored/unignored。

  • 检查到目前为止的完整路径 与所有anchored 忽略表达式。对于 path,这是 /path/path/path/to/path;对于 to,这是 /to/path/to/to/to 之一。 (请记住,我们同时找到了 /path/to,并且我们可能正在查看两者的内部。)如果此 path-so-far 与其中一个锚定表达式匹配,则此名称为 ignored/unignored 根据 positive/negative 标志。

请注意,当我们检查锚定路径时,我们正在查看工作树中的完整路径,而.gitignore本身可能来自sub-path 在 .gitignore 树中。因此,如果我们正在读取目录 /path 并且我们有/path/.gitignore 并且它有一个锚定条目 /xyzzy,我们实际上是在检查这个 /xyzzy/path/xyzzy(因为它来自 /path/.gitignore,而不是来自 /.gitignore).这有点复杂,但是一旦你考虑它就有意义了:锚点是相对于 .gitignore 的位置的。这使您可以重命名目录,而无需编辑任何子 .gitignore 文件中的所有锚定路径。

进一步注意,“匹配”测试可能需要目录条目本身命名一个目录。如果 ignore 条目被标记为 directories-only,就会出现这种情况。因此,要检查这一点,我们需要知道条目——例如 pathto——是否命名了 OS 文件系统中的一个目录。

至此,我们已经完成了对 this 条目必须进行的所有检查。它要么匹配了一些 .gitignore 条目,在这种情况下,匹配 .gitignorelast 就是那个条目,或者没有。并且,子目录 .gitignore 在链的后面匹配,因此 deepest .gitignore 可以 匹配此条目将始终具有 last 匹配项,如果它有匹配项的话。

如果此条目不匹配任何 .gitignore 规则,则此特定名称不会 被忽略。如果它 did 匹配 .gitignore 规则,则最后一个的 positive/negative 标志决定是否忽略此特定名称。

现在我们知道名字是否被忽略了,我们有两个选项,每个选项都有两个sub-options:

  • 被忽略:

    • 如果是目录,我们根本不扫描它。
    • 如果是 文件,我们不会 auto-add 文件(例如 git add .),或者 git status,我们不会抱怨 未跟踪 文件(假设它实际上未被跟踪)。
  • 不忽略:

    • 如果它是一个目录,我们递归扫描它并应用所有这些规则。
    • 如果它是一个 文件,我们 git add 它(例如 git add .)或者如果它没有被追踪一定要投诉(git status).

这决定了 git status 是否抱怨它未被跟踪(对于 git status 命令)或者是否 git add 某种递归风格(git add --allgit add ., git add somedir, 等等) 添加它。

请注意,您可以使用 git add --force 覆盖忽略条目,例如,即使 ignored-file 会被正常的 .gitignore 规则忽略,git add --force ignored-file 也会添加它。我从来没有尝试 git add --force . 看看这里发生了什么,但它可能不太好。它可能完全忽略所有 .gitignore 规则,这看起来很糟糕,或者它可能完全遵守它们,这看起来也很糟糕。我会把它留给 reader 来尝试,看看它做了什么,然后决定它有多糟糕。

另请注意,一旦某个路径名出现在 Git 的索引中,并且 Git 的索引包含 完整路径名 ,例如,path/to/file,作为其中包含文字斜杠的文字字符串——该文件 不会被忽略 即使它列在 .gitignore 文件中。忽略规则特定于递归目录移动过程,但是 Git 的索引 中列出的文件被 跟踪并且 检查en-masse git add . 操作。一旦你越过了 OS-interaction 的东西并进入 Git 正确的地方,文件就不再有“包含目录”,它们只有长路径字符串,如果需要的话,它们会嵌入正斜杠。

Git 的索引无法存储裸目录名称,1,这就是您无法提交空目录的原因。扫描过程将 扫描 目录以获取 文件 并将(在适当的条件下)将这些文件添加到索引中,但不会添加包含目录。最接近的 Git 是 submodule 条目存储为 so-called gitlink ,一个带有 mode 160000 的“文件”,如果它是一个 Linux file-system 实体,它将是 directory-and-symbolic-link 的组合(这在文件系统中是不允许的)。这就是 store an empty directory 尝试出错的原因(但您可以存储没有文件的子模块!)。


1从技术上讲,它可以,只是不能存储为Git使用的那种条目为下一次提交跟踪 files。 Git 的索引增长了一大堆奇怪的 add-ons 以提高效率,其中包括跟踪未跟踪的内容(so-called 未跟踪的缓存 ),其中包括未跟踪的目录。所以它不能 track 一个目录但是它可以 untrack 一个!