【问题标题】:How is the Git hash calculated?Git 哈希是如何计算的?
【发布时间】:2016-02-16 10:53:19
【问题描述】:

我试图了解 Git 如何计算 refs 的哈希值。

$ git ls-remote https://github.com/git/git  

....
29932f3915935d773dc8d52c292cadd81c81071d    refs/tags/v2.4.2
9eabf5b536662000f79978c4d1b6e4eff5c8d785    refs/tags/v2.4.2^{}
....

在本地克隆 repo。通过 sha 检查refs/tags/v2.4.2^{} ref

$ git cat-file -p 9eabf5b536662000f79978c4d1b6e4eff5c8d785 

tree 655a20f99af32926cbf6d8fab092506ddd70e49c
parent df08eb357dd7f432c3dcbe0ef4b3212a38b4aeff
author Junio C Hamano <gitster@pobox.com> 1432673399 -0700
committer Junio C Hamano <gitster@pobox.com> 1432673399 -0700

Git 2.4.2

Signed-off-by: Junio C Hamano <gitster@pobox.com>

复制解压后的内容,以便我们对其进行哈希处理。 (AFAIK Git 在散列时使用未压缩的版本)

git cat-file -p 9eabf5b536662000f79978c4d1b6e4eff5c8d785 > fi

让我们使用 Git 自己的哈希命令对内容进行 SHA-1 处理

git hash-object fi
3cf741bbdbcdeed65e5371912742e854a035e665

为什么输出不是[9e]abf5b536662000f79978c4d1b6e4eff5c8d785?我知道前两个字符(9e)是十六进制的长度。我应该如何对 fi 的内容进行哈希处理,以便获得 Git ref abf5b536662000f79978c4d1b6e4eff5c8d785

【问题讨论】:

  • (1) git hash-object 正在添加文件,而不是其他对象类型。显然类型以某种方式附加到散列内容。我敢打赌 9e 不是长度,整行是一个哈希,因为 sha1 算法返回它。
  • SHA=9eabf5b536662000f79978c4d1b6e4eff5c8d785; git cat-file -p $SHA | git hash-object -t $(git cat-file -t $SHA) --stdin。阅读:你需要git hash-object -t commit fi

标签: git hash


【解决方案1】:

如“How is git commit sha1 formed ”中所述,公式为:

(printf "<type> %s\0" $(git cat-file <type> <ref> | wc -c); git cat-file <type> <ref>)|sha1sum

对于 commit 9eabf5b536662000f79978c4d1b6e4eff5c8d785(即v2.4.2^{},并引用一棵树):

(printf "commit %s\0" $(git cat-file commit 9eabf5b536662000f79978c4d1b6e4eff5c8d785 | wc -c); git cat-file commit 9eabf5b536662000f79978c4d1b6e4eff5c8d785 )|sha1sum

这将给出 9eabf5b536662000f79978c4d1b6e4eff5c8d785。

如愿以偿:

(printf "commit %s\0" $(git cat-file commit v2.4.2{} | wc -c); git cat-file commit v2.4.2{})|sha1sum

(仍然是 9eabf5b536662000f79978c4d1b6e4eff5c8d785)

同样,计算标签 v2.4.2 的 SHA1 将是:

(printf "tag %s\0" $(git cat-file tag v2.4.2 | wc -c); git cat-file tag v2.4.2)|sha1sum

这将给出 29932f3915935d773dc8d52c292cadd81c81071d。

【讨论】:

  • 我不知道为什么,但我得到了不同的数据 $ (printf "tree %s\0" $(git cat-file tree 9eabf5b536662000f79978c4d1b6e4eff5c8d785 | wc -c); git cat-file tree 9eabf5b536662000f79978c4d1b6e4eff5c8d785 )|sha1sum 655a20f99af32926cbf6d8fab092506ddd70e49c
  • 你混合了提交和树:使用相同的类型
  • 那么你必须得到相同的sha1。使用取消引用标签时是否有效? v2.4.2{}
  • 我认为问题在于使用 tree 。这有效(使用 -pretty 和提交)。知道如果它是“树”,为什么它使用提交工作吗? (printf "commit %s\0" $(git cat-file -p 9eabf5b536662000f79978c4d1b6e4eff5c8d785 | wc -c); git cat-file -p 9eabf5b536662000f79978c4d1b6e4eff5c8d785 )|sha1sum 9eabf5b536662000f79978c4d1b6e4eff5c8d785
  • @Theuserwithnohat 我的错:我在我的 repo 中测试了它,它确实与 commit 一起工作(在你的情况下,它引用了树 655a20f99af32926cbf6d8fab092506ddd70e49c)。我已经相应地更新了答案。
【解决方案2】:

这里有点混乱。 Git 使用不同类型的对象:blob、树和提交。 以下命令:

git cat-file -t <hash>

告诉您给定哈希的对象类型。 因此,在您的示例中,哈希 9eabf5b536662000f79978c4d1b6e4eff5c8d785 对应于提交对象。

现在,你自己想通了,运行这个:

git cat-file -p 9eabf5b536662000f79978c4d1b6e4eff5c8d785

根据对象的类型(在本例中为提交)为您提供对象的内容。

但是,这个:

git hash-object fi

...计算 blob 的哈希,其内容是上一个命令的输出(在您的示例中),但它可以是其他任何内容(例如“hello world!”)。试试这个:

echo "blob 277\0$(cat fi)" | shasum

输出与上一个命令相同。这基本上就是 Git 散列 blob 的方式。因此,通过散列 fi,您正在生成一个 blob 对象。但正如我们所见,9eabf5b536662000f79978c4d1b6e4eff5c8d785 是提交,而不是 blob。因此,您不能按原样对 fi 进行哈希处理以获得相同的哈希值。

提交的哈希值基于使其唯一的几个其他信息(例如提交者、作者、日期等)。以下文章准确地告诉您提交哈希是由什么组成的:

The anatomy of a git commit

因此,您可以通过为文章中指定的所有数据提供与原始提交中使用的完全相同的值来获得相同的哈希值。

这也可能有帮助:

Git from the bottom up

【讨论】:

  • echo "blob 277\0$(cat fi)" | shasum 对我来说产生了与git hash-object fi 不同的结果,原因有两个:首先,我不知道 277 指的是fi 的大小以及我特定的fi 的大小不等于 277。其次,echo 的 GNU coreutils 版本添加了一个换行符,并且不会转义 \0 以表示 NUL 字节(echo -en 修复了这个问题)。以下命令产生与git hash-object fi 相同的结果:printf "blob $(wc -c &lt; fi)\0$(cat fi)" | sha1sum
【解决方案3】:

This video by John Williams 概述了用于计算 Git 提交哈希的数据。以下是视频截图:

在没有 Git 的情况下重新实现提交哈希

为了更深入地了解 Git 的这一方面,我重新实现了在 Rust 中生成 Git 提交哈希的步骤,而不使用 Git。它目前适用于在提交单个文件时获取散列。这里的答案有助于实现这一目标,谢谢。

这些是我们需要计算以达到 Git 提交哈希的单个数据:

  1. 文件的对象 ID,涉及使用 SHA-1 对文件内容进行哈希处理。在 Git 中,hash-object 提供此 ID。
  2. 进入树对象的对象条目。在 Git 中,您可以通过 ls-tree 了解这些条目,但它们在树对象中的格式略有不同:[mode] [file name]\0[object ID]
  3. 树对象的散列,其形式为:tree [size of object entries]\0[object entries]。在 Git 中,使用以下命令获取树哈希:git cat-file commit HEAD | head -n1
  4. 提交哈希通过对您使用cat-file 看到的数据进行哈希处理。这包括树对象哈希和提交信息,例如作者、时间、提交消息和父提交哈希(如果不是第一次提交)。

每一步都依赖于前一步。让我们从第一个开始。

获取文件的对象ID

第一步是重新实现Git的hash-object,如git hash-object your_file

我们通过连接和散列这些数据从我们的文件创建对象散列:

  • 开头的字符串“blob”(注意尾随空格),后跟
  • 文件中的字节数,后跟
  • 一个空字节,在printf和Rust中用\0表示,后跟
  • 文件内容。

在 Bash 中:

file_name="your_file";
printf "blob $(wc -c < "$file_name")\0$(cat "$file_name")" | sha1sum

在锈中:

// Get the object ID
fn git_hash_object(file_content: &[u8]) -> Vec<u8> {

    let file_size = file_content.len().to_string();
    let hash_input: Vec<u8> = vec![
        "blob ".as_bytes(),
        file_size.as_bytes(),
        b"\0",
        file_content,
    ]
    // Flatten the Vec<&[u8]> to Vec<u8> with concat
    .concat();
    to_sha1(&hash_input)
}

我在to_sha1 中使用crate sha1 0.6.0 版:

fn to_sha1(hash_me: &[u8]) -> Vec<u8> {

    let mut m = Sha1::new();
    m.update(hash_me);
    m.digest().bytes().to_vec()
}

获取文件的对象入口

对象条目是 Git 的 tree object 的一部分。树对象代表文件和目录。

文件的对象条目具有这种形式:[mode] [file name]\0[object ID]

我们假设该文件是一个常规的、不可执行的文件,它在 Git 中转换为模式 100644。有关模式的更多信息,请参阅this

这个Rust函数将上一个函数git_hash_object的结果作为参数object_id

fn object_entry(file_name: &FileName, object_id: &[u8]) -> Vec<u8> {

    // It's a regular, non-executable file
    let mode = "100644";

    // [mode] [file name]\0[object ID]
    let object_entry: Vec<u8> = vec![
        mode.as_bytes(),
        b" ",
        file_name.as_bytes(),
        b"\0",
        object_id,
    ]
    .concat();

    object_entry
}

FileNameString 的新类型,以避免混淆参数,没什么花哨的:

pub struct FileName(pub String);

impl std::fmt::Display for FileName {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        self.0.fmt(f)
    }
}
impl FileName {
    fn as_bytes(&self) -> &[u8] {
        self.0.as_bytes()
    }
}

我尝试在 Bash 中编写 object_entry 的等价物,但 Bash 变量 cannot contain null bytes。可能有一些方法可以绕过这个限制,但我现在决定,如果我不能在 Bash 中使用变量,那么代码将变得非常难以理解。欢迎编辑提供可读的 Bash 等效项。

获取树对象哈希

如上所述,树对象代表 Git 中的文件和目录。您可以通过运行查看树对象的哈希值,例如,git cat-file commit HEAD | head -n1

树对象有这种形式:tree [size of object entries]\0[object entries]

在我们的例子中,我们只有一个object_entry,在上一步中计算出来:

fn tree_object_hash(object_entry: &[u8]) -> String {

    let object_entry_size = object_entry.len().to_string();

    let tree_object: Vec<u8> = vec![
        "tree ".as_bytes(),
        object_entry_size.as_bytes(),
        b"\0",
        object_entry,
    ]
    .concat();

    to_hex_str(&to_sha1(&tree_object))
}

其中to_hex_str定义为:

// Converts bytes to their hexadecimal representation.
fn to_hex_str(bytes: &[u8]) -> String {
    bytes.iter().map(|x| format!("{:02x}", x)).collect()
}

在 Git 存储库中,您可以使用 ls-tree 查看树对象的内容。例如,运行git ls-tree HEAD 将产生如下行:

100644 blob b8c0d74ef5ccd3dab583add7b3f5367efe4bf823    your_file

虽然这些行包含对象条目的数据(模式、对象 ID 和文件名),但它们的顺序不同,包括制表符以及输入到对象 ID,而不是对象条目。对象条目具有这种形式:[mode] [file name]\0[object ID]

获取提交哈希

最后一步创建提交哈希。

我们使用 SHA-1 散列的数据包括:

  • 上一步的树对象哈希。
  • 如果提交不是 repo 中的第一个提交,则父提交的哈希。
  • 作者姓名和创作日期。
  • 提交者名称和提交日期。
  • 提交消息。

您可以使用git cat-file commit HEAD 查看所有这些数据,例如:

tree a76b2df314b47956268b0c39c88a3b2365fb87eb
parent 9881a96ab93a3493c4f5002f17b4a1ba3308b58b
author Matthias Braun <m.braun@example.com> 1625338354 +0200
committer Matthias Braun <m.braun@example.com> 1625338354 +0200

Second commit (that's the commit message)

您可能已经猜到1625338354 是一个时间戳,即自Unix epoch 以来的秒数。您可以从git log的日期和时间格式转换,例如“Wed Jun 23 18:02:18 2021”,用date

date --date='Wed Jun 23 18:02:18 2021' +"%s"

在本例中,时区表示为+0200

根据cat-file 的输出,您可以使用此 Bash 命令(使用git cat-file,因此无需重新实现)创建 Git 提交哈希:

cat_file_output=$(git cat-file commit HEAD);
printf "commit $(wc -c <<< "$cat_file_output")\0$cat_file_output\n" | sha1sum

Bash 命令说明了——类似于前面的步骤——我们散列的是:

  • 此步骤中的前导字符串“commit”,后跟
  • 一堆数据的大小。这是上面详述的cat-file 的输出。其次是
  • 一个空字节,后跟
  • 数据本身(cat-file 的输出),末尾有换行符。

如果您保持得分:创建 Git 提交哈希涉及使用 SHA-1 至少 3 次。

以下是用于创建 Git 提交哈希的 Rust 函数。它使用上一步中生成的tree_object_hash 和一个结构CommitMetaData,其中包含您在调用git cat-file commit HEAD 时看到的其余数据。该函数还负责提交是否有父提交。

fn commit_hash(commit: &CommitMetaData, tree_object_hash: &str) -> Vec<u8> {

    let author_line =
        commit.author_name_and_email.to_owned() + " " +
        commit.author_timestamp_and_timezone;
    let committer_line =
        commit.committer_name_and_email.to_owned() + " " +
        commit.committer_timestamp_and_timezone;

    // If it's the first commit, which has no parent,
    // the line starting with "parent" is omitted
    let parent_commit_line = match &commit.parent_commit_hash {
        Some(parent_commit_hash) => "\nparent ".to_owned() + parent_commit_hash,
        None => "".to_string(),
    };
    let git_cat_file_str = format!(
        "tree {}{}\nauthor {}\ncommitter {}\n\n{}\n",
        tree_object_hash,
        parent_commit_line,
        author_line,
        committer_line,
        commit.commit_message
    );

    let git_cat_file_len = git_cat_file_str.len().to_string();

    let commit_object: Vec<u8> = vec![
        "commit ".as_bytes(),
        git_cat_file_len.as_bytes(),
        b"\0",
        git_cat_file_str.as_bytes(),
    ].concat();

    // Return the Git commit hash
    to_sha1(&commit_object)
}

这里是CommitMetaData

#[derive(Debug, Copy, Clone)]
pub struct CommitMetaData<'a> {
    pub(crate) author_name_and_email: &'a str,
    pub(crate) author_timestamp_and_timezone: &'a str,
    pub(crate) committer_name_and_email: &'a str,
    pub(crate) committer_timestamp_and_timezone: &'a str,
    pub(crate) commit_message: &'a str,
    // All commits after the first one have a parent commit
    pub(crate) parent_commit_hash: Option<&'a str>,
}

