git config core.filemode false 的后果是什么?

14

为了背景,我正在经历以下问题并试图解决它:Git在Visual Studio Code中显示文件已修改,即使没有更改。我在Windows 10机器上使用cygwin,但我的所有同事都在使用Mac。

最高票答案建议使用 git config core.filemode false ,但我无法弄清楚这样做的后果。这是否安全?这是否意味着如果我创建一个shell脚本,推送它将失去可执行位?这是否意味着当我拉取一个新的可执行文件时,它会失去可执行位?如果有的话,有什么需要注意的地方吗?

我已经查阅了文档,但它也没有回答那个问题,它只是解释了何时需要更改。

3个回答

19
core.filemode设置为false会使Git忽略工作树中文件的st_modelstat()结果的可执行位。相反,除非使用git update-index --chmod,否则保留任何现有索引(暂存区)条目的模式。新文件索引条目的模式为100644。这在您自己系统上的lstat()仿真不正确地支持模式时是明智的。
通常情况下,更改任何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>

那个100644100755mode。它存储在Git的tree object中,Git在运行git commit时构建它(尽管我们可以使用git write-tree提前构建一些)。树对象存储文件的名称和这个模式,就像索引/暂存区一样。2(索引或暂存区是git ls-files --stage显示的内容。)
所以,模式是100644 = -x100755 = +x。这留下了另一个谜:为什么它们是这些奇怪的数字?这就是Git如何确定这些问题的答案。
由于Git最初是为Linux和其他类Unix系统编写的,因此Git严重依赖于lstat系统调用。一些其他非Unix系统没有这个作为实际的系统调用,但大多数至少在某种兼容性库中模拟它。(参见,例如,Windows中的lstat()替代方法是什么?)Unix的stat族调用在C中填充了一个struct stat,而这个结构包含一个字段st_modest_mode字段由各种可组合的位组成:
权限:这些是最低的三个八进制数字。一个文件如果是rw-r--r--,则在这些位中为644。一个文件如果是rwxr-xr-x,则在这些位中为755
不适用于Git的三个位:它们占据了下一个更高的八进制数字。由于它们不适用于Git,因此这里总是为零(如果操作系统提供非零值,则Git会将其屏蔽)。也就是说,一旦包括底部的三个八进制数字,我们将看到06440755
“格式”位(S_IFMT)在前几个八进制数字中(例如10xxxx04xxxx中的1004):这些确定实体是一个文件、一个目录、一个符号链接和各种其他不适用的情况。一个目录在该字段中具有04位,而普通文件在该字段中具有10位。因此,一个目录,在使用这些位掩码后,最终成为mode 040xxx,其中xxx是某些权限位。一个文件最终成为mode 100xxx,其中xxx是某些权限位。
当我们将它们组合起来时,我们会看到Git显示的两个模式:100755用于可执行文件,而100644用于非可执行常规文件。当然,目录的st_mode将是040755040700等,但是Git不会在目录上使用读/写/执行位,因此它只是屏蔽它们:在这里,我们看到Git显示的第三种模式,即树对象链接到另一个树对象的0400004 这也是symlink条目模式120000的来源:这里的S_IFMT位在Linux和Unix上为12。提交或gitlink条目类型160000不对应任何Linux/Unix模式,但它是按位OR-ing S_IFDIRS_IFLNK模式位(120000|040000)的结果。
所以,索引中所有模式条目的来源都是:它们直接来自于struct statst_mode字段,由lstat填充,并做出以下更改:
  • 对于树对象,权限不相关且被清零。(树对象一开始就不会出现在索引中;当文件名需要时,它们由git write-tree按需创建。)Unix类系统上忽略权限位的符号链接和Git链接也是如此。

  • 对于文件,用户、组和其他读写位总是被假定为rw-r--r--,无论底层文件的实际模式如何。存在x位会导致索引模式中的三个x位全部设置。5

这样做可以容纳历史错误(见脚注5),因此有些混乱。如果存储格式只保存文件类型和文件的+x-x,例如,那么将会简单得多,但它也为未来的扩展留下了空间(例如,整个setuid+setgid+sticky的3位组合当前总是零,因此非零值可能会获得含义)。

所有这些在类Unix环境中都是有意义的,因为模式位在普通的磁盘文件中被保留。但在其他系统中,lstat模式位被真正伪造了。Windows是这里的典型例子。没有"可执行位",所以在Windows上lstat一个文件必须要么显示所有文件都是可执行的,要么不显示任何文件是可执行的,如果我们要编造一个任意的x位结果。

因此,当您运行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获取每个文件的stat数据,但是将完全忽略st_mode结果中的三个x位。它将读取该文件的现有索引条目,以获取要在任何新的更新的索引条目中设置的x位。唯一的例外是git update-index操作,用户可以指定整个模式,或使用--chmod标志:
git update-index --chmod=+x path/to/file.ext

这将获取现有的索引条目,检查其是否为文件(mode 100xxx),如果是,则将xxx部分替换为755:文件现在被标记为+x。类似地,--chmod=-xxxx部分替换为644(仅适用于普通文件;您不能--chmod符号链接或gitlink)。
然而,如果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


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

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

所以我们可以看到,每个只包含一行hello world的文件在任何Git仓库中都有相同的blob哈希ID:3b18e512dba79e4c8300dd08aeb37f8e728b8dad。试一试:
$ echo 'hello world' > hello.txt
$ git add hello.txt
$ git ls-files --stage hello.txt
100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0       hello.txt

注意blob哈希ID,3b18e512dba79e4c8300dd08aeb37f8e728b8dad,正是我们预计的值。
2.树条目和索引条目之间存在一些重要的区别。特别是,索引条目将文件的完整名称拼写出来,包括正斜杠,因此例如文件path/to/file.ext在索引中就是path/to/file.ext3但作为一组树对象,Git将其分解为伪目录,因此我们有了pathtofile.ext。路径部分存储在提交的顶级树中;to部分存储为path树的子树;file.ext部分存储为to树中的blob条目。顶级树具有一个名为path的子树条目,其中包含持有名称to和持有名称file.ext的子树的哈希ID。(呼!)从底层向上递归地工作可以更容易地看到这一点:
  • 我们在底层构建一个树,其中包含100644 file.ext和任何其他位于to名称下的名称。我们将此树对象存储在对象数据库中,找到其内部哈希ID。

  • 现在我们构建另一个树,其中包含40000 to和我们刚刚构建的树的哈希ID,以及任何其他需要放在path下面的条目。

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

这组树是git write-tree使用当前Git索引中的内容构建的。然后,git write-tree程序发出顶级树的哈希ID,这就是git commit-tree构建的提交对象中的内容。
3.当前的索引格式使用压缩技巧来避免重复的前导字符串。有关详细信息,请参见technical documentation
4.在tree对象中存储的模式中剥离了前导零,但为了显示目的,在git ls-tree -r输出中重新插入了前导零。

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

6如果我没记错的话,实际测试包括:对文件进行stat操作,翻转所有的X位(new_mode = old_mode ^ 0111),chmod操作,再次进行stat操作,并查看结果是否发生了变化。如果有变化,至少有一位X被遵守。如果没有变化,则没有X位被遵守。


在这四个八进制数字之上的八进制数字中,我很难理解这句话的意思。你能用不同的措辞表达吗? - Daniel Kaplan
那已经是第二或第三次尝试了,但我会再试一次 :-) - torek
谢谢您的详细回答,我学到了很多。 - wtfzambo

6

似乎 git 只关心可执行位,因此在 git 中的文件只能是 644 或 755。 源代码

我刚刚做了一个测试:

$ mkdir test && cd test && git init
$ touch before && chmod a+x before && git add before && git commit -m 'before' && git ls-tree HEAD

> [master (root-commit) 1cb9c41] before
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100755 before
100755 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    before

$ git config core.fileMode false

$ touch after && chmod a+x after && git add after && git commit -m 'after' && git ls-tree HEAD

> [master b4d7a48] after
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 after
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    after
100755 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    before

正如您所看到的,在core.fileMode更改之前,Git会保留文件的可执行位(0755),而在更改后,新创建的文件会失去可执行位(0644),旧文件保持旧的可执行位。

因此,总结一下:
使用git config core.filemode false,Git将忽略本地存储库上的可执行位更改。由于Git只关心可执行位,因此这不会导致0000文件,而是0644文件。

这意味着如果我创建一个shell脚本,推送它会失去可执行位吗?

是的

这意味着当我拉取新的可执行文件时,它会失去可执行位吗?

这取决于您的文件系统。一些文件系统(例如NTFS)会将每个文件的权限更改为0777,而其他文件系统则可能会丢失可执行位。


2
简短回答:Windows 和 Linux 有不同的权限模型。Git 在内部使用 Linux 模型。
在 Windows 上,git config core.filemode false 告诉 Git 在确定文件是否更改时不考虑文件权限。

详细阐述“不考虑文件权限”意味着什么,这是我要询问的主要问题。 - Daniel Kaplan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接