Всем привет. На связи Михаил, технический лидер проекта Axelix.

Я уже довольно давно хотел написать небольшую статью о нашей модели ветвления git в надежде, что она может пригодиться и другим.

В мире существует немало стратегий ветвления git, например:

По своему опыту могу сказать, что команды обычно не берут какую-либо стратегию в точности в её исходной форме, а либо изобретают нечто совершенно новое сами, либо просто берут, к примеру, уже упомянутый GitFlow и адаптируют его под свои нужды.

Так вот, за уже целый год разработки Axelix мы пришли к слегка модифицированной модели Trunk Based Development, и, как мне кажется, она хорошо нам послужила. Надеюсь, она окажется полезной и для вас. У этой модели, безусловно, есть свои недостатки, но в целом мы ею довольны.

Прежде чем мы начнём, важно понимать, каким требованиям мы пытались соответствовать. В конце концов, каждый должен решать сам, отталкиваясь от своих проблем.

Небольшая вводная: версионирование Axelix

Две самые сложные проблемы в разработке ПО - это именование и версионирование.

Это не точная цитата, но примерно так звучала шутка Томаса Вёртингера (руководителя проекта GraalVM) на BoF на Devoxx Belgium 2025, когда он рассказывал о той самой статье “Detaching GraalVM from the Java Ecosystem Train”.

Чтобы дальше было понятно, почему мы делаем то, что делаем, полезно разобраться в используемой нами схеме версионирования.

В Axelix мы используем семантическое версионирование (SemVer) компонентов. Я бы даже сказал, что SemVer - это де-факто стандарт версионирования в современном мире. Конечно, существуют и другие схемы версионирования, как у Windows, где есть Windows 7, 8, 10 или 11. Есть также ПО, которое выпускается с использованием различных вариантов CalVer. Хорошим примером будут IDE от OpenIDE: например, 2026.1 или 2026.2.

И всё же большинство проектов используют SemVer, и Axelix не исключение. Но когда речь идёт о современном ПО - оно часто состоит из множества компонентов. Например, в Axelix есть:

  • Плагины сборки для Maven/Gradle

  • Стартеры Spring Boot

  • Master в виде отдельного исполняемого JAR

  • Docker-образ Master

  • Helm Chart для Master

Так вот, каждый из перечисленных выше пунктов представляет собой независимый артефакт, который распространяется независимо от остальных, а значит, имеет собственные “координаты”.

В нашем случае мы решили придерживаться “синхронного” (в англ. литературе часто говорят “Lockstep”) версионирования проекта Axelix. Если кратко, это стратегия версионирования компонентов продукта, при которой все компоненты системы имеют одну и ту же версию X.Y.Z. Это означает, что если мы выпускаем Axelix 1.4.2, то все перечисленные выше компоненты получают одну и ту же версию - 1.4.2.

Да, мы знаем, что синхронное версионирование в целом весьма спорная тема по целому ряду причин. И я бы сказал, что команде обычно стоит дважды подумать, прежде чем его внедрять. Наверное, мне стоит написать ещё одну статью о том, почему мы выбрали semver с синхронным версионированием и почему это может быть хорошей идеей для некоторых из вас. Но для этой статьи это уже off-top.

Проблема

Итак, вы начинаете новый проект или по каким-либо причинам собираетесь пересмотреть модель ветвления git в своей команде. Когда вы пытаетесь внедрить любое техническое решение (скажем, хотите выбрать модель ветвления git для проекта), обычно полезно сделать шаг назад и ответить на вопрос:

Какую именно проблему мы решаем?

Действительно, прежде чем что-либо объяснять, сначала нужно понять, каким конкретным требованиям должна соответствовать любая модель ветвления git. И эти требования будут отличаться для разных типов ПО с разной частотой релизов. В случае Axelix нам была нужна модель ветвления, которая позволила бы:

1. Возможность выпускать патчи

Мы хотим иметь возможность быстро выпускать патчи.

