Стратегии ветвления: Git Flow, GitHub Flow, trunk-based

Урок про то, как команда договаривается, какие ветки создавать, когда их сливать и когда выпускать релиз.

Стратегия ветвления — это согласованный набор правил о том, какие ветки живут в репозитории, как они называются, куда вливаются и в какой момент попадают в продакшен.

Зачем вообще нужна стратегия

Пока вы работаете в одиночку, ветки — просто удобство. Как только в репозитории несколько человек, отсутствие договорённостей превращает историю в кашу: кто-то держит фичу неделями, кто-то коммитит прямо в основную ветку, релиз собирают вручную «по памяти». Стратегия ветвления отвечает на три практических вопроса: где живёт код, готовый к релизу; как новая работа попадает туда; как чинят прод, когда он горит. Три популярных ответа — Git Flow, GitHub Flow и trunk-based development — различаются в основном тем, насколько долго живут ветки и как часто вы выкатываете.

Git Flow: много веток, явные релизы

Git Flow (модель Винсента Дриссена, 2010) держит две долгоживущие ветки: main (то, что в проде) и develop (накопитель следующего релиза). Вся работа идёт во временных ветках трёх типов.

  • feature/* — отходят от develop и вливаются обратно в develop.
  • release/* — отходят от develop, когда набралось достаточно фич; в них только стабилизация (фикс багов, бамп версии), затем вливаются и в main, и обратно в develop.
  • hotfix/* — отходят от main, чинят горящий прод, вливаются и в main, и в develop.
git checkout develop
git checkout -b feature/oauth-login
# ... работаем, коммитим ...
git checkout develop
git merge --no-ff feature/oauth-login

# Стабилизируем релиз
git checkout -b release/1.4.0 develop
# правим версию, ловим баги
git checkout main && git merge --no-ff release/1.4.0
git tag -a v1.4.0 -m "Release 1.4.0"
git checkout develop && git merge --no-ff release/1.4.0

Флаг --no-ff заставляет Git создавать merge-коммит даже там, где возможна перемотка (fast-forward), — так в истории видны явные «пакеты» фич. Git Flow хорош для продуктов с явными нумерованными релизами: десктопные приложения, мобильные приложения с ревью в сторах, библиотеки, прошивки. Минус — тяжеловесность: две постоянные ветки, ветки-релизы, частые слияния туда-обратно. Для веб-сервиса, который катится по десять раз в день, это лишняя бюрократия.

GitHub Flow: одна основная ветка и Pull Request

GitHub Flow радикально проще: есть одна вечная ветка main, которая всегда деплоима. Любая работа — это короткоживущая ветка от main, оформленная как Pull Request; после ревью и зелёных проверок она вливается обратно и сразу (или почти сразу) выкатывается.

git checkout main && git pull
git checkout -b fix/cart-total-rounding
# правим, коммитим, пушим
git push -u origin fix/cart-total-rounding
# открываем Pull Request, проходим ревью и CI, мёржим, деплоим

Здесь нет develop и нет release-веток: релиз — это просто очередное слияние в main. Модель идеальна для веб-приложений с непрерывным деплоем. Цена простоты — дисциплина: main обязан всегда быть зелёным, поэтому без автотестов и защиты ветки (об этом отдельный урок) GitHub Flow быстро ломается.

Trunk-based development: ветки на часы, не на дни

Trunk-based доводит идею до предела: все коммитят в один «ствол» (main, он же trunk) либо напрямую, либо через ветки, которые живут часы, максимум день-два. Долгих feature-веток нет принципиально — это убирает «ад слияний», когда ветка отстаёт на сотни коммитов.

А как же незаконченные фичи?

Их прячут за feature-флагами: код вливается в trunk сразу, но включается флагом, когда будет готов. Так недописанная функция уже интегрирована, но не видна пользователю.

FEATURES = {"new_checkout": False}

def checkout(cart):
    if FEATURES["new_checkout"]:
        return new_flow(cart)
    return legacy_flow(cart)

def new_flow(cart):  return "new checkout: " + cart
def legacy_flow(cart): return "legacy checkout: " + cart

print(checkout("book"))
FEATURES["new_checkout"] = True
print(checkout("book"))

Вывод:

legacy checkout: book
new checkout: book

Trunk-based — основа практик Google и многих CI/CD-команд. Требует зрелой автоматизации (быстрые тесты, надёжный CI) и культуры маленьких частых коммитов, зато даёт минимальные конфликты и мгновенную интеграцию. Логика тут контринтуитивная, но проверенная: чем чаще и мельче вы интегрируете, тем дешевле каждое слияние. Боль возникает не от частоты слияний, а от их размера — а размер прямо пропорционален тому, сколько ветка прожила в отрыве от ствола.

Как выбрать модель под команду

Практическое правило простое: отталкивайтесь от того, как часто вы реально выкатываете и насколько зрелы ваши тесты. Редкие нумерованные релизы и потребность вести несколько версий одновременно — это Git Flow. Веб-сервис с деплоем по нажатию кнопки и хорошим CI — GitHub Flow. Большая команда, которая хочет минимума конфликтов и готова вкладываться в автоматизацию и feature-флаги, — trunk-based. Размер команды тоже влияет: для двух-трёх человек тяжёлая церемония Git Flow избыточна, а для полусотни разработчиков без строгих правил любой репозиторий быстро превращается в хаос. Наконец, не бойтесь упрощать: команды чаще страдают от слишком сложного процесса, чем от слишком простого.

Как это работает под капотом

С точки зрения Git любая ветка — это просто двигающийся указатель на коммит (файл в .git/refs/heads/). Все три стратегии оперируют одним и тем же примитивом, разница только в политике: сколько указателей вы держите и как долго. «Долгоживущая ветка» расходится со стволом тем сильнее, чем дольше живёт, поэтому риск тяжёлого слияния растёт со временем — отсюда и стремление trunk-based сливать как можно чаще. Merge-коммит с --no-ff отличается от fast-forward тем, что имеет двух родителей и физически фиксирует точку интеграции; это решение о форме истории, а не о содержимом кода.

Частые ошибки

  • Тянуть Git Flow в веб-сервис. Если вы деплоите по несколько раз в день, две постоянные ветки и release-ветки только мешают — берите GitHub Flow или trunk-based.
  • «Бессмертные» feature-ветки. Ветка, которая живёт месяц, гарантирует болезненное слияние и потерю контекста. Дробите задачу.
  • GitHub Flow без тестов и защиты. Раз main всегда деплоится, один непроверенный мёрж кладёт прод. Без CI и branch protection эта модель опасна.
  • Trunk-based без feature-флагов. Вливать в ствол полусырую фичу без флага — значит ломать всем сборку.
  • Хотфикс мимо процесса. В Git Flow забыли влить hotfix обратно в develop — и в следующем релизе баг возвращается.

Итоги

  • Стратегия ветвления — это договорённость о форме работы, а не фича Git.
  • Git Flow: две постоянные ветки + feature/release/hotfix; для продуктов с явными нумерованными релизами.
  • GitHub Flow: одна деплоимая main + короткие PR-ветки; для непрерывного деплоя.
  • Trunk-based: ветки на часы + feature-флаги; минимум конфликтов, но требует зрелого CI.
  • Чем чаще релизы, тем короче должны жить ветки.
Проверьте себя
1. В классическом Git Flow от какой ветки создаётся hotfix-ветка?
AОт develop
BОт main
CОт release/*
DОт предыдущей feature-ветки
2. Как trunk-based development решает проблему незаконченных фич в общем стволе?
AДержит отдельную ветку develop
BПрячет код за feature-флагами и включает его, когда готово
CЗапрещает мёржить, пока фича не закончена
DИспользует release-ветки для стабилизации
3. Для какого продукта GitHub Flow подходит лучше всего?
AВеб-сервис с непрерывным деплоем
BДесктопное приложение с редкими нумерованными релизами
CПрошивка устройства с долгим циклом QA
DБиблиотека с поддержкой нескольких старых версий