Git 如何(以及多久)扫描工作目录以查找"untracked"文件?



在一个目录上运行git init后,git就会知道该目录及以下目录中存在的所有文件。这些是未跟踪的文件。我很熟悉当你把这些文件放在后台,它们变成"文件"时会发生什么;被跟踪的";文件。但我想知道的是,Git是如何找到未跟踪的文件的?git是每隔几分钟运行一次树命令,还是每隔固定的时间运行一次?它是否在每次键入git status或git add时都运行tree命令?还是每次保存文件?发生了更聪明的事情吗?

简而言之,我的问题是:

  1. Git如何找到未跟踪的文件?

  2. 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_REGDT_DIRDT_LNK作为三种可能性,这意味";"常规文件"目录";(文件夹)或"文件夹";符号链接";。(在类Unix系统上也可以使用其他值,但Git不会存储此类条目。)

如果类型是DT_UNKNOWN,Git将需要在文件名上调用lstat,这是通过将原始worktree路径与斜线和dp->d_name中的C字符串组合而构建的(尽管它也有d_namlen字段,但它是NUL终止的)。lstat调用应该成功,并返回来自任何stat系统调用的所有常规信息。请注意,lstat调用非常昂贵(通常比打开和读取目录要贵得多),因此我们希望尽可能避免它们,一旦完成,我们希望尽可能长时间地保留这些信息。所以Git倾向于这样做;请参阅下面的更多内容。

Git现在可以在.gitignore(或通过读取.gitignore构建的数据结构)中查看是否应该跳过目录(DT_DIRS_ISDIR())。如果是这样,则会使所有递归短路。否则,对于每个目录,Git都必须递归地完成所有这些相同的工作,但请参见下文。

对于不是目录并且可以存储在存储库中的东西,Git现在可以测试该条目是否已经在Git的索引中——注意,目录条目不是作为这种索引条目存储的,但索引具有";"扩展";可以存储它们;我们将在下面看到更多关于这一点的信息——或者不会。如果这个进程找到的文件在Git的索引中,那么它就是跟踪的文件。如果这个进程找到的文件在Git的索引中不是,那么它就是一个未跟踪的文件。这几乎就是追踪与未追踪的全部。请注意,被跟踪的文件,其信息现在存储在Git的索引中,如果我们对其进行了lstat,它的lstat数据也会被转录

git是每隔几分钟运行一次树命令,还是每隔固定的时间运行一次?

(所谓"树命令",我假设你指的是我刚才描述的逻辑递归读取树操作。)

两者都没有。它根据需要扫描顶层工作树。

每次键入git-status或git-add时,它都会运行tree命令吗?

对于git status,是。对于git add,仅当您使用了";所有";选项,或者要求git add添加一个作为目录的实体,在这种情况下,它必须递归地为您命名的实体执行读取树(前提是它没有列在.gitignore中)。

有更聪明的事情发生吗?

是。上面的描述是逻辑,但实际实现尝试使用快捷方式。此外,现在还有一种叫做";fsmonitor守护进程";。

可能最重要的快捷方式取决于一些在类Unix文件系统上运行良好的技巧,但在其他文件系统上可能不适用。这是因为每个目录都有一个修改时间。如果对目录执行lstatstat系统调用,则此修改时间显示为struct statst_ctime1字段。我们还知道上次写入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。备份软件通常对增量备份使用相同的技巧。

最新更新