首页 > 解决方案 > 初始化“空”目录

问题描述

文件/目录结构:

main/home/script.py
main/home/a/.init
main/home/b/.init

我想设置我gitignore排除主目录中的所有内容,但要包含特定的文件类型。

我尝试了什么:

home/*      #exclude everything in the home directory and subdirectories
!home/*.py  #include python files immediately in the home directory
!**.init    #include .init files in all directories and subdirectories.

问题,我似乎无法确保.init包含文件。该文件的目的是确保 git 将创建我所有的目录,即使它们还没有文件。因此,我想在每个目录中放置一个空的 0 字节 .init 文件,以确保“空”目录由 git 提交。

谢谢。

标签: gitgitignore

解决方案


例如,如果你想创建home/foo/.init这个文件并将其放入 Git 的索引中(有关索引的更多信息,请参见下文),你需要告诉 Git不要切断对home/*/目录的搜索:

!home/*/

然后,正如Fady Adal 指出的(但我稍作调整),您可能还想要:

!**/.init

这样当 Git 搜索时home/*/,它会找到并取消忽略名为.init. 请注意,这将忽略所有 .init文件;也许你想要:

!home/**/.init

在这里,您可以忽略一个名为的文件,例如nothome/foo/.init. (您甚至可以home/**/*在不忽略home/**/*/and时忽略home/**/.init。)

龙:这是怎么回事?

我喜欢说 Git 只存储文件,而不存储目录,这是真的——但它是真的原因与 Git 构建新提交的方式有关,即从 Git 的index

提交和索引

每个提交都存储了 Git 知道的每个文件的完整副本。然而,这个完整的副本以一种特殊的、只读的、仅限 Git 的、永久冻结的格式存储,其中重复的文件会自动进行重复数据删除。这样,您的第一次提交(例如)一个README.md几乎不会更改的文件这一事实意味着每个提交都只是共享README.md文件。如果它确实发生了变化,新的提交将开始共享新文件。如果它变回,之后的新提交将返回共享原始文件。因此,如果只有 3 个版本README.md尽管有 300 万次提交,这 300 万次提交都共享文件的三个版本

但请注意,这些文件实际上是只读的。你不能改变它们。甚至Git也无法更改它们(出于与哈希 ID 有关的技术原因;所有现有提交也是如此)。它们也不是大多数计算机程序都可以使用的格式。这意味着要处理文件,甚至只是查看它,Git 必须将冻结和压缩的、仅 Git 提交的文件扩展为普通的日常形式。

这意味着当您选择某个提交来处理它时,Git 必须从该提交中提取所有文件。因此,每个文件已经有两个副本:冻结的一个,以仅 Git 压缩和去重的形式,以及有用的一个,在您的工作树中。

大多数版本控制系统 (VCS-es) 具有相同的模式:每个文件都有一个提交的副本,以某种 VCS 特定的形式保存在 VCS 中,并且有一个纯文本/普通格式的版本供您使用。许多 VCS 到此为止,只有两个活动文件(其中一个可能存储在某个中央存储库中,而不是在您的计算机上;Git 将 VCS 副本存储在您的计算机上)。

那么,要进行的提交,VCS 显然必须打包您的所有工作树(普通格式)文件。一些版本控制系统确实做到了这一点。大多数情况下,至少在这里放置一个缓存以使其更快,因为以这种方式做事非常缓慢。然而,Git 使用了一个鬼鬼祟祟的把戏。

在 Git 中,每个活动文件都有第三个副本。这第三个副本位于 Git 所称的不同的地方,即indexstaging area,或者——现在很少见—— cache。从技术上讲,这通常不是副本,因为 Git 以内部、压缩和去重的形式存储它,所以它实际上只是对 blob-hash-ID 的引用。这也意味着它已准备好进入下一次提交。

这意味着索引(或暂存区,如果您更喜欢该术语)可以被描述为持有您打算进行的下一次提交 索引在冲突合并期间发挥了扩展的作用,因此这不是一个完整的描述,但足以考虑它。当你git commit用来进行新的提交时,Git 只是从索引中打包所有准备好的、冻结格式、预先去重的文件。但是索引只包含文件——例如,具有长名称的文件home/a/.init,但包含文件,而不是目录。

签出一些提交,进行处理,意味着从该提交中提取文件。Git 将它们(以它们的冻结格式,但现在可以更改)放入索引中,以便它们准备好进行的提交,并将它们解压缩为您的工作树中的普通格式,以便您可以查看和工作在他们。然后,当您使用 时git add,您是在告诉 Git:使某个文件的索引副本与该文件的工作树副本匹配。

  • 如果已经有一个索引副本,则该索引副本将被启动(尽管它可能在某些提交中是安全的)并且 Git 将工作树副本去重复为适当的压缩、冻结格式副本并将放入索引中。

  • 如果没有索引副本,现在有。(它仍然是重复数据删除:如果你创建一个包含一些旧文件内容的新文件,旧提交中的旧内容会被重新使用。)

无论哪种方式,它现在都准备好进行新的提交了。

这是.gitignore进来的地方

这些.gitignore文件的名称有些错误。它们并没有真正让 Git忽略文件。文件是否存在于您所做的新提交中,严格取决于您运行时该文件是否在索引中git commit

相反,.gitignore它是双重的。首先,当您使用 时git status,Git 会抱怨存在于您的工作树中但不在 Git 索引中的文件。此投诉的形式是告诉您某些文件未被跟踪。这就是 untracked 的字面意思:在你的工作树中有一个文件,你可以在其中查看和编辑它等等,但它现在不在 Git 的索引。仅此而已,因为您可以随时将文件放入 Git 的索引 ( git add) 或取出文件 (git rmgit rm --cached)。但是因为索引是每个提交的来源,所以知道某个文件是否在索引中很重要——这就是为什么 Git 会抱怨它不在。

不过,有时这种抱怨只是烦人:是的,我知道这个编译的目标代码文件不在索引中。别告诉我!我已经知道了,这并不重要! 因此,为了防止 Git 抱怨,您将文件列在另一个可能应该被调用的文件中.git-do-not-complain-about-these-untracked-files

但这不是您通过在.gitgnore. 它不仅会关闭git status,而且git add 不会实际添加文件。所以git add *或者git add . 不会添加目标代码文件,或者其他什么。因此,为了防止 Git 添加,您将文件列在一个可能应该被称为.git-do-not-auto-add-these-files.

因此.gitignore可能被称为.git-do-not-complain-about-these-untracked-files-and-do-not-automatically-add-them-either. 但是一旦这些文件索引中,一个.gitignore条目就没有效果,所以也许它应该是.git-do-not-complain-about-these-untracked-files-and-do-not-automatically-add-them-either-but-if-they-are-in-the-index-go-ahead-and-commit-them. 但这太荒谬了,.gitignore事实就是如此。

扫描目录很慢

当你有一个庞大的 Git 存储库,其中包含数百万文件时,Git 通常会很快完成的一些事情开始真正陷入困境。即使只有几十万个文件,有些事情也可能很慢。最慢的方法之一是扫描目录(或文件夹)以查找未跟踪的文件。2

通过在文件中列出一个目录,例如,您授予 Git 使用快捷方式的权限。通常,Git 会对自己说:啊,这是一个目录我必须打开它并读出其中的每个文件,并查看这些文件是否在索引中,以确定这些文件是否未被跟踪和/或需要添加。 但如果要忽略整个目录,Git 可以稍作停顿:等等!我看是可以忽略的!我可以完全跳过它! 所以它继续而不是往里看。home/a/.gitignorehome/ahome/ahome/b/home/a/

要确保 Git不会跳过目录,您必须确保它不会被忽略。这是.gitignore条目中尾随斜杠的来源。


1大多数甚至都没有这么大,但微软正在努力让 Git 在这种大小的存储库中运行。

2解决这类速度问题的常用技巧是插入缓存。这里的问题是,根据定义,未跟踪的文件不在索引中。Git 的索引确实有一个扩展来做一些未跟踪的缓存,但这永远无法捕获所有内容。


.gitignore格式

行的格式.gitignore为:

  • 空白行和注释行被忽略;
  • !以否定开头的行;
  • 以引用目录结尾的行;/
  • 该行的其余部分命名文件,并带有前导和/或嵌入的斜杠。

否定只对撤消前一行的影响才有意义。一般来说,后面的行会覆盖前面的行,但是有一个很大的例外与跳过整个目录有关。

在任何!标记否定之后-<em>以斜杠开头的行提供了锚定路径。3 因此/home,例如,这意味着——<code>/home——而不是类似a/home. 包含嵌入斜线的行也有根,因此home/a两者/home/a的含义相同。

最后的斜线(如果存在)将从“已扎根/锚定”测试中删除。也就是说,home/并且/home/是不同的,因为home它是无根/无锚定的,但/home它是有根/锚定的。

当 Git 扫描目录(文件夹)和子目录(子文件夹)时,它会尝试将在每个级别找到的每个文件或目录名称与所有非根 / 非锚定名称匹配。但是,只有特定级别的那些.gitignore才能与根/锚定名称匹配。

模式中的尾部斜杠表示仅当这是一个目录时才匹配。所以 ifhome/a是一个目录,它同时匹配home/*and home/*/; ifhome/xyz是一个文件,它只匹配home/*,不匹配home/*/

因此,如果我们想忽略下面的所有文件home,我们使用:

home/*

忽略它们。这有一个嵌入的斜线,所以它是根/锚定的。不幸的是,它允许 Git跳过所有子目录,所以我们必须通过以下方式来应对:

!home/*/

它有一个尾部斜杠,因此它仅适用于目录。它也被锚定了。


3我在这里借用了正则表达式描述 中的锚定术语。Rooted是指 Git 存储库工作树的顶层。这两个术语都应该传达正确的想法;使用您喜欢的更好。


推荐阅读