Более того, важно то, что мы заранее не знаем, сколько патч-релизов будет в рамках конкретной минорной версии. Общее требование состоит в том, чтобы можно было вернуться к любой минорной версии и выпустить для неё любое количество патчей (например, чтобы бекпортнуть CVE патч).

2. Избежать merge-hell

Многие стратегии ветвления git легко приводят к merge hell, например GitFlow печально известен тем, что заводит команды в такие грабли.

Итак, зная вот это всё, давайте, наконец, обсудим, какую модель ветвления мы адоптировали.

Модель ветвления Git в Axelix

В Axelix у нас монорепозиторий. И в этом монорепозитории есть основной trunk - ветка ‘master’ (это мы позаимствовали у TBD). Некоторые коммиты с небольшими доработками или базовым housekeeping-ом попадают прямо в master trunk. Для фич мы создаём короткоживущие ветки (непосредственно от master), которые затем сливаются обратно в master trunk:

Короткоживущие feature branches создаются от master и сливаются обратно в неё
Короткоживущие feature branches создаются от master и сливаются обратно в неё

В некоторых крайних случаях адепты TBD выступают за полный отказ от веток фич в пользу прямого пуша в master из-за риска того, что они станут долгоживущими. Я понимаю логику этого подхода. Однако для OSS-проектов вроде Axelix мы не очень хотим так делать из-за внешних контрибьюторов и ряда других причин.

Тем не менее, в идеале feature branches должны умирать быстро. Чтобы ускорить “смерть” feature веток, мы вводим для них несколько правил и, в частности, устанавливаем знаменитый лимит PR в 500 строк (об этом я уже писал). Другими словами, feature branch не может добавить в основной trunk больше 500 строк кода (у этого правила есть некоторые исключения, но сейчас это неважно). Этот лимит гарантирует, что:

  1. PR достаточно мал, чтобы его можно было тщательно проверить.

  2. Feature branch действительно будет короткоживущей: чем меньше PR, тем быстрее он будет готов к ревью.

Довольно известен тот факт, что у небольших PR общий TTM (time-to-merge) ниже, а лимит гарантирует, что feature ветки умирают быстро и не превращаются в долгоживущие. Так что здесь мы полностью согласны с TBD.

Когда мы хотим выпустить минорный/мажорный релиз, мы создаём тег в master trunk-е нашего монорепозитория и запускаем релизный пайплайн (я сейчас сильно упрощаю, но это не так важно):

Тег коммита в master trunk для запуска минорного/мажорного релиза
Тег коммита в master trunk для запуска минорного/мажорного релиза

Поскольку у нас монорепозиторий с lockstep версионированием, именно из этого помеченного тегом коммита собираются все релизные артефакты. А значение тега становится версией релиза Axelix.

Если мы хотим выпустить патч-релиз, то создаём ветку от тега git, например ветку с именем 1.0.x. Это долгоживущая ветка, которая дальше будет жить своей жизнью. После того как мы внесли необходимые патчи в ветку 1.0.x, мы создаём тег в этой долгоживущей ветке патчей, называем его, например, v1.0.1, а затем выпускаем релиз:

Долгоживущая ветка патчей 1.0.x, созданная от тега, с собственным тегом патч-релиза
Долгоживущая ветка патчей 1.0.x, созданная от тега, с собственным тегом патч-релиза

И вот здесь наступает переломный момент, когда мы существенно и намеренно отходим от TBD. В TBD запрещены любые долгоживущие ветки, кроме основного trunk-а. Но я хочу сделать шаг назад и объяснить почему TBD выступает против этого, и, что важно, почему в нашем случае это допустимо.

TBD: почему ветки патчей допустимы

TBD - это известная и проверенная стратегия ветвления git. Её используют Google, Facebook и другие компании. И она не допускает долгоживущих веток (за исключением master trunk) по целому ряду причин. Одна из них и, пожалуй, самая известная - это проблема merge hell-а. Слияние долгоживущих веток довольно сложно и на масштабе отнимает много времени, чревато ошибками, багами, приводит к дублированию кода. Это всё реально так.

