Noveo

Наш блог Подходы к управлению ветками в системах контроля версий (часть 1)

Подходы к управлению ветками в системах контроля версий (часть 1)

Мартин Фаулер (Martin Fowler) объединил в одной статье максимум информации о работе с ветками в системах контроля версий — получилась настоящая мини-энциклопедия! Предлагаем вашему вниманию перевод первой ее части, посвященной базовым паттернам. Следите за обновлениями! ;)

Современные системы управления исходным кодом предоставляют мощные инструменты, облегчающие создание ветвей в исходном коде. Но в конце концов эти ветки приходится объединять (merge) обратно, и многие команды тратят неоправданно много времени на то, чтобы разобраться в хитросплетениях веток. Существует несколько подходов, которые могут позволить командам эффективно использовать ветки, концентрируясь на интеграции работы нескольких разработчиков и организации пути к выпускам в продакшн. Общей темой является то, что ветви должны часто интегрироваться, а усилия должны быть сосредоточены на основной ветке, которая может быть развернута в продакшн с минимальными усилиями.

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

Такое разделение разработки на направления (ветки), которые разделяются и сливаются, является основным для рабочего процесса команд разработчиков ПО, и появилось несколько подходов для того, чтобы помочь нам держать руку на пульсе всей этой деятельности. Как и с большинством подходов к разработке ПО, лишь немногие из них являются золотыми стандартами, которым должны следовать все команды. Рабочий процесс разработки ПО очень зависит от контекста, в частности от социальной структуры команды и других практик, которым следует команда.

Моя задача в этой статье — обсудить эти паттерны, и я делаю это, описывая паттерны и чередуя их объяснения с повествовательными разделами, которые лучше объясняют контекст и взаимосвязи между ними. Чтобы облегчить их различение, я выделил секции с описанием подходов с помощью символа «✣».

Базовые паттерны

Анализируя паттерны, я нахожу полезным выделить две основные категории. Одна группа ориентирована на интеграцию, на то, как несколько разработчиков объединяют свою работу в единое целое. Другая смотрит на путь к продакшн, используя разветвление, чтобы помочь управлять маршрутом от интегрированной кодовой базы к функционирующему продукту. Некоторые паттерны относятся к обеим группам, и я буду рассматривать их как базовые. Кроме того, остается пара паттернов, которые не являются ни фундаментальными, ни принадлежащими к одной из групп — поэтому я оставлю их напоследок.

Разветвление исходного кода

Создайте копию и сохраните в ней все изменения.

Если несколько человек работают над одной и той же кодовой базой, быстро становится невозможным работать над одними и теми же файлами. Если я хочу запустить компиляцию, а мой коллега как раз пишет выражение, то компиляция будет неудачной. Нам придется кричать друг другу: «Я компилирую, ничего не меняй!» Даже двум разработчикам это было бы трудно, а в большой команде это был бы полный хаос.

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

Система управления исходным кодом значительно облегчает этот процесс. Ключевой момент — каждое изменение, сделанное в каждой ветке, записывается в ней в виде коммита. Это не только гарантирует, что никто не забудет небольшое изменение, сделанное в utils.java, но и облегчает мерж, особенно когда несколько человек внесли изменения в один и тот же файл.

Это приводит меня к определению ветки, которое я буду использовать для этой статьи: я определяю ветку как определенную последовательность коммитов в базу кода. Вершина (head, or tip) ветки — это последний коммит в этом ряду.

Martin Fowler Source Code Management Patterns

Это существительное, но есть и глагол «разветвлять». Под этим я подразумеваю создание новой ветки, которое также можно рассматривать как разделение исходной ветки на две. Ветки мержатся, когда коммиты из одной ветки применяются к другой.

Martin Fowler Source Code Management Patterns

Определения, которые я использую для «ветки», соответствуют моим наблюдениям о том, как использует это понятие большинство разработчиков. Но системы управления исходным кодом склонны использовать «ветку» в более узком смысле.

