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

发布于 2025-01-18 16:19:46 字数 1378 浏览 4 评论 0 原文

我有以下文件夹结构:

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

我想拥有 git 不要忽略 a2.txt ,而不是忽略 B/的全部。其他一切都应忽略。

根据建议/注释/答案提供 .gitignore#1 的内容是:

a/
c.txt

这实际上忽略了 c.txt 以及文件夹中的所有内容 a/,后者受到不被更深的嵌套 .gitignore 覆盖的约束。

.gitignore#2 的内容是:

!a1/a2.txt

我希望更深的嵌套 gitignore 文件将导致不忽略文件 a2.txt

但是,运行 git状态 - 签名结果:

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 以澄清两个。

I have the following folder structure:

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

I would like to have git not ignore a2.txt, and not ignore entirety of b/. Everything else should be ignored.

Based on suggestions/comments/answers provided here, the content of .gitignore #1 is:

a/
c.txt

This essentially ignores c.txt and everything in folder a/, the latter being subject to not being overridden by a deeper nested .gitignore.

The content of .gitignore #2 is:

!a1/a2.txt

I was hoping this deeper nested gitignore file would lead to not ignoring file a2.txt.

However, running git status --ignored results in:

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)

That is, the entirety of a/ seems to be ignored despite the exception I was hoping would be provided by .gitignore #2.

How can nested .gitignore's be correctly used to achieve the requirement above?

