git树对象的内部格式是什么?

时间:2013-02-09 19:30:56

标签: git git-ls-tree

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

blob对象的内容是blob [size of string] NUL [string],但树对象的内容是什么?

5 个答案:

答案 0 :(得分:33)

树对象的格式:

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

答案 1 :(得分:16)

我试图通过测试回购来详细说明@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相同。

打包对象

您可能会发现,对于您的存储库,您无法找到这些文件 .git/objects/XX/XXX...存储Git对象。发生这种情况是因为部分或全部"松散"对象已打包到一个或多个.git\objects\pack\*.pack文件中。

要解压缩仓库,首先将包文件从原始位置移开,然后git-unpack对象。

$ mkdir .git/pcache   
$ mv .git/objects/pack/*.pack .git/pcache/     
$ git unpack-objects < .git/pcache/*.pack

完成实验后重新包装:

$ git gc

答案 2 :(得分:10)

以类似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树以

标题开头
  1. 文字字符串tree
  2. SPACE(即,字节0x20
  3. 未压缩内容的ASCII编码十进制长度
  4. 在NUL(即。,字节0x00)终结符之后,树包含一个或多个表单的条目

    1. ASCII编码的八进制模式
    2. SPACE
    3. 名称
    4. NUL
    5. SHA1哈希编码为20个无符号字节
    6. 然后,Git将树数据提供给zlib’s deflate以进行紧凑存储。

      请记住,git blob是匿名的。 Git树将名称与其他内容的SHA1哈希相关联,这些内容可能是blob,其他树等等。

      为了演示,请考虑与git的v2.7.2标记关联的树,您可能需要browse on GitHub

      $ git rev-parse v2.7.2^{tree}
      802b6758c0c27ae910f40e1b4862cb72a71eee9f
      

      以下代码要求树对象采用“松散”格式。我不知道从packfile中提取单个原始对象的方法,所以我首先在包克文件上运行git unpack-objects从我的克隆到新的存储库。请注意,这扩展了一个.git目录,该目录从大约90 MB开始,大约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 <(cd ~/src/git; git ls-tree 802b6758c0) <(../rawtree)
      --- /dev/fd/63  2016-03-09 14:41:37.011791393 -0600
      +++ /dev/fd/62  2016-03-09 14:41:37.011791393 -0600
      @@ -1,3 +1,4 @@
      +tree 15530
       100644 blob 5e98806c6cc246acef5f539ae191710a0c06ad3f   .gitattributes
       100644 blob 1c2f8321386f89ef8c03d11159c97a0f194c4423   .gitignore
       100644 blob e5b4126bec557db55924b7b60ed70349626ea2c4   .mailmap

答案 3 :(得分:3)

@lemiorhan的答案是正确的,但错过了一些重要的细节。 树格式为:

[mode] [file/folder name]\0[SHA-1 of referencing blob or tree]

但重要的是[SHA-1 of referencing blob or tree]是二进制形式,而不是十六进制形式。这是用于将树对象解析为条目的Python代码段:

entries = [
   line[0:2]+(line[2].encode('hex'),)
   for line in
   re.findall('(\d+) (.*?)\0(.{20})', body, re.MULTILINE)
]

答案 4 :(得分:2)

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

git cat-file -p 4c975c5f5945564eae86d1e933192c4a9096bfe5

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

git cat-file tree 4c975c5f5945564eae86d1e933192c4a9096bfe5

结构基本相同,哈希存储为二进制和以null结尾的文件名。