这个视频由John Williams制作,概述了计算Git提交哈希值所需的数据。以下是视频中的截图:
不使用Git重新实现提交哈希
为了更深入地理解Git的这个方面,我在Rust中重新实现了生成Git提交哈希的步骤,而不使用Git。目前它可以在“提交单个文件”时获取哈希值。这里的答案对我实现这一点非常有帮助,谢谢。
这个答案的源代码可以在这里找到。使用cargo run
来执行它。
以下是我们需要计算的各个数据部分,以得到一个Git提交哈希:
- 文件的对象ID,涉及使用SHA-1对文件内容进行哈希。在Git中,
hash-object
提供了这个ID。
- 进入树对象的对象条目。在Git中,你可以通过
ls-tree
来了解这些条目,但它们在树对象中的格式略有不同:[模式] [文件名]\0[对象ID]
- 树对象的哈希值的形式为:
tree [对象条目的大小]\0[对象条目]
。在Git中,可以使用以下命令获取树的哈希值:git cat-file commit HEAD | head -n1
- 通过对你看到的数据进行哈希,得到提交哈希值。这包括树对象的哈希值以及提交信息,如作者、时间、提交消息,以及父提交哈希(如果不是第一个提交)。
每一步都依赖于前一步。让我们从第一步开始。
获取文件的对象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
在Rust中:
fn git_hash_object(file_content: &[u8]) -> Vec<u8> {
let file_size = file_content.len().to_string();
let hash_input = [
"blob ".as_bytes(),
file_size.as_bytes(),
b"\0",
file_content,
]
.concat();
to_sha1(&hash_input)
}
我在
to_sha1
中使用
crate sha1版本0.10.5。
fn to_sha1(hash_me: &[u8]) -> Vec<u8> {
use sha1::{Digest, Sha1};
let mut hasher = Sha1::new();
hasher.update(hash_me);
hasher.finalize().to_vec()
}
获取文件的对象条目
对象条目是Git的树对象的一部分。树对象代表文件和目录。
文件的对象条目具有以下形式:[模式] [文件名]\0[对象ID]
我们假设该文件是一个普通的非可执行文件,在Git中对应的模式是100644。请参阅此处了解更多关于模式的信息。
这个Rust函数将前一个函数git_hash_object
的结果作为参数object_id
传入:
fn object_entry(file_name: &str, object_id: &[u8]) -> Vec<u8> {
let mode = "100644";
let object_entry = [
mode.as_bytes(),
b" ",
file_name.as_bytes(),
b"\0",
object_id,
]
.concat();
object_entry
}
我试图在Bash中写出与
object_entry
等效的代码,但是Bash变量
不能包含空字节。可能有办法绕过这个限制,但是我决定暂时如果在Bash中不能使用变量,那么代码会变得非常难以理解。欢迎提供一个可读的Bash等效代码的修改。
获取树对象哈希值
如上所述,树对象代表Git中的文件和目录。您可以通过运行例如git cat-file commit HEAD | head -n1
来查看树对象的哈希值。
树对象的形式为:tree [对象条目的大小]\0[对象条目]
在我们的情况下,我们只有一个单独的object_entry
,在前一步中计算出来:
fn tree_object_hash(object_entry: &[u8]) -> String {
let object_entry_size = object_entry.len().to_string();
let tree_object = [
"tree ".as_bytes(),
object_entry_size.as_bytes(),
b"\0",
object_entry,
]
.concat();
to_hex_str(&to_sha1(&tree_object))
}
其中to_hex_str
被定义为:
fn to_hex_str(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
在 Git 存储库中,您可以使用
ls-tree
查看树对象的内容。例如,运行
git ls-tree HEAD
将生成以下类似的行:
100644 blob b8c0d74ef5ccd3dab583add7b3f5367efe4bf823 your_file
虽然这些行包含了一个对象条目的数据(模式、对象ID和文件名),但它们的顺序不同,并且还包括一个制表符以及作为对象ID输入的字符串"blob"。对象条目的形式如下:[模式] [文件名]\0[对象ID]
获取提交哈希值
最后一步是创建提交哈希值。
我们使用SHA-1进行哈希的数据包括:
- 来自上一步的树对象哈希。
- 如果提交不是仓库中的第一个提交,则包括父提交的哈希。
- 作者姓名和作者日期。
- 提交者姓名和提交日期。
- 提交消息。
你可以通过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纪元以来的秒数。你可以通过
date
将
git log
中的日期和时间格式(如"Sat Jul 3 20:52:34 2021")转换为Unix纪元秒数:
date --date='Sat Jul 3 20:52:34 2021' +"%s"
在这个例子中,时区被表示为+0200
。
根据cat-file
的输出,您可以使用以下Bash命令创建Git提交哈希(该命令使用git cat-file
,因此不需要重新实现):
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 三次。
下面是用于创建 Git 提交哈希的 Rust 函数。它使用在上一步中生成的 tree_object_hash
和一个包含调用 git cat-file commit HEAD
时看到的其余数据的结构体 CommitMetaData
。该函数还会处理提交是否有父提交。
fn commit_hash(commit: &CommitMetaData, tree_object_hash: &str) -> Vec<u8> {
let author_line = format!(
"{} {}",
commit.author_name_and_email, commit.author_timestamp_and_timezone
);
let committer_line = format!(
"{} {}",
commit.committer_name_and_email, commit.committer_timestamp_and_timezone
);
let parent_commit_line = match commit.parent_commit_hash {
Some(parent_commit_hash) => format!("\nparent {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 = [
"commit ".as_bytes(),
git_cat_file_len.as_bytes(),
b"\0",
git_cat_file_str.as_bytes(),
]
.concat();
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,
pub(crate) parent_commit_hash: Option<&'a str>,
}
这个函数创建CommitMetaData
,其中作者和提交者的信息是相同的,在以后运行程序时将非常方便:
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提交哈希包括以下步骤:
- 文件的对象 ID,它涉及使用SHA-1对文件内容进行散列。在Git中,
hash-object
提供了该ID。
- 进入树对象的对象条目。在Git中,你可以通过
ls-tree
获取这些条目的一个概念,但它们在树对象中的格式略有不同:[模式] [文件名]\0[对象ID]
- 树对象的散列值,其形式为:
tree [对象条目的大小]\0[对象条目]
。在Git中,使用以下命令获取树对象的散列值:git cat-file commit HEAD | head -n1
- 通过对看到的数据进行散列来获得提交散列值,其中包括树对象的散列值以及作者、时间、提交消息和父提交散列值(如果它不是第一个提交)等提交信息。使用
cat-file
进行散列。
在Rust中:
pub fn get_commit_hash(
file_name: &str,
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, io};
fn main() -> io::Result<()> {
let file_name = "your_file";
let file_content = fs::read(file_name)?;
let first_commit = simple_commit(
"Firstname Lastname <test@example.com>",
"1624464138 +0200",
"Message of first commit",
None,
);
let first_commit_hash = get_commit_hash(file_name, &file_content, &first_commit);
Ok(println!("Git commit hash: {first_commit_hash}"))
}
要创建第二个提交的哈希值,你需要将第一个提交的哈希值放入第二个提交的
CommitMetaData
中。
let second_commit = simple_commit(
"Firstname Lastname <test@example.com>",
"1625388354 +0200",
"Message of second commit",
Some(first_commit_hash),
);
除了这里的其他答案和链接之外,以下是我在创建有限重新实现时使用的一些有用资源:
- JavaScript中
git hash-object
的重新实现。
- Git树对象的格式,如果我想使我的重新实现更完整,这将是我下一个查找的地方:处理涉及多个文件的提交。
git hash-object
是用来添加文件的,而不是其他类型的对象。显然,类型会以某种方式附加到散列内容上。我敢打赌9e不是长度,整行都是一个哈希值,因为SHA1算法返回的就是这样的结果。 - max630SHA=9eabf5b536662000f79978c4d1b6e4eff5c8d785; git cat-file -p $SHA | git hash-object -t $(git cat-file -t $SHA) --stdin
. Read: You needgit hash-object -t commit fi
- Tino