Я могу проиллюстрировать это типичной ситуацией в современной команде разработчиков, которая хранит свой исходный код в общем git-репозитории. Одному разработчику, Алле, нужно внести несколько изменений, поэтому она клонирует этот git-репозиторий и синхронизирует мастер-ветку. Она делает пару изменений и коммитит их обратно в свою мастер-ветку. Тем временем другой разработчик, давайте назовем ее Виолеттой, клонирует репозиторий на свой компьютер и выгружает мастер-ветку. Алла и Виолетта работают на одной ветке или на разных? Они обе работают над «мастер-веткой», но их коммиты независимы друг от друга и должны быть объединены, когда они пушат свои изменения обратно в общий репозиторий. Что произойдет, если Алла решит, что она не уверена во внесенных изменениях, добавит тег на последний коммит и восстановит свою мастер-ветку до исходного состояния (последний коммит, клонированный ею из общего репозитория)?

Martin Fowler Source Code Management Patterns

В соответствии с определением ветки, которое я дал ранее, Алла и Виолетта работают над отдельными ветками — как относительно друг друга, так и относительно мастер-ветки на общем репозитории. Когда Алла завершает свою работу, ставя тег, это всё равно ветка по моему определению (и она вполне может думать о ней как о ветке), но на языке git’а это строчка кода с тегом.

С распределенными системами управления версиями, такими как git, это означает, что мы также получаем дополнительные ветки всякий раз, когда в дальнейшем клонируем репозиторий. Если Алла клонирует свой локальный репозиторий, чтобы поставить его на ноутбук и поработать в электричке до дома, она создает третью мастер-ветку. То же самое происходит и с вилкой в github — каждый форкнутый репозиторий имеет свой собственный дополнительный набор веток.

Эта терминологическая путаница усиливается, когда мы сталкиваемся с разными системами контроля версий, так как все они имеют свои собственные определения того, что представляет собой ветка. Ветка в Mercurial сильно отличается от ветки в git’е, которая по понятиям Mercurial является скорее закладкой. Mercurial также может иметь ветку с безымянными вершинами, и пользователи из Mercurial часто создают ветки, клонируя репозитории.

Вся эта терминологическая путаница приводит к тому, что некоторые и вовсе избегают этого термина. Здесь будет полезен более общий термин — состояние кода (codeline). Я понимаю состояние кода как определенную последовательность версий кодовой базы. Она может заканчиваться тегом, быть веткой или теряться в git’овском reflog’е. Вы заметите сильное сходство между моими определениями ветки и состояния кода. Во многих отношениях codeline — более полезный термин, и я использую именно его, но он не так широко используется на практике. Поэтому для данной статьи, если я не буду входить в конкретный контекст терминологии git (или другого инструмента), я буду использовать понятия ветки и состояния кода как взаимозаменяемые.

Следствием этого определения является то, что какую бы систему управления версиями вы ни использовали, каждый разработчик имеет как минимум одну персональную codeline в рабочей копии на своей машине, как только он вносит локальные изменения. Если я клонирую git-репозиторий проекта, подтягиваю мастер-ветку и обновляю некоторые файлы — это новое состояние кода, даже если я еще ничего не закоммитал. Аналогично, если я делаю свою рабочую копию транка репозитория в Subversion, то эта рабочая копия является самостоятельным состоянием кода, даже если я не работаю с веткой Subversion.

Когда это использовать

Есть такая бородатая шутка: если вы упадете с высокого здания, падение вам не страшно, а вот приземление — очень даже. Так же с исходным кодом: делать ветки легко, мержить сложнее.

Системы управления исходным кодом, которые записывают каждое изменение на коммите, действительно облегчают процесс слияния, но не делают его тривиальным. Если Алла и Виолетта обе изменят имя переменной, но на разные имена, возникнет конфликт, который система управления исходным кодом не может разрешить без участия человека. И это еще цветочки; по крайней мере, такой текстовый конфликт может быть замечен системой управления исходным кодом, и она предупредит людей, что нужно вмешаться. Но конфликты часто возникают там, где текст мержится без проблем, а система все равно не работает. Представьте, что Алла меняет имя функции, а Виолетта добавляет в свою ветку код, вызывающий эту функцию под ее старым именем. Это то, что я называю семантическим конфликтом. Когда случаются такие конфликты, система может не собраться, или собраться, но не заработать.

Martin Fowler Source Code Management Patterns
Джонни Леруа (Jonny LeRoy) любит подчеркнуть, что этот способ рисования диаграмм разветвления (которым пользуюсь и я сам) — неверный.

Проблема знакома каждому, кто работал с параллельными или распределенными вычислениями. У нас есть некоторое общее состояние (база кода) с разработчиками, делающими обновления параллельно. Нам нужно как-то комбинировать их, сериализуя изменения в некое единое обновление. Наша задача усложняется тем, что для корректного выполнения и запуска системы нужны очень сложные критерии валидности этого общего состояния. Невозможно создать детерминированный алгоритм для поиска бесконфликтного объединения. Его необходимо найти людям, и решение может лежать в объединении отдельных частей разный обновлений. Часто разрешить конфликты могут только новые обновления.

Начну с допущения: а если бы не было веток? Все бы редактировали живой код, полуготовые изменения загромождали бы систему, люди бы буквально наступали друг на друга. А мы даём людям иллюзию замороженного времени, представление, что они единственные, кто изменяет систему, и эти изменения могут подождать, пока они не будут полностью готовы, прежде чем рисковать системой. Но это иллюзия, и в конечном счете за нее приходится платить. Кто платит? Когда? Сколько? Вот чем отличаются эти паттерны: альтернативами оплаты.

    — Кент Бек (Kent Beck)

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

Основная ветвь (mainline)

Единая общая ветвь, которая представляющая собой актуальное состояние продукта.

Основная ветвь — это особое состояние, которую мы рассматриваем как актуальное состояние кода команды. Всякий раз, когда я захочу начать новую работу, я буду выгружать код из главной ветки в свой локальный репозиторий. Всякий раз, когда я захочу поделиться своей работой с остальными членами команды, я буду обновлять эту ветку, добавляя свою работу, в идеале используя подход Mainline Integration, который мы обсудим чуть ниже.

Разные команды используют разные имена для этой специальной ветки, часто поощряемые соглашениями используемых систем контроля версий. Пользователи git’а часто называют его «master», пользователи Subversion обычно называют его «trunk» (ствол).

Я должен подчеркнуть, что магистраль — это именно единое, общее состояние. Когда люди говорят о «мастере» в git’е, они могут иметь в виду несколько разных вещей, так как у каждого клона репозитория есть свой локальный мастер. Обычно такие команды имеют исходный репозиторий, а его мастер и есть основная ветвь. Начинать новую работу с нуля означает клонировать этот исходный репозиторий. Если у меня уже есть клон, я начинаю с выгрузки мастера, чтобы он соответствовал основной ветке.

Пока я работаю над своей фичей, у меня есть своя личная ветка разработки, которая может быть моим локальным мастером, или я могу создать отдельную локальную ветку. Если я работаю над этим в течение некоторого времени, я могу следить за изменениями в основной ветке, выгружая ее изменения через определенные промежутки времени и объединяя их с моей личной веткой разработки.

Аналогично, если я хочу создать новую версию продукта для релиза, я могу начать с текущей основной ветки. Если мне нужно исправить баги, чтобы сделать продукт достаточно стабильным для релиза, я могу использовать Release Branch.

Когда это использовать

