Понедельник, день выпуска. Одна команда собирает релизную ветвь: разруливает конфликты, копившиеся три недели, поднимает номера версий, пересобирает сборку, находит поломку и откладывает выпуск на день. Другая команда в то же утро успела выкатить два обновления до обеда, потому что её главная ветвь всегда готова к выпуску. Языки одни и те же, Git один и тот же, различие ровно одно: устройство ветвления. От этой "мелочи" зависит, сколько часов уходит на слияния, как быстро исправление доезжает до пользователей и живёт ли в проекте рефакторинг вообще. Выбор между GitFlow и trunk-based development часто делают по моде, а определять его должны три вещи: размер команды, частота выпусков и отношение к незаконченной работе в общей ветви.

Откуда взялся GitFlow и под какие продукты он придумывался

Схему описал Винсент Дриссен в январе 2010 года в статье "A successful Git branching model", и диаграмма с ветвями быстро разошлась по презентациям всего мира. Постоянных ветвей две: main для выпущенных версий и develop для интеграции. Задача уходит в отдельную ветвь от develop, готовая работа вливается обратно командой git merge --no-ff, чтобы в истории остался явный узел слияния. Перед выпуском от develop отрезается релизная ветвь: новые функции в неё не попадают, правятся только дефекты, поднимается номер версии. Срочные исправления идут отдельным маршрутом: ветвь hotfix отрезается от main, правка уходит в продакшен, затем вливается и в main, и в develop, чтобы расхождение не накапливалось.

Уже отсюда видно главное свойство модели: каждая задача проходит минимум два слияния, своё в develop, затем develop в main при выпуске, плюс обратные вливания из релизной ветви. Для продукта с номерами версий это осмысленная плата. Настольное приложение, мобильное приложение в магазине, библиотека, сервер, который заказчик разворачивает у себя: каждая версия живёт годами, старым версиям нужна поддержка, тег с номером обязателен. GitFlow ложится на такой мир идеально: релизная ветвь соответствует выпущенной версии, hotfix соответствует срочному патчу, история читается как журнал.

Показательно, что автор позже сам смягчил позицию. В 2020 году Дриссен добавил к статье примечание: для веб-приложений с непрерывной поставкой модель избыточна, постоянная ветвь develop добавляет шагов без пользы, и разумнее взять более простую схему. Человек, придумавший самую популярную модель ветвления, первым признал, что её место в мире изменилось, потому что изменился способ выпуска.

Вокруг схемы выросла целая инфраструктура: расширение git-flow упаковывает рутинные действия в короткие выражения:

git flow init
git flow feature start invoice-export
git flow feature finish invoice-export
git flow release start 2.4.0
git flow release finish 2.4.0

Удобно, когда выпуск действительно версионируется. Утомительно, когда веб-сервис выкатывается по кнопке каждый день, и половина этих команд существует только для перестановки номеров.

Что предлагает trunk-based development и на каком правиле он держится

Вместо дерева ветвей один ствол. Правило одно: изменения попадают в ствол минимум раз в сутки. Ветви отводить можно, но живут они часы, в крайнем случае день-два, и принадлежат одному рабочему месту. Pull request в такой схеме это ворота проверки кода, а не парковка для недоделанной работы: ветвь, висящая дольше пары дней, уже сигнал беды. Незаконченное поведение прячется за флаги функций, крупные перестройки идут через временную абстракцию, а сломанная сборка чинится раньше всего остального, потому что стоит на пути всей команды.

Выпускается ствол напрямую: тег на нужном коммите и поставка. Для мелких дефектов работает стратегия fix forward, исправление вперёд, новым коммитом, без отката. Нужна версия для заказчика со старой сборкой: отрезается временная релизная ветвь, гасится и удаляется после выпуска. Постоянной ветви develop в картине нет, и вместе с ней исчезает целый класс работ по её синхронизации в обе стороны.

Самый известный пример масштаба приходит из Google: по описаниям практиков порядка 35 тысяч разработчиков и инженеров автоматизации тестов работают со стволом в одном монорепозитории, и модель выдерживает такой размер. Книга "Continuous Delivery" Джеза Хамбла и Дэвида Фарли, вышедшая в 2010 году, закрепила ствол как базу непрерывной поставки, а ежегодные отчёты DORA по состоянию поставки ПО устойчиво связывают работу со стволом с высокой скоростью и стабильностью доставки.

Суточный ритм выглядит буднично:

git switch -c fix-uuid-parse
# правки, локальные тесты, зелёная сборка
git push origin fix-uuid-parse
# ревью и слияние в main в тот же день

Темп задаёт не релизный календарь, а рабочий день.

Ствол предъявляет жёсткие требования к инфраструктуре. Тесты, которые гоняются часами, при ежедневных вливаниях превращаются в пробку: сборка должна идти минуты, автотесты должны быть быстрыми и надёжными, а локальная проверка перед отправкой обязана ловить самые частые поломки. Отсюда скромное, но важное следствие: переход на ствол обычно начинается не с Git, а с ускорения сборки и чистки нестабильных тестов. Без этого команда просто утонет в очереди на проверку.