Так вот, что тут важно. Эта проблема становится реальной в тех случаях, когда мы разрабатываем некую функциональность, которую позже планируется слить с чем-то ещё, например с основным trunk-ом. Но в случае таких patch-branch-ей мы вообще не собираемся их когда-либо сливать. Они не предназначены для слияния.

Согласно SemVer, в патч-релизы попадают (если говорить очень широко):

  • Обратно совместимые исправления ошибок

  • Обратно совместимые улучшения производительности

  • Бэкпорты исправлений CVE

Конечно, можно весь день обсуждать, что именно означает обратная совместимость, включает ли она “поведенческую” обратную совместимость, обратную совместимость на уровне исходного кода и так далее. Давайте оставим это за скобками.

Главная идея в том, что в ветках патчей никогда не будут разрабатываться никакие фичи, которые затем попадут обратно в master trunk. Этого просто не произойдёт.

В нашем случае, например, в ветки патчей попадают вполне конкретные коммиты, которые просто переносятся через cherry-pick из основного master trunk.

Сложности и проблемы

Конечно, у описанной выше модели есть свои сложности и я не хочу, чтобы вы подумали, что это silver bullet для SemVer Software. Проблем тут несколько, давайте перечислю.

1. Дилемма минорного и мажорного релизов

Одна из важнейших сложностей такой архитектуры - её негибкость в отношении минорных/мажорных версий.

Что я имею в виду?

Ну, например, допустим, вы разрабатываете версию 1.7 своего ПО. И вот вы задумываетесь о выпуске новой мажорной версии 2.0. Принимая это решение, вы фактически начинаете сливать в master все изменения, которые хотели включить в новую мажорную версию (например, обратно несовместимые, подробнее о них чуть позже). И как только вы начали это делать, развернуться и сказать что-нибудь вроде:

Ой, нет, мы передумали, мы просто хотим выпустить 1.8

становится очень сложно, а в некоторых случаях и вовсе невозможно. Вот о какой негибкости я говорю.

Однако эта проблема (с разной степенью серьёзности), когда простого пути назад уже нет, характерна для многих похожих моделей git, которые топят за true CI, включая TBD. Так что в индустрии эта проблема обычно становится следствием компромисса, на который идут ради отсутствия merge hell. Просто знайте о том, что такая проблема у вас будет.

2. Долгоживущие ветки с обратно несовместимыми изменениями

Так вот, когда я говорил, что master trunk и patch branch-и - единственные долгоживущие ветки, я кое о чём не сказал. Об одной очень важной вещи.

А именно: наша модель ветвления git допускает долгоживущие feature branches, содержащие ломающие изменения. И поскольку такие ветки предназначены для слияния, нам приходится откладывать их как минимум до следующей мажорной версии. А если совместить это с описанной выше проблемой, становится понятно, что всё как-то грустно.

Впрочем, хоть это и проблема, то насколько именно всё плохо - вопрос дискуссионный.

Прежде всего, это в значительной степени зависит от того, какие изменения мы считаем не backward compatible. И для каждой зрелой системы такие изменения могут быть разными. В нашем случае есть целая страница документации, где объясняется наша периодичность релизов и общий взгляд на ломающие изменения.

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

Так что, надо признать, это ещё одна сложность, с которой нам приходится жить. Мы можем попытаться её смягчить, но в какой-то степени она всё равно останется.

Выводы: уроки Axelix

Итак, ещё раз: я не хотел сказать, что мы изобрели какую-то поразительную или умопомрачительную модель git. Просто внутри команды мы почувствовали, что, возможно, стоит поделиться с миром нашей моделью ветвления git и причинами, по которым мы её выбрали.

Цель статьи была скорее в том, чтобы рассказать, как мы подошли к проблеме, в надежде, что вы сможете извлечь из этого что-то полезное. У Axelix открытое ядро, так что если хотите узнать больше - добро пожаловать.

Помните, что компромиссы есть всегда и иногда вполне нормально отступать от некоторых устоявшихся практик. Но такие отступления всегда нужно чётко обосновывать и, что не менее важно, понимать слабые стороны собственного решения.

Берегите себя! Михаил

Комментарии (0)