Я помню, как собирался поговорить с DevOps-разработчиком клиента в начале 2000-х. Его задачей была сборка продукта, над которым работала команда. Он посылал электронное письмо каждому члену команды, и они отвечали, посылая различные файлы из своей кодовой базы, которые были готовы к интеграции. Затем он копировал эти файлы в дерево интеграции и пытался скомпилировать кодовую базу. Обычно ему требовалась пара недель, чтобы создать сборку, которая бы скомпилировалась и была готова к какому-то тестированию.

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

Одной из альтернатив основной ветке является Release Train.

Здоровая ветка

При каждом коммите выполняйте автоматические проверки, например, сборку проекта и запуск тестов, чтобы убедиться в отсутствии дефектов в ветке.

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

Чтобы бороться с этим, мы можем стремиться сохранить ветку здоровой — это означает, что она успешно собирается, а продукт работает с небольшим количеством ошибок или совсем без них. Для обеспечения этого я считаю критически важным написание самотестирующегося кода. Такая практика разработки означает, что по мере написания продакшн-кода мы также пишем комплекс автоматизированных тестов, чтобы быть уверенными: если эти тесты пройдут, то код не будет содержать ошибок. Если мы это сделаем, то с каждым коммитом мы сможем сохранять ветку стабильной, запуская сборку, включающую в себя этот набор тестов. Если система не компилируется, или тесты не проходят, то нашим приоритетом номер один будет исправить их перед тем, как мы сделаем что-либо ещё в этой ветке. Часто это означает, что мы «замораживаем» ответвление — не разрешается делать никаких коммитов, кроме исправлений, делающих ветку снова была здоровой.

Необходимое количество тестов для подобных проверок всегда спорно. Некоторые тесты занимают много времени при запуске, тем самым «задерживая» общий результат проверок. Зачастую команды выделяют такие тесты в несколько небольших групп, которые запускают при сборке проекта. Первая стадия этих тестов должна выполняться быстро, обычно не более десяти минут, но при этом быть достаточно полной. Я имею в виду набор тестов, которые запускаются на каждое изменение (часто это юнит-тесты).

В идеале на каждом коммите должен быть запущен полный набор тестов. Однако, если тесты медленные, например, тесты производительности, которые занимают сервер на пару часов, это непрактично. В наши дни команды обычно могут выделить группу тестов, которые могут быть запущены на каждом коммите, и запускать остальную часть тестов при сборке.

Если код работает без ошибок, это еще не значит, что он хороший. Для того, чтобы поддерживать стабильный темп поставки, мы должны держать внутреннее качество кода на высоком уровне. Самый популярный способ — это просмотр кода (Code Review) другим человеком, хотя, как мы увидим, есть и другие альтернативы.

Когда это использовать

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

Здоровая основная ветка также упрощает доставку кода в продакшн. Новый релиз всегда может быть собран из последней версии главной ветки. У лучших команд уходит совсем немного усилий на то, чтобы стабилизировать такую кодовую базу, которая часто позволяет делать релиз в продакшн непосредственно из основной ветки.

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

Ваши собственные ветки разработки тоже имеет смысл сохранять стабильными, так как это делает возможным Diff Debugging. Но это желание идет вразрез с идеей частых коммитов для сохранения состояния. Я могу сделать контрольную точку даже при неудачной компиляции, если собираюсь попробовать другую идею в коде. Такие ситуации я решаю удалением любых мусорных коммитов после того, как я закончил свою непосредственную работу. Таким образом, через несколько часов в моей ветке останутся только желаемые коммиты.

Если я поддерживаю свою личную ветку здоровой, то это также значительно облегчает работу с основной веткой — я знаю, что любые ошибки, возникающие при интеграции с ней, происходят исключительно из-за проблем с интеграцией, а не из-за ошибок в моем коде. Это делает поиск и исправление ошибок намного быстрее и проще.

Оригинал: https://martinfowler.com/articles/branching-patterns.html#BasePatterns

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

НазадПредыдущий пост ВпередСледующий пост

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: