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

What are the consequences of git config core.filemode false?

对于上下文,我遇到了这个问题并试图解决它: 我在 Windows 10 机器上使用 cygwin,但我所有的同事都在使用 macs。

投票最高的答案是 git config core.filemode false,但我不知道这样做的后果。安全吗?这是否意味着如果我创建一个 shell 脚本,推送它会丢失可执行位?这是否意味着当我提取一个新的可执行文件时,它会丢失可执行文件位?如果有的话,陷阱是什么?

我已经 checked the documentation,但它也没有回答这个问题,它只是说明了您何时需要更改它。

好像git只关心可执行位,所以git中的文件只能是644或755。source code

我刚刚做了一个测试:

$ 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 文件。

Does it mean if I create a shell script, pushing it will lose the executable bit?

Does it mean when I pull a new executable, it will lose the executable bit?

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

TL;DR

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

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

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

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

100644 <hash> 0       <name>

可执行的显示为:

100755 <hash> 0       <name>

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

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

由于 Git 最初是为 Linux 和其他类 Unix 系统编写的,Git 很大程度上依赖于 the lstat system call. Some other non-Unix systems don't have this as an actual system call, but most at least fake it in some sort of compatibility library. (See, e.g., What is lstat() alternative in windows?) Unix stat 家族在C中调用填写一个struct stat,这个结构包含一个字段,st_modest_mode 字段由各种可组合位组成:

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

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

  • “格式”位 (S_IFMT),在前几个八进制数字中(例如,10xxxx 中的 100404xxxx): 这些确定实体是否是文件目录符号link,以及其他各种不适用的情况。一个目录在这个字段中有位 04,一个普通文件在这个字段中有位 10。所以一个目录,在用这些位屏蔽之后,对于某些权限位 xxx 最终是 mode 040xxx。对于某些权限位 xxx.

    ,文件最终成为模式 100xxx

当我们组合这些时,我们看到 Git 显示的两种模式:100755 用于可执行文件,100644 用于不可执行的常规文件。当然,一个目录的 st_mode 将是 040755040700 或类似的东西,但是 Git 不会理会 目录上的 read/write/execute 位,所以它只是将它们屏蔽掉:在这里,我们看到 Git 显示的第三种模式,040000 树对象 linked 到另一个树对象。4 这也是 120000symlink 入口模式的来源:这里的 S_IFMT 位是 Linux 上的 12 和Unix。 commitgitlink 条目类型 160000 不对应于任何 Linux/Unix 模式,而是按位结果或运算 S_IFDIRS_IFLNK 模式位 (120000|040000).

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

  • 对于树对象,权限是无关紧要的并且被归零。 (树对象首先不会出现在索引中;当文件名需要一个时,它们是由 git write-tree 按需创建的。)symlinks 也是如此——在 Unix 上-像系统一样,权限位通常被忽略——对于 gitlinks(无论如何都是 Git 的内部)。

  • 对于文件,用户、组和其他读写位总是伪装成rw-r--r--,而不管底层文件的实际模式如何。 x 位的存在导致所有三个 x 位都设置为索引模式。5

这包含历史错误(见脚注 5),因此有些混乱。如果存储格式只包含文件类型,例如 +x-x,那么存储格式会简单得多,但它也为将来的扩展留有空间(例如,整个 setuid+ setgid+sticky 的 3 位集合目前始终为零,因此非零值可以获得意义)。

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

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

稍后,如果 core.filemodefalse,Git 将像往常一样调用 lstat 来获取每个文件的统计数据,但会完全忽略 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 symlink 或 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 space (0x20),以十进制表示的以字节为单位的数据大小,以及一个 ASCII NUL (0x00) 字节。校验和函数目前是 SHA-1,尽管即将进行的 Git 更改将开始使用 SHA-256。这种散列实际上是重复数据删除的工作原理:给定相同的字节序列,Git 产生相同的散列 ID。因此,如果文字文本 hello world 加上换行符 CTRL-J 字节作为 blob 对象存储在 Git 中,使用 SHA-1,我们有:

$ printf 'blob 12[=14=]hello 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 哈希 ID 3b18e512dba79e4c8300dd08aeb37f8e728b8dad,这正是我们计算得出的结果。

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

  • 我们在底层构建一棵树,其中包含 100644 file.extto 名称下的任何其他名称。我们将这个树对象存储在对象数据库中,找到它的内部哈希 ID。

  • 现在我们构建另一棵树,其中包含 40000 to 和我们刚刚构建的树的哈希 ID,以及 path.[=160 下需要的任何其他条目=]

  • 最后,我们构建一棵树,其中包含 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 允许 现有的 mode of 100664 (rw-rw-r--),但永远不会创建任何 new 个,以便可以读取可追溯到 Git 这个早期版本的现有 Git 存储库。

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

简答:Windows 和 Linux 有不同的权限模型。 Git 在内部使用 Linux 模型。

在 Windows 上,git config core.filemode false 告诉 Git 在确定文件是否已更改时不考虑文件权限。