【发布时间】:2013-01-25 06:54:41
【问题描述】:
Git 树对象的内容格式是什么?
blob 对象的内容是blob [size of string] NUL [string],但是对于树对象来说呢?
【问题讨论】:
-
我写了一个脚本,从字节中提取树对象的格式。您可以查看我的答案以获取更多详细信息。
标签: git git-ls-tree
Git 树对象的内容格式是什么?
blob 对象的内容是blob [size of string] NUL [string],但是对于树对象来说呢?
【问题讨论】:
标签: git git-ls-tree
我尝试通过测试回购详细说明@lemiorhan 的答案。
在空文件夹中创建一个测试项目:
$ echo ciao > file1
$ mkdir folder1
$ echo hello > folder1/file2
$ echo hola > folder1/file3
即:
$ find -type f
./file1
./folder1/file2
./folder1/file3
创建本地 Git 存储库:
$ git init
$ git add .
$ git write-tree
0b6e66b04bc1448ca594f143a91ec458667f420e
最后一个命令返回顶层树的哈希值。
要以人类可读的格式打印树的内容,请使用:
$ git ls-tree 0b6e66
100644 blob 887ae9333d92a1d72400c210546e28baa1050e44 file1
040000 tree ab39965d17996be2116fe508faaf9269e903c85b folder1
在这种情况下,0b6e66 是顶层树的前六个字符。您可以对folder1 执行相同的操作。
要获得相同的内容,但使用原始格式:
$ git cat-file tree 0b6e66
100644 file1 ▒z▒3=▒▒▒$ ▒►Tn(▒▒♣D40000 folder1 ▒9▒]▒k▒◄o▒▒▒i▒♥▒[%
内容类似于以压缩格式物理存储为文件的内容,但缺少初始字符串:
tree [content size]\0
要获取实际内容,我们需要解压缩存储c1f4bf 树对象的文件。我们想要的文件是 -- 给定 2/38 路径格式 --:
.git/objects/0b/6e66b04bc1448ca594f143a91ec458667f420e
这个文件是用 zlib 压缩的,因此我们通过以下方式获取它的内容:
$ openssl zlib -d -in .git/objects/0b/6e66b04bc1448ca594f143a91ec458667f420e
tree 67 100644 file1 ▒z▒3=▒▒▒$ ▒►Tn(▒▒♣D40000 folder1 ▒9▒]▒k▒◄o▒▒▒i▒♥▒[%
我们知道树的内容大小是67。
请注意,由于终端不是为打印二进制文件而设计的,它可能会吃掉字符串的某些部分或显示其他奇怪的行为。在这种情况下,使用| od -c 将上述命令传递给管道,或者使用下一节中的手动解决方案。
为了理解树的生成过程,我们可以从人类可读的内容开始自己生成它,例如对于顶层树:
$ git ls-tree 0b6e66
100644 blob 887ae9333d92a1d72400c210546e28baa1050e44 file1
040000 tree ab39965d17996be2116fe508faaf9269e903c85b folder1
每个对象的 ASCII SHA-1 哈希都以二进制格式转换和存储。 如果您需要的只是 ASCII 哈希的二进制版本,您可以这样做:
$ echo -e "$(echo ASCIIHASH | sed -e 's/../\\x&/g')"
所以 blob 887ae9333d92a1d72400c210546e28baa1050e44 被转换为
$ echo -e "$(echo 887ae9333d92a1d72400c210546e28baa1050e44 | sed -e 's/../\\x&/g')"
▒z▒3=▒▒▒$ ▒►Tn(▒▒♣D
如果我们要创建整个树对象,这里有一个 awk 单行:
$ git ls-tree 0b6e66 | awk -b 'function bsha(asha)\
{patsplit(asha, x, /../); h=""; for(j in x) h=h sprintf("%c", strtonum("0x" x[j])); return(h)}\
{t=t sprintf("%d %s\0%s", $1, $4, bsha($3))} END {printf("tree %s\0%s", length(t), t)}'
tree 67 100644 file1 ▒z▒3=▒▒▒$ ▒►Tn(▒▒♣D40000 folder1 ▒9▒]▒k▒◄o▒▒▒i▒♥▒[%
函数bsha 将 SHA-1 ASCII 哈希转换为二进制文件。树的内容首先被放入变量t,然后计算它的长度并打印在END{...}部分。
如上所述,控制台不太适合打印二进制文件,因此我们可能希望将它们替换为等效的 \x## 格式:
$ git ls-tree 0b6e66 | awk -b 'function bsha(asha)\
{patsplit(asha, x, /../); h=""; for(j in x) h=h sprintf("%s", "\\x" x[j]); return(h)}\
{t=t sprintf("%d %s\0%s", $1, $4, bsha($3))} END {printf("tree %s\0%s", length(t), t)}'
tree 187 100644 file1 \x88\x7a\xe9\x33\x3d\x92\xa1\xd7\x24\x00\xc2\x10\x54\x6e\x28\xba\xa1\x05\x0e\x4440000 folder1 \xab\x39\x96\x5d\x17\x99\x6b\xe2\x11\x6f\xe5\x08\xfa\xaf\x92\x69\xe9\x03\xc8\x5b%
输出应该是理解树内容结构的一个很好的折衷方案。将上面的输出与一般的树内容结构进行比较
tree [content size]\0[Object Entries]
每个对象条目是这样的:
[mode] [Object name]\0[SHA-1 in binary format]
模式是 UNIX 文件系统模式的子集。有关更多详细信息,请参阅 Git 手册上的 Tree Objects。
我们需要确保结果一致。为此,我们可能会将 awk 生成树的校验和与 Git 存储树的校验和进行比较。
至于后者:
$ openssl zlib -d -in .git/objects/0b/6e66b04bc1448ca594f143a91ec458667f420e | shasum
0b6e66b04bc1448ca594f143a91ec458667f420e *-
至于自制树:
$ git ls-tree 0b6e66 | awk -b 'function bsha(asha)\
{patsplit(asha, x, /../); h=""; for(j in x) h=h sprintf("%c", strtonum("0x" x[j])); return(h)}\
{t=t sprintf("%d %s\0%s", $1, $4, bsha($3))} END {printf("tree %s\0%s", length(t), t)}' | shasum
0b6e66b04bc1448ca594f143a91ec458667f420e *-
校验和是一样的。
或多或少官方的获取方式是:
$ git ls-tree 0b6e66 | git mktree
0b6e66b04bc1448ca594f143a91ec458667f420e
要手动计算,我们需要将脚本生成树的内容通过管道传送到shasum 命令中。实际上我们已经在上面做了这个(比较生成和存储的内容)。结果是:
0b6e66b04bc1448ca594f143a91ec458667f420e *-
和git mktree一样。
您可能会发现,对于您的 repo,您无法找到文件
.git/objects/XX/XXX... 存储 Git 对象。发生这种情况是因为一些或所有“松散”对象已被打包到一个或多个 .git\objects\pack\*.pack 文件中。
要解压 repo,首先将包文件从原来的位置移开,然后 git-unpack 对象。
$ mkdir .git/pcache
$ mv .git/objects/pack/*.pack .git/pcache/
$ git unpack-objects < .git/pcache/*.pack
完成实验后重新打包:
$ git gc
【讨论】:
OpenSSL 1.0.2o 27 Mar 2018:openssl:Error: 'zlib' is an invalid command.你用的是哪个版本?
表示为类似 BNF 的模式,git 树包含以下形式的数据
(?<tree> tree (?&SP) (?&decimal) \0 (?&entry)+ )
(?<entry> (?&octal) (?&SP) (?&strnull) (?&sha1bytes) )
(?<strnull> [^\0]+ \0)
(?<sha1bytes> (?s: .{20}))
(?<decimal> [0-9]+)
(?<octal> [0-7]+)
(?<SP> \x20)
也就是说,一个git树以一个header开头
tree
0x20)在一个 NUL(i.e.,字节 0x00)结束符之后,树包含一个或多个形式的条目
Git 然后将树数据提供给zlib’s deflate 以进行压缩存储。
记住 git blob 是匿名的。 Git 树将名称与其他内容的 SHA1 哈希相关联,这些内容可能是 blob、其他树等。
为了演示,请考虑与 git 的 v2.7.2 标签关联的树,您可能想要 browse on GitHub。
$ git rev-parse v2.7.2^{tree}
802b6758c0c27ae910f40e1b4862cb72a71eee9f
下面的代码要求树对象为“松散”格式。我不知道从包文件中提取单个原始对象的方法,所以我首先在包文件上运行 git unpack-objects 从我的克隆到新存储库。请注意,这会将开始时大约 90 MB 的 .git 目录扩展为大约 1.8 GB。
更新:感谢 max630 显示 how to unpack a single object。
#! /usr/bin/env perl
use strict;
use warnings;
use subs qw/ git_tree_contents_pattern read_raw_tree_object /;
use Compress::Zlib;
my $treeobj = read_raw_tree_object;
my $git_tree_contents = git_tree_contents_pattern;
die "$0: invalid tree" unless $treeobj =~ /^$git_tree_contents\z/;
die "$0: unexpected header" unless $treeobj =~ s/^(tree [0-9]+)\0//;
print $1, "\n";
# e.g., 100644 SP .gitattributes \0 sha1-bytes
while ($treeobj) {
# /s is important so . matches any byte!
if ($treeobj =~ s/^([0-7]+) (.+?)\0(.{20})//s) {
my($mode,$name,$bytes) = (oct($1),$2,$3);
printf "%06o %s %s\t%s\n",
$mode, ($mode == 040000 ? "tree" : "blob"),
unpack("H*", $bytes), $name;
}
else {
die "$0: unexpected tree entry";
}
}
sub git_tree_contents_pattern {
qr/
(?(DEFINE)
(?<tree> tree (?&SP) (?&decimal) \0 (?&entry)+ )
(?<entry> (?&octal) (?&SP) (?&strnull) (?&sha1bytes) )
(?<strnull> [^\0]+ \0)
(?<sha1bytes> (?s: .{20}))
(?<decimal> [0-9]+)
(?<octal> [0-7]+)
(?<SP> \x20)
)
(?&tree)
/x;
}
sub read_raw_tree_object {
# $ git rev-parse v2.7.2^{tree}
# 802b6758c0c27ae910f40e1b4862cb72a71eee9f
#
# NOTE: extracted using git unpack-objects
my $tree = ".git/objects/80/2b6758c0c27ae910f40e1b4862cb72a71eee9f";
open my $fh, "<", $tree or die "$0: open $tree: $!";
binmode $fh or die "$0: binmode: $!";
local $/;
my $treeobj = uncompress <$fh>;
die "$0: uncompress failed" unless defined $treeobj;
$treeobj
}
观看我们可怜的 git ls-tree 的实际操作。除了输出tree 标记和长度之外,输出是相同的。
$ diff -u
【讨论】:
@lemiorhan 的答案是正确的,但遗漏了一些重要的细节。 树格式为:
[mode] [file/folder name]\0[SHA-1 of referencing blob or tree]
但重要的是[SHA-1 of referencing blob or tree] 是二进制形式,而不是十六进制形式。这是 Python sn-p 将树对象解析为条目:
entries = [
line[0:2]+(line[2].encode('hex'),)
for line in
re.findall('(\d+) (.*?)\0(.{20})', body, re.MULTILINE)
]
【讨论】:
树对象的格式:
tree [content size]\0[Entries having references to other trees and blobs]
引用其他树和 blob 的每个条目的格式:
[mode] [file/folder name]\0[SHA-1 of referencing blob or tree]
我写了一个脚本给树对象放气。输出如下:
tree 192\0
40000 octopus-admin\0 a84943494657751ce187be401d6bf59ef7a2583c
40000 octopus-deployment\0 14f589a30cf4bd0ce2d7103aa7186abe0167427f
40000 octopus-product\0 ec559319a263bc7b476e5f01dd2578f255d734fd
100644 pom.xml\0 97e5b6b292d248869780d7b0c65834bfb645e32a
40000 src\0 6e63db37acba41266493ba8fb68c76f83f1bc9dd
数字 1 作为模式的第一个字符表示对 blob/文件的引用。上面的例子,pom.xml 是一个 blob,其他都是树。
请注意,我在\0 之后添加了新的行和空格,以使打印更美观。通常所有内容都没有新行。我还将 20 个字节(即引用 blob 和树的 SHA-1)转换为十六进制字符串以更好地可视化。
【讨论】:
正如建议的那样,Pro Git 很好地解释了结构。要显示漂亮打印的树,请使用:
git cat-file -p 4c975c5f5945564eae86d1e933192c4a9096bfe5
要以原始但未压缩的形式显示同一棵树,请使用:
git cat-file tree 4c975c5f5945564eae86d1e933192c4a9096bfe5
结构基本相同,散列存储为二进制文件名和以空结尾的文件名。
【讨论】:
printf()a 字符串在 C 中包含空字节。