Эта заметка является последней в серии «Внутреннее устройство Git». В первой части мы познакомились с моделью хранения объектов в git, а во второй части мы рассмотрели, как объекты хранятся в «packfiles» для экономии места.
В этой статье мы переключим наше внимание с локальных git-репозиториев (хранящихся на вашем компьютере) на удаленные (хранящиеся на сервере, например, GitHub). Мы рассмотрим протокол, который git использует для связи с «удаленными» репозиториями, и реализуем git fetch
с нуля.
Если вы часто пользуетесь онлайн-репозиториями git (например, на GitHub), вы можете знать, что можно git clone
репозиторий, используя либо HTTP/HTTPS URL (например, https://github.com/git/git.git
), либо «SSH» URL (например, git@github.com:git/git.git
). Разница между этими URL заключается в протоколе, который git использует для связи с сервером GitHub во время git clone
, git fetch
, git pull
или git push
. git реализует несколько протоколов: «тупой» HTTP/HTTPS, «умный» HTTP/HTTPS, SSH и «git». Тупой протокол менее эффективен, поэтому он редко используется на практике. Умные протоколы используют те же процедуры, но отличаются базовым протоколом, используемым для подключения к серверу. Мы сосредоточимся на SSH, потому что он так распространен и является интересным применением SSH.
Протокол является человекочитаемым, поэтому мы в основном узнаем, как он работает, наблюдая за тем, что клиент и сервер посылают друг другу. Если вы хотите изучить документацию git по этой теме, вот несколько хороших ресурсов:
- Глава книги git о протоколах передачи предоставляет высокоуровневый обзор доступных транспортов и того, как они работают.
- Внутренняя документация git «pack protocol» подробно описывает транспортный протокол SSH
- Внутренняя документация git «protocol capabilities» объясняет каждую из дополнительных функций для транспорта SSH.
Извините за задержку с третьей частью! В последние несколько месяцев моя жизнь была более насыщенной, чем ожидалось.
Исходный код этого поста можно найти здесь.
Где используется SSH?
Использование SSH git URL требует загрузки вашего открытого ключа SSH на сервер. SSH-ключи обычно используются для аутентификации SSH-соединений, поэтому вы можете догадаться, что ваш git-клиент взаимодействует с git-сервером через SSH-соединение.
Если вы не знакомы с SSH, вот краткий обзор того, как он используется. (Мы не будем говорить о том, как SSH реализован, но это тоже интересно). SSH позволяет выполнять команды терминала на удаленном компьютере. Например, я могу выполнить команду hostname
, чтобы узнать имя своего компьютера:
csander:~ $ hostname
csander-mac.local
Я также могу использовать SSH, чтобы открыть терминал на экземпляре EC2, на котором размещен calebsander.com
, и запустить там hostname
:
csander:~ $ ssh ubuntu@calebsander.com # ubuntu is the user to log in as
ubuntu@ip-172-31-52-11:~ $ hostname
ip-172-31-52-11
ubuntu@ip-172-31-52-11:~ $ exit
Connection to calebsander.com closed.
По умолчанию ssh
запускает терминальный процесс (например, bash
) на сервере. Вы можете указать ssh
запустить вместо этого другую команду:
csander:~ $ ssh ubuntu@calebsander.com hostname
ip-172-31-52-11
Ключевой особенностью git является то, что SSH-соединение является двунаправленным: ваш локальный стандартный ввод подключается к вводу удаленного процесса, а удаленный стандартный вывод — к локальному. Это легче всего увидеть при выполнении команды типа cat
(копирование стандартного ввода в стандартный вывод). Если вы посылаете строку текста, она отправляется в процесс cat
, запущенный на другом компьютере, который печатает строку, вызывая ее обратную отправку.
csander:~ $ ssh ubuntu@calebsander.com cat
abc # input sent to server
abc # output sent back
123 # input
123 # output
(enter Ctrl+D to end the standard input, terminating the process)
SSH обеспечивает как аутентификацию (сервер проверяет, что SSH-ключ клиента может получить доступ к хранилищу), так и шифрование (связь скрыта от тех, кто подглядывает за соединением), и, вероятно, именно поэтому git решил использовать его.
Если выполнить команду git clone git@github.com:git/git.git
и использовать ps aux | grep ssh
для списка процессов SSH во время его работы, можно увидеть команду SSH, которую использовал git:
/usr/bin/ssh -o SendEnv=GIT_PROTOCOL git@github.com git-upload-pack 'git/git.git'
-o SendEnv=GIT_PROTOCOL
не нужен, поэтому команду SSH можно упростить до:
ssh git@github.com git-upload-pack git/git.git
Здесь мы видим все части URL git@github.com:git/git.git
! Часть перед :
— это логин SSH (например, user@domain.name
), а часть после :
— это аргумент исполняемого файла git-upload-pack
, указывающий репозиторий. (Может смутить то, что git-upload-pack
используется для clone
/fetch
/pull
, а git-receive-pack
используется для push
, но это с точки зрения сервера).
Если вам интересно, SSH-сервер GitHub ограничен, поэтому вы не можете выполнять другие команды:
$ ssh git@github.com
PTY allocation request failed on channel 0
Hi calebsander! You've successfully authenticated, but GitHub does not provide shell access.
Connection to github.com closed.
$ ssh git@github.com echo Hello world
Invalid command: 'echo Hello world'
You appear to be using ssh to clone a git:// URL.
Make sure your core.gitProxy config option and the
GIT_PROXY_COMMAND environment variable are NOT set.
Теперь, когда мы знаем команду SSH, мы можем выполнить ее самостоятельно и посмотреть, что сервер отправляет в ответ:
$ ssh git@github.com git-upload-pack git/git.git
014e74cc1aa55f30ed76424a0e7226ab519aa6265061 HEADmulti_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed allow-tip-sha1-in-want allow-reachable-sha1-in-want symref=HEAD:refs/heads/master filter object-format=sha1 agent=git/github-g2faa647c16c3
003d74cc1aa55f30ed76424a0e7226ab519aa6265061 refs/heads/main
003e4c53a8c20f8984adb226293a3ffd7b88c3f4ac1a refs/heads/maint
003f74cc1aa55f30ed76424a0e7226ab519aa6265061 refs/heads/master
003dd65ed663a79d75fb636a4602eca466dbd258082e refs/heads/next
003d583a5781c12c1d6d557fae77552f6cee5b966f8d refs/heads/seen
003db1b3e2657f1904c7c603ea4313382a24af0fd91f refs/heads/todo
003ff0d0fd3a5985d5e588da1e1d11c85fba0ae132f8 refs/pull/10/head
0040c8198f6c2c9fc529b25988dfaf5865bae5320cb5 refs/pull/10/merge
...
003edcba104ffdcf2f27bc5058d8321e7a6c2fe8f27e refs/tags/v2.9.5
00414d4165b80d6b91a255e2847583bd4df98b5d54e1 refs/tags/v2.9.5^{}
0000(waiting for input)
Ладно, многовато для распаковки (каламбур, безусловно, подразумевается), так что давайте разберем все по полочкам!
Открытие SSH-соединения в Rust
Сначала мы посмотрим, как открыть это SSH-соединение в Rust. Мы можем создать ту же команду ssh
, которую запустили сами, используя Stdio::piped()
для входного и выходного потоков, чтобы получить ssh_input
, реализующий Write
, и ssh_output
, реализующий Read
.
use std::env;
use std::io;
use std::process::{ChildStdin, ChildStdout, Command, Stdio};
// Using the types and functions implemented in previous posts
struct Transport {
ssh_input: ChildStdin,
ssh_output: ChildStdout,
}
impl Transport {
fn connect(repository: &str) -> io::Result<Self> {
// `repository` will look like "git@github.com:git/git.git".
// "git@github.com" is the SSH login (user "git", hostname "github.com").
// "git/git.git" specifies the repository to fetch on this server.
let repository_pieces: Vec<_> = repository.split(':').collect();
let [login, repository] = <[&str; 2]>::try_from(repository_pieces)
.map_err(|_| {
make_error(&format!("Invalid SSH repository: {}", repository))
})?;
// Start an SSH process to connect to this repository.
// We don't wait for the `ssh` command to finish because we are going to
// communicate back and forth with the server through its standard input and output.
let mut ssh_process = Command::new("ssh")
.args([login, "git-upload-pack", repository])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let ssh_input = ssh_process.stdin.take().ok_or_else(|| {
make_error("Failed to open ssh stdin")
})?;
let ssh_output = ssh_process.stdout.take().ok_or_else(|| {
make_error("Failed to open ssh stdout")
})?;
Ok(Transport { ssh_input, ssh_output })
}
}
fn main() -> io::Result<()> {
let args: Vec<_> = env::args().collect();
let [_, repository] = <[String; 2]>::try_from(args).unwrap();
let mut transport = Transport::connect(&repository)?;
// Print the SSH output
io::copy(&mut transport.ssh_output, &mut io::stdout())?;
Ok(())
}
Запуск этой программы дает тот же результат, что и выполнение команды SSH напрямую:
$ cargo run git@github.com:git/git.git
014e74cc1aa55f30ed76424a0e7226ab519aa6265061 HEADmulti_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed allow-tip-sha1-in-want allow-reachable-sha1-in-want symref=HEAD:refs/heads/master filter object-format=sha1 agent=git/github-g2faa647c16c3
003d74cc1aa55f30ed76424a0e7226ab519aa6265061 refs/heads/main
003e4c53a8c20f8984adb226293a3ffd7b88c3f4ac1a refs/heads/maint
003f74cc1aa55f30ed76424a0e7226ab519aa6265061 refs/heads/master
003dd65ed663a79d75fb636a4602eca466dbd258082e refs/heads/next
003d583a5781c12c1d6d557fae77552f6cee5b966f8d refs/heads/seen
003db1b3e2657f1904c7c603ea4313382a24af0fd91f refs/heads/todo
003ff0d0fd3a5985d5e588da1e1d11c85fba0ae132f8 refs/pull/10/head
0040c8198f6c2c9fc529b25988dfaf5865bae5320cb5 refs/pull/10/merge
...
003edcba104ffdcf2f27bc5058d8321e7a6c2fe8f27e refs/tags/v2.9.5
00414d4165b80d6b91a255e2847583bd4df98b5d54e1 refs/tags/v2.9.5^{}
(waiting)
Поиск удаленного URL по умолчанию
В приведенном выше примере мы передали программе нужный URL репозитория. Но при использовании git часто приходится запускать git fetch
/pull
/push
без указания репозитория. По умолчанию git использует URL, указанный при первоначальном git clone
, поэтому он должен быть где-то сохранен. Исследуя каталог .git
, мы видим:
$ git clone git@github.com:git/git.git
Cloning into 'git'...
remote: Enumerating objects: 325167, done.
remote: Total 325167 (delta 0), reused 0 (delta 0), pack-reused 325167
Receiving objects: 100% (325167/325167), 185.01 MiB | 7.77 MiB/s, done.
Resolving deltas: 100% (242985/242985), done.
Updating files: 100% (4084/4084), done.
$ cd git
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = git@github.com:git/git.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
Для каждого удаленного хранилища существует раздел [remote ...]
. По умолчанию хранилище, используемое в команде git clone
, называется origin
. Параметр url
дает нам URL для этого удаленного хранилища.
Существует также секция [branch ...]
для каждой ветви, например master
, указывающая, с какого пульта и имени remote ref по умолчанию push
и pull
ветвь.
Например, запустите git pull
с проверенным master
. Разделы конфигурации [branch "master"]
и [remote "origin"]
переводят это в получение git@github.com:git/git.git
и слияние origin/master
в master
.
Мы можем найти URL для origin
, разобрав файл конфигурации и затем извлекая параметр url
из секции [remote "origin"]
:
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
const CONFIG_FILE: &str = ".git/config";
// `r#` is handy for string literals with quotes
const REMOTE_ORIGIN_SECTION: &str = r#"[remote "origin"]"#;
const URL_PARAMETER: &str = "url";
// A parsed .git/config file, represented as
// a map of section -> parameter -> value
#[derive(Debug)]
struct ConfigFile(HashMap<String, HashMap<String, String>>);
impl ConfigFile {
fn read() -> io::Result<Self> {
let config_file = File::open(CONFIG_FILE)?;
let mut sections = HashMap::new();
// The parameter values for the current section
let mut parameters: Option<&mut HashMap<String, String>> = None;
for line in BufReader::new(config_file).lines() {
let line = line?;
if let Some(parameter_line) = line.strip_prefix('t') {
// The line is indented, so it's a parameter in a section
let (parameter, value) = parameter_line.split_once(" = ")
.ok_or_else(|| {
make_error(&format!("Invalid parameter line: {:?}", parameter_line))
})?;
// All parameters should be under a section
let parameters = parameters.as_mut().ok_or_else(|| {
make_error("Config parameter is not in a section")
})?;
parameters.insert(parameter.to_string(), value.to_string());
}
else {
// The line starts a new section
parameters = Some(sections.entry(line).or_default());
}
}
Ok(ConfigFile(sections))
}
fn get_origin_url(&self) -> Option<&str> {
let remote_origin_section = self.0.get(REMOTE_ORIGIN_SECTION)?;
let url = remote_origin_section.get(URL_PARAMETER)?;
Some(url)
}
}
fn main() -> io::Result<()> {
let config = ConfigFile::read()?;
println!("Config file: {:#?}", config);
let origin_url = config.get_origin_url().ok_or_else(|| {
make_error("Missing remote 'origin'")
})?;
println!("Remote 'origin' URL: {}", origin_url);
Ok(())
}
Запуск печатает:
Config file: ConfigFile(
{
"[remote "origin"]": {
"url": "git@github.com:git/git.git",
"fetch": "+refs/heads/*:refs/remotes/origin/*",
},
"[core]": {
"repositoryformatversion": "0",
"bare": "false",
"ignorecase": "true",
"filemode": "true",
"logallrefupdates": "true",
"precomposeunicode": "true",
},
"[branch "master"]": {
"remote": "origin",
"merge": "refs/heads/master",
},
},
)
Remote 'origin' URL: git@github.com:git/git.git
Транспортный протокол SSH
Чанки
Давайте попробуем понять, что сервер отправил по SSH-соединению. Это выглядит как ряд строк, каждая из которых начинается с шестнадцатеричной строки. Они похожи на хэши, и на самом деле почти так и есть, только длина их 44 символа вместо 40. Вы можете заметить, что первые 4 шестнадцатеричных символа в основном следуют шаблону «003x» или «004x», а последняя (пустая) строка имеет «0000» в качестве первых 4 символов. Вы можете проверить, что эти 4 символа кодируют длину каждой строки (включая 4 символа в начале и символ новой строки в конце) в шестнадцатеричном формате. Строка «0000» является специальной; она указывает на конец отправляемых строк. Документация git называет префикс каждой строки с ее длиной в шестнадцатеричном виде форматом «pkt-line». Я буду называть эти строки «чанками».
Позже мы увидим чанки с двоичными данными вместо текста, поэтому мы начнем с метода чтения чанка в виде байтов:
const CHUNK_LENGTH_DIGITS: usize = 4;
impl Transport {
fn read_chunk(&mut self) -> io::Result<Option<Vec<u8>>> {
// Chunks start with 4 hexadecimal digits indicating their length,
// including the length digits
let length_digits: [_; CHUNK_LENGTH_DIGITS] =
read_bytes(&mut self.ssh_output)?;
let chunk_length = length_digits.iter().try_fold(0, |value, &byte| {
let char_value = hex_char_value(byte)?;
Some(value << 4 | char_value as usize)
}).ok_or_else(|| {
make_error(&format!("Invalid chunk length: {:?}", length_digits))
})?;
// The chunk "0000" indicates the end of a sequence of chunks
if chunk_length == 0 {
return Ok(None)
}
let chunk_length = chunk_length.checked_sub(CHUNK_LENGTH_DIGITS)
.ok_or_else(|| {
make_error(&format!("Chunk length too short: {}", chunk_length))
})?;
let mut chunk = vec![0; chunk_length];
self.ssh_output.read_exact(&mut chunk)?;
Ok(Some(chunk))
}
}
Затем мы можем прочитать текстовый фрагмент, преобразовав байты в строку и удалив n
в конце:
impl Transport {
fn read_text_chunk(&mut self) -> io::Result<Option<String>> {
let chunk = self.read_chunk()?;
let chunk = match chunk {
Some(chunk) => chunk,
_ => return Ok(None),
};
let mut text_chunk = String::from_utf8(chunk).map_err(|_| {
make_error("Invalid text chunk")
})?;
// Text chunks should end with a newline character, but don't have to.
// Remove it if it exists.
if text_chunk.ends_with('n') {
text_chunk.pop();
}
Ok(Some(text_chunk))
}
}
fn main() -> io::Result<()> {
// ...
let mut transport = Transport::connect(origin_url)?;
// Print each text chunk the server sends back
while let Some(chunk) = transport.read_text_chunk()? {
println!("{:?}", chunk);
}
Ok(())
}
Эта программа показывает каждый разобранный фрагмент текста до строки 0000
, которая указывает на конец фрагментов. Чанки выглядят идентично SSH-выводу с 4 шестнадцатеричными символами, удаленными из начала каждой строки. Мы также можем видеть 0 байт (u{0}
) в первом текстовом фрагменте, который был скрыт моим терминалом.
"74cc1aa55f30ed76424a0e7226ab519aa6265061 HEADu{0}multi_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed allow-tip-sha1-in-want allow-reachable-sha1-in-want symref=HEAD:refs/heads/master filter object-format=sha1 agent=git/github-g2faa647c16c3"
"74cc1aa55f30ed76424a0e7226ab519aa6265061 refs/heads/main"
"4c53a8c20f8984adb226293a3ffd7b88c3f4ac1a refs/heads/maint"
"74cc1aa55f30ed76424a0e7226ab519aa6265061 refs/heads/master"
"d65ed663a79d75fb636a4602eca466dbd258082e refs/heads/next"
"583a5781c12c1d6d557fae77552f6cee5b966f8d refs/heads/seen"
"b1b3e2657f1904c7c603ea4313382a24af0fd91f refs/heads/todo"
"f0d0fd3a5985d5e588da1e1d11c85fba0ae132f8 refs/pull/10/head"
"c8198f6c2c9fc529b25988dfaf5865bae5320cb5 refs/pull/10/merge"
...
"dcba104ffdcf2f27bc5058d8321e7a6c2fe8f27e refs/tags/v2.9.5"
"4d4165b80d6b91a255e2847583bd4df98b5d54e1 refs/tags/v2.9.5^{}"
Ссылка
Глядя на строки, отправленные сервером, мы видим, что в каждой из них указан хэш фиксации и имя (HEAD
, refs/heads/main
и т.д.). В первой строке также есть дополнительная строка возможностей, которую мы обсудим в ближайшее время. Эти комбинации коммит-имя называются «refs» (сокращение от «references») и указывают клиенту, какие коммиты он может получить. Они делятся на несколько категорий:
Вот код для чтения ссылок и возможностей, возвращаемых сервером:
use std::collections::HashSet;
struct Refs {
capabilities: HashSet<String>,
// Map of ref name (e.g. "refs/heads/main") to commit hashes
refs: HashMap<String, Hash>,
}
impl Transport {
fn receive_refs(&mut self) -> io::Result<Refs> {
// The first chunk contains the HEAD ref and a list of capabilities.
// Even if the repository is empty, capabilities are still needed,
// so a hash of all 0s is sent.
let head_chunk = match self.read_text_chunk()? {
Some(chunk) => chunk,
_ => return Err(make_error("No chunk received from server")),
};
let (head_ref, capabilities) = head_chunk.split_once(' ').ok_or_else(|| {
make_error("Invalid capabilities chunk")
})?;
let capabilities = capabilities.split(' ').map(str::to_string).collect();
let mut refs = HashMap::new();
let mut add_ref = |chunk: &str| -> io::Result<()> {
// Each subsequent chunk contains a ref (a commit hash and a name)
let (hash, ref_name) = chunk.split_once(' ').ok_or_else(|| {
make_error("Invalid ref chunk")
})?;
let hash = Hash::from_str(hash)?;
refs.insert(ref_name.to_string(), hash);
Ok(())
};
add_ref(head_ref)?;
while let Some(chunk) = self.read_text_chunk()? {
add_ref(&chunk)?;
}
Ok(Refs { capabilities, refs })
}
}
fn main() -> io::Result<()> {
// ...
let Refs { capabilities, refs } = transport.receive_refs()?;
println!("Capabilities: {:?}", capabilities);
for (ref_name, hash) in refs {
println!("Ref {} has hash {}", ref_name, hash);
}
Ok(())
}
Запуск этой программы выводит возможности и рефссылки, присланные сервером. Обратите внимание, что порядок случайный, так как мы итерируем по HashSet
и HashMap
.
Capabilities: {"deepen-since", "symref=HEAD:refs/heads/master", "object-format=sha1", "allow-reachable-sha1-in-want", "include-tag", "shallow", "thin-pack", "allow-tip-sha1-in-want", "side-band-64k", "deepen-not", "filter", "agent=git/github-g2faa647c16c3", "side-band", "multi_ack_detailed", "deepen-relative", "ofs-delta", "no-progress", "multi_ack"}
Ref refs/pull/531/head has hash 1572444361982199fdab9c6f6b7e94383717b6c9
Ref refs/pull/983/merge has hash d217f9ec363d5ed88a37ab15a72fad6b4d90acf1
Ref refs/pull/891/head has hash 7d7e794ab7286db0aea88c6e1eab881fc5d188f7
Ref refs/tags/v2.14.1^{} has hash 4d7268b888d7bb6d675340ec676e4239739d0f6d
...
Ref refs/tags/v1.2.3 has hash 51f2164fdc92913c3d1c6d199409b43cb9b6649f
Возможности
И сервер, и клиент сообщают о поддерживаемых ими «возможностях». Это позволяет им реализовывать новые возможности git, оставаясь обратно совместимыми со старыми клиентами и серверами. Например, возможность ofs-delta
означает, что сервер может посылать (или клиент может понимать) объекты «дельта смещения» в пакетах.
Сервер отправляет список своих возможностей, а клиент запрашивает подмножество из них для включения. Таким образом, и сервер, и клиент поддерживают все включенные возможности.
git также использует возможности для передачи различной информации (например, symref=HEAD:refs/heads/master
указывает, что master
является веткой по умолчанию).
Пока мы будем запрашивать только возможность ofs-delta
(если сервер ее поддерживает). В последнем посте (часть 2) подробно обсуждаются дельты смещения, но суть в том, что они делают пакфайлы меньше, чем дельты хэша (которые всегда поддерживаются). Точно так же, как сервер отправляет свои возможности в первом чанке ref, клиент запрашивает возможности в первом чанке «want», который мы обсудим далее.
Хотелки
После того, как сервер объявил доступные ссылки, клиент выбирает, какие из них ему нужны, отвечая на запрос их хэшами. Например, выполняя команду git pull origin main
, клиент запросит коммит только для ссылки refs/heads/main
. Сервер отправляет только запрошенные объекты фиксации и объекты фиксации, дерева и блоба, на которые он (косвенно) ссылается.
Желаемые ссылки отправляются в виде текстовых блоков, начинающихся с want
. При отправке кусков на сервер используется тот же формат (префикс шестнадцатеричной длины), что и при получении кусков. Единственное отличие заключается в том, что они записываются на вход SSH, а не считываются с выхода SSH.
Вот реализация на языке Rust. Обратите внимание, что мы можем отправить пустой чанк (transport.write_text_chunk(None)
) точно так же, как мы получаем пустой чанк в конце рефссылки.
// git reserves chunk lengths 65521 to 65535
const MAX_CHUNK_LENGTH: usize = 65520;
impl Transport {
fn write_text_chunk(&mut self, chunk: Option<&str>) -> io::Result<()> {
let chunk_length = match chunk {
// Includes the 4 hexadecimal digits at the start and the n at the end
Some(chunk) => CHUNK_LENGTH_DIGITS + chunk.len() + 1,
_ => 0,
};
if chunk_length >= MAX_CHUNK_LENGTH {
return Err(make_error("Chunk is too large"))
}
write!(self.ssh_input, "{:04x}", chunk_length)?;
if let Some(chunk) = chunk {
write!(self.ssh_input, "{}n", chunk)?;
}
Ok(())
}
}
Чтобы запросить хэш, мы отправляем текстовый чанк, начинающийся с want
. Первое want, как и первый чанк ref, может также включать возможности, которые запрашивает клиент.
impl Transport {
fn send_wants(&mut self, hashes: &[Hash], capabilities: &[&str])
-> io::Result<()>
{
let mut first_want = true;
for hash in hashes {
println!("Requesting {}", hash);
let mut chunk = format!("want {}", hash);
if first_want {
// Only the first want should list capabilities
for capability in capabilities {
chunk.push(' ');
chunk += capability;
}
}
self.write_text_chunk(Some(&chunk))?;
first_want = false;
}
self.write_text_chunk(None)
}
}
Собрав все это вместе, мы можем сообщить серверу, какие ссылки отправлять. Мы получим все ветви (т.е. ссылки, начинающиеся с refs/heads/
).
const BRANCH_REF_PREFIX: &str = "refs/heads/";
const REQUESTED_CAPABILITIES: &[&str] = &["ofs-delta"];
impl Transport {
fn fetch(&mut self) -> io::Result<()> {
let Refs { capabilities, refs } = self.receive_refs()?;
// Request all the capabilities that we want and the server supports
let use_capabilities: Vec<_> = REQUESTED_CAPABILITIES.iter()
.copied()
.filter(|&capability| capabilities.contains(capability))
.collect();
// Request all refs corresponding to branches
// (not tags, pull requests, etc.)
let branch_refs: Vec<_> = refs.iter()
.filter_map(|(ref_name, &hash)| {
ref_name.strip_prefix(BRANCH_REF_PREFIX).map(|branch| (branch, hash))
})
.collect();
let wants: Vec<_> = branch_refs.iter().map(|&(_, hash)| hash).collect();
self.send_wants(&wants, &use_capabilities)?;
// TODO: there's another negotiation with the server about which objects
// the client already has, but for now we'll pretend it has none.
// We'll implement this later (see "Haves").
self.write_text_chunk(Some("done"))?;
self.read_text_chunk()?;
// TODO: receive the objects the server sends back
Ok(())
}
}
fn main() -> io::Result<()> {
// ...
transport.fetch()
}
Запуск этой программы показывает, что было запрошено 6 ветвей (main
, maint
, master
, next
, seen
, и todo
). Поскольку main
и master
взаимозаменяемы, один из коммитов запрашивается дважды (это излишне, но допускается).
Requesting b1b3e2657f1904c7c603ea4313382a24af0fd91f
Requesting 583a5781c12c1d6d557fae77552f6cee5b966f8d
Requesting 74cc1aa55f30ed76424a0e7226ab519aa6265061
Requesting 74cc1aa55f30ed76424a0e7226ab519aa6265061
Requesting d65ed663a79d75fb636a4602eca466dbd258082e
Requesting 4c53a8c20f8984adb226293a3ffd7b88c3f4ac1a
Триумфальное возвращение пакетов
Как только сервер узнает, какие объекты нужны клиенту, он должен отправить их. Потенциально это тысячи коммитов, деревьев и блобов, поэтому важно кодировать их компактно. Если вы читали предыдущий пост (часть 2), вы увидите, что это основной случай использования пакетных файлов.
Поэтому сервер создает packfile, содержащий все объекты, и отправляет его клиенту через SSH-соединение. git может распаковать объекты из этого packfile, но, как мы видели в предыдущем посте, по умолчанию он оставляет их упакованными, чтобы сэкономить место для хранения.
Мы сделаем то же самое, создав файл temp.pack
в каталоге packfile. Так как содержимое packfile отправляется на выход SSH, мы можем просто скопировать вывод в файл:
const TEMP_PACK_FILE: &str = ".git/objects/pack/temp.pack";
impl Transport {
fn fetch(&mut self) -> io::Result<()> {
// ...
let mut pack_file = File::create(TEMP_PACK_FILE)?;
io::copy(&mut self.ssh_output, &mut pack_file)?;
Ok(())
}
}
fn main() -> io::Result<()> {
// ...
transport.fetch()
}
Запуск этой программы успешно загружает файл pack!
$ mkdir git
$ cd git
$ git init # create an empty git repository to test fetching all the objects
Initialized empty Git repository
$ git remote add origin git@github.com:git/git.git
$ cargo run
$ file .git/objects/pack/temp.pack
.git/objects/pack/temp.pack: Git pack, version 2, 324311 objects
Сохранение ссылок
Теперь у нас есть все необходимые объекты, но, к сожалению, попытка использовать их в команде git по-прежнему не работает:
$ git log origin/main
fatal: ambiguous argument 'origin/main': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
Это происходит потому, что мы не обновили «удаленные рефссылки», которые получили от сервера. Например, сервер сообщил нам, что main
в настоящее время находится на коммите 74cc1aa55f30ed76424a0e7226ab519aa6265061
:
74cc1aa55f30ed76424a0e7226ab519aa6265061 refs/heads/main
Поэтому нам нужно сохранить эту удаленную ссылку в локальном репозитории.
В первом посте мы видели, что локальные ветки (и рефссылки в целом) хранятся в каталоге .git/refs
. Каждая удаленная ветка (например, origin
) имеет свой собственный подкаталог в .git/refs
со всеми рефссылками, полученными с этой ветки.
Вот код для создания файлов ссылок во время выборки:
use std::fs;
use std::path::Path;
const REMOTE_ORIGIN_REFS_DIRECTORY: &str = ".git/refs/remotes/origin";
fn save_remote_ref(branch: &str, hash: Hash) -> io::Result<()> {
let origin_ref_path = Path::new(REMOTE_ORIGIN_REFS_DIRECTORY).join(branch);
// Create .git/refs/remotes and .../origin if they don't exist.
// Also, if the branch includes '/' (e.g. "feature/abc"), the path will be
// .../feature/abc, so the "feature" directory must also be created.
fs::create_dir_all(origin_ref_path.parent().unwrap())?;
let mut origin_ref_file = File::create(origin_ref_path)?;
write!(origin_ref_file, "{}n", hash)
}
impl Transport {
fn fetch(&mut self) -> io::Result<()> {
// ...
for (branch, hash) in branch_refs {
save_remote_ref(branch, hash)?;
}
Ok(())
}
}
Теперь запуск нашей программы записывает удаленные рефссылки:
$ ls -R .git/refs/remotes
origin
.git/refs/remotes/origin:
main maint master next seen todo
Попробуем снова запустить git log
. На этот раз он завершается с другой ошибкой: он знает, что origin/main
— это коммит 74cc1aa55f30ed76424a0e7226ab519aa6265061
, но не может прочитать этот объект.
$ git log origin/main
fatal: bad object origin/main
git не может найти объект, потому что отсутствует файл .idx
для файла temp.pack
, который мы создали. Мы исправим это дальше.
Создание индексного файла
Как мы видели в предыдущем сообщении, сканирование packfile происходит медленно, поэтому git зависит от соответствующего файла «pack index» для поиска объектов в packfile. Индексный файл действует как HashMap<Hash, u64>
, позволяя быстро найти, где находится объект в соответствующем packfile. Он может быть сгенерирован из пакетного файла путем распаковки (и распаковки, если необходимо) каждого объекта в пакете и вычисления его хэша. Сервер не отправляет его, поскольку он не содержит никакой дополнительной информации, поэтому мы должны создать его сами.
Для чтения объектов из пакетов мы будем использовать код, который мы написали в прошлый раз, с одним основным изменением. Раньше мы хотели распаковать только один объект, поэтому если объект был HashDelta
или OffsetDelta
, нам приходилось распаковывать его базовый объект, базовый объект его базового объекта и т.д., пока мы не находили нераспакованный объект. Если мы используем этот подход для всех объектов в packfile, мы можем пересчитать каждый базовый объект много раз. Например, если оба объекта B
и C
делетированы с базовым объектом A
, то распаковка объектов приведет к распаковке A
3 раза (при вычислении каждого из A
, B
и C
). А для HashDelta
, которые ссылаются на базовые объекты внутри packfile, мы даже не можем найти базовый объект по хэшу, потому что мы еще не создали индекс пакета! Поэтому я изменил код, чтобы запоминать объекты, распакованные из packfile на данный момент, как по хэшу, так и по смещению. Все подробности см. в исходном коде.
Сначала мы прочитаем временный пакетный файл (подробное обсуждение формата пакетного файла см. в последнем сообщении):
// Creates a temporary pack index for the temporary packfile
// and returns the packfile's checksum
fn build_pack_index() -> io::Result<Hash> {
let mut pack_file = File::open(TEMP_PACK_FILE)?;
let magic = read_bytes(&mut pack_file)?;
if magic != *b"PACK" {
return Err(make_error("Invalid packfile"))
}
let version = read_u32(&mut pack_file)?;
if version != 2 {
return Err(make_error("Unexpected packfile version"))
}
let total_objects = read_u32(&mut pack_file)?;
// Cache the unpacked objects by offset and hash
let mut object_cache = PackObjectCache::default();
// Count how many objects have a hash starting with each byte
let mut first_byte_objects = [0u32; 1 << u8::BITS];
// Store where each hash is located in the packfile
// (the sorted version of this is the index)
let mut object_offsets = Vec::with_capacity(total_objects as usize);
// Unpack each object
for _ in 0..total_objects {
let offset = get_offset(&mut pack_file)?;
let object = read_pack_object(&mut pack_file, offset, &mut object_cache)?;
let object_hash = object.hash();
first_byte_objects[object_hash.0[0] as usize] += 1;
let offset = u32::try_from(offset).map_err(|_| {
make_error("Packfile is too large")
})?;
object_offsets.push((object_hash, offset));
}
let pack_checksum = read_hash(&mut pack_file)?;
assert!(at_end_of_stream(&mut pack_file)?);
// TODO: produce index file
Ok(pack_checksum)
}
Хотя в прошлой заметке обсуждались индексные файлы версии 2, для простоты мы сделаем индексный файл версии 1. git все равно понимает их; единственное ограничение — они могут представлять только смещения, укладывающиеся в u32
(отсюда и проверка выше). Вот реализация:
const TEMP_INDEX_FILE: &str = ".git/objects/pack/idx.pack";
fn build_pack_index() -> io::Result<Hash> {
// ...
let mut index_file = File::create(TEMP_INDEX_FILE)?;
let mut cumulative_objects = 0;
for objects in first_byte_objects {
cumulative_objects += objects;
// The number (u32) of hashes with first byte <= 0, 1, ..., 255
index_file.write_all(&cumulative_objects.to_be_bytes())?;
}
// Each hash and its offset (u32) in the pack file,
// sorted for efficient lookup
object_offsets.sort();
for (hash, offset) in object_offsets {
index_file.write_all(&offset.to_be_bytes())?;
index_file.write_all(&hash.0)?;
}
// A SHA-1 checksum of the pack file
index_file.write_all(&pack_checksum.0)?;
// TODO: this should be a SHA-1 hash of the contents of the index file.
// But git doesn't check it when reading the index file, so we'll skip it.
index_file.write_all(&[0; HASH_BYTES])?;
Ok(pack_checksum)
}
И, наконец, мы переименовываем временные файлы pack и index с контрольной суммой пакета, как это делает git:
impl Transport {
fn fetch(&mut self) -> io::Result<()> {
// ...
let pack_hash = build_pack_index()?;
// Rename the packfile to, e.g.
// pack-bda11b853cfa9131a39b2e3e55f15bb7f7485450.pack
let pack_file_name = Path::new(PACKS_DIRECTORY)
.join(format!("pack-{}{}", pack_hash, PACK_FILE_SUFFIX));
fs::rename(TEMP_PACK_FILE, pack_file_name)?;
// Rename the index file to, e.g.
// pack-bda11b853cfa9131a39b2e3e55f15bb7f7485450.idx
let index_file_name = Path::new(PACKS_DIRECTORY)
.join(format!("pack-{}{}", pack_hash, INDEX_FILE_SUFFIX));
fs::rename(TEMP_INDEX_FILE, index_file_name)?;
// ...
}
}
Если мы сделаем еще одну выборку, мы создадим индексный файл, и наш git log
наконец-то заработает! Если вы пробуете это дома, убедитесь, что работаете в режиме релиза, иначе все будет слишком медленно! (Код можно значительно ускорить, если использовать BufReader
s и BufWriter
s с этими файлами и выходом SSH).
$ ls -lh .git/objects/pack
total 394896
-rw-r--r-- 1 csander staff 7.4M Mar 19 15:56 pack-bda11b853cfa9131a39b2e3e55f15bb7f7485450.idx
-rw-r--r-- 1 csander staff 185M Mar 19 15:56 pack-bda11b853cfa9131a39b2e3e55f15bb7f7485450.pack
$ git log origin/main
commit 74cc1aa55f30ed76424a0e7226ab519aa6265061 (origin/master, origin/main)
Author: Junio C Hamano <gitster@pobox.com>
Date: Wed Mar 16 17:45:59 2022 -0700
The twelfth batch
Signed-off-by: Junio C Hamano <gitster@pobox.com>
...
Мы даже можем использовать git show
, чтобы показать diff этого коммита, что требует чтения объектов commit, tree и blob из packfile:
$ git show origin/main
commit 74cc1aa55f30ed76424a0e7226ab519aa6265061
Author: Junio C Hamano <gitster@pobox.com>
Date: Wed Mar 16 17:45:59 2022 -0700
The twelfth batch
Signed-off-by: Junio C Hamano <gitster@pobox.com>
diff --git a/Documentation/RelNotes/2.36.0.txt b/Documentation/RelNotes/2.36.0.txt
index 6b2c6bfcc7..d67727baa1 100644
--- a/Documentation/RelNotes/2.36.0.txt
+++ b/Documentation/RelNotes/2.36.0.txt
@@ -70,6 +70,10 @@ UI, Workflows & Features
* The level of verbose output from the ort backend during inner merge
has been aligned to that of the recursive backend.
+ * "git remote rename A B", depending on the number of remote-tracking
+ refs involved, takes long time renaming them. The command has been
+ taught to show progress bar while making the user wait.
+
Performance, Internal Implementation, Development Support etc.
@@ -122,6 +126,12 @@ Performance, Internal Implementation, Development Support etc.
* Makefile refactoring with a bit of suffixes rule stripping to
optimize the runtime overhead.
+ * "git stash drop" is reimplemented as an internal call to
+ reflog_delete() function, instead of invoking "git reflog delete"
+ via run_command() API.
+
+ * Count string_list items in size_t, not "unsigned int".
+
Fixes since v2.35
-----------------
@@ -299,6 +309,17 @@ Fixes since v2.35
Adjustments have been made to accommodate these changes.
(merge b0b70d54c4 fs/gpgsm-update later to maint).
+ * The untracked cache newly computed weren't written back to the
+ on-disk index file when there is no other change to the index,
+ which has been corrected.
+
+ * "git config -h" did not describe the "--type" option correctly.
+ (merge 5445124fad mf/fix-type-in-config-h later to maint).
+
+ * The way generation number v2 in the commit-graph files are
+ (not) handled has been corrected.
+ (merge 6dbf4b8172 ds/commit-graph-gen-v2-fixes later to maint).
+
* Other code cleanup, docfix, build fix, etc.
(merge cfc5cf428b jc/find-header later to maint).
(merge 40e7cfdd46 jh/p4-fix-use-of-process-error-exception later to maint).
Имеет
Отлично, мы можем клонировать настоящий репозиторий!
Теперь представим, что в репозитории произошло небольшое изменение (например, один коммит был перенесён в main
). Если мы снова выполним Transport::fetch()
, мы загрузим новый packfile со всеми объектами, которые сейчас находятся в удаленном репозитории. Это сработает, но, к сожалению, в итоге мы получим две копии каждого объекта, который уже был в хранилище!
Мы определенно хотели бы избежать траты места на хранение дубликатов объектов. Мы можем сделать это, определив дублирующие объекты и создав новый пакетный файл без них. Но в идеале сервер не должен был бы отправлять их в первую очередь, поскольку это делает выборку неоправданно медленной.
Чтобы сервер точно знал, какие объекты нужны пакетному файлу, клиент должен сообщить серверу, какие объекты у него уже есть. После того, как в транспортном протоколе отправлены фрагменты want
, клиент сообщает серверу о том, какие объекты у него уже есть, отправляя фрагменты have
. Куски «haves» завершаются чанком "done"
. Сервер отвечает чанком ACK
, если он распознает любой из have
клиента, или чанком NAK
в противном случае. (См. документацию multi_ack
для более сложного согласования, которое git использует на практике).
Клиент может сообщить серверу каждый объект, который у него есть, но их может быть сотни тысяч, и это займет много места даже при 20 байтах на каждый. git использует тот факт, что когда клиент получает объекты от сервера, он всегда получает именно те, на которые ссылается один или несколько коммитов. Например, предположим, что есть коммиты C1
, C2
и C3
с деревьями T1
, T2
и T3
, соответственно, и несколько блобов:
C1 <-- C2 <-- C3
| | |
v v v
T1 T2 T3
| / /|
v v v v v
B1 B2 B3 B4 B5
Когда клиент раньше посылал want C2
, он получал C1
, C2
, T1
, T2
, B1
, B2
, и B3
, потому что на них C2
(косвенно) ссылается. Поэтому если клиент говорит серверу, что у него есть C2
, сервер знает, что у него есть все эти объекты, но не C3
, T3
, B4
или B5
.
Таким образом, клиент может просто сказать последний полученный им коммит на каждой удалённой ветке, и сервер будет точно знать, какие из его объектов уже есть у клиента. (Реализация git также проверяет наличие коммитов от клиента, которые находятся на удалённой ветке без ведома клиента. Например, клиент сделал коммит на удаленном A, а затем кто-то другой взял и сделал коммит на удаленном B. Но мы не будем беспокоиться об оптимизации для такой ситуации).
Мы отправим have
для хэша коммита, который мы записали для каждой удаленной ветки:
use std::path::PathBuf;
impl Transport {
// Sends haves for all refs under the given ref directory
fn send_haves_dir(&mut self, ref_path: &mut PathBuf) -> io::Result<()> {
let entries = fs::read_dir(&ref_path);
if let Err(err) = &entries {
if err.kind() == ErrorKind::NotFound {
// If .git/refs/remotes/origin doesn't exist, there are no haves
return Ok(())
}
}
for entry in entries? {
let entry = entry?;
ref_path.push(entry.file_name());
let entry_type = entry.file_type()?;
if entry_type.is_dir() {
// Explore subdirectories recursively (to find refs containing '/')
self.send_haves_dir(ref_path)?;
}
else {
let hash = fs::read_to_string(&ref_path)?;
let hash = Hash::from_str(hash.trim_end())?;
self.write_text_chunk(Some(&format!("have {}", hash)))?;
}
ref_path.pop();
}
Ok(())
}
fn send_haves(&mut self) -> io::Result<()> {
fn valid_have_response(response: Option<&str>) -> bool {
// Expect "ACK {HASH}" if acknowledged, "NAK" otherwise
match response {
Some("NAK") => true,
Some(response) => {
match response.strip_prefix("ACK ") {
Some(hash) => Hash::from_str(hash).is_ok(),
_ => false,
}
}
_ => false,
}
}
// Send haves for all the most recent commits we have fetched
self.send_haves_dir(&mut PathBuf::from(REMOTE_ORIGIN_REFS_DIRECTORY))?;
self.write_text_chunk(Some("done"))?;
let response = self.read_text_chunk()?;
if !valid_have_response(response.as_deref()) {
return Err(make_error("Invalid ACK/NAK"))
}
Ok(())
}
fn fetch(&mut self) -> io::Result<()> {
// ...
self.send_wants(&wants, &use_capabilities)?;
self.send_haves()?;
// ...
}
}
Если мы снова получим git-репозиторий без новых коммитов, сервер отправит пустой packfile, потому что у клиента уже есть все необходимые объекты:
$ cargo run
$ ls -lh .git/objects/pack
total 410624
-rw-r--r-- 1 csander staff 1.0K Mar 19 17:29 pack-029d08823bd8a8eab510ad6ac75c823cfd3ed31e.idx
-rw-r--r-- 1 csander staff 32B Mar 19 17:29 pack-029d08823bd8a8eab510ad6ac75c823cfd3ed31e.pack
-rw-r--r-- 1 csander staff 7.4M Mar 19 16:38 pack-8641e8298f69b5dc78c3eb224dc508757f59a13f.idx
-rw-r--r-- 1 csander staff 185M Mar 19 16:37 pack-8641e8298f69b5dc78c3eb224dc508757f59a13f.pack
$ file .git/objects/pack/pack-029d08823bd8a8eab510ad6ac75c823cfd3ed31e.pack
.git/objects/pack/pack-029d08823bd8a8eab510ad6ac75c823cfd3ed31e.pack: Git pack, version 2, 0 objects
Обновления прогресса в боковой полосе
Серверу может потребоваться некоторое время для подготовки и передачи пакетного файла, поэтому полезно предоставлять пользователю обновления о ходе работы. Протокол, который мы видели до сих пор, не позволяет этого, но есть еще одна возможность, side-band-64k
, чтобы сделать это.
Вместо того чтобы отправлять пакетный файл непосредственно по SSH-соединению, сервер разбивает его на части и отправляет каждую часть в чанке. Между чанками packfile сервер может отправлять чанки с сообщениями о ходе выполнения или ошибках. Первый байт каждого чанка указывает на тип чанка (1 — данные пакетного файла, 2 — сообщение о ходе выполнения или 3 — сообщение о фатальной ошибке). Остальная часть чанка — это либо следующий фрагмент пакетного файла, либо сообщение для печати. Пустой чанк посылается для завершения чанков боковой полосы.
Вот реализация:
const SIDE_BAND_CAPABILITY: &str = "side-band-64k";
const REQUESTED_CAPABILITIES: &[&str] = &["ofs-delta", SIDE_BAND_CAPABILITY];
impl Transport {
fn receive_side_band_pack(&mut self, pack_file: &mut File) -> io::Result<()> {
while let Some(chunk) = self.read_chunk()? {
let (&chunk_type, chunk) = chunk.split_first().ok_or_else(|| {
make_error("Missing side-band chunk type")
})?;
match chunk_type {
// Packfile data
1 => pack_file.write_all(chunk)?,
// Progress message; print to stderr
2 => io::stderr().write_all(chunk)?,
// Fatal fetch error message
3 => {
let err = format!("Fetch error: {}", String::from_utf8_lossy(chunk));
return Err(make_error(&err))
}
_ => {
let err = format!("Invalid side-band chunk type {}", chunk_type);
return Err(make_error(&err))
}
}
}
Ok(())
}
fn fetch(&mut self) -> io::Result<()> {
// ...
let mut pack_file = File::create(TEMP_PACK_FILE)?;
// Check whether we were able to enable side-band-64k
if capabilities.contains(SIDE_BAND_CAPABILITY) {
// The packfile is wrapped in side-band chunks
self.receive_side_band_pack(&mut pack_file)?;
}
else {
// The SSH stream has the packfile contents
io::copy(&mut self.ssh_output, &mut pack_file)?;
}
// ...
}
}
Если теперь мы вызовем Transport::fetch()
, мы увидим индикаторы прогресса сервера:
Enumerating objects: 324311, done.
Total 324311 (delta 0), reused 0 (delta 0), pack-reused 324311
Здесь сервер уже создал пакетный файл с необходимыми объектами и просто отправляет его нам. Если git-серверу потребуется создать новый пакетный файл, мы увидим дополнительные индикаторы состояния, например:
Enumerating objects: 7605, done.
Counting objects: 100% (630/630), done.
Compressing objects: 100% (292/292), done.
Total 7605 (delta 421), reused 448 (delta 333), pack-reused 6975
На этапе «Подсчет объектов» git определяет, каких объектов еще нет в packfile (630 = 7605 — 6975). На этапе «Сжатие объектов» git создает дельтифицированные представления для некоторых из этих новых объектов.
При длительных загрузках вы могли заметить, что эти индикаторы прогресса периодически обновляются. Если вам интересно, как это работает, то это происходит путем печати символа r
(возврат каретки), за которым следует новое содержимое строки. r
сбрасывает место печати терминала на начало текущей строки, но, в отличие от n
, не переходит на следующую строку.
протокол push
Мы рассмотрели все основные части git fetch
через транспорт SSH. Но как работает git push
? Возможно, неудивительно, что git повторно использует большую часть протокола SSH для push. Это настолько одно и то же, что я не думаю, что есть чему поучиться, реализуя git push
тоже.
Основные различия между fetch
и push
следующие:
- Команда SSH вызывает
git-receive-pack
вместоgit-upload-pack
. - Не требуется согласование
have
, поскольку клиент уже знает, какие из его коммитов есть у сервера (поскольку он их выложил). - Получив от сервера список ссылок, клиент указывает, какие из них он хочет создать (например, новую ветку), обновить (например, новый коммит на ветке) или удалить (например, удалить ветку).
- Клиент отправляет пакетный файл с новыми объектами на сервер.
Конец
На этом мы завершаем серию статей о внутреннем устройстве git! Мы узнали, как работает большая часть git под капотом, от каталога .git
до того, как история репозитория хранится в объектах, от того, как пакфайлы объединяют и сжимают объекты до того, как git-клиент и сервер обмениваются данными для совместного использования репозитория. Надеюсь, в следующий раз, когда вы запустите команду git, вы сможете по-новому понять и оценить, как она выполняет свою задачу.
Извините, что этот пост получился таким длинным, просто в головоломке git очень много интересных деталей! Пожалуйста, дайте мне знать, если есть другие темы по git, которые вы хотели бы осветить. У меня есть несколько других (надеюсь, более коротких!) постов, которые я хотел бы написать на различные темы, так что следите за новостями.