git提交对象数据结构的文件格式是什么?

24

背景: 我希望能够搜索我的git提交消息和提交记录,而不必使用复杂的git grep命令,因此我决定查看一下git提交消息的存储方式。

我查看了.git文件夹,对我来说看起来提交记录是存储在

.git/objects 

.git对象文件夹包含一堆名为a6和9b的文件夹。这些文件夹每个都包含一个文件,其名称看起来像提交sha 2f29598814b07fea915514cfc4d05129967bf7。当我在文本编辑器中打开其中一个文件时,我得到了乱码。

  1. 这些乱码是什么文件格式/ Git提交对象是如何存储的?
  2. 在此Git提交日志中,文件夹9b包含一个提交sha

aed8a9f773efb2f498f19c31f8603b6cb2a4bc

为什么文件9b中会存储不止一个提交SHA,有这样的情况吗?

  • 是否有一种方法可以将这些无意义的文本转换为纯文本,以便我可以在文本编辑器中操作提交记录?


  • 2
    git grep 有什么问题吗? - Matthieu Moy
    有关标签对象,请参见:什么是git标签对象的格式以及如何计算其SHA? - kenorb
    有关树对象,请参见:Git 树对象的内部格式是什么? - kenorb
    我认为这个问题的正确答案应该是引用git源代码,以及可能的git政策或计划,如果有的话,关于更改对象格式。 - fuzzyTew
    4个回答

    32

    创建一个最小化示例并反向工程格式

    创建一个简单的仓库,在创建任何打包文件之前 (git gc, git config gc.auto, git-prune-packed ...), 使用 如何使用命令行工具进行DEFLATE以提取git对象? 中的方法之一解压缩一个提交对象。

    export GIT_AUTHOR_DATE="1970-01-01T00:00:00+0000"
    export GIT_AUTHOR_EMAIL="author@example.com"
    export GIT_AUTHOR_NAME="Author Name" \
    export GIT_COMMITTER_DATE="2000-01-01T00:00:00+0000" \
    export GIT_COMMITTER_EMAIL="committer@example.com" \
    export GIT_COMMITTER_NAME="Committer Name" \
    
    git init
    
    # First commit.
    echo
    touch a
    git add a
    git commit -m 'First message'
    # (for python2, remove the two `.buffer`s in the next line)
    python -c "import zlib,sys;sys.stdout.buffer.write(zlib.decompress(sys.stdin.buffer.read()))" \
      <.git/objects/45/3a2378ba0eb310df8741aa26d1c861ac4c512f | hd
    
    # Second commit.
    echo
    touch b
    git add b
    git commit -m 'Second message'
    # (for python2, remove the two `.buffer`s in the next line)
    python -c "import zlib,sys;sys.stdout.buffer.write(zlib.decompress(sys.stdin.buffer.read()))" \
      <.git/objects/74/8e6f7e22cac87acec8c26ee690b4ff0388cbf5 | hd
    

    输出结果为:
    Initialized empty Git repository in /home/ciro/test/git/.git/
    
    [master (root-commit) 453a237] First message
     Author: Author Name <author@example.com>
     1 file changed, 0 insertions(+), 0 deletions(-)
     create mode 100644 a
    00000000  63 6f 6d 6d 69 74 20 31  37 34 00 74 72 65 65 20  |commit 174.tree |
    00000010  34 39 36 64 36 34 32 38  62 39 63 66 39 32 39 38  |496d6428b9cf9298|
    00000020  31 64 63 39 34 39 35 32  31 31 65 36 65 31 31 32  |1dc9495211e6e112|
    00000030  30 66 62 36 66 32 62 61  0a 61 75 74 68 6f 72 20  |0fb6f2ba.author |
    00000040  41 75 74 68 6f 72 20 4e  61 6d 65 20 3c 61 75 74  |Author Name <aut|
    00000050  68 6f 72 40 65 78 61 6d  70 6c 65 2e 63 6f 6d 3e  |hor@example.com>|
    00000060  20 30 20 2b 30 30 30 30  0a 63 6f 6d 6d 69 74 74  | 0 +0000.committ|
    00000070  65 72 20 43 6f 6d 6d 69  74 74 65 72 20 4e 61 6d  |er Committer Nam|
    00000080  65 20 3c 63 6f 6d 6d 69  74 74 65 72 40 65 78 61  |e <committer@exa|
    00000090  6d 70 6c 65 2e 63 6f 6d  3e 20 39 34 36 36 38 34  |mple.com> 946684|
    000000a0  38 30 30 20 2b 30 30 30  30 0a 0a 46 69 72 73 74  |800 +0000..First|
    000000b0  20 6d 65 73 73 61 67 65  0a                       | message.|
    000000ba
    
    [master 748e6f7] Second message
     Author: Author Name <author@example.com>
     1 file changed, 0 insertions(+), 0 deletions(-)
     create mode 100644 b
    00000000  63 6f 6d 6d 69 74 20 32  32 33 00 74 72 65 65 20  |commit 223.tree |
    00000010  32 39 36 65 35 36 30 32  33 63 64 63 30 33 34 64  |296e56023cdc034d|
    00000020  32 37 33 35 66 65 65 38  63 30 64 38 35 61 36 35  |2735fee8c0d85a65|
    00000030  39 64 31 62 30 37 66 34  0a 70 61 72 65 6e 74 20  |9d1b07f4.parent |
    00000040  34 35 33 61 32 33 37 38  62 61 30 65 62 33 31 30  |453a2378ba0eb310|
    00000050  64 66 38 37 34 31 61 61  32 36 64 31 63 38 36 31  |df8741aa26d1c861|
    00000060  61 63 34 63 35 31 32 66  0a 61 75 74 68 6f 72 20  |ac4c512f.author |
    00000070  41 75 74 68 6f 72 20 4e  61 6d 65 20 3c 61 75 74  |Author Name <aut|
    00000080  68 6f 72 40 65 78 61 6d  70 6c 65 2e 63 6f 6d 3e  |hor@example.com>|
    00000090  20 30 20 2b 30 30 30 30  0a 63 6f 6d 6d 69 74 74  | 0 +0000.committ|
    000000a0  65 72 20 43 6f 6d 6d 69  74 74 65 72 20 4e 61 6d  |er Committer Nam|
    000000b0  65 20 3c 63 6f 6d 6d 69  74 74 65 72 40 65 78 61  |e <committer@exa|
    000000c0  6d 70 6c 65 2e 63 6f 6d  3e 20 39 34 36 36 38 34  |mple.com> 946684|
    000000d0  38 30 30 20 2b 30 30 30  30 0a 0a 53 65 63 6f 6e  |800 +0000..Secon|
    000000e0  64 20 6d 65 73 73 61 67  65 0a                    |d message.|
    000000eb
    

    然后我们推断格式如下:
    • Top level:

      commit {size}\0{content}
      

      where {size} is the number of bytes in {content}.

      This follows the same pattern for all object types.

    • {content}:

      tree {tree_sha}
      {parents}
      author {author_name} <{author_email}> {author_date_seconds} {author_date_timezone}
      committer {committer_name} <{committer_email}> {committer_date_seconds} {committer_date_timezone}
      
      {commit message}
      

      where:

      • {tree_sha}: SHA of the tree object this commit points to.

        This represents the top-level Git repo directory.

        That SHA comes from the format of the tree object: What is the internal format of a Git tree object?

      • {parents}: optional list of parent commit objects of form:

        parent {parent1_sha}
        parent {parent2_sha}
        ...
        

        The list can be empty if there are no parents, e.g. for the first commit in a repo.

        Two parents happen in regular merge commits.

        More than two parents are possible with git merge -Xoctopus, but this is not a common workflow. Here is an example: https://github.com/cirosantilli/test-octopus-100k

      • {author_name}: e.g.: Ciro Santilli. Cannot contain <, \n

      • {author_email}: e.g.: cirosantilli@mail.com. Cannot contain >, \n

      • {author_date_seconds}: seconds since 1970, e.g. 946684800 is the first second of year 2000

      • {author_date_timezone}: e.g.: +0000 is UTC

      • committer fields: analogous to author fields

      • {commit message}: arbitrary.

    我已经制作了一个最小的Python脚本,可以在https://github.com/cirosantilli/test-git-web-interface/blob/864d809c36b8f3b232d5b0668917060e8bcba3e8/other-test-repos/util.py#L83生成一个git仓库,并添加了一些提交记录。
    我用它来做一些有趣的事情,比如:

    这是关于标签对象格式的类比分析:Git标签对象的格式是什么,如何计算其SHA值?


    Python3版本需要使用.buffer (str vs bytes): python -c“import zlib, sys; sys.stdout.buffer.write(zlib.decompress(sys.stdin.buffer.read()))”<.git/... - IndustryUser1942

    23
    在你进一步探索之前,我建议你阅读一下Git手册中关于它内部机制的章节。我发现了解这一章内容通常是喜欢Git和讨厌Git之间的区别。理解Git为什么要以这种方式运行往往能使所有奇怪的命令更有意义。
    回答你的问题,你看到的无意义字符是使用zlib压缩后的对象数据。如果你查看上面链接中“对象存储”标题下的一些详细信息,就可以了解一些关于它的工作原理。这是Git中文件存储的简短版本:
    1. 为内容创建Git特定的头文件。 2. 生成头文件+内容连接后的哈希值。 3. 压缩头文件+内容连接。 4. 将压缩数据存储到磁盘上一个名称与数据哈希值的前两个字符相同的文件夹中,并将文件名设置为剩余的38个字符。
    所以这回答了你的第二个问题,一个文件夹将包含所有以相同两个字符开头的压缩对象,而不管它们的内容是什么。
    如果你想查看blob的内容,你只需解压它即可。如果你只想查看文件的内容,在大多数编程语言中都可以轻松完成。但是我要警告你不要尝试修改数据。修改文件中的任何一个字节都会改变它的哈希值。Git中的所有元数据(即目录结构和提交)都使用对哈希的引用进行存储,因此修改单个文件意味着你必须更新所有下游对象,这些对象引用该文件的哈希值。然后你还要更新所有引用这些哈希值的对象。如此循环往复... 尝试实现这一点会变得非常复杂,非常快速地让你失去时间和耐心。学习Git内置的命令会为你节省大量时间和痛苦。

    5

    注意事项

    请不要在编辑器中编辑对象。如果您不小心操作,可能会损坏Git仓库。学习使用git grep是值得的。它与grep并没有太大区别,但速度更快。要搜索提交信息,请查看git log --grep

    在底层,Git有一个对象的概念。对象通常由标题和一些数据组成。文件内容以Blob对象的形式存储。Tree对象包含文件名,并指向表示文件的Blob对象和表示其他目录的Tree对象。然后还有记录日志消息并指向表示适当的树状态的Tree对象的Commit对象。此外还有注释标签对象,通常指向放置在提交上的标签。

    在线书籍提供了有关不同类型的对象及其查看方式的一些信息,并介绍了用于存储对象的格式的一些详细信息。

    请记住,您正在查看的是松散的对象。还有包文件中包含的对象,其格式不同

    Git用户手册还提供了有关对象数据库的良好信息。


    这本在线书籍是智慧的源泉。非常感谢您指引我去看它! - Tara Roys
    非常欢迎!技术文档也可能非常有帮助。 - John Szakmeister
    使用 git grep 进行日志消息搜索的建议(以及 OP 最初倾向于使用它)并不正确,它只适用于搜索文件内容。 - Ken Williams
    @KenWilliams 没错。 - John Szakmeister

    3

    这是一个比较老的问题,但如果有人仍然想知道如何读取对象的内容 - 最简单的方法是运行

    git cat-file -p

    因此,如果您有一个包含以下对象的文件夹

    ├── 4a
    │   ├── 2500cb9eb44d57fd7abfd04a9911ab8e2b6733
    

    您需要运行以下命令:

    git cat-file -p 4a2500cb9eb44d57fd7abfd04a9911ab8e2b6733


    您还可以使用更具表现力的 git cat-file commit <commit-ish> 命令查看提交的内部内容。<type> 可以为以下四种之一:blob,tree,commit,tag。 - Julien

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接