这个函数会创建CommitMetaDataCommitMetaData,这里的作者和提交者信息是一致的,这样我们以后运行程序时会很方便:

pub fn simple_commit<'a>(
    author_name_and_email: &'a str,
    author_timestamp_and_timezone: &'a str,
    commit_message: &'a str,
    parent_commit_hash: Option<&'a str>,
) -> CommitMetaData<'a> {
    CommitMetaData {
        author_name_and_email,
        author_timestamp_and_timezone,
        committer_name_and_email: author_name_and_email,
        committer_timestamp_and_timezone: author_timestamp_and_timezone,
        commit_message,
        parent_commit_hash,
    }
}

把它们放在一起

作为总结和提醒,创建 Git 提交哈希包括获取:

  1. 文件的对象 ID,涉及使用 SHA-1 对文件内容进行哈希处理。在 Git 中,hash-object 提供此 ID。
  2. 进入树对象的对象条目。在 Git 中,您可以通过 ls-tree 了解这些条目,但它们在树对象中的格式略有不同:[mode] [file name]\0[object ID]
  3. 树对象的散列,其形式为:tree [size of object entries]\0[object entries]。在 Git 中,使用以下命令获取树哈希:git cat-file commit HEAD | head -n1
  4. 提交哈希通过对您使用cat-file 看到的数据进行哈希处理。这包括树对象哈希和提交信息,例如作者、时间、提交消息和父提交哈希(如果不是第一次提交)。

在锈中:

pub fn get_commit_hash(
    file_name: &FileName,
    file_content: &[u8],
    commit: &CommitMetaData,
) -> String {

    let file_object_id = git_hash_object(file_content);
    let object_entry = object_entry(file_name, &file_object_id);
    let tree_object_hash = tree_object_hash(&object_entry);

    let commit_hash = commit_hash(commit, &tree_object_hash);
    to_hex_str(&commit_hash)
}

使用上述函数,您可以在 Rust 中创建文件的 Git 提交哈希,无需 Git:

use std::fs::File;
use std::io;
use std::io::prelude::*;

fn main() -> io::Result<()> {

    let file_name = FileName("your_file".to_string());
    let file_content = read_all_bytes(&file_name)?;

    let first_commit = simple_commit(
        "Firstname Lastname <test@example.com>",
        // Timestamp calculated using: date --date='Wed Jun 23 18:02:18 2021' +"%s"
        "1625338354 +0200",
        "Message of first commit",
        // No parent commit hash since this is the first commit
        None,
    );

    let first_commit_hash = get_commit_hash(&file_name, &file_content, &first_commit);
    Ok(println!("Git commit hash: {} ", first_commit_hash))
}

fn read_all_bytes(file_name: &FileName) -> io::Result<Vec<u8>> {

    let mut f = File::open(file_name.to_string())?;
    let mut file_content = Vec::new();
    f.read_to_end(&mut file_content)?;
    Ok(file_content)
}

要创建第二次提交的哈希,您需要将第一次提交的哈希放入第二次提交的CommitMetaData

let second_commit = simple_commit(
    "Firstname Lastname <test@example.com>",
    "1625388354 +0200",
    "Message of second commit",
    // The first commit is the parent of the second commit
    Some(first_commit_hash),
);

除了此处的其他答案及其链接之外,这些是创建我有限的重新实现的一些有用资源:

【讨论】:

    猜你喜欢
    • 2019-05-31
    • 2011-11-05
    • 2011-08-26
    • 1970-01-01
    • 1970-01-01
    • 2016-06-23
    • 1970-01-01
    相关资源
    最近更新 更多