git是如何检测文件变化的?

新手上路,请多包涵

具体而言, 在执行git status的时候, git到底做了什么?

目前在网上找到的疑似正确答案: git会调用系统函数lstat来读取文件属性, 从而判断文件的大小和修改时间是否有所改变; 于是, 假设某个文件修改后的大小正好和修改前一致, 然后在保存时又强制使用了和之前相同的修改时间来保存, 那么此时git是感知不到这一文件已经发生改变的。

经过个人测试, 发现确实如此, 所以该答案似乎是对的, 但随之产生一些疑惑, 不妨以一个具体的例子说明:
1, 对一个初始化的空库, 新建文档t1.txt, 执行git status, 此时git能将之识别为未追踪文件. 问: git调用lstat具体读取的是t1.txt的哪一些属性, 从而判断出来t1.txt是一个新文件? 个人猜测是检测创建时间是否等于修改时间, 不知是否正确, 但反正不可能是上述答案中的看文件大小和修改时间是否有变.
2, 将t1.txt暂存, 修改其内容后, 再次git status, 此时git能将之识别为已修改文件. 问: git调用lstat具体读取的是t1.txt的哪一些属性, 从而判断出来t1.txt是一个有新内容的旧文件? 按照上述答案, 读取的是其当前的文件大小与修改时间, 然后就要与它以往的文件大小与修改时间做对比, 看是否有改变. 但是问题来了, t1.txt之前的文件大小与修改时间现在已经被新值覆盖掉了, 那还怎么比对? 最直观的想法肯定是存进了版本里, 但发现起码对于修改时间, index和blob里都是没有的, 所以目前猜测是由操作系统负责保存的? 比如大小与修改时间等文件属性其实都存在一个列表里, 所以git才有历史数值可供比对当前数值是否改变.
3, 将t1.txt改名为t2.txt, 再次git status, 此时git能同时识别到t1.txt的删除与t2.txt的新增. 问: 对前者t1.txt的删除, 这应该是比对了index与工作区的结果, 但对于后者t2.txt的新增, git又是怎么调用lstat识别的? 这回是重命名而非新建文件, 所以它的文件大小与修改日期均是未改变的, 亦即其修改日期现在并不等于创建日期, 那git又是怎么识别它是一个新文件的?
4, 再将t2.txt暂存, 会发现相应blob文件的修改日期变了, 亦即改一次文件名就要重新把相同的内容备份完了覆盖一遍, 这个设计不合理吧, 尤其当文件特别巨大时, 不该是这样才对. 对此该怎么理解?


贴一下复现的测试结果: 文件内容从1改到2, git检测不到.
image.png

阅读 2.4k
3 个回答

初始化时,对象库中为空
图片
创建一个 t1.txt 文件,在第一行输入 123456进行 git add操作后在 46 目录下发现新增的 blob 对象,所谓的 SHA 算法也就是 Git 对象中的对象 ID,拆分成“2 位文件目录名+38 位对象名”用于快速查找 Git 中的文件
图片

图片
查看 t1.txt 在 Index 树下如何存储,在 Git 中可以看到其内容:git write-tree是使用当前索引 Index 创建树对象,同时会创建一个树目录,也就是 aa,我们可以忽略它
图片
如果将 t1.txt 文件内容修改,增加一行 123456 并操作 git add,查看当前对象库:
图片
发现创建了一个新的目录,再查看其内部文件:
图片
实际上 t1.txt 的内容修改,Git 会再创建一个新 blob 对象存放整个内容,而不是在原 blob 对象下增量存储
图片
修改 t1.txt 为 t2.txt,再次 git add,新增了 8c 目录和里面的文件实际上 Git 并没有在对象库中删除之前的 t1.txt,创建了一个新对象,新对象里面的内容指向原来的 t1.txt:
图片
最后再将 t2.txt 修改为 t1.txt 并操作 git add 查看 Index 文件修改时间,我们得知 Index 树变化了,但是其他文件目录没有发生变化:
图片
查看当前的 Index 索引树,发现最后结果还是指向了和上面相同的那个 t1.txt 文件:
图片

先看下git status执行过程:
在执行git status时,Git会比较工作区(Working Directory)和暂存区(Index)的状态,然后显示相应的结果。具体来说,Git会执行以下步骤:

1.检查工作区文件状态: Git会遍历工作区中的文件,检查它们的状态,包括是否被修改、是否是新文件或已删除的文件等。

2.比较工作区和暂存区: Git会比较工作区和暂存区中文件的差异,以确定哪些文件已被修改但尚未暂存。

3.比较暂存区和最后一次提交: Git还会比较暂存区和最后一次提交(HEAD指向的提交)的差异,以确定哪些修改已经暂存但尚未提交。

关于您提到的关于文件属性的疑问:

1.旧文件属性的存储: Git并没有存储旧文件的属性,而是在执行git status时,通过调用系统函数lstat来读取当前文件的属性,并与之前保存的状态进行比较。旧的文件属性并不在版本库中,而是在工作区和暂存区的比较中使用的。

