【问题标题】:What is the internal format of a Git tree object?Git 树对象的内部格式是什么?
【发布时间】:2013-01-25 06:54:41
【问题描述】:

Git 树对象的内容格式是什么?

blob 对象的内容是blob [size of string] NUL [string],但是对于树对象来说呢?

【问题讨论】:

标签: git git-ls-tree


【解决方案1】:

我尝试通过测试回购详细说明@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

【讨论】:

  • 为什么模式不存储为二进制?
  • @Random832:可能是因为它会以牺牲清晰度为代价,带来最小的性能提升。
  • OpenSSL 1.0.2o 27 Mar 2018:openssl:Error: 'zlib' is an invalid command.你用的是哪个版本?
  • @Random832 zlib 可能在压缩模式方面做得很好,而随机散列的压缩时间更长也更难(我想与模式相比,它们需要更大的字典)。跨度>
【解决方案2】:

表示为类似 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开头

  1. 文字字符串tree
  2. SPACE(字节0x20
  3. 未压缩内容的 ASCII 编码十进制长度

在一个 NUL(i.e.,字节 0x00)结束符之后,树包含一个或多个形式的条目

  1. ASCII 编码的八进制模式
  2. 空间
  3. 姓名
  4. NUL
  5. SHA1 哈希编码为 20 个无符号字节

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 
    

【讨论】:

【解决方案3】:

@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)
]

【讨论】:

    【解决方案4】:

    树对象的格式:

    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)转换为十六进制字符串以更好地可视化。

    【讨论】:

    • 你能发布或描述你的脚本来缩小这些吗? (它似乎和其他 git 对象不同,而不仅仅是 zlib deflate)
    • 啊哈,你刚才提到了(这是十六进制或 20 字节,太棒了!)
    • 如果您有兴趣从其内容手动创建 git 树对象,请务必注意 [Entries have references to other trees and blobs] 需要按 [file/folder] 的字母顺序排序名称](又名路径)。
    • @user2284570 re: content size - 它是整个树对象本身的大小。
    • @qbolec,如果您更改条目的顺序,那么 2 个相同的树将具有不同的哈希并显示为不同的树。
    【解决方案5】:

    正如建议的那样,Pro Git 很好地解释了结构。要显示漂亮打印的树,请使用:

    git cat-file -p 4c975c5f5945564eae86d1e933192c4a9096bfe5
    

    要以原始但未压缩的形式显示同一棵树,请使用:

    git cat-file tree 4c975c5f5945564eae86d1e933192c4a9096bfe5
    

    结构基本相同,散列存储为二进制文件名和以空结尾的文件名。

    【讨论】:

    • 这并没有说明未压缩的二进制结构。
    • @user2284570 “Pro Git 很好地解释了结构。”值得一看。
    • 好的,我明白了,我的问题是 toprintf()a 字符串在 C 中包含空字节。
    猜你喜欢
    • 1970-01-01
    • 2022-12-18
    • 2019-10-05
    • 2010-11-04
    • 1970-01-01
    • 2016-01-09
    • 1970-01-01
    • 2015-03-23
    相关资源
    最近更新 更多