(Note: I have only named the .gitignore files in the description above as #1 and #2 for clarificatory purposes to differentiate between the two. In my actual computer, these files are properly named just .gitignore.)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

梦里寻她 2025-01-25 16:19:46

这里的一般规则是:

  • GIT将使用操作系统的设施读取目录。
  • 要扫描目录,git调用和关联的 readdir (以及最终 nocleir )功能。
  • readdir 然后返回目录条目一次。每个条目都保留了以下定义的A 名称组件。条目 May 还保留其他信息,尤其是目录与文件区别,但这与Git一样多,可以真正依靠此处。如果操作系统填充 d_type 带有 dt_dir dt_file 等。回到调用 lstat (这很昂贵)。

阅读了整个目录后,Git现在具有一组名称组件。 a name component 基本上是斜线之间的路径名的一部分:路径,<代码> 和 file.ext 。请注意,对于/path/to/file.ext :领先的斜线仅表示“从顶部”而不是“从我们在树上的任何地方”而言,这也是如此。 git可以使用相同的想法(相当奇特)的使用 - 从斜线开始的路径是“ root相对”,其余的是“当前位置相对”,当使用 .gitignore中的“锚定”条目文件(请参见下文)。因此,如果 path/to/file 存在于工作树的顶层中,则git在扫描顶级目录时仅会看到 path 零件。

(旁注:POSIX还包括 scandir < /a>,但人们发现此接口很难正确使用。 git使用 readdir 。)

现在,Git具有名称组件,Git可以根据该特定级别的 .gitignore (如果存在)检查它们。它还可以将每个组件与任何领先的路径名相结合,这些路径名首先在这里获得git。 对于初始扫描没有这样的领先组件,也不会发生组合,但是让我们在下面观察如果允许我们进入 path/(这是目录)。

现在,组件可能需要A 类型检查:文件与目录。 .REAL-WORLD文件系统可能具有其他类型,包括符号链接,但就我们的目的而言,符号链接 目前将被视为文件。我们只想知道组件是否代表目录。

现在,我们到目前为止已阅读的任何 .gitignore 文件中的条目 - 包括我们现在正在阅读的 this 目录中的一个条目 - 以三种独立的方式标记:

  • < p>有些是锚定,例如/path/to a/b ,而有些则不是,例如 in *.o 例如。 锚定条目是一个包含任何斜线 删除单个尾随的斜线。

  • 有些仅适用于目录,而有些则用于所有名称。如果条目以直接斜线结束,则将其标记为仅目录。 (由于尾斜线是“仅目录”标志的意思,因此在决定是否设置“锚定”标志时必须忽略它。)

  • 。 (“不要忽略”)条目。负面条目是以作为第一个字符开始的条目。 (/path 的锚定负条目必须读取!/path ; /!路径在这里不起作用。)

因此,让我们想象一下我们正在阅读最高级别,或者我们正在读取目录 path 在顶级。假设我们在此级别上遇到两个名称组件: path , to 。现在,我们或多或少同时检查所有这些内容(按顺序,“最后一个条目”覆盖):

  • 检查目录条目本身本身忽略表达式。 路径是否适合其中任何一个?如果是这样,则根据正/负标志忽略/Unigning。

  • 检查到到目前为止的完整路径 针对所有锚定忽略表达式。对于路径这是/path /path/path/path ,或/to/path ;对于,这是/to /path/to /to/to/to/to/to 之一。 (请记住,我们同时找到了/path /to ,并且大概是我们在内部都在内部。表达式,根据正/负标志对此名称被忽略/无标记。

请注意,当我们 do 检查锚固路径时,我们正在查看工作树中的完整路径,而 .gitignore 本身可能来自内部的子路径 .gitignore 树。因此,如果我们正在读取目录/path 例如,我们有/path/.gitignore ,并且它具有锚定的条目读取/xyzzy ,我们真的在检查此/Xyzzy 针对/path/xyzzy (因为它来自/path/.gitignore ,而不是/ .gitignore )。这有点复杂,但是一旦考虑一下它是有道理的:锚是相对于 .gitignore 的位置。这使您可以重命名目录,而无需在任何子 .gitignore 文件中编辑所有锚定路径。

进一步注意,“是匹配”测试可能要求目录条目本身名称为目录。如果忽略条目被标记为仅目录,则是这种情况。因此,要检查这一点,我们需要知道输入是否是 path 或 to 例如 - 将OS文件系统中的目录命名为“名称”。

在这一点上,我们已经完成了所有必须在此上进行的检查。它要么匹配一些 .gitignore 条目或条目,在这种情况下, last 匹配的 .gitignore 是摄取的,或者未进行。并且,subdirectory .gitignore s稍后在链条中匹配,因此最深 .gitignore 可以匹配如果具有匹配项,则此条目将始终具有Last 匹配。

如果此条目与任何 .gitignore 规则不匹配,则此特定名称是不忽略的。如果它 did 匹配 .gitignore 规则,则最后一个人的正/负标志确定该特定名称是否被忽略。

现在我们知道该名称是否被忽略,我们有两个选项,每个选项都有两个子选项:

  • 它被忽略:

    • 如果是目录,我们根本不扫描它。
    • 如果是 file ,我们不自动添加文件(使用 git add。实例)或 git状态,我们不抱怨 未跟踪文件(假设它实际上是未跟踪的)。


  • 不忽略它:

    • 如果是目录,我们会递归扫描并应用所有这些规则。
    • 如果是 file ,我们 git add it(对于 git add。添加。 git状态)。


这确定 git状态是否抱怨它是未经跟踪的( git状态命令),还是 git git add 某种递归风味( git add - all , git add。 git add add somedir 等)添加了它。

请注意,您可以使用 git add-force ,例如, git add add-force忽略 - 忽略f-file 添加它,即使忽略忽略代码>将被普通 .gitignore 规则忽略。我从未尝试过 git add -force。查看这里会发生什么,但这可能不好。

The general rule here is this:

  • Git will use the OS's facilities to read directories.
  • To scan a directory, Git calls opendir and the associated readdir (and eventually closedir) functions.
  • readdir then returns directory entries, one at a time. Each entry holds a name component as defined below. Entries may also hold additional information—in particular the directory vs file distinction—but that's as much as Git can really count on here. If the OS fills in a d_type field with DT_DIR, DT_FILE, etc., Git will try to use that, otherwise Git may have to fall back to calling lstat (which is expensive).

Having read the entire directory, Git now has a set of name components. A name component is basically the part of a path-name that goes between slashes: for instance, with path/to/file.ext we have three components, path, to, and file.ext. Note that the same is true for /path/to/file.ext: the leading slash just means "from the top" rather than "from wherever we are in the tree". Git makes some (rather peculiar) use of this same idea—that paths starting with a slash are "root relative" and the rest are "current position relative"—when using "anchored" entries in .gitignore files (see below). So if path/to/file exists in the top level of a working tree, Git will see only the path part when it scans the top level directory.

(Side note: POSIX also includes scandir, but people find this interface hard to use correctly. It's also "more efficient" in various senses on some systems, although not always or very predictably, to use the lower level readdir routines, and Git uses readdir.)

Now that Git has the name components, Git can check them against this particular level's .gitignore, if it exists. It can also combine each component with any leading path name that got Git here in the first place. For the initial scan there is no such leading component, and no combining happens, but let's observe below what happens if we are allowed to proceed into path/ (which is a directory).

The components may now need a type check: file vs directory. .Real-world file systems may have additional types, including symbolic link, but for our purposes here symbolic link is to be treated like a file for the moment. We just want to know whether component represents a directory.

Now, entries in any .gitignore file that we have read so far—including the one in this directory that we're reading now—are flagged in three independent ways:

  • Some are anchored, as in /path/to or a/b for instance, and some are not, as in *.o for instance. An anchored entry is one containing any slashes after removing a single trailing slash if it exists.

  • Some are for directories only and some are for all names. An entry is flagged as directory-only if it ends in a trailing slash. (Since the trailing slash is meant as the "directory only" flag, it has to be ignored while deciding whether to set the "anchored" flag.)

  • Some are positive ("do ignore") entries, and some are negative ("do not ignore") entries. A negative entry is one that starts with ! as the first character. (An anchored negative entry for /path would have to read !/path; /!path does not work here.)

So let's imagine that we're reading the top level, or that we're reading directory path within the top level. Let's suppose we encounter two name components at this level: path, and to. We now check all of these things more or less at the same time (in order, so that "last entry" overrides):

  • Check the directory entry itself against all non-anchored ignore expressions. Is path a match for any of those? If so, this name is ignored/unignored as per the positive/negative flag.

  • Check the full path so far against all anchored ignore expressions. For path this is /path, /path/path, or /to/path; for to, this is one of /to, or /path/to, or /to/to. (Remember that we found both /path and /to and presumably we're looking inside both.) If this path-so-far is a match against one of the anchored expressions, this name is ignored/unignored as per the positive/negative flag.

Note that when we do check an anchored path, we're looking at the full path in the working tree, while the .gitignore itself might be from a sub-path within the .gitignore tree. So if we're reading directory /path for instance and we have /path/.gitignore and it has an anchored entry reading /xyzzy, we're really checking this /xyzzy against /path/xyzzy (because it's from /path/.gitignore, not from /.gitignore). This is a little complicated, but makes sense once you think about it: the anchor is relative to the .gitignore's location. This lets you rename directories without having to edit all anchored paths in any sub-.gitignore files.

Note further that the "is a match" test may require that the directory entry itself name a directory. This is the case if the ignore entry is flagged as directories-only. So to check for that, we need to know if the entry—path or to for instance—names a directory in the OS's file system.

At this point, we have done all the checks we must do on this entry. It either matched some .gitignore entry or entries, in which case the last matching .gitignore is the one taken, or it did not. And, subdirectory .gitignores are matched later in the chain, so that the deepest .gitignore that could match this entry will always have the last match, if it has a match.

If this entry did not match any .gitignore rule, this particular name is not ignored. If it did match a .gitignore rule, the last one's positive/negative flag determines whether this particular name is ignored or not.

Now that we know if the name is ignored, we have two options, each of which has two sub-options:

  • It is ignored:

    • If it's a directory, we simply don't scan it at all.
    • If it's a file, we don't auto-add the file (with git add . for instance), or for git status, we don't complain about the untracked file (assuming it is in fact untracked).
  • It is not ignored:

    • If it's a directory, we scan it recursively and apply all these rules.
    • If it's a file, we git add it (for git add . for instance) or make sure to complain if it's untracked (git status).

This determines whether git status complains about it being untracked (for git status commands) or whether git add of some sort of recursive flavor (git add --all, git add ., git add somedir, etc.) adds it.

Note that you can override ignore entries with git add --force, e.g., git add --force ignored-file adds it even if ignored-file would be ignored by the normal .gitignore rules. I have never tried git add --force . to see what happens here, but it's probably not good. ???? It might completely ignore all .gitignore rules, which seems bad, or it might completely obey them, which also seems bad. I will leave it to the reader to try it, see what it does, and decide how bad that is.

Note also that once some pathname is in Git's index—and Git's index holds full path names, e.g., path/to/file, as a literal string with literal slashes in it—that file is not ignored even if it's listed in a .gitignore file. The ignoring rules are specific to the recursive directory travel process, but files listed in Git's index are tracked and are checked by en-masse git add . operations. Once you get past the OS-interaction stuff and into Git proper, files no longer have "containing directories", they just have long path strings with embedded forward slashes if needed.

Git's index is unable to store a bare directory name,1 and that's why you cannot commit an empty directory. The scanning process will scan directories for files and will (under appropriate conditions) add those files to the index, but it won't add the containing directory. The closest Git gets to this is that a submodule entry is stored as a so-called gitlink, a "file" with mode 160000, which if it were a Linux file-system entity would be a combination of directory-and-symbolic-link (which is not allowed in the file system). This is why the attempts to store an empty directory go awry (but you can store a submodule that has no files!).


1Technically, it can, it just can't be stored as the kind of entry that Git uses to keep track of files for the next commit. Git's index has grown a whole bunch of weird add-ons for efficiency, and that includes keeping track of untracked stuff (the so-called untracked cache), which includes untracked directories. So it can't track a directory but it can untrack one! ????

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文