2.对新文件的处理: 对于新文件,Git会将其视为未跟踪的文件,而不是与旧文件进行比较。新文件的检测是通过检查工作区中是否存在但未被添加到暂存区的文件来完成。创建时间通常不是Git关心的属性,而是文件系统的特定属性。Git主要关注文件内容和修改时间。

关于改名的情况,Git确实可能会识别为新文件。在Git中,文件改名被视为一系列的文件删除和新增操作,而不是直接的改名。这可能导致Git在执行git status时认为发生了文件的新增。 Git并不关心文件的创建时间,因此对于Git来说,文件改名主要是基于文件内容和修改时间的比较。

问题

这个问题还是比较麻烦的,本质上应该是说 git 到底是什么时候知道应该要去算 SHA-1 的。

这个问题,我在 unix、linux、windows,三个平台都做了测试,发现结果都不一样。

表现

unix 与 linux

其中 unix、linux 表现是相同的,那就是:只要修改了文件,不管 mtime 有没有被修改为刚提交时间,git status 一定会检测到

macos 需要使用 gstat,用 stat 只能看到秒,安装:brew install coreutils

下面是 unix、linux 测试的代码,创建文件时,也使用该代码创建,保证文件前后的 mtime 相同:

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <time.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

int main(int argc, char **argv) {
    const char *filename = argv[1];
    char *content = argv[2];

    FILE *file = fopen(filename, "w");
    fprintf(file, "%s", content);
    fclose(file);

    struct timespec times[2];

    time_t sec = 1703983139;

    times[0].tv_sec = sec;
    times[0].tv_nsec = 431009920;
    times[1].tv_sec = sec;
    times[1].tv_nsec = 431009920;

    if (utimensat(AT_FDCWD, filename, times, 0) < 0) {
        perror("utimensat");
        return 1;
    }

    return 0;
}

windows

但 windows 的表现是不相同的,如果使用 notepad3 设置了 使用原始修改时间保存,不改变大小的情况下修改文件,那么 git status 就不会检测到。

结论

从表现上来看,unix 与 linux 应该都是使用了除 mtime 之外的手段,但是 git 什么时候去计算 SHA-1,用的手段是什么,这个暂时无法得知。

windows 上,则应该就是用的 mtime + 文件大小的方式了。

补充实验 1

之前我怀疑是我的程序和 notepad3 的写入逻辑不一样。

因此我对实验做了改进,我将 windows 上面的目录共享出来,在 macos 上面挂载,使用编译出来的可执行文件修改内容,依然得出和上面一样的结论。

补充实验 2

由于想知道 git 检测的机制,因此跟踪了 git 的源码。

发现关键点在 read_cache.c 文件里=的 ce_match_stat_basic 方法中,调用的 match_stat_data 方法,该方法在 statinfo.c

关键代码如下:

 if (trust_ctime && check_stat &&
     sd->sd_ctime.sec != (unsigned int)st->st_ctime)
         changed |= CTIME_CHANGED;

可以从这段代码中看出,主要是由 trust_ctime 来控制,是否信任上次变化时间。

trust_ctime 是由 git 的 config 中的 core.trustctime 控制的。

下面是 chatgpt 的解释。

core.trustctime 是 Git 配置中的一个选项,它控制 Git 是否信任文件的 ctime(change time)变化作为文件修改的指示。在某些情况下,比如文件系统的缺陷或者使用某些备份和恢复工具时,文件的 ctime 可能会发生变化,但文件内容本身并没有更改。如果 Git 信任 ctime,那么它会将这样的变化视为文件内容的变化,从而可能导致不必要的重新提交。

默认情况下,core.trustctime 是设置为 true 的,这意味着 Git 会考虑文件的 ctime 在检测文件修改时。如果你设置这个选项为 false,Git 将不会考虑 ctime,仅仅依赖于文件的 mtime(modification time)来检测文件内容的变化。

要查看当前 core.trustctime 的设置,你可以在命令行中运行以下命令:

git config --global core.trustctime

这将显示全局配置中的设置。如果你想查看特定仓库的设置,可以在该仓库目录下运行:

git config core.trustctime

如果没有输出,那么就是使用默认值 true。如果你想改变这个设置,你可以使用 git config 命令来设置它:

# 设置全局配置为不信任 ctime
git config --global core.trustctime false

# 设置当前仓库配置为不信任 ctime
git config core.trustctime false

请记住,更改这个设置可能会影响 Git 的性能,因为 Git 将不再使用 ctime 作为检测文件变化的快捷方式。在某些情况下,这可能导致 Git 在执行某些操作时变得更慢,因为它需要更彻底地检查文件内容的实际变化。

最终结论

git 检测文件是否被修改,就是通过一系列的文件属性来确定的。而是否开启文件属性检测,能够通过一系列的配置决定。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