Как рассчитывается git-хэш?
Я пытаюсь понять, как git вычисляет хэш ссылок.
$ git ls-remote https://github.com/git/git
....
29932f3915935d773dc8d52c292cadd81c81071d refs/tags/v2.4.2
9eabf5b536662000f79978c4d1b6e4eff5c8d785 refs/tags/v2.4.2^{}
....
Клонируйте репо локально. Проверить refs/tags/v2.4.2^{}
ref by sha
$ 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
Давайте SHA-1 содержимое, используя собственную команду хита git
git hash-object fi
3cf741bbdbcdeed65e5371912742e854a035e665
Почему на выходе нет [9e]abf5b536662000f79978c4d1b6e4eff5c8d785
? Я понимаю первые два символа (9e
) это длина в шестнадцатеричном формате. Как я должен хэшировать содержимое fi
так что я могу получить Git Ref abf5b536662000f79978c4d1b6e4eff5c8d785
?
3 ответа
Как описано в разделе " Как формируется git commit sha1", формула имеет вид:
(printf "<type> %s\0" $(git cat-file <type> <ref> | wc -c); git cat-file <type> <ref>)|sha1sum
В случае коммита 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)
Аналогично, вычисление SHA1 тега v2.4.2 будет:
(printf "tag %s\0" $(git cat-file tag v2.4.2 | wc -c); git cat-file tag v2.4.2)|sha1sum
Это дало бы 29932f3915935d773dc8d52c292cadd81c81071d.
Повторная реализация хеша коммита без Git
Чтобы глубже понять этот аспект Git, я повторно реализовал шаги, которые создают хэш фиксации Git при фиксации одного файла без использования Git. Ответы здесь были полезны в достижении этого, спасибо.
Вот отдельные фрагменты данных, которые нам нужно вычислить, чтобы получить хеш фиксации Git:
- Идентификатор объекта файла, который включает хеширование содержимого файла с помощью SHA-1. В Git предоставляет этот идентификатор.
- Записи объекта внутри объекта дерева. В Git вы можете получить представление об этих записях с помощью, но их формат в древовидном объекте немного отличается:
- Хэш объекта дерева, имеющего форму:. В Git получите хэш дерева с помощью:
- Хеш фиксации путем хеширования данных, которые вы видите, с помощью . Это включает в себя хэш объекта дерева и информацию о фиксации, такую как автор, время, сообщение фиксации и хеш родительской фиксации, если это не первая фиксация.
Каждый шаг зависит от предыдущего. Начнем с первого.
Получить идентификатор объекта файла
Первый шаг - переопределить Git , как в
git hash-object your_file
.
Мы создаем хэш объекта из нашего файла путем объединения и хеширования этих данных:
- Строка "blob" в начале, за которой следует
- размер файла, за которым следует
- нулевой байт, выраженный с помощью
\0
вprintf
и Rust, а затем - содержимое файла.
В Баше:
file_name="your_file";
printf "blob $(wc -c < "$file_name")\0$(cat "$file_name")" | sha1sum
В Rust:
// 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)
}
Я использую crate sha1 версии 0.6.0 в
to_sha1
:
fn to_sha1(hash_me: &[u8]) -> Vec<u8> {
let mut m = Sha1::new();
m.update(hash_me);
m.digest().bytes().to_vec()
}
Получить объектную запись файла
Записи объектов являются частью древовидного объекта Git . Объекты дерева представляют файлы и каталоги.
Мы предполагаем, что файл является обычным неисполняемым файлом, что соответствует режиму 100644 в Git. См. Это для получения дополнительной информации о режимах.
Эта функция 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";
// Object entries for files have this form:
// [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
}
FileName
это новый тип для
String
чтобы не путать аргументы, ничего особенного:
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, но переменные Bash не могут содержать нулевые байты . Вероятно, есть способы обойти это ограничение, но пока я решил, что, если у меня не будет переменных в Bash, код будет довольно трудно понять. Приветствуются изменения, обеспечивающие читаемый эквивалент Bash.
Получить хэш объекта дерева
Как упоминалось выше, древовидные объекты представляют файлы и каталоги в Git. Вы можете увидеть хэш объекта дерева, запустив, например,.
Мы вычисляем этот хеш, используя
object_entry
из предыдущего шага:
fn tree_object_hash(object_entry: &[u8]) -> String {
// We create the tree object which has this form:
// tree [size of object entries]\0[object entries]
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 вы можете просмотреть содержимое объекта дерева с помощью . Например, бег
git ls-tree HEAD
создаст такие строки:
100644 blob b8c0d74ef5ccd3dab583add7b3f5367efe4bf823 your_file
Хотя эти строки содержат данные записи объекта (режим, строка «blob», идентификатор объекта и имя файла), они расположены в другом порядке и включают символ табуляции. Записи объектов имеют такую форму:
Получить хеш фиксации
Последний шаг создает хеш фиксации.
Данные, которые мы хэшируем с помощью SHA-1, включают:
- Хэш объекта дерева из предыдущего шага.
- Родительский коммит, если коммит не самый первый в репо.
- Имя автора и дата создания.
- Имя коммиттера и дата фиксации.
- Сообщение фиксации.
Вы можете увидеть все эти данные, например:
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 . Вы можете конвертировать из формата даты и времени
git log
, например "среда, 23 июня 18:02:18 2021", с date
:
date --date='Wed Jun 23 18:02:18 2021' +"%s"
Часовой пояс обозначается как
+0200
в этом примере.
На основе выходных данных вы можете создать хэш-коммит Git с помощью этой команды Bash (которая использует
git cat-file
, поэтому повторной реализации нет):
cat_file_output=$(git cat-file commit HEAD);
printf "commit $(wc -c <<< "$cat_file_output")\0$cat_file_output\n" | sha1sum
Но мы видим, что - аналогично предыдущим шагам - мы хешируем:
- Начальная строка "commit" на этом этапе, за которой следует
- размер кучи данных. Вот результат, который подробно описан выше. С последующим
- нулевой байт, за которым следует
- сами данные (вывод) с разрывом строки в конце.
Если вы сохранили счет: создание хэша коммита Git предполагает использование SHA-1 как минимум три раза.
Ниже представлена функция Rust для создания хэша коммита Git. Он использует
tree_object_hash
созданный на предыдущем шаге, и структура, содержащая остальные данные, которые вы видите при вызове
git cat-file commit HEAD
. Функция также заботится о том, есть ли у фиксации родительская фиксация или нет.
fn commit_hash(commit: &CommitMetaData, tree_object_hash: &str) -> Vec<u8> {
let author =
commit.author_name_and_email.to_owned() + " " + commit.author_timestamp_and_timezone;
let committer =
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, committer, 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)
}
Вот:
#[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>,
}
Эта функция создает
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 состоит из получения:
- Идентификатор объекта файла, который включает хеширование содержимого файла с помощью SHA-1. В Git
hash-object
предоставляет этот идентификатор. - Записи объекта внутри объекта дерева. В Git вы можете получить представление об этих записях с помощью
ls-tree
, но их формат в древовидном объекте немного отличается:[mode] [file name]\0[object ID]
- Хэш объекта дерева, имеющего форму:
tree [size of object entries]\0[object entries]
. В Git получите хэш дерева с помощью:git cat-file commit HEAD | head -n1
- Хеш фиксации путем хеширования данных, которые вы видите, с помощью
cat-file
. Это включает в себя хэш объекта дерева и информацию о фиксации, такую как автор, время, сообщение фиксации и хеш родительской фиксации, если это не первая фиксация.
В Rust:
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)
}
С помощью вышеперечисленных функций вы можете создать хеш фиксации Git файла в Rust без 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 commit = simple_commit(
"Firstname Lastname <test@example.com>",
// Timestamp calculated using: date --date='Wed Jun 23 18:02:18 2021' +"%s"
"1625338354 +0200",
"First commit message",
// No parent commit since this is the first one
None,
);
let hash = get_commit_hash(&file_name, &file_content, &commit);
Ok(println!("Git commit hash: {} ", 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)
}
Помимо других ответов здесь и их ссылок, это были некоторые полезные ресурсы при создании моей ограниченной повторной реализации:
- Повторная реализация
git hash-object
в JavaScript: - Формат объекта дерева Git , это следующее место, куда я бы посмотрел, если бы я хотел сделать мою повторную реализацию более полной: для работы с коммитами, включающими более одного файла.
Здесь немного путаницы. Git использует разные типы объектов: BLOB-объекты, деревья и коммиты. Следующая команда:
git cat-file -t <hash>
Сообщает вам тип объекта для данного хэша. Итак, в вашем примере хеш 9eabf5b536662000f79978c4d1b6e4eff5c8d785 соответствует объекту фиксации.
Теперь, как вы сами поняли, запустите это:
git cat-file -p 9eabf5b536662000f79978c4d1b6e4eff5c8d785
Предоставляет вам содержимое объекта в соответствии с его типом (в данном случае, коммитом).
Но это:
git hash-object fi
... вычисляет хэш для большого двоичного объекта, содержимое которого является выводом предыдущей команды (в вашем примере), но это может быть что-то еще (например, "привет мир!"). Вот попробуйте это:
echo "blob 277\0$(cat fi)" | shasum
Вывод такой же, как и в предыдущей команде. Это в основном то, как Git хэширует блоб. Таким образом, хэшируя fi, вы генерируете объект blob. Но, как мы видели, 9eabf5b536662000f79978c4d1b6e4eff5c8d785 - это коммит, а не BLOB-объект. Таким образом, вы не можете хешировать фи, как это делается для того, чтобы получить тот же хеш.
Хеш коммита основан на нескольких других данных, которые делают его уникальным (например, коммиттер, автор, дата и т. Д.). Следующая статья расскажет вам точно, из чего сделан хеш коммита:
Таким образом, вы можете получить тот же хеш, предоставив всем данным, указанным в статье, те же значения, что и в исходном коммите.
Это также может быть полезно: