Инженерная практика
Короткоживущие ветки против gitflow
Команда читает «trunk-based development» и слышит «коммить прямо в main, без веток, без PR». Они отключают защиту веток. За неделю trunk краснеет дважды в день, незапрошенное изменение утекает с кредами, и они заключают, что trunk-based «не масштабируется». Они выбросили не то. Trunk-based никогда не был про удаление веток — он про то, чтобы держать их настолько короткоживущими, чтобы они не накапливали разрыв. Ветка живёт час, за зелёным гейтом и ревью, а потом её нет.
Что на самом деле задаёт «короткоживущая»
Определение trunk-based от DORA конкретно, а не «по ощущениям». Держите три или меньше активных веток в репозитории в любой момент. Ветки должны жить не более одного дня до слияния в trunk — часто всего несколько часов. Нет заморозок кода и нет фаз интеграции. Каждая ветка — это одно малое, узкое изменение, которое открывает PR, проходит быстрый CI-гейт, получает быстрое ревью и мёржится в тот же день. Ветка — это зона ожидания для ревью и гейтинга, а не место, где работа живёт.
Иногда это запускают как чистый «коммит в trunk» (вообще без веток, частое в малых co-located командах с парным программированием), а чаще как «короткоживущие feature-ветки» (версия, которую использует большинство, потому что она сохраняет код-ревью и PR-гейт). Обе — trunk-based. Разделительная черта не «ветка или не ветка» — это время жизни. Ветка, живущая часы, — trunk-based; ветка feature/*, живущая две недели, — это долгоживущая ветка, ради устранения которой существует практика, как бы вы ни называли свой workflow.
Почему gitflow оптимизирован под противоположный мир
Gitflow даёт вам постоянные ветки main и develop, плюс ветки feature/*, release/* и hotfix/* с формальными фазами: фичи интегрируются в develop, ветка release стабилизируется, затем мёржится в main и тегается. Это связная модель — для мира, под который она была спроектирована: «коробочный», версионированный софт с плановыми релизами, несколько поддерживаемых в поле версий и без непрерывного развёртывания. В том мире явная фаза стабилизации и параллельные релизные линии оправдывают себя.
Для непрерывно развёртываемого веб-сервиса gitflow институционализирует ровно тот разрыв, о котором предупреждал прошлый урок. Долгоживущие ветки feature/* накапливают расхождение; продвижение develop-в-release-в-main — это встроенная фаза интеграции; «готовые» фичи лежат в develop и гниют, пока ждут следующего релизного поезда. Вы наследуете big-bang слияния и заморозки кода по дизайну. Исследование DORA явно отмечает фазы интеграции и заморозки кода как анти-паттерны для производительности доставки — gitflow делает их несущими.
| Измерение | Gitflow | GitHub flow | Trunk-based |
|---|---|---|---|
| Долгоживущие ветки | main + develop (+ release) | только main | только trunk |
| Жизнь feature-ветки | Дни–недели | До слияния PR (варьируется) | Часы, < 1 дня |
| Фаза интеграции | Явная (release-ветка) | Нет | Нет |
| Под что построен | Версионированные плановые релизы | Веб-приложения, непрерывный деплой | Непрерывный деплой в масштабе |
| Релиз | Продвинуть develop → release → main | Развернуть слитый main | Тег/ветка от trunk в точке релиза |
Релизить без долгоживущих веток
Возражение: «если есть только trunk, откуда берутся релизы, и как мне пропатчить старую версию?» У trunk-based есть конкретный ответ — ветка для релиза, а не для фичи. Когда вы нарезаете релиз, вы создаёте короткоживущую ветку release/x.y от trunk на этом коммите. Повседневная разработка на ней никогда не идёт. Если нужен критический фикс, вы чините его сначала на trunk (чтобы следующий релиз тоже его имел) и cherry-pick-аете единственный коммит на release-ветку. Release-ветка — это read-mostly снимок, а не параллельная линия разработки, так что она никогда не накапливает разрыв. Команды на непрерывном деплое часто пропускают даже это и просто тегают trunk.
Для больших изменений, которые действительно нельзя выкатить за день — замена ORM, смена платёжного провайдера — инструмент trunk-based — это branch by abstraction, а не долгоживущая ветка. Вы вводите слой абстракции над тем, что заменяете, инкрементально строите новую реализацию за ним на trunk (каждый шаг мёржится ежедневно, тёмный, пока не готов), переключаете абстракцию на новую реализацию, затем удаляете старую и саму абстракцию. Большой рефакторинг всё это время живёт на trunk как последовательность малых зелёных коммитов — никогда как трёхнедельная ветка.
Почему это работает
Ловушка именования реальна: ветка feature/* в gitflow-репозитории и короткоживущая ветка в trunk-based репозитории могут выглядеть в git идентично. Разница невидима в ветке и видна только в её времени жизни и дисциплине вокруг. «Мы используем feature-ветки» не говорит вам ничего о том, trunk-based вы или нет — «наши ветки мёржатся или удаляются за день» говорит всё.
Вы заменяете ORM в непрерывно развёртываемом сервисе. Миграция займёт шесть недель. Как сделать это по trunk-based?
Где проходит настоящая разделительная черта между trunk-based и workflow с долгоживущими ветками?
Как в trunk-based development обрабатывается критический фикс к уже выпущенной версии?
Упорядочьте миграцию branch by abstraction на trunk:
- 1 Ввести слой абстракции над компонентом, который заменяете
- 2 Строить новую реализацию за абстракцией малыми ежедневно-сливаемыми коммитами, тёмной
- 3 Переключить абстракцию на новую реализацию
- 4 Проверить в проде, затем удалить старую реализацию
- 5 Убрать ставший лишним слой абстракции
- 01Команда отключила всю защиту веток, потому что прочла, что trunk-based означает «без веток». Что они недопоняли, и что trunk-based на самом деле задаёт?
- 02Если есть только trunk, как вы нарезаете релизы и делаете шестинедельный рефакторинг без долгоживущей ветки?
Trunk-based development запрещает долгоживущие ветки, а не сами ветки — DORA задаёт три или меньше активных веток, время жизни менее дня и отсутствие фаз интеграции или заморозок кода. Коммитите вы прямо в trunk или используете короткоживущие feature-ветки с PR — отличающая переменная это время жизни, а не наличие ветки. Структура gitflow main/develop/release/hotfix связна для «коробочного», планового, мульти-версионного софта, но для непрерывно развёртываемого сервиса институционализирует тот самый разрыв, big-bang слияния и фазы интеграции, ради устранения которых существует trunk-based. Релизы приходят из короткоживущих веток, нарезанных от trunk, с фиксами, сделанными на trunk первыми и cherry-pick-нутыми вниз, чтобы release-ветка никогда не становилась параллельной линией разработки. А изменения, слишком большие для выката за день, используют branch by abstraction — слой абстракции, новую реализацию за ним малыми ежедневными коммитами, переключение, затем зачистку — так что даже шестинедельный рефакторинг живёт на trunk как малые зелёные шаги вместо долгоживущей ветки.