Git 2.55: новые возможности и оптимизации

Открытый проект Git выпустил версию Git 2.55 с новыми функциями, оптимизациями и экспериментальными возможностями. GitHub представил обзор ключевых изменений и их влияния на производительность, обслуживание репозиториев и повседневную работу разработчиков.

В релиз вошли улучшения в работе с многопакетными индексами (MIDX), новые режимы репаковки, экспериментальные команды для редактирования истории, параллельный запуск хуков, расширение файлового монитора на Linux, серьёзные ускорения генерации bitmap-структур, усовершенствования для partial clone, новый инструмент форматирования коммитов и ряд доработок интерфейса командной строки.

Инкрементальные MIDX и геометрическая репаковка

Git давно использует pack-файлы и индексы паков для хранения объектов репозитория. Для крупных репозиториев применяется multi-pack index (MIDX), который даёт единый индекс по нескольким pack-файлам. Ранее MIDX обычно хранился в виде одного файла, что делало чтение быстрым, но обновление в больших репозиториях требовало значительных перезаписей.

Формат инкрементального MIDX, представленный в Git 2.47, позволил разбивать индекс на цепочку слоёв. Каждый слой покрывает часть паков, а специальный файл-цепочка задаёт порядок слоёв. При добавлении нового слоя Git индексирует новые пак-файлы, не переписывая индекс, который охватывает весь репозиторий.

В Git 2.55 команда git repack научилась напрямую записывать такие инкрементальные цепочки MIDX. В режиме без дополнительных параметров Git просто дописывает новый слой для паков, созданных текущей repack-операцией, не изменяя существующие слои. Это уменьшает объём метаданных, которые приходится переписывать во время обслуживания.

Однако бесконечно увеличивать длину цепочки нельзя. Поэтому Git 2.55 позволяет совмещать –write-midx=incremental с геометрической репаковкой. В этом режиме каждая repack-запись добавляет новый верхний слой, а затем решает, нужно ли сжимать соседние слои в один. Поведение контролируется параметром repack.midxSplitFactor: если суммарный объём объектов в более новых слоях становится достаточно велик по сравнению с ближайшим старшим слоем, Git объединяет такие слои в один новый слой, иначе старые слои остаются без изменений.

При этом учитываются и новые пак-файлы, которые ещё не покрыты никаким MIDX-слоем. Они всегда рассматриваются как кандидаты для геометрической репаковки. Пак-файлы из более глубоких слоёв цепочки по умолчанию не трогаются. Пак-файлы из верхнего слоя попадают в набор кандидатов, только если их количество достигло порога repack.midxNewLayerThreshold. Если верхний слой меньше этого порога, Git ограничивается добавлением нового слоя для новых паков.

Алгоритм репаковки строит геометрическую прогрессию по размерам паков. Он шаг за шагом сдвигает точку разделения наборов паков вправо или влево, пока не будет найден участок, где размер пак-файла слева как минимум вдвое больше суммарного размера выбранного справа суффикса. Именно этот суффикс сворачивается в новый pack, а затем создаётся новый верхний MIDX-слой, который покрывает этот pack и уцелевший pack из прежнего верхнего слоя.

После оптимизации pack-файлов Git применяет похожую стратегию к слоям MIDX: если новый слой стал достаточно крупным по отношению к соседнему старшему слою, их метаданные объединяются в слой-замену. Эта операция затрагивает только индексы, без перепаковки самих паков. Более глубокие слои не меняются, что позволяет сохранять инкрементальный характер обслуживания.

В итоге достигается компромисс: один MIDX-файл минимизирует количество операций поиска, но требует крупных перезаписей при обслуживании, в то время как чисто добавочный инкрементальный MIDX снижает объём записей, но даёт бесконтрольный рост длины цепочки. Геометрическая инкрементальная репаковка удерживает число слоёв примерно логарифмическим от числа объектов и чаще переписывает новые и небольшие слои, чем старые и крупные.

Для репозиториев с постоянным притоком новых объектов это означает, что регулярное обслуживание может обновлять метаданные паков постепенно, без необходимости при каждом запуске переписывать MIDX, который охватывает всё хранилище объектов.

Новый экспериментальный fixup в git history

Разработчики, которые перед отправкой серии коммитов на ревью «полируют» историю, часто сталкиваются с ситуацией, когда новая правка логически относится к более раннему коммиту, а не к вершине ветки. Распространённая тактика — создать fixup-коммит и затем применить autosquash.

