Инженерная практика
Частота интеграции — вот рычаг
Двое инженеров в понедельник ответвляются от одного и того же OrderService. Алиса в понедельник днём выкатывает однострочный фикс: её ветка примиряет полдня чужих коммитов, мёржится за тридцать секунд, готово. Боб три недели держит свою «переделку checkout» на ветке. В день слияния trunk уехал на 600 коммитов под ним — вызываемый им хелпер валидации переименовали, денежный тип сменился с центов-как-int на класс Money, миграция перекроила таблицу. Его фича была закончена ещё на второй неделе. Остальные полторы недели — это налог на интеграцию, и он платит его весь сразу.
Цена слияния — функция разрыва, а не размера диффа
Интуитивная модель — «большую фичу тяжело мёржить, потому что она большая» — неверна, причём важным образом. Цена слияния ветки задаётся расхождением: тем, насколько далеко trunk ушёл от точки, в которой вы ответвились. Фича на 2000 строк, слитая в тот же час, когда вы начали, почти ничего не стоит при слиянии, потому что trunk не сдвинулся. А фикс на 50 строк, слитый через три недели, может стать мучением, потому что эти 50 строк теперь лежат поверх фундамента, перестроенного под ними.
Расхождение растёт лавинообразно, потому что интеграция — это общая система. Пока ваша ветка жива, каждый другой инженер тоже мёржит в trunk. Ветка, живущая один день, примиряет один день чужой работы. Ветка, живущая три недели, примиряет три недели — и хуже того, вы потеряли мысленный контекст для кода, который менялся, пока вы не смотрели. Кривая цены загибается вверх с возрастом ветки, а не линейно. Именно поэтому внутреннее исследование Microsoft показало, что команды, не интегрировавшиеся более трёх дней, тратили примерно в 12× больше времени на разрешение конфликтов, чем команды с ежедневным слиянием, и почему работа над merge-конфликтами съедает, по оценкам, 10–25% времени разработчиков в командах с обилием веток.
Текстовые конфликты — дешёвые
Слияние git помечает текстовый конфликт, когда две ветки правят одни и те же строки — они шумные, и инструмент вас останавливает. Дорогие провалы — это семантические конфликты: обе стороны накладываются чисто, git доволен, а код теперь неправильный. Алиса переименовывает validate(order) в validateOrder(order) и обновляет все 40 мест вызова в trunk. Ветка Боба добавила 41-е место вызова к старому имени. Git мёржит оба без единого маркера конфликта; сборка падает, или, хуже, она компилируется из-за перегрузки и тихо делает не то.
Семантические конфликты масштабируются с той же переменной разрыва. Чем сильнее изменился trunk, пока вас не было, тем больше невидимых допущений, на которых построена ваша ветка, тихо стали ложными. Частая интеграция — единственная дешёвая защита: ветка возрастом в один день могла унаследовать лишь один день сломанных допущений, и ваш CI-гейт ловит их, пока вызвавшее их изменение ещё свежо у кого-то в памяти.
| Возраст ветки при слиянии | Разрыв trunk для примирения | Характер конфликта | У кого ещё есть контекст |
|---|---|---|---|
| Часы | Несколько коммитов | Крошечный, в основном текстовый | У всех — это только что случилось |
| 3+ дня | Десятки коммитов | ~12× времени на разрешение | Угасает |
| Недели | Сотни коммитов | Семантические + текстовые, big-bang | Ни у кого — код менялся под вами |
Частота — рычаг; инструменты — погрешность округления
Команды в боли от слияний первым делом тянутся к лучшему merge-инструменту. Это не та ручка. Более умный трёхсторонний дифф снижает цену разрешения данного конфликта, но не может снизить его размер — он зафиксирован тем, насколько уехал trunk, а это зафиксировано тем, сколько вы ждали. Три недели расхождения — это три недели расхождения, разрешаете вы их в vim или в навороченном GUI. Рычаг, который реально двигает цену, — тот, что управляет разрывом: интегрируйся чаще.
Это переосмысляет «непрерывную интеграцию» — не как «у нас есть CI-сервер». CI — это практика: каждый разработчик мёржит малые изменения в общий trunk хотя бы ежедневно — а сервер лишь гейт, делающий практику безопасной. Принцип малых партий из lean проявляется здесь точь-в-точь: малые партии вскрывают проблемы рано и дёшево; большие прячут их, пока они не сдетонируют вместе. Ежедневное слияние — малая партия. Трёхнедельная ветка — одна огромная партия, которую нельзя «расвыкатить».
Почему это работает
«Интегрируйся ежедневно» звучит как занудство про продуктивность, но на самом деле это утверждение о задержке обратной связи. Каждый час, что ваша ветка расходится с trunk, — это час решений, принимаемых на устаревшей информации: вы вызываете функции, которых больше нет, опираетесь на инварианты, которые только что сломали. Сокращение времени жизни ветки сокращает окно, в котором вы можете ошибаться, не зная об этом. В этом вся игра.
Слияния в вашей команде стабильно болезненны. Трёхнедельная ветка только что заняла у двух инженеров целый день, чтобы её приземлить. Какое изменение даёт наибольший рычаг?
Почему крошечное изменение на 50 строк может оказаться тяжелее для слияния, чем большое изменение, закоммиченное в тот же час, когда вы ответвились?
Что делает семантический конфликт опаснее текстового?
Упорядочьте, как цена слияния нарастает на долгоживущей ветке:
- 1 Вы ответвляетесь от trunk; расхождение нулевое, слияние было бы бесплатным
- 2 Проходят дни; коллеги мёржат десятки коммитов в trunk
- 3 Код, от которого вы зависите, переименовали или поменяли его инварианты
- 4 Ваша ветка теперь несёт устаревшие допущения, которых вы не видите
- 5 День слияния: текстовые + семантические конфликты детонируют вместе, контекста нет ни у кого
- 01Коллега говорит: «большие фичи тяжело мёржить, потому что они большие». Поправьте модель и объясните, что на самом деле определяет цену слияния.
- 02Почему «интегрируйся ежедневно» — это на самом деле утверждение о задержке обратной связи, и как сюда вписываются семантические конфликты?
Цена слияния ветки управляется разрывом — тем, насколько уехал trunk с момента ответвления, — а не размером вашего собственного изменения. Разрыв растёт лавинообразно, потому что интеграция общая: каждый другой инженер продолжает коммитить, пока живёт ваша ветка, так что ветка в один день примиряет день работы, а трёхнедельная — три недели плюс потерянный контекст, и именно поэтому время разрешения конфликтов растёт примерно в 12× после трёх дней и может съедать четверть времени разработчиков. Текстовые конфликты — дешёвые и шумные; дорогие провалы — это семантические конфликты, которые мёржатся чисто и ломаются тихо, и они масштабируются с тем же разрывом. Лучший merge-инструмент снижает усилие на конфликт, но никогда — его размер, поэтому единственный реальный рычаг — частота: интегрируйся в trunk хотя бы ежедневно, держи ветки живущими часы, а не недели, и относись к малым партиям как к механизму, который вскрывает проблемы рано, а не детонирует их вместе. Непрерывная интеграция — это и есть та практика, а сервер лишь гейт, делающий её безопасной.