Назад к блогу

Git 2.56 и подготовка к 3.0: что меняется в работе с историей, ссылками и хешами

Git 2.56 и подготовка к 3.0: что меняется в работе с историей, ссылками и хешами

В ближайшем релизе Git 2.56 появятся прикладные улучшения — от новой подкоманды для правки истории до низкоуровневых операций над ссылками и более удобного удаления влитых веток. Но куда важнее, что проект正式 озвучил план перехода к Git 3.0 с рядом ломающих изменений: сменой хеш-функции по умолчанию, новым форматом хранения ссылок и обязательным Rust для сборки. Разбираем, что уже можно попробовать и почему главный блокер перехода на SHA-256 — это не сам Git, а отставание GitHub.

В Git 2.56 войдёт набор прикладных улучшений: подкоманда drop для экспериментального инструментария git history, низкоуровневые операции над ссылками в git refs, опция удаления влитых веток и подсказка о git pull в git status. Одновременно объявлен план выпусков, ведущий к Git 3.0: декабрьский релиз 2026 года получит номер 2.98 как сигнал о приближении перехода, в апреле 2027 года выйдут 2.99 с долгосрочной поддержкой и 3.0 одновременно с ним, и 3.0 станет базой для всех последующих релизов.

Что именно меняется в 2.56

В экспериментальный инструментарий git history добавлена подкоманда drop. Она принимает идентификатор коммита и удаляет его из истории текущей ветки, переигрывая все коммиты, добавленные после него. Это более простой способ избавиться от нежелательного коммита, но команда отказывается работать, если в истории есть коммиты слияния, — из-за этого она неприменима во многих репозиториях.

git refs получает подкоманды create, delete, update и rename для низкоуровневой работы со ссылками: в отличие от git branch, которая управляет ветками как привычными пользователю объектами, эти подкоманды работают со ссылками напрямую и позволяют создавать, удалять, изменять и переименовывать их без привязки к веткам. git branch обзаводится опцией --delete-merged, которая удаляет локальные ветки, влитые в их удалённую отслеживаемую ветку; влитость определяется тем, что все коммиты локальной ветки уже присутствуют в соответствующей удалённой отслеживаемой ветке. Попытка удалить ветку через git branch -d теперь завершается неудачей с полезным сообщением, если эта ветка используется для bisection. git status для ветки, отстающей от отслеживаемой, предлагает выполнить git pull. Блокировка файла конфигурации повторяется при неудаче — это снимает раздражение, когда несколько команд пытаются изменить файл одновременно. У git add появляется опция --resolved, добавляющая только файлы с разрешёнными конфликтами слияния.

Что заявлено для 3.0

Git 3.0 будет содержать ряд нарушений совместимости. Наиболее значительное — переход по умолчанию на хеш-функцию SHA-256 вместо SHA-1, применявшейся с самого начала. хеш используется для идентификации каждого объекта — файлов, деревьев каталогов, коммитов — и для проверки цепочки коммитов, поэтому смена хеша затрагивает совместимость репозиториев. SHA-1 давно считается слабым.

Второе изменение — приём идентификаторов объектов только в нижнем регистре. Сейчас два внешне разных идентификатора, например f00f00 и F00F00, оказываются одним и тем же, и эта неоднозначность порождала ошибки и уязвимости.

Третье — переход на механизм хранения ссылок reftable вместо хранения каждой ссылки отдельным файлом под .git/refs/. Это важно при большом числе ссылок: в репозитории Android их более 800 000.

Четвёртое — Rust станет обязательным для сборки Git. После 3.0 любая платформа без работающего компилятора Rust не сможет обновиться.

Почему переход на SHA-256 затянулся

Git перешёл на усиленную реализацию SHA-1 по умолчанию в версии 2.13.0, но SHA-1 всё ещё слаб. Среди кандидатов на замену рассматривались SHA-256, SHA-512/256, SHA-256x16, K12 и BLAKE2bp-256; в конце 2018 года проект выбрал SHA-256. Переход шёл пошагово: сначала появился API хеш-функции, затем — расширение objectFormat, ещё без указания совместимого формата; после этого добавили compatObjectFormat, и лишь затем — поддержку fetch и push при обмене с репозиториями на SHA-1. Неэкспериментальная поддержка SHA-256 появилась в Git 2.42 в 2023 году, но часть связующего кода для взаимодействия со старыми репозиториями заняла больше времени.

Главной задержкой была поддержка на крупных хостингах: GitLab поддерживает SHA-256 с 2024 года, Forgejo тоже, а GitHub всё ещё отсутствует. Выпускать версию Git, создающую несовместимые с GitHub репозитории, — тревожная перспектива. libgit2 блокером не станет: Patrick Steinhardt добавил в неё поддержку reftable, а поддержка SHA-256 была включена по умолчанию в августе.

Как устроен reftable

Файл reftable состоит из секций: сначала идёт first_block с заголовком и первым блоком ссылок, затем последовательности блоков ссылок, индексов, блоков объектов и логов, в конце — footer. Блоки ссылок хранят сами ссылки, индексы позволяют быстро находить нужный блок, блоки объектов содержат объекты, а логи — записи о переходах ссылок. Каждый блок ссылок начинается с байта 'r', за которым следует длина блока, затем записи ссылок, таблица смещений restart-точек, счётчик restart-точек и, возможно, padding.

Префиксное сжатие работает так: в записи ссылки поле prefix_length задаёт, сколько ведущих байт имени предыдущей записи нужно скопировать, чтобы получить имя текущей. Это значение обязано быть нулевым для первой записи в блоке и для любой записи, чей offset перечислен в таблице restart-смещений. Для restart-точек prefix_length принудительно равен нулю — именно поэтому с них можно начинать восстановление имени. В одном блоке поддерживается не более 65535 restart-точек. Для репозиториев с множеством ссылок рекомендуются большие размеры блоков (64 KiB) и менее частые restart-точки (каждые 64) — это даёт лучшее сжатие за счёт большего числа ссылок, сжимаемых относительно предыдущей.

Версия 2 формата добавляет в заголовок 4-байтовый идентификатор хеша: "sha1" для SHA-1 и "s256" для SHA-256. Для обратной совместимости при записи SHA-1-таблиц рекомендуется версия 1.

Как включается reftable

Чтобы включить reftable в репозитории, в конфигурации задаётся версия формата репозитория и расширение хранения ссылок:

[core]
    repositoryformatversion = 1
[extensions]
    refStorage = reftable

Сочетание core.repositoryFormatVersion=1 и заполнения extensions.* гарантирует, что все версии Git новее v0.99.9l завершатся с ошибкой вместо попытки работать с репозиторием, выдав сообщение об ошибке.

Для обратной совместимости со старыми клиентами, чтобы они продолжали распознавать каталог как git-репозиторий, в нём должны присутствовать фиктивные файлы: .git/HEAD (обычный файл с содержимым ref: refs/heads/.invalid), каталог .git/refs/ и обычный файл .git/refs/heads.

Сами данные ссылок хранятся в каталоге $GIT_DIR/reftable/ в виде набора reftable-файлов, а порядок их применения задаётся файлом $GIT_DIR/reftable/tables.list, где файлы перечислены от самого старого (базового) к самому новому. Читатели должны прочитать tables.list и искать по стеку в обратном порядке — последний reftable проверяется первым. Для получения согласованного снимка нужно открыть все упомянутые файлы и, если какой-то отсутствует, начать заново.

Запись изменений устроена так: хотя reftable-файлы неизменяемы, мутации выполняются путём создания нового reftable и атомарного добавления его в стек через блокировку tables.list.lock. Из-за единственного файла блокировки репозиторий однопоточный для писателей, и они могут ожидать с backoff, прерываясь, если репозиторий слишком занят. Удаление ссылки хранится явно как tombstone: устанавливается type в 0x0 и опускается поле value в записи ссылки, что перекрывает любые утверждения о существовании ссылки из более ранних файлов стека.

Сосуществование SHA-1 и SHA-256

Расширение, задающее второй, совместимый формат хеша, позволяет репозиторию работать сразу с двумя формами имён объектов — например, objectFormat = sha256 вместе с compatObjectFormat = sha1. Для сосуществования двух форматов служит translation table — двунаправленное отображение между SHA-1-именами и SHA-256-именами объектов.

Поиск в ней идёт как обычный поиск объекта: сначала объект ищется в idx-файлах, затем читается соответствие порядка имён SHA-1 порядку имён в паке, проверяется полная таблица SHA-1-имён и берётся соответствующая запись из полной таблицы SHA-256-имён. Если объекта нет в idx, проверяется loose-объект: строки читаются из loose-object-idx до совпадения. Формат этого файла — строки вида «sha256-имя, пробел, sha1-имя, перевод строки», имена в шестнадцатеричном виде, файл не отсортирован. Все операции, создающие новые объекты (например, git commit), добавляют их в соответствующий индекс, поэтому такое отображение возможно для всех объектов в хранилище.

Замеры reftable

В документации приводятся замеры занимаемого места. Для packed-refs и reftable: android — 62.2 M против 36.1 M (58.0% от исходного), rails — 1.8 M против 1.1 M (57.7%), git — 78.7 K против 48.1 K (61.0%), git (heads) — 332 b против 269 b (81.0%). Средние размеры на ссылку и объект: для android 33 bytes и 5 bytes, для rails 29 bytes и 4 bytes, для git 50 bytes и 4 bytes, для git (heads) 33 bytes и 0 bytes. Для логов: 149 932 записи для 43 061 ссылок — $GIT_DIR/logs занимает 173 M (1209 bytes на запись), reftable — 5 M (37 bytes на запись).

По скорости на сканировании 866k ссылок, поиске по имени и по SHA-1: cold — reftable 112 ms, 33.9 usec, 323.2 usec; hot — reftable 20.2 usec, 320.8 usec. Три числа в каждой строке — это время сканирования всех ссылок, время поиска ссылки по имени и время поиска по SHA-1 соответственно. Отдельно отмечено, что bzip2 сжимает большой packed-refs (например, 62 MiB до 23 MiB, 37%), но не поддерживает произвольный доступ к отдельной ссылке. В сравнении с альтернативным форматом Michael Haggerty reftable быстрее для поиска (51% быстрее, 11.2 usec против 5.4 usec), но файл немного больше (+ ~3.2%, 28.3M против 29.2M).

Ограничения и открытые вопросы

Переход на SHA-256 по умолчанию сдерживается поддержкой на крупных хостингах: GitLab и Forgejo её имеют, а GitHub — нет, и когда GitHub добавит поддержку, остаётся неясным. Открытым остаётся и вопрос о смене поведения с идентификаторами: carlson хочет принимать только нижний регистр, но почти наверняка найдётся тот, кто полагается на старое поведение.

Переход на reftable тоже отложен до 3.0, и хотя libgit2 уже готова, carlson упоминал её как потенциальную проблему. Поддержка Rust пока пробная, но после 3.0 платформы без рабочего компилятора Rust не смогут обновляться. Решение о том, будет ли следующий выпуск 3.0, ещё не объявлено публично и ждёт решения Hamano.

Источники

Похожее