在 gitattributes 中使用 `**/` 匹配目录

Match dirs using `**/` in gitattributes

我原以为 **/ 可以匹配 git 存储库中的任何目录,但实际上它什么都不匹配。 man gitattributes 说:

patterns that match a directory do not recursively match paths inside that directory (so using the trailing-slash path/ syntax is pointless in an attributes file; use path/** instead)

但在我的例子中 **/ 甚至不匹配 目录本身 。我的意思是,如果我的存储库中有 a/b.txt,那么 **/ 甚至不匹配目录 a。我期待它匹配目录 a 但不匹配文件 a/b.txt.

如果我将 **/ 更改为 **,则目录及其内容都匹配。在同一示例中,dir a 和 file a/b.txt 都匹配。

那么 git 属性如何作用于目录?如果 git 认为目录的属性无关紧要,为什么 git 在 ** 的情况下列出它们?

执行 特殊情况目录处理的 .gitignore 不同,似乎 .gitattributes 代码从不处理特殊情况,但有一个重要的例外一个目录。这部分是因为 Git 并不真正 store 目录。1 所以每个属性都应用于某些文件集。尽管如此,文件 do 驻留在目录中——在两个真实的操作系统中,以及它们在提交中的存储方式(见脚注 1)——所以它可能对 .gitattributes 对尾部斜线有特殊情况,而不是将属性应用于文件。

不过正如您所观察到的,这并没有发生。尽管 attr.cpath_matches 中)有一些代码检查目录。但这并不重要,因为 Git 属性的有用部分(同样有一个例外) 无论如何只适用于文件.

我上面强调的文字是这个意思。考虑,例如:

*.txt    text
*.jpg    -text diff=jpg
version  export-subst

可能会在某些 .gitattributes 文件中找到。这告诉 Git *.txt 文件是文本,因此任何 CRLF 转换都适用,而 *.jpg 文件是二进制的,它们不会(并且将使用“jpg”生成差异diff 驱动程序:参见 gitattributes)。 version 文件将在执行 git archive 时执行哈希 ID 替换。 所有这些操作都发生在文件上。如果我们有:

sub/    -text

这根本什么都不做,因为目录 sub 永远不会在 Git 的索引中(因此影响 index/worktree 转换的操作不适用)并且只会作为树存储在 Git 存储库中(再次参见脚注 1),您(用户)没有可用的操作或选项。如果你想让sub内的文件被当作二进制文件,你可以这样写:

sub/*    -text
sub/**/* -text

诚然,这是两行,如果 Git 允许 sub/ 作为此处的模式,则其中一行会起作用。所以能够通过目录前缀命名文件可能有点用处,但缺乏这种能力并不是一个严重的问题。

异常和未来的考虑

此处的一个例外是 export-ignore 属性。 git archive 命令从某个存储库中的特定提交构建 tar 或 zip 存档,服从正在存档的快照中的 .gitattributes 文件,如果该文件说不导出一些文件 或目录 ,它不会导出该文件或目录中包含的任何文件。

因为 确实 属性进行目录匹配似乎是明智的,未来的某个 Git 版本可能会这样做,这样你就可以写,例如, assets/ -text。不过,这并没有比 assets/* -text 多多少,目前你必须使用类似后者的东西。


1当Git提交时,Git为顶级目录创建一个对象文件和子目录,每个包含文件的子目录再加一个树对象。它基于 Git 的 index 的内容来执行此操作,它不存储目录本身。索引仅存储 文件.

从技术上讲,对于每个文件,索引中的内容是一个信息元组:。您可以通过 运行 git ls-files --stage 查看此信息。这增加了缓存数据和标志——您也可以通过将 --debug 添加到 git ls-files 命令来看到其中的大部分内容——但是阶段编号通常为零的四元组是关键索引的一部分。这是 Git 存储库索引中 Git 本身的示例:

100644 c2f5fe385af1bbc161f6c010bdcf0048ab6671ed 0       .cirrus.yml
100644 c592dda681fecfaa6bf64fb3f539eafaf4123ed8 0       .clang-format
100644 f9d819623d832113014dd5d5366e8ee44ac9666a 0       .editorconfig
100644 b08a1416d86012134f823fe51443f498f4911909 0       .gitattributes
100644 e7b4e2f3c204c2c94c60222abbc702bd7d72de39 0       .github/CONTRIBUTING.md
100644 952c7c3a2aa11ea1087390be61eab6f7c0013599 0       .github/PULL_REQUEST_TEMPLATE.md
100644 84a5dcff7a05fb724d78826212c5fa22ba5df958 0       .github/workflows/main.yml
100644 ee509a2ad263989fcebe3c3543aa32efed1cacda 0       .gitignore
100644 cbeebdab7a5e2c6afec338c3534930f569c90f63 0       .gitmodules
100644 bde7aba756ea74c3af562874ab5c81a829e43c83 0       .mailmap

只要所有文件都处于零阶段,Git 就可以将索引变成树对象。树对象在那些条目之上包含一个子树,这些条目中似乎有一个目录名,例如 .github/* 文件。在索引中,目录不存在:文件只是命名为 .github/CONTRIBUTING.md 等等。但是,在一次提交中,它们变成了子树。请注意,一个文件名为 github/workflows/main.yml,因此 .github 子树将包含一个名为 workflows 的子树,该子树将包含 main.yml 文件。

从某种意义上说,Git 存储 文件以类似目录树的方式;但它 使用 这些文件,当你在你的存储库中工作时,不考虑目录,因为索引将目录展平。这可能有点设计错误,因为这是 Git 无法正确存储空目录的主要原因。 (已经进行了各种尝试来解决这个问题,唯一正确工作的方法——使用空子模块——非常笨拙。)