11.1 Стратегия ветвления - это правила, а не команды
Git ничего не знает про «релизные ветки» или «фича-флаги». В нём есть только коммиты и указатели на них (branch). Любая «стратегия» - это набор человеческих правил поверх:
- какие ветки бывают (
main,develop,release/*,feature/*); - как они называются;
- кто может в них коммитить;
- когда и куда сливать;
- что значит «релиз».
Эти правила можно расставить четырьмя способами:
- Никак. Все делают, как привыкли. Через полгода в репо
сорок веток с непонятным статусом, в
mainлежит код, который нельзя задеплоить, и никто не знает, какой коммит сейчас на продакшене. - GitFlow. Жёсткий каркас из четырёх типов веток. Подходит для продуктов с долгими циклами и параллельными версиями.
- GitHub Flow. Одна вечная ветка
mainплюс короткие фича-ветки. Подходит для SaaS, где «релиз» - это «деплой». - Trunk-based. Все коммитят в
main(или почти все), ветки живут часами. Опирается на CI и feature flags.
Эти три модели - не «уровни зрелости» и не «правильный→неправильный».
Это три разных решения под три разных контекста. Выбор делается
один раз, фиксируется в CONTRIBUTING.md, и команда держится
выбранного. Хуже всех живут команды, у которых стратегия не
выбрана: каждый делает «по интуиции», и история - компот из
несовместимых подходов.
Ниже разберём все три по очереди, и в конце - как выбрать.
11.2 GitFlow: ветка под всё
Описан Vincent Driessen в 2010 году в статье «A successful Git
branching model». На несколько лет стал отраслевым стандартом
де-факто - настолько, что даже появился плагин git flow с
командами git flow feature start, git flow release finish и
прочими. Идея - формализовать всё, оставив минимум места для
разнобоя.
Ветки в GitFlow:
main ← только релизы, в каждый коммит - тег версии
develop ← следующая версия в разработке
feature/* ← фичи, ветвятся от develop, мерджатся в develop
release/* ← подготовка релиза: bugfix, обновление версии
hotfix/* ← срочный фикс из main, мерджится и в main, и в develop
Жизненный цикл фичи:
develop → feature/login → develop
Жизненный цикл релиза:
develop → release/1.4 → main (тег v1.4.0) + обратно в develop
Жизненный цикл хотфикса:
main → hotfix/critical-bug → main (тег v1.4.1) + обратно в develop
main в GitFlow - это только прошлые релизы. Туда ничего не
попадает напрямую, только через release/* или hotfix/*.
«Текущее состояние разработки» живёт в develop. У main и
develop всегда есть гэп: они сходятся в момент релиза.
Это даёт жёсткую двухлинейную историю. По графу видно: вот релизы, вот разработка, вот срочные фиксы.
# Стартуем фичу
git switch -c feature/login develop
# ... разработка, коммиты ...
# Мерджим обратно в develop
git switch develop
git merge --no-ff feature/login
# Готовим релиз
git switch -c release/1.4 develop
# ... финальные правки, bump версии ...
# Финиш релиза
git switch main
git merge --no-ff release/1.4
git tag -a v1.4.0 -m "release 1.4.0"
git switch develop
git merge --no-ff release/1.4
--no-ff (--no-fast-forward, см. fast-forward) тут
обязателен: даже если fast-forward возможен, создаётся
merge-коммит, чтобы факт «здесь была ветка» оставался в истории.
Где GitFlow родной:
- Продукты, которые поставляются в виде версий (мобильные приложения в App Store/Play Store, desktop-софт, embedded).
- Несколько релизных линий одновременно (
v1.4,v1.5,v2.0живут параллельно, в каждую идут свои хотфиксы). - Длинный цикл подготовки релиза с фазой стабилизации.
Где GitFlow ломается:
- Web и SaaS, где деплой каждый час. Релиз-ветки тут - это
бюрократия без пользы;
mainиdevelopвсегда расходятся, и операция «слить релиз» превращается в постоянный геморрой. - Команды меньше десяти человек. Накладные расходы на пять типов веток не окупаются.
- CI, где каждый PR - кандидат на деплой. GitFlow добавляет целый этап «через release/*», который для непрерывной поставки не нужен.
Сегодня GitFlow в чистом виде используют редко. Чаще встречается
его упрощённый вариант: main + develop + feature/*, без
release/* и hotfix/*. Это уже на полпути к GitHub Flow.
Сам Driessen в 2020 году опубликовал апдейт: «GitFlow проектировался под software with explicit version releases; для continuously delivered web - берите модель попроще». Полезное напоминание, что даже автор не пытался выдать модель за универсальную.
11.3 GitHub Flow: одна ветка, одно правило
Реакция на сложность GitFlow и одновременно отражение того, как устроены команды, которые деплоят несколько раз в день. Описана Scott Chacon (один из основателей GitHub) в 2011 году.
Правила:
mainвсегда деплоима. Любой коммит вmainможно выкатить на прод.- Чтобы что-то изменить - создай ветку от
mainс осмысленным именем (feat/...,fix/...). - Коммить туда, пушь, открывай Pull Request (pull-request).
- Получай ревью (code-review), правь.
- Когда зелёный CI и есть аппрув - мерджи в
main. - Из
mainдеплой автоматически (или почти).
Всё. Никаких develop, release/*, hotfix/*. Если нужен
срочный фикс - это просто короткая ветка от main, как любая
другая. Если нужно «отложить релиз» - этого нельзя, релиз
непрерывный.
Главное требование к main: всегда зелёная и
деплоима. Если кто-то смерджил коммит, который ломает прод,
это не «вернёмся через две недели на release/», а «откатить
или починить прямо сейчас». Поэтому GitHub Flow намертво
завязан на:
- сильный CI - все тесты гоняются на каждом PR;
- branch protection на main - без зелёного CI и аппрувов мердж невозможен;
- быстрые тесты - иначе мерджить будет страшно.
Размер ветки: от нескольких часов до нескольких дней. Не недели. Если фича большая - нарезается на серию мелких PR, каждый из которых самостоятелен и не ломает прод.
git switch main
git pull --ff-only
git switch -c feat/email-notifications
# ... несколько коммитов ...
git push -u origin feat/email-notifications
# Открыть PR на GitHub, пройти ревью, дождаться зелёного CI
# Кнопка «Merge» - обычно через squash или rebase merge
# (см. главу 8)
git switch main
git pull --ff-only
git branch -d feat/email-notifications
Где GitHub Flow родной:
- SaaS, веб-приложения, API-сервисы.
- Команды от 3 до ~50 человек.
- CI/CD на каждый PR.
- Один production environment без «версий», только «текущее состояние».
Где GitHub Flow ломается:
- Параллельные версии (нужно поддерживать
v1.xпосле релизаv2.0). Тут одной ветки не хватает, появляютсяsupport/v1.x, и стратегия мигрирует ближе к GitFlow. - Очень крупные команды (>100), где даже короткие ветки начинают конфликтовать друг с другом. Там лучше работает trunk-based.
- Регулируемые отрасли с жёсткими релизными процедурами (медицина, финансы, госсектор). Здесь «релиз = событие» с подписями и аудитом, и непрерывная поставка не подходит.
GitHub Flow - самая распространённая стратегия в 2020-х. Если вы открываете новый проект и не знаете, что выбрать, - берите GitHub Flow. Это не оптимум, это разумный дефолт.
11.4 Trunk-based development
Логическое продолжение GitHub Flow: «если ветка живёт максимум несколько часов, а лучше - несколько минут, то зачем она нужна?»
В trunk-based development разработчики коммитят прямо в main
(англ. «trunk» - ствол), либо в очень короткие ветки длиной
<1 дня. История main - это поток коммитов от всех, без
выделенных feature-веток.
Сегодня: * Alice: refactor parser
* Bob: add metric for /api/users
* Alice: fix off-by-one in pagination
* Carol: extract helper for date math
* Bob: wire up new metric in dashboard
Каждый коммит - самостоятельная единица. Если что-то на проде
сломалось -git revert <sha>. Никаких длинных PR.
Главный вопрос: «А как же незаконченные фичи? Я неделю писал новый раздел, он не работает в середине». Ответ -feature flags:
if feature_enabled("new_billing_page", user):return new_billing_view(user)
return legacy_billing_view(user)
Код новой страницы биллинга попадает в main и едет на прод
каждый день. Но флаг new_billing_page выключен у всех. Когда
разработка закончится - флаг включается сначала на 1% пользователей,
потом на 10%, потом на 100%. Когда стабилизировалось - флаг и
legacy-ветка кода удаляются.
Trunk-based опирается на четыре столпа:
- CI с быстрыми тестами. В идеале -<10 минут от коммита до
«зелёное». Без этого никто не будет коммитить в
main. - Feature flags. Не одна-две, а инфраструктура: панель управления, постепенные выкатки, метрики по флагу.
- Возможность отката за минуты. Деплой автоматизирован, откат тоже.
- Культура мелких изменений. Большие фичи нарезаются на десятки мелких коммитов, каждый из которых проходит CI и деплоится.
Где trunk-based родной:
- Большие команды (>50 человек). Здесь длинные ветки гарантируют
merge hell. Лучше много мелких коммитов в
main, чем десять веток, которые расходятся всё дальше. - Команды с зрелым CI/CD и feature-flag платформой.
- Продукты, где «эксперимент» - часть процесса (A/B тесты, ML, rollout по сегментам).
Поэтому trunk-based используют Google, Facebook, Stripe, Shopify - и почти любая крупная вэб-компания. Это не «модно», это следствие того, что иначе на таком масштабе не работается.
Где trunk-based ломается:
- Без CI. Если автотесты гоняются ночью, в
mainбудет лежать нерабочий код по 12 часов в сутки. - Без feature flags. Без них разработчик не может «положить в main недоделанную фичу», и придётся вернуться к веткам.
- В маленьких командах без необходимости. Trunk-based - это накладные расходы на инфраструктуру. Если у вас три человека и один деплой в неделю - GitHub Flow проще и достаточен.
Trunk-based - это не «коммитьте без ревью». Ревью никуда не девается, оно происходит просто иначе: либо через короткие PR с быстрым аппрувом, либо через post-commit review (правка зашла, но коллега посмотрел в течение дня).
11.4.1 Подводный камень: feature flag - не бесплатный
На бумаге feature flag выглядит элегантно. На практике у каждого флага есть цена:
- Развилка кода. В одной части файла -
if flag_on: new else: old. Полгода спустя кто-то трогает блок, не понимает, какая ветка живая, ломает обе. - Тестирование на N²: теперь нужно тестировать комбинации флагов. Десять флагов - тысяча комбинаций. На практике тестируется три-четыре, остальные - на удачу.
- «Вечные флаги». Флаг включили на 100%, забыли удалить. Через год это уже не flag, это техдолг.
Trunk-based требует уметь добавлять флаг и уметь его удалять (это разные навыки). Без второго накапливается код-помойка.
Полезная практика -жизненный цикл флага:
- Создан, выключен у всех.
- Включён у разработчиков (внутренний дог-фуд).
- Постепенный rollout: 1%, 10%, 50%, 100%.
- Зафиксирован. Через 2 недели после 100% - pull request, который удаляет флаг и старую ветку кода.
Если шаг 4 не происходит автоматически или через регулярную ревизию - flag-долг растёт линейно числу новых фич.
11.5 Влияние на CI/CD
Выбранная стратегия определяет, как устроен CI и деплой.
GitFlow:
feature/* → CI (тесты + линт)
develop → CI (тесты + интеграционные) + деплой на staging
release/* → CI (полный регресс) + деплой на pre-prod
main → CI + деплой на production по тегу
CI устроен пирамидой: чем «выше» ветка, тем больше тестов. Деплой привязан к merge в определённую ветку. Релизы - это события, а не побочный эффект мержа.
GitHub Flow:
feature/* → CI (всё, что есть) + preview-окружение
main → CI + автоматический деплой в production
CI и деплой одинаковые для всех веток. PR-окружение (preview)
позволяет посмотреть фичу до мержа. После мержа в main -
автоматический деплой.
Trunk-based:
main → CI + автоматический деплой в production
feature flags решают, что включено для кого
У всех коммитов один путь: тесты → деплой → флаги. Никаких preview-окружений на ветку - потому что нет долгих веток. «Превью» - это «включить флаг для тестового аккаунта на проде».
Чем правее по этой шкале, тем больше требуется от инфраструктуры деплоя. CI/CD не «прикладывается» к стратегии - оно её и определяет. Команда, у которой нет автоматического деплоя, не сможет жить в trunk-based, даже если очень хочет.
11.6 Размер команды и сложность
Размер команды грубо предсказывает, какая стратегия выживет:
| Размер команды | Что обычно работает |
|---|---|
| 1–3 | Любая, включая «никакая». GitHub Flow - минимум усилий. |
| 4–15 | GitHub Flow. Trunk-based, если есть CI и культура мелких изменений. |
| 15–50 | GitHub Flow с branch protection, либо trunk-based. GitFlow - только если поставляете версии. |
| 50+ | Trunk-based, иначе merge hell. Альтернатива - Monorepo + Bazel-подобная сборка. |
| Любой размер с версиями | GitFlow или его упрощённый вариант. |
Это не строгие границы. Команда из десяти человек может успешно жить в trunk-based, если у них хороший CI и зрелая культура. Команда из ста может работать в GitHub Flow, если у них дисциплина и приоритет - короткие PR.
Но есть закономерность: чем больше людей, тем дороже длинные ветки. Конфликт мержа двух людей решается за полчаса. Конфликт мержа десяти веток между собой - это уже отдельная работа на несколько дней. Trunk-based - это решение не «удобства», а математики: при росте команды стоимость длинных веток растёт быстрее линейного.
11.7 Как выбрать (и не переиграть)
Алгоритм:
- Поставляешь версии? (мобильное приложение, desktop, embedded, on-premise enterprise) - GitFlow или его упрощённый вариант.
- SaaS / API / веб с непрерывным деплоем? - GitHub Flow по умолчанию.
- Команда >50 человек и есть CI/CD + feature flags? - trunk-based.
- Не уверен? - GitHub Flow. Это разумный дефолт, который потом легко мигрировать в любую сторону.
Зафиксируй выбор в CONTRIBUTING.md:
## Branching
We use GitHub Flow:
-main is always deployable
-feature work happens in short-lived branches (≤3 days)
-all changes go through pull requests with ≥1 approval
-main is protected: no direct pushes, no force pushes
Long-lived branches (release/*, develop) are NOT used.
Эти десять строк экономят месяцы недопониманий. Новый человек видит правила сразу, без необходимости спрашивать «а как у вас?»
Что НЕ делать:
- Не миксуй стратегии в одном репо. «У нас иногда GitFlow, а иногда trunk-based» - это «у нас нет стратегии».
- Не выбирай «потому что Google так делает». Google так делает, потому что у них 50 000 разработчиков. У тебя три. Контекст разный.
- Не переходи с одной стратегии на другую «между прочим». Это требует отдельного решения, переучивания и обычно - отдельной миграционной фазы.
- Не пытайся улучшить GitFlow «своими дополнениями». Если кажется, что нужны ещё ветки сверху - скорее всего, ты пытаешься компенсировать что-то ещё (плохой CI, отсутствие owners, нехватка staging-окружения).
Один из верных признаков «вы выбрали неудачную стратегию»:
мерж в main ощущается рискованным. Если все боятся мержить,
и от этого PR копятся неделями - стратегия не подходит к
команде или продукту. Простое решение - упрощать (меньше
веток, короче PR), не усложнять.
11.7.1 Подводный камень: release train для смешанных случаев
Промежуточный случай - продукт, который в основном живёт по GitHub Flow, но иногда нужен «релиз с подписью» (например, обновление прошивки или клиента, которое выкатывается раз в месяц всем разом).
Здесь подходит release train: ежемесячно (или по фиксированному
расписанию) от main отрезается ветка release/2025-05, на ней
делается финальная стабилизация (только bugfix, никаких новых
фич), потом тег и подпись. После релиза ветка живёт ещё месяц
на случай экстренного фикса, потом замораживается.
Это не GitFlow: нет develop, нет постоянных release/*. Это
GitHub Flow + cut-and-freeze раз в период. Близко по духу к
тому, как живут Chrome, Firefox, Node.js.
Полезно, когда часть пользователей хочет «стабильную версию
ежемесячно», а часть -«всегда последнее». Тогда ежемесячные
tag-релизы - это для первых, а main - для вторых.
Уроки в sandbox
lab-11.1. Один и тот же проект через GitHub Flow и trunk-based
Цель - почувствовать разницу руками. Проиграем простой репо двумя стратегиями: сначала GitHub Flow с короткими фича-ветками, потом trunk-based с feature flag. Увидим, как меняется история, и почему обе стратегии валидны под разный контекст.
Создай чистый репо:
mkdir branch-strategies-lab && cd branch-strategies-lab && git init && echo "def main(): print('hello')" > app.py && git add app.py && git commit -m "init".Прогон 1 - GitHub Flow. Создай фича-ветку:
git switch -c feat/add-name-arg. Добавь поддержку аргумента: измени app.py так, чтобыmain(name)печаталhello, {name}. Коммить:git commit -am "feat: accept name argument".Сделай вторую фичу параллельно (типа от другого разработчика):
git switch main && git switch -c feat/add-greeting-time. Измениapp.pyтак, чтобы добавилось время суток в приветствии. Коммить:git commit -am "feat: add greeting time".Слей обе ветки в main по очереди:
git switch main && git merge --no-ff feat/add-name-arg && git merge --no-ff feat/add-greeting-time. Будет конфликт во второй - это нормально, GitHub Flow тоже не избавляет от мержа. Разруливай (выбери компромиссную версию),git add app.py && git commit.Посмотри
git log --oneline --graph --all. Увидишь две ветки, два merge-коммита. Это след того, что фичи разрабатывались параллельно.Прогон 2 - trunk-based. Сбрось всё к init-коммиту:
git reset --hard <sha-init> && git branch -D feat/add-name-arg feat/add-greeting-time.Имитируй feature flag прямо в коде: добавь в
app.pyFLAGS = {'name_arg': False, 'greeting_time': False}, коммить:git commit -am "add feature flag stub". Это прямо в main, без ветки.Добавь поддержку name_arg за флагом: внутри
main()проверьif FLAGS['name_arg']. Коммить прямо в main:git commit -am "add name arg, gated". Сразу следующим коммитом - поддержка greeting_time:git commit -am "add greeting time, gated".Включи флаги, имитируя rollout: измени FLAGS на True, коммить:
git commit -am "enable name_arg", потом ещё коммит:git commit -am "enable greeting_time".Удали флаги (последний шаг жизненного цикла): убери
if FLAGS[...], удали FLAGS dict, коммить:git commit -am "remove flags, features GA". Теперь код чистый.Посмотри
git log --oneline --graph --allснова. Увидишь линейную последовательность из 6 коммитов в main, без веток. Это история trunk-based: один поток, фичи мерджились по одной, под флагами.Итог для головы: одна и та же функциональность. В GitHub Flow видно «здесь были две ветки», есть merge-коммиты. В trunk-based - плоская линия, история разделена на единицы из 1-2 строк. Какой вариант лучше - зависит от того, какие вопросы потом задают истории.
sandbox с автопроверкой - открыть в песочнице
Резюме
- Стратегия ветвления - это правила команды, не свойство Git. Git одинаково позволяет любую модель; выбор делается людьми и фиксируется в CONTRIBUTING.md.
- GitFlow (main + develop + feature/* + release/* + hotfix/*) подходит продуктам, которые поставляются версиями: мобильные приложения, desktop, embedded. В вебе создаёт лишний слой бюрократии.
- GitHub Flow (main + короткие фича-ветки + PR) - разумный дефолт для SaaS и API. Опирается на сильный CI и branch protection на main.
- Trunk-based (всё в main, ветки часами) вытекает из больших команд: длинные ветки начинают конфликтовать друг с другом всё чаще по мере роста. Опирается на CI/CD и feature flags.
- Feature flag - не бесплатный. Без жизненного цикла (создан → rollout → 100% → удалён) флаги превращаются в техдолг и тестовый N².
- Размер команды - главный предсказатель: до 15 человек GitHub Flow, дальше - либо GitHub Flow с дисциплиной, либо trunk-based. >50 человек длинные ветки уже не работают.
- Не миксуй стратегии в одном репо. Не выбирай по моде. Не пытайся улучшить GitFlow «своими дополнениями» - обычно это компенсация других проблем (CI, owners, staging).
Контрольные вопросы
Команда из 6 человек, разрабатывают SaaS. Сейчас работают по GitFlow: main, develop, release/*, feature/*, hotfix/*. Деплоят раз в неделю в пятницу с release-ветки. Жалуются на «всё запутанно, постоянно мерджим develop в release». Что им посоветовать?
Показать ответ
Перейти на GitHub Flow. У них классический случай «GitFlow на контексте, под который он не проектировался». SaaS с непрерывным деплоем не нуждается в
developотдельно отmain- это просто два указателя на почти одно и то же, которые нужно постоянно сводить. И не нуждается вrelease/*, потому что релиз - это просто мердж PR. После перехода у них останетсяmain+ фича-ветки, и деплой можно делать тем же еженедельным расписанием, просто по тегу изmain, без отдельной ветки. Сократится 80% митингов про «как двигать релиз», и пятница перестанет быть особенным днём.В чём принципиальная разница между GitHub Flow и trunk-based, если в обоих есть main и короткие ветки?
Показать ответ
Длина веток. В GitHub Flow ветка живёт от нескольких часов до нескольких дней; в trunk-based - от нескольких минут до часов, либо ветки нет вовсе (коммит сразу в main). И главное - в trunk-based незаконченный код мерджится в main под feature flag и едет на прод. В GitHub Flow незаконченный код не попадает в main: ждёт, пока PR доделают и пройдёт ревью. Это два разных подхода к «когда коду пора уезжать с твоей машины».
Можно ли коммитить в main без ревью при trunk-based?
Показать ответ
Технически можно, обычно - не нужно. Trunk-based не означает «без ревью», означает «без длинных PR». Ревью просто становится быстрым: PR из 1–3 коммитов, аппрув в течение нескольких часов. В некоторых компаниях (например, Google) практикуют post-commit review: код заехал в main, но коллега всё равно посмотрит в течение дня и попросит fixup, если что. Главное в trunk-based - это не «без ревью», а «изменения мелкие и заезжают часто», и ревью встраивается в этот темп.
У нас десять разработчиков, мы поставляем мобильное приложение в App Store раз в две недели. Зачем нам думать про GitHub Flow или trunk-based - GitFlow же про нас?
Показать ответ
Скорее всего да, GitFlow или его упрощённый вариант - ваш нативный контекст. Релизы как события, версии как ценность, хотфиксы на нескольких поддерживаемых версиях - это всё, под что GitFlow проектировался. Но обрати внимание: GitFlow часто используют в упрощённой форме - main + develop + feature/, без отдельных release/ и hotfix/. Это уменьшает накладные расходы, оставляя основную идею: develop = в работе, main = выпущенные релизы. Полная схема с release/ нужна, если ты реально стабилизируешь релиз неделями. Если выпускаешь примерно «то, что в develop» - release/* можно убрать.
Зачем `--no-ff` при merge feature-ветки в GitFlow, если fast-forward возможен?
Показать ответ
Чтобы в истории остался merge-коммит - явная отметка «здесь была ветка». При fast-forward (см. fast-forward) merge-коммита нет: коммиты ветки просто «приклеиваются» к main, и потом по графу не видно, что это была отдельная фича. В GitFlow факт ветвления - часть структуры истории, его сохраняют намеренно. В GitHub Flow это менее важно, часто наоборот выбирают squash или rebase-merge, чтобы main был линейной. В trunk-based вопроса не возникает - там обычно нет веток дольше часа.