将
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。但这只是文件的
数据,而不是它的+x或-x状态,那么这从哪里来?
如果我们运行
git ls-files --stage
并查看一些可执行和不可执行的文件,我们发现那些
不可执行的文件显示为:
100644 <hash> 0 <name>
可执行的文件会显示为:
100755 <hash> 0 <name>
那个
100644
或
100755
是
mode
。它存储在Git的
tree object中,Git在运行
git commit
时构建它(尽管我们可以使用
git write-tree
提前构建一些)。树对象存储文件的名称和这个模式,就像索引/暂存区一样。
2(索引或暂存区是
git ls-files --stage
显示的内容。)
所以,模式是
100644
=
-x
和
100755
=
+x
。这留下了另一个谜:为什么它们是这些奇怪的数字?这就是
Git如何确定这些问题的答案。
由于Git最初是为Linux和其他类Unix系统编写的,因此Git严重依赖于
lstat
系统调用。一些其他非Unix系统没有这个作为实际的
系统调用,但大多数至少在某种兼容性库中
模拟它。(参见,例如,
Windows中的lstat()替代方法是什么?)Unix的
stat
族调用在C中填充了一个
struct stat
,而这个结构包含一个字段
st_mode
。
st_mode
字段由各种可组合的位组成:
权限:这些是最低的三个八进制数字。一个文件如果是
rw-r--r--
,则在这些位中为
644
。一个文件如果是
rwxr-xr-x
,则在这些位中为
755
。
不适用于Git的三个位:它们占据了下一个更高的八进制数字。由于它们不适用于Git,因此这里总是为零(如果操作系统提供非零值,则Git会将其屏蔽)。也就是说,一旦包括底部的三个八进制数字,我们将看到
0644
或
0755
。
“格式”位(
S_IFMT
)在前几个八进制数字中(例如
10xxxx
或
04xxxx
中的
10
或
04
):这些确定实体是一个
文件、一个
目录、一个
符号链接和各种其他不适用的情况。一个目录在该字段中具有
04
位,而普通文件在该字段中具有
10
位。因此,一个目录,在使用这些位掩码后,最终成为
mode 040xxx
,其中
xxx
是某些权限位。一个文件最终成为
mode 100xxx
,其中
xxx
是某些权限位。
当我们将它们组合起来时,我们会看到Git显示的两个模式:
100755
用于可执行文件,而
100644
用于非可执行常规文件。当然,目录的
st_mode
将是
040755
或
040700
等,但是Git不会在
目录上使用读/写/执行位,因此它只是屏蔽它们:在这里,我们看到Git显示的第三种模式,即树对象链接到另一个树对象的
040000
。
4 这也是
symlink
条目模式
120000
的来源:这里的
S_IFMT
位在Linux和Unix上为
12
。提交或
gitlink条目类型
160000
不对应任何Linux/Unix模式,但它是按位OR-ing
S_IFDIR
和
S_IFLNK
模式位(
120000|040000
)的结果。
所以,索引中所有模式条目的来源都是:它们直接来自于
struct stat
的
st_mode
字段,由
lstat
填充,并做出以下更改:
这样做可以容纳历史错误(见脚注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.filemode
为true
。如果不是这样,操作系统必须忽略x
位,因此Git将设置core.filemode
为false
。
如果
core.filemode
为
false,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=-x
将
xxx
部分替换为
644
(仅适用于普通文件;您不能
--chmod
符号链接或gitlink)。
然而,如果
core.filemode
为
true,则对文件进行任何普通的
git add
操作都会读取并遵守工作树的
x
位。例如,如果
lstat
的
st_mode
设置为
100700
,则索引条目将变为
100755
。如果
lstat
的
st_mode
设置为
100444
,则索引条目将变为
100644
。
也就是说,在不完全匹配Git内部的C样式代码中,任何普通文件的新模式为:
ce = lookup_existing_cache_entry(path);
if (core_filemode) {
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.ext
。
3但作为一组树对象,Git将其分解为伪目录,因此我们有了
path
、
to
和
file.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允许现有的mode
为100664
(rw-rw-r--
),但不会创建任何新的mode
,以便可以读取追溯到早期版本的Git的现有Git存储库。
6如果我没记错的话,实际测试包括:对文件进行stat操作,翻转所有的X位(new_mode = old_mode ^ 0111
),chmod操作,再次进行stat操作,并查看结果是否发生了变化。如果有变化,至少有一位X被遵守。如果没有变化,则没有X位被遵守。