Цена слияния растёт быстрее возраста ветви

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

# ветвь A: убрала второй аргумент
send_report(user, template)

# ветвь B: добавила третий
send_report(user, template, retry=2)

Каждая строка верна в своей ветви, вместе это одна сломанная строка, и найдут её разве что интеграционные тесты, если они есть.

Главная жертва долгих ветвей рефакторинг. Переименование метода, разъезд класса на два, перенос модуля задевают десятки файлов и конфликтуют с любой параллельной работой. Команда быстро усваивает урок: общие места не трогать. Общие места обрастают одноразовыми обёртками, код стареет, а "спокойный период" для большой чистки не наступает никогда. Мартин Фаулер в разборе моделей ветвления отмечал ровно это: изоляция ветви откладывает риск слияния на конец задачи и отбивает охоту к рефакторингу, а команды, тянущие задачу дольше пары дней, собирают весь набор этих проблем. Ствол переворачивает расклад: маленькие слияния по несколько раз в день почти бесплатны, и рефакторинг снова становится обычным делом, а не отдельным проектом с согласованием.

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

Цена слияний измеряется прямо по журналу Git, и два коротких выражения дают исходные цифры для разговора:

git log --merges --since="1 month ago" --pretty=format:"%ad" | wc -l
git rev-list --count main..develop

Первая строка считает слияния за месяц, вторая показывает, насколько develop убежала от main. Если первая цифра подбирается к сотням, а вторая к тысячам коммитов, разговор о модели ветвления перестаёт быть абстрактным спортом и становится вопросом бюджета.

Флаги функций и абстракции как обязательная инфраструктура ствола

Ствол ломает привычку прятать недоделанное в ветви и взамен требует прятать его в коде. Главный инструмент флаг функции: незаконченное поведение попадает в ствол, но остаётся выключенным, и пользователи его не видят.

def build_invoice(order):
    if features.enabled("invoice_v2", user=order.user):
        return NewInvoiceEngine(order).build()
    return LegacyInvoice(order).build()

Доля раскатки задаётся конфигурацией: новому движку включают сначала внутренний аккаунт, затем пятую часть пользователей, затем всех. Настройка живёт в сервисе флагов вроде Unleash или Flagsmith, а в маленькой команде и в обычном конфиге приложения. Крупные перестройки, которые флагом не прячутся, идут через место расширения: старая и новая реализации ставятся за один интерфейс, переключение делается кодом, после проверки старая версия удаляется целиком. Приём известен как branch by abstraction и заменяет долгоживущую ветвь парой маленьких шагов в стволе.

У флагов есть обратная сторона, о которой молчат восторженные описания. Флаги копятся, условия ветвятся, через год конфигурация перестаёт помещаться в голове. Зрелые команды держат дисциплину: у каждого флага есть задача на удаление и срок жизни, а флаги пользовательского поведения не переживают двух-трёх выпусков. Без этой дисциплины ствол превращается в склад выключенных ветвлений, и половина выигрыша теряется.

Выбор по размеру команды и частоте выпусков

Критерии проще, чем кажется после чтения методологической литературы. Публичный сервис с непрерывной поставкой, веб-приложение с ежедневными выпусками: ствол, без вариантов. Продукт с номерами версий и поддержкой старых сборок: GitFlow или его облегчённый вариант с релизными ветвями, потому что версиям нужна отдельная жизнь. Малая команда из двух-пяти человек: допустимы прямые коммиты в ствол вообще без ветвей задач, и это самый быстрый режим из существующих. Крупная организация: ствол плюс короткоживущие ветви, pull request и очередь слияний, иначе сотня людей затопчет друг друга в одном стволе.

Опенсорс занимает отдельное место. Сторонние участники работают в собственных копиях проекта, долгоживущие ветви по задачам там естественны, и требовать от них темпа внутренней команды бессмысленно: у опенсорса нет общего работодателя и общего рабочего дня. Полезно признать честно: те же диаграммы с ветвями, что отлично служат добровольцам по всему миру, внутри команды с ежедневной поставкой работают в убыток.

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

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

План перехода со зрелого GitFlow на ствол без остановки работы

Команда с многолетним GitFlow не переключается одним приказом, риск развалить поставку слишком велик. Разумная последовательность выглядит так:

  1. Сократить срок жизни ветвей задач до одного-двух дней, крупные изменения резать на маленькие шаги;
  2. Ввести требование зелёной сборки: коммит в ствол с падающими тестами откатывается немедленно, его починка важнее новой работы;
  3. Завести механизм флагов и перевести незаконченные работы за флаги;
  4. Перевести выпуск на теги и временные релизные ветви, отказавшись от постоянной ветви develop;
  5. Договориться о маршруте срочных исправлений и о стратегии fix forward для мелких дефектов на живом сервисе;
  6. Установить правило уборки: у каждого флага задача на удаление и предельный срок жизни.

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

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