Git 2.55 расширяет экспериментальную команду git history, введённую в 2.54, и добавляет подкоманду fixup. Она берёт изменения, уже подготовленные в индексе, и «вклеивает» их в выбранный более ранний коммит, автоматически воспроизводя последующие коммиты поверх обновлённой версии.

В примере из документации первый коммит добавляет рецепт блинов, затем идут другие коммиты. Позже обнаруживается, что в рецепте нет кленового сиропа. После индексирования этой одной строки команда git history fixup <commit> вносит изменение прямо в исходный коммит с рецептом и переписывает дочерние коммиты, чтобы ветка завершалась эквивалентной историей, но с исправлением в нужном месте.

По умолчанию целевой коммит сохраняет своё сообщение и авторство; опция –reedit-message позволяет изменить сообщение. Как и вся команда git history, fixup остаётся экспериментальной и ведёт себя консервативно: она требует наличия рабочей копии, работает через индекс и прекращается при возникновении конфликта, не оставляя репозиторий в промежуточном состоянии переписывания истории.

Параллельное выполнение конфигурационных хуков

Git 2.54 добавил поддержку хуков, определяемых через конфигурацию, а не только как исполняемые файлы в каталоге $GIT_DIR/hooks. Такие конфигурационные хуки проще распространять, комбинировать и выборочно отключать без копирования скриптов в каждый репозиторий.

В Git 2.55 совместимые конфигурационные хуки могут выполняться параллельно. Например, если в проекте есть независимые pre-commit хуки для линтинга и юнит‑тестов, и оба объявляют hook.<name>.parallel = true, Git запускает их одновременно. Общее число параллельных задач задаётся глобально через hook.jobs, для конкретного события через hook.<event>.jobs, или из командной строки опцией git hook run -j. Хуки, которым нужен общий доступ к состоянию (например, commit-message или хуки, которые анализируют индекс или рабочую копию), продолжают выполняться последовательно.

Файловый монитор Git на Linux

Встроенный файловый монитор Git ускоряет команды вроде git status, когда включен параметр core.fsmonitor. Вместо обхода всего дерева файлов команда обращается к постоянно работающему демону, который сообщает, какие пути изменились.

До этого встроенный демон был доступен только на macOS и Windows. В Git 2.55 добавлена поддержка Linux с использованием inotify. Реализация не требует повышенных привилегий, но использует по одному наблюдателю на каждый каталог. Поэтому очень крупным репозиториям может потребоваться увеличить лимит fs.inotify.max_user_watches. Как и на других платформах, для репозиториев на сетевых файловых системах этот механизм включается только по явному решению пользователя.

Ускорение генерации reachability bitmap и pseudo-merge bitmap

Reachability bitmaps помогают Git отвечать на вопросы вроде «какие объекты достижимы из этого коммита?» без обхода всего графа объектов. Они ускоряют обходы истории, но сами bitmaps нужно строить и обновлять, например, при выполнении git repack –write-midx-bitmaps.

Git 2.55 делает этот процесс быстрее за счёт пропуска лишней рекурсии по деревьям, повторного использования уже вычисленных bitmap-выборок, кеширования позиций объектов и сортировки bitmap-структур перед операциями XOR. В одном из тестов для крупного репозитория время генерации bitmap сократилось примерно с 612 секунд до 294 секунд.

В том же наборе изменений улучшены pseudo-merge bitmaps, которые группируют связанные ссылки, чтобы Git мог комбинировать заранее подготовленные битовые массивы, вместо многократного обхода одних и тех же объектов. Ранее pseudo-merge bitmap почти вдвое увеличивали время генерации bitmap, хотя ускоряли обходы (например, git rev-list –objects –use-bitmap-index) почти в 20 раз. После доработок они сохраняют большую часть ускорения обходов и создают меньше дополнительной нагрузки при генерации.

Улучшения pack-objects и фильтрации для partial clone

В сценариях с partial clone, отфильтрованными pack-файлами и намеренным пропуском части объектов размер паков по‑прежнему важен. Режим git pack-objects –path-walk, появившийся в Git 2.51, группирует объекты по пути перед вторым проходом сжатия, что помогает получить лучшие дельты за счёт локальности путей.

В Git 2.55 режим –path-walk можно комбинировать с фильтрами вроде blob:none, blob:limit=<n>, tree:0, object:type=<type>, sparse:<oid> и совместимыми фильтрами combine:. Это делает упаковку с использованием –path-walk применимой в более широком наборе сценариев partial clone. В тесте на репозитории Git репаковка в режиме «без blob» и с path-walk дала примерно на 16% меньший pack за счёт более медленного расчёта свежих дельт.

Новая экспериментальная команда git format-rev

Git 2.55 добавил экспериментальную команду git format-rev для форматирования ревизий, поступающих через стандартный ввод. В отличие от git log, который обходит диапазон истории, git format-rev рассчитан на сценарии, где коммиты появляются по одному или встроены в другой текст.

Например, если команда git last-modified выводит коммит, который последним изменил каждый путь в каталоге, и нужно заменить его в выводе на имя автора, раньше приходилось запускать отдельный процесс Git для каждого коммита. Теперь это делает git format-rev как часть конвейера, что снижает накладные расходы.

Текстовый режим git format-rev также может переписывать полные имена объектов-коммитов, найденные в произвольном тексте, что полезно для commit-message хуков и других сценариев автоматической обработки сообщений.

Безопасная фильтрация управляющих последовательностей в sideband-выводе

При fetch или push Git использует несколько потоков поверх одного соединения: один несёт pack-файл, другой — прогресс, третий — ошибки. Сообщения о прогрессе обычно выводятся на stderr и приходят с удалённой стороны, что позволяло серверу включать произвольные управляющие последовательности терминала, включая перемещение курсора или очистку текста.

В Git 2.55 такие управляющие символы теперь по умолчанию маскируются, при этом ANSI‑последовательности для цвета продолжают работать. Это повышает безопасность и предсказуемость вывода, сохраняя читаемый цветной прогресс.

Автоstash для git checkout -m

Команда git checkout -m <branch> пытается перенести локальные изменения при переключении на другую ветку. Если целевая ветка изменила те же файлы, раньше Git выдавал конфликт и предлагал решить его сразу.

Git 2.55 делает этот сценарий безопаснее: при использовании git checkout -m Git автоматически создаёт stash-запись с конфликтующими локальными изменениями. Их можно сразу разрешить или восстановить позже, что снижает риск потери работы при сложных переключениях веток.

Группы remote для git push

Некоторым проектам нужно публиковать одну и ту же ветку на несколько удалённых хостов (основной сервер и зеркала). Для git fetch давно поддерживаются remote groups, где группа описывается конфигурацией remotes.<name> как список удалённых репозиториев.

В Git 2.55 такие группы теперь может использовать и git push. Это эквивалентно последовательной отправке на каждый remote в группе. При этом опция –atomic не поддерживается для групп, так как атомарность гарантируется только в пределах одного транспортного соединения.

Управление шириной графа в git log

Опция git log –graph удобна для визуализации структуры веток, но в репозиториях с большим числом параллельных веток ширина графа может занимать почти всю ширину терминала.

Git 2.55 добавил параметр –graph-lane-limit=<n> для git log –graph и родственных команд. Линии графа сверх заданного лимита заменяются символом ~. Это делает вывод более компактным и читаемым в репозиториях с очень широкой историей.

Новый способ выбрать самые старые коммиты — –max-count-oldest

Вывести десять последних коммитов просто: git log -n 10. Но выбор десяти самых старых коммитов из диапазона раньше был менее очевиден. Наивное git log –reverse -10 работает иначе: Git берёт десять последних коммитов и меняет их порядок на обратный.

До выхода Git 2.55 можно было использовать постобработку, например git log –reverse <range> | tail -10, но при этом Git по-прежнему печатал все коммиты, которые оболочка затем отбрасывала. Теперь добавлена опция –max-count-oldest=<n> для git rev-list и семейства git log, которая выбирает именно n самых старых коммитов в диапазоне.

Гибкий выбор ссылок для протокольной договорённости при fetch

Во время fetch клиент и сервер договариваются об общем префиксе истории: клиент сообщает серверу, какие коммиты у него уже есть, в виде строк have. Это позволяет серверу не отправлять объекты, которые клиент уже может достичь. В репозиториях с большим числом ссылок алгоритм может случайно пропустить ссылку, особенно полезную для нахождения общей истории.

Git 2.55 добавляет новые настройки, определяющие, какие ссылки участвуют в такой договорённости. Новые опции include и restrict, а также соответствующие параметры remote.*, позволяют требовать включения определённых ссылок в список have или ограничивать договорённость только определённым набором ссылок.

Другие изменения и автор обзора

Релиз Git 2.55 также включает множество мелких улучшений и исправлений, которые не вошли в краткий обзор. Полный список новшеств можно найти в официальных release notes для версии 2.55, а также в заметках к предыдущим релизам в репозитории Git.

Автор обзора: Тейлор Блау (Taylor Blau), Principal Software Engineer в GitHub, работающий над Git.

Источник: блог GitHub о выпуске Git 2.55.

Читать новости ИИ и технологий в Telegram.


Оцените статью
Gimal-Ai