Git中的一个特殊hash

最近了解了一点Git的内部原理,看到了一个特殊的hash,所以写了这一篇文章来分享自己的看法。

SMJ
loading... read

⚠️ This post was last updated on January 30, 2022 and the content may be OUTDATED!

If you encounter any issues, please feel free to reachout to me!

最近了解了一点 Git 的内部原理,看到了一个特殊的 hash,所以写了这一篇文章来分享自己的看法。

==============

既然你读这篇文章,那就意味着你应该比较熟悉 Git 的一系列操作,不过,在你使用 Git 的时候,你有没有遇到以下 hash:

4b825dc642cb6eb9a060e54bf8d69288fbee4904

可能你会觉得 git 中的每个对象都有一个 hash 值,谁会注意 hash 的数值。确实,没有人会注意。

但是上面的这个 hash 确实是一个很特别的 hash,接下来就来说明为什么这个 hash 是一个特殊的存在。

git 中 hash 从哪里来?

每个 git 存储库,即使是空存储库也将包含这段 hash。这可以通过 git show 验证:

    $ git show 4b825dc642cb6eb9a060e54bf8d69288fbee4904
    tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904

那么这个 hash 是从哪里来的呢?在这之前我们需要了解一点 Git 的知识:Git 的核心部分是一个简单的键值对数据库(key-value data store)。 你可以向 Git 仓库中插入任意类型的内容,它会返回一个唯一的键,通过该键可以在任意时刻再次取回该内容。

我们可以使用git hash-object命令来存储一个对象并获取该对象的键。

    $ echo 'test' | git hash-object -w --stdin
    9daeafb9864cf43055ae93beb0afd6c7d144bfa4

Git 内部存储的数据类似下面这样,其中每个对象都有其对应的 hash 值:

image

ps:如果你仍然好奇,Pro Git 的 Git Internals 章节有更多详细介绍。

那么接下来说正题,这个特殊 hash 是如何产生的呢?它实际上是一棵空树的哈希值。可以通过为空字符串的/dev/null创建对象哈希来验证:

    $ git hash-object -t tree /dev/null
    4b825dc642cb6eb9a060e54bf8d69288fbee4904
    //或者
    $ echo -n '' | git hash-object -t tree --stdin
    4b825dc642cb6eb9a060e54bf8d69288fbee4904

空树 hash 的特殊用处

空树 hash 可以与git diff一起使用。例如,如果你想检查目录中的空白错误,您可以使用 --check 选项并将 HEAD 与空树进行比较:

    $ echo "test  " > readme.md
    $ git add . && git commit -m "init"
    [master 6d8e897] init
     1 file changed, 1 insertion(+), 3 deletions(-)
    $ git diff $(git hash-object -t tree /dev/null) HEAD --check -- readme.md
    readme.md:1: trailing whitespace.
    +test

在编写 git hooks 时,空树 hash 也非常有用。一个相当常见的用法是在使用类似于以下的代码在接受新提交之前验证它们:

    for changed_file in $(git diff --cached --name-only --diff-filter=ACM HEAD)
    do
      if ! validate_file "$changed_file"; then
        echo "Aborting commit"
        exit 1
      fi
    done

如果有以前的提交,这可以正常工作,但是如果没有提交,则 HEAD 引用将不存在。为了解决这个问题,可以在检查初始提交时使用空树 hash:

    if git rev-parse --verify -q HEAD > /dev/null; then
      against=HEAD
    else
      # Initial commit: diff against an empty tree object
      against="$(git hash-object -t tree /dev/null)"
    fi

    for changed_file in $(git diff --cached --name-only --diff-filter=ACM "$against")
    do
      if ! validate_file "$changed_file"; then
        echo "Aborting commit"
        exit 1
      fi
    done

参考

https://git-scm.com/book/en

https://floatingoctothorpe.uk/2017/empty-trees-in-git.html

Sooner or later, everything ends.