首页 > 解决方案 > git config core.filemode false 的后果是什么?

问题描述

就上下文而言,我遇到了这个问题并试图解决它:Visual Studio 代码中的 Git 说文件已修改,即使没有任何更改我在 Windows 10 机器上使用 cygwin,但我所有的同事都在使用 mac。

投票最高的答案是git config core.filemode false,但我无法弄清楚这样做的后果。安全吗?这是否意味着如果我创建一个 shell 脚本,推送它会丢失可执行位?这是否意味着当我拉出一个新的可执行文件时,它会丢失可执行文件位?如果有的话,有什么问题?

检查了文档,但它也没有回答这个问题,它只是解释了你什么时候需要改变它。

标签: gitcygwin

解决方案


TL;博士

设置core.filemodefalse使 Git 忽略st_mode lstat()工作树中文件的可执行结果位。相反,任何现有索引(暂存区)条目的模式都会保留,除非您使用git update-index --chmod. 新文件索引条目获取模式100644lstat()当您自己的系统上的仿真不正确支持模式时,这主要是明智的。

更改任何设置通常都是错误的core.*,包括core.fileMode(或者core.filemode——关于是否给它一个大写的文档并不一致M,但实际上无论如何都无关紧要)。在某些特殊情况下,您可以手动设置它,而您的问题是正确的:确切地说,这是做什么的?

要回答这个问题,我们必须首先从什么是“文件模式”以及 Git 如何确定它们开始。在Git 中,文件模式实际上是已提交或将要提交的blob 对象(即普通文件)上的“+x”或“-x”。在 Git 中,文件——或者更确切地说,文件内容——作为这些“blob 对象”存储在提交中:压缩、去重和全能只读,通过哈希 ID 找到。1 但这只是文件的数据,而不是它的 +x 或 -x 状态,那么它是从哪里来的呢?

好吧,如果我们运行git ls-files --stage并查看一些可执行文件和不可执行文件,我们会发现不可执行的文件显示为:

100644 <hash> 0       <name>

而可执行的显示为

100755 <hash> 0       <name>

100644或者100755mode。它存储在 Git树对象中,Git 在我们运行时构建它git commit(尽管我们可以更早地使用 构建一些git write-tree)。树对象存储文件名和此模式,就像索引/暂存区域一样。2 (显示的是索引或暂存区域git ls-files --stage。)

所以,模式是100644=-x100755= +x。这给我们留下了另一个谜团:为什么它们是这些奇怪的数字?这就是Git 如何确定这些问题的来源。

由于 Git 最初是为 Linux 和其他类 Unix 系统编写的,因此 Git 在很大程度上依赖于系统lstat调用。其他一些非 Unix 系统没有将此作为实际的系统调用,但至少在某种兼容性库中伪造它。(参见,例如,Windows 中的 lstat() 替代方案是什么?) Unixstat系列调用struct stat在 C 中填充 a,并且该结构包含一个字段st_mode. 该st_mode字段由各种可组合位组成:

  • 权限:这些是最低的三个八进制数字。这些位中rw-r--r--包含的文件。这些位中包含644的文件。rwxr-xr-x755

  • 不适用于 Git 的三个位:它们占据下一个更高的八进制数字。因为它们不适用于 Git,所以我们总是在这里得到一个零(如果操作系统提供了一个非零值,Git 只是将其屏蔽掉)。也就是说,我们将看到0644or 0755,例如,一旦我们包含了底部的三个八进制数字。

  • “格式”位 ( S_IFMT),在前几个八进制数字中(例如,10or 04in 10xxxxor 04xxxx):这些确定实体是否是文件目录符号链接以及各种其他不适用的情况。目录04在此字段中具有位,而常规文件10在此字段中具有位。所以一个目录,在用这些位屏蔽之后,最终是mode 040xxx,对于一些权限位xxx100xxx对于某些权限位,文件最终成为模式xxx

当我们将这些结合起来时,我们会看到 Git 显示的两种模式:100755可执行文件和100644不可执行的常规文件。当然,目录st_mode将是040755040700类似的,但 Git 不会打扰目录上的读/写/执行位,所以它只是将它们屏蔽掉:在这里,我们看到 Git 显示的第三种模式,040000用于链接到的树对象另一个树对象。4 这也是symlink进入模式的来源120000S_IFMT这里的位是12在Linux和Unix上。commitor gitlink条目类型 ,不对应任何 Linux/Unix 模式,而是将 and 组合在一起的按结果160000S_IFDIRS_IFLNK模式位 ( 120000|040000)。

所以这就是索引中所有模式条目的来源:它们直接来自st_modea 的字段,struct stat由 填充lstat,具有以下更改:

  • 对于树对象,权限是不相关的并且被归零。(树对象最初不会出现在索引中;它们是在git write-tree文件名需要时按需创建的。)符号链接也是如此——在类 Unix 系统上,权限位通常被忽略——以及gitlinks(无论如何都是 Git 内部的)。

  • rw-r--r--对于文件,无论底层文件的实际模式如何,用户、组和其他读写位都被假装为始终存在。一个x位的存在导致所有三个x位都设置为索引模式。5

这容纳了历史错误(见脚注 5),因此有些混乱。如果存储格式只是简单地保存文件类型,并且对于文件,+x或者-x,例如,它会更简单,但它也为将来的扩展留出了空间(例如,整个 setuid+setgid+sticky 的 3 位集合目前总是零,因此非零值可以获得意义)。

所有这些在类 Unix 环境中都是有意义的,其中模式位保存在普通的磁盘文件中。 但在其他系统中,lstat模式位实际上是伪造的。Windows 是这里的典型示例。没有“可执行位”,因此lstat- 如果我们要组成任意x位结果,Windows 上的文件必须将所有文件显示为可执行文件,或者没有文件显示为可执行文件。

因此,当您运行git init创建新存储库时,Git 会探测系统的底层行为。Git 使用模式 0644 的操作系统“创建新文件”调用 ( open(name, O_CREAT|other_open_flags, mode)) 创建一个文件。然后尝试使用操作系统chmod调用将模式更改为 0755,然后使用操作系统lstat调用查看更改是否“生效”。6 如果是这样,操作系统必须尊重x位,因此 Git 将设置core.filemodetrue. 如果不是,操作系统必须忽略x位,因此 Git 将设置core.filemodefalse.

稍后,如果core.filemodefalse,Git 将lstat照常调用以获取每个文件的统计数据,但将完全忽略结果中的三位。它将读取该文件的现有索引条目以获取要在该文件的任何新更新索引条目中设置的位。此规则的一个例外是操作,用户可以指定整个模式,或使用标志:xst_modexgit update-index--chmod

git update-index --chmod=+x path/to/file.ext

这会抓取现有的索引条目,检查它是否用于文件 ( mode 100xxx),如果是,则将xxx部分替换为755: 文件现在标记为+x。同样,将部分--chmod=-x替换为(同样仅适用于常规文件;您不能使用符号链接或 gitlink)。xxx644--chmod

但是,如果core.filemodetrue,则git add文件上的任何普通文件都将读取并服从工作树的x位。例如,如果lstatst_mode设置为100700,则索引条目将变为100755。如果lstatst_mode设置为100444,则索引条目变为100644

也就是说,在与 Git 内部不完全匹配的类 C 代码中,对于任何普通文件,新模式是:

ce = lookup_existing_cache_entry(path);
if (core_filemode) {
    // Note: the link in banyudu's answer goes to code
    // that checks `& 0100`, not `& 0111`.  Perhaps Git
    // only inspects the user's bit.
    new_mode = st.st_mode & 0111 ? 100755 : 100644;
} else {
    new_mode = ce != NULL && ce->ce_mode == 100755 ? 100755 : 100644;
}

添加文件后,缓存条目(索引)mode字段设置为new_mode.


1 blob 对象的哈希 ID 严格由内容决定:它是以 word 为前缀的数据的校验和blob、一个 ASCII 空格 (0x20)、以十进制表示的数据大小(以字节为单位)和一个 ASCII NUL (0x00) 字节. 校验和功能目前是 SHA-1,尽管即将进行的 Git 更改将开始使用 SHA-256。这种散列实际上是重复数据删除的工作原理:给定相同的字节序列,Git 会产生相同的散列 ID。因此,如果文字文本hello world加上换行CTRL-J字节作为 blob 对象存储在 Git 中,使用 SHA-1,我们有:

$ printf 'blob 12\0hello world\n' | shasum
3b18e512dba79e4c8300dd08aeb37f8e728b8dad  -

所以我们看到每个只包含一行的文件hello world都有 blob 哈希 ID 3b18e512dba79e4c8300dd08aeb37f8e728b8dad,在每个 Git 存储库中无处不在。试试看:

$ echo 'hello world' > hello.txt
$ git add hello.txt
$ git ls-files --stage hello.txt
100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0       hello.txt

请注意,blob 哈希 ID3b18e512dba79e4c8300dd08aeb37f8e728b8dad正是我们计算出来的。

2条目和索引条目之间有一些重要的区别。特别是,索引条目的文件全名用正斜杠完整拼写,因此,例如,文件path/to/file.ext就是:path/to/file.ext在索引中。3 但作为一组树对象,Git 将其分解为伪目录,因此我们有pathtofile.ext. 该path部分存储在提交的顶层树中;该to部分存储为树的子path树;并且该file.ext部分存储为to树中的 blob 条目。顶层树有一个名为的子树条目path,其中包含包含该名称的子树的哈希 IDto以及包含 name 的子树的哈希 ID file.ext。(哇!)从下往上递归地工作更容易看出这一点:

  • 我们在底层控股100644 file.ext和名称下的任何其他名称中建立一棵树to。我们将此树对象存储在对象数据库中,查找其内部哈希 ID。

  • 现在我们构建另一棵树,保存40000 to我们刚刚构建的树的哈希 ID,以及任何其他需要进入的条目path

  • 最后,我们构建了一个树40000 path,其中包含我们在中间步骤中构建的树的哈希 ID,以及进入顶层所需的任何其他条目。

这组树就是git write-tree构建的,此时使用 Git 索引中的任何内容。然后git write-tree程序发出顶层树的哈希 ID,这是进入git commit-tree构建的提交对象的内容。

3当前的索引格式使用压缩技巧来避免重复前导字符串。有关详细信息,请参阅技术文档

4前导零在存储在对象中的模式中被去除,但为了在输出tree中显示等目的而重新插入。git ls-tree -r

5在非常早期的 Git 版本中,在 Gitmode字段中保留了更多模式位。结果证明这是一个错误。今天,为了向后兼容,Git允许现有mode100664( rw-rw-r--),但永远不会创建任何的,以便可以读取追溯到这个早期版本的 Git 的现有 Git 存储库。

6如果我没记错的话,实际测试包括:stat 文件,翻转所有X 位(new_mode = old_mode ^ 0111),chmod,再次 stat,看看结果是否改变。如果是这样,则至少遵循一个 X 位。如果不是,则不遵循 X 位。


推荐阅读