在一个目录上运行git init后,git就会知道该目录及以下目录中存在的所有文件。这些是未跟踪的文件。我很熟悉当你把这些文件放在后台,它们变成"文件"时会发生什么;被跟踪的";文件。但我想知道的是,Git是如何找到未跟踪的文件的?git是每隔几分钟运行一次树命令,还是每隔固定的时间运行一次?它是否在每次键入git status或git add时都运行tree命令?还是每次保存文件?发生了更聪明的事情吗?
简而言之,我的问题是:
-
Git如何找到未跟踪的文件?
-
Git搜索";未跟踪";文件?
5年或更长时间前,这个曾经有一个简单易行的答案。现在,它是。。。要复杂得多。然而,今天的行为应该与以前的行为相匹配,所以如果你不关心实际的实现——你不应该必须关心——我们可以描述旧的实现。
在一个目录上运行gitinit后,git就会知道该目录及其下的所有文件。
事实并非如此:git init
将创建.git
目录/文件夹并填充它,但此时Git还没有查看工作树,除了看看那里是否有.git
(这样现有存储库中的git init
就会"重新初始化"它,而不是创建一个新的存储库——重新初始化步骤通常什么都不做,尽管从技术上讲,它或多或少被定义为复制模板挂钩)。
然而,工作树中的任何现有文件现在都是未跟踪的文件:
这些都是未跟踪的文件。我很熟悉当你把这些文件放在后台,它们变成"文件"时会发生什么;被跟踪的";文件。但我想知道的是,Git是如何找到未跟踪的文件的?
从逻辑上讲,Git只需费力地从工作树的顶层读取一个条目:
DIR *dirp = opendir(worktree);
然后是调用readdir
的循环。
每个返回的条目都有几个条目。如果我们假设持有struct dirent *
指针的变量命名为dp
,那么我们至少有两个或三个字段:
dp->d_name
dp->d_namlen
dp->d_type
d_type
字段可以包含DT_UNKNOWN
,这意味着系统没有提供文件类型,或者DT_REG
、DT_DIR
或DT_LNK
作为三种可能性,这意味";"常规文件"目录";(文件夹)或"文件夹";符号链接";。(在类Unix系统上也可以使用其他值,但Git不会存储此类条目。)
如果类型是DT_UNKNOWN
,Git将需要在文件名上调用lstat
,这是通过将原始worktree
路径与斜线和dp->d_name
中的C字符串组合而构建的(尽管它也有d_namlen
字段,但它是NUL终止的)。lstat
调用应该成功,并返回来自任何stat系统调用的所有常规信息。请注意,lstat
调用非常昂贵(通常比打开和读取目录要贵得多),因此我们希望尽可能避免它们,一旦完成,我们希望尽可能长时间地保留这些信息。所以Git倾向于这样做;请参阅下面的更多内容。
Git现在可以在.gitignore
(或通过读取.gitignore
构建的数据结构)中查看是否应该跳过目录(DT_DIR
或S_ISDIR()
)。如果是这样,则会使所有递归短路。否则,对于每个目录,Git都必须递归地完成所有这些相同的工作,但请参见下文。
对于不是目录并且可以存储在存储库中的东西,Git现在可以测试该条目是否已经在Git的索引中——注意,目录条目不是作为这种索引条目存储的,但索引具有";"扩展";可以存储它们;我们将在下面看到更多关于这一点的信息——或者不会。如果这个进程找到的文件在Git的索引中,那么它就是跟踪的文件。如果这个进程找到的文件在Git的索引中不是,那么它就是一个未跟踪的文件。这几乎就是追踪与未追踪的全部。请注意,被跟踪的文件,其信息现在存储在Git的索引中,如果我们对其进行了lstat
,它的lstat
数据也会被转录
git是每隔几分钟运行一次树命令,还是每隔固定的时间运行一次?
(所谓"树命令",我假设你指的是我刚才描述的逻辑递归读取树操作。)
两者都没有。它根据需要扫描顶层工作树。
每次键入git-status或git-add时,它都会运行tree命令吗?
对于git status
,是。对于git add
,仅当您使用了";所有";选项,或者要求git add
添加一个作为目录的实体,在这种情况下,它必须递归地为您命名的实体执行读取树(前提是它没有列在.gitignore
中)。
有更聪明的事情发生吗?
是。上面的描述是逻辑,但实际实现尝试使用快捷方式。此外,现在还有一种叫做";fsmonitor守护进程";。
可能最重要的快捷方式取决于一些在类Unix文件系统上运行良好的技巧,但在其他文件系统上可能不适用。这是因为每个目录都有一个修改时间。如果对目录执行lstat
或stat
系统调用,则此修改时间显示为struct stat
的st_ctime
1字段。我们还知道上次写入Git索引的时间,因为我们将其存储在索引的字段中。
假设我们已经知道path/to/dir
中有,比如说,八个未跟踪的文件,并且知道它们都被忽略了。进一步假设我们在某个地方为这个path/to/dir
实体保留一个条目,该条目存储其st_ctime
字段。假设我们现在可以stat
它,并看到它的ctime没有变化,并且早于Git索引的更新。然后我们可以确定它没有获得任何新的未跟踪的文件,因此我们可以重复使用旧信息。
Git也使用了同样的技巧来避免为所有跟踪的文件重建blob哈希ID,这一点要重要得多。这比未跟踪的缓存更容易,因此自2005年早期Git以来,跟踪的文件(在正常索引项中列出的文件)就已经应用了这种魔法。
早在2015年,未经跟踪的缓存代码就进入了Git 2.5,在很长一段时间内,它都有点古怪,但现在已经相当稳定了。不幸的是,它依赖于在NTFS上不正确的假设(所以它在Windows系统上经常被禁用)。
Git在2017年获得了一个名为fsmonitor的东西,作为Git 2.16的一部分,它也一直很脆弱。最近,为了减少它的脆弱性,并使它在Windows以外的系统上运行,我们做了很多工作:特别是,它现在有macOS和Linux实现。fsmonitor代码与上面的代码非常不同:它侦听操作系统报告的文件系统操作,这些操作可能发生在甚至不在工作树中的目录中,在某些系统上,如果事件队列填满,一些事件可能会被丢弃。
fsmonitor的工作基本上是让它更快地更新Git的索引。细节还在变化,所以我不想再做进一步的描述了。
有关Git索引中内容的技术信息,请参阅Git存储库中的Documentation/technical/index-format.txt
文件。
1您会期望这是st_mtime
,事实确实如此,除了st_mtime
可以通过utimes
或类似的系统调用故意设置之外。然而,还有第二个字段st_ctime
,它存储一个通常不递减的值,不能以这种方式设置该值。因此,我们使用了索引节点更改时间ctime
,而不是修改时间mtime
。备份软件通常对增量备份使用相同的技巧。