Разработка СУБД ЛИНТЕР и ПО на заказ
32,73
рейтинг
22 мая 2015 в 14:06

Разработка → Модель ветвления и управления модулями git для большого проекта

Без малого два года назад мы начали использовать в разработке нашего флагманского проекта СУБД ЛИНТЕР новую модель ветвления и управления подмодулями git-а. Десятки тысяч коммитов, сделанные за это время группой разработчиков, позволяют с определенной долей уверенности считать нововведения успешными. Эта статья — краткий обзор принципов организации хранилища исходных кодов в большом проекте на базе альтернативной реализации модулей git, сложившейся стратегии ветвления и инструментария linflow.



Монорепозиторий, git submodules, git subtree или...


Раньше исходные коды ЛИНТЕР-а хранились в CVS. Несмотря на моральное устаревание этой системы контроля версий, она обладала определенными особенностями, которые мы активно использовали (отчасти это позволило продержаться этому «динозавру» так долго в строю): для работы над конкретной задачей можно было извлечь только необходимые модули с его зависимостями. Это удобно, поскольку модули в нашем проекте имеют преимущественно взаимно низкое и свободное сопряжение.

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

#RepositoryDir	Unix 
                                
RELAPI relapi 
LINDESKX lindeskx 
KERNEL5/SQL sql 
KERNEL5/TSP tsp

Нетрудно заметить, что правила определяли не только какие модули следует извлекать, но и куда их извлекать, т. е. дерево в центральном репозитории и дерево в рабочей копии отличались. Эти файлы-описатели менялись для разных целевых дистрибутивов, операционных систем и версий СУБД.

Но, как известно, git не предоставляет простого механизма клонирования части репозитория. Поэтому, когда стал вопрос о миграции с CVS на git, в первую очередь мы рассматривали два самых очевидных способа его организации: использовать единое хранилище (монорепозиторий) для всего дерева проекта с неизбежным внесением изменений в процесс сборки продукта или хранить проект в совокупности независимых модулей и использовать git-овские submodule/subtree для работы с ними.

Монорепозиторий


От идеи использования одного хранилища для всего дерева проекта отказались практически сразу. И на то были веские причины:
  • Производительность. Нет, у нас не было 1.3 миллиона файлов, как в тестах от Facebook (http://habrahabr.ru/post/137615/), но и 114800 коммитов экспортированной истории для ~14000 файлов оказалось достаточным, чтобы зафиксировать заметное падение производительности при работе с индексом.
  • История. Монорепозиторий имеет общую историю правок: в одной цепочке логов могут быть перемешены правки ядра, примеров, утилит, документации и т. п.
  • Поддержка. Унификация дерева исходных кодов привела бы к изменению механизмов сборки для всех версий продукта, выпущенных с конца прошлого века. Разработка этих релизов, конечно, уже не ведется, но лишаться возможности «сдуть пыль» с архивной версии совсем не хотелось.


git submodules / git subtree


Если с точки зрения хранения группы модулей в главном репозитории трудностей не возникало, то с их клонированием в правильное рабочее дерево были сложности. Конечно, git поддерживает нативные submodules и subtree, однако недостатков в их использовании хватает (см. http://habrahabr.ru/post/75964/), таковы уж архитектурные особенности этой системы контроля версий. Самым же неприятным моментом оказалась необходимость следить, чтобы в главный репозиторий модуля-контейнера не попадали ссылки на непубличные состояния дочерних модулей. Так, поработав с экспериментальным репозиторием, мы пришли к тому, что нам нужен альтернативный механизм управления модулями на стороне клиента.

linmodules


Для устранения недостатков нативных git submodules и git subtree мы разработали собственный механизм управлением модулями, который функционирует над уровнем git-а. Реализация этого механизма стала частью инструментария, названного нами linflow.

Схема достаточна проста: каждый из модулей проекта хранится в отдельном репозитории с экспортированной историей, а один из модулей (в нашем случае он имеет имя linter.git) является модулем-контейнером, который не содержит каких-либо исходных кодов и его основная задача — задавать дерево глобальных ветвей для всех остальных. На каждой из этих глобальных ветвей в модуле-контейнере может находиться свой файл-описатель (с именем .linmodules), необходимый для извлечения корректного дерева проекта.

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


Иллюстрация 1: Порядок получения варианта дерева исходников. 1 — клонирование модуля-контейнера с файлом-описателем, 2 — инициализация, 3 — клонирование зарегистрированных модулей в целевые директории.

Синтаксис описания шаблона (файл .linmodules) полностью повторяет «родной», который используется в git-е для файла .gitmodules. Сделано это было намеренно с целью обратной совместимости.

Приведем фрагмент шаблона размещения с иллюстрации 1:

[submodule "tick"] 
      path = lib/tick 
      url = git@linter-git.common.relex.ru:TICK 
[submodule "odbc"] 
      path = odbc 
      url = git@linter-git.common.relex.ru:ODBC 
[submodule "inl"] 
      path = app/inl 
      url = git@linter-git.common.relex.ru:INL

Таким образом, нам удалось сохранить возможность извлечения модулей в произвольную структуру рабочей копии. Первоначальное формирования шаблонов и их последующее изменение производится средствами linflow.

Модель ветвления


Не будет большой ошибкой предположить, что многие разработчики, искавшие оптимальную организацию своих репозиториев на основе git, знакомы со работой Vincent Driessen «A successful Git branching model» (те же, кто не успел этого сделать всегда могут ознакомиться с оригиналом http://nvie.com/posts/a-successful-git-branching-model/ или переводом на хабре http://habrahabr.ru/post/106912/). Мы не стали исключением и, начав апробацию модели, вносили в нее корректировки, в результате чего пришли к собственной, которая унаследовала некоторые черты «родительской». И хоть заглавие оригинальной статьи не лукавит (модель действительно удачная), но это верно ровно до тех пор, пока не возникает необходимость применить ее к действительно большому проекту с длинной историей.

Причин, по которым «удачная» модель от Vincent Driessen потребовала изменений, было несколько. Приведем только самые важные для нас в порядке их возникновения и решения:
  • Оригинальная модель не уточняет поведения при декомпозиции исходных проектов на подмодули и модули с зависимостями.
  • Ветви релизов не могут быть закрыты пока осуществляется сопровождение продукта, так на момент написания этих строк багфиксы и часть нового функционала вносятся на все версии выпущенные с начала 2009 года. Из-за этого билды разных версий продукта не могут быть представлены единой последовательностью коммитов на какой-либо ветви.
  • Ветви исправлений и функционала могут переноситься на старые версии, которые не содержат всех изменений ветви разработки, поэтому merge попросту невозможен.
  • Подавляющее большинство исправлений содержат один коммит (на момент написания этих строк из 2598 ветвей с исправлениями только 262 имели два и более коммита), поэтому использование слияния по стратегии no-ff, порождающее каждый раз дополнительный merge-коммит, не очень удобно.

Итогом работы над модификацией «удачной» модели стало создание собственной стратегии, которая отчасти наследует некоторые термины, соглашения, именования и рабочие процессы оригинальной. Ради простоты выделим ключевые изменения, которые будут более подробно рассмотрены ниже:
  • Оговорены правила ведения ветвей в подмодулях одного проекта;
  • Изменены правила работы с develop ветвью;
  • Изменены правила выхода версий и, следовательно;
  • Изменены правила ведения релизных ветвей;
  • Изменены правила переноса правок с ветви на ветвь.



Иллюстрация 2: Вариант ветвления и переноса кода в модуле. Релизная ветвь RELEASE#2 является самой новой и позволяет переносить правки с помощью merge, RELEASE#1 поддерживает предыдущую версию и получает изменения выборочно.

Главные ветви


Центральный репозиторий содержит группу ветвей, существующую все время и во всех модулях:
  • release branches — группа ветвей проекта, дополняемая по мере выхода версий;
  • develop(master) — главная ветвь разработки.

Ветви origin/release считаются главными продукционными, т. е. исходный код на них должен позволять выпустить версию или билд в любой момент времени. Ветвь origin/master считается главной производственной, которая содержит все изменения проекта и служит источником для создания origin/release. Когда исходный код в origin/master готов к релизу, изменения должны быть определенным образом перенесены на соответствующие origin/release или породить новую версию, а следовательно — ветвь в origin/release.

Вспомогательные ветви


Помимо главных ветвей, структура репозиториев (как центрального, так и рабочих копий) подразумевает наличие вспомогательных ветвей следующих типов:
  • feature branches — ветви новых функциональностей;
  • fix branches — ветви исправлений.

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


Иллюстрация 3: Распределение ветвей по модулям: главные ветви присутствуют во всех, вспомогательные — только в необходимых.

Общие правила


Общие правила ведения ветвей в центральном и локальных репозиториях:
  • develop(master) содержит стабильный код;
  • develop(master) существует во всех модулях;
  • разработка на ветви develop(master) запрещена;
  • develop(master) хранит код, необходимый для выпуска новой версии или релиза;
  • при необходимости нового релиза от develop(master) ветвятся release branches;
  • release branches хранят код для выпуска нового билда;
  • выпуски билдов отмечаются тегами на головных коммитах соответствующих release branches;
  • создание ветви типа release branches в модуле-контейнере порождает создание одноименной ветви в каждом из подмодулей (см. рис. 2);
  • fix branches могут ветвится от develop(master) или release branches и могут вливаться как в develop(master), так и release branches;
  • feature branches ветвятся только от develop(master) и обязательно вливаются в него же;
  • коммиты, составляющие feature branches, в случае необходимости могут быть перенесены на одну или несколько release branches (но этот факт не отменяет предыдущее правило об обязательном слиянии с develop);
  • ветви feature branches и hotfix branches регулярно публикуются в центральном репозитории.


Ветви релизов/версий (release branches)


Ветви релизов (release branches) именуются как release/Blinter_AB_C, где A — мажорная версия, B — минорная, а С — номер релиза. Ветви релизов порождаются от develop и существуют все время поддержки версии ЛИНТЕР-а. Ветвь является реципиентом кода: какая-либо разработка в ней не ведется. Каждый факт выпуска нового билда отмечается соответствующим тегом вида Blinter_AB_C_D, где D — номер сборки. Ветви этого типа могут являться ссылками (с точки зрения организации на origin) на другую релизную ветвь. В этом случае публикация в одну из таких ветвей приведет к обновлению всех связанных. Релизная ветвь является глобальной, т. е. существует во всех модулях, если создана в модуле-контейнере. Теги с метками билда выставляются единовременно во всех модулях.

Ветви исправлений (fix branches)


Ветви исправлений (fix branches) именуются как hotfix/*, могут порождаться от develop (преимущественно) или release, могут вливаться в develop(master) и release. Если исправления содержат один коммит, то слияние осуществляется без создания merge коммита. Итоговый коммит в теле комментария содержит отсылку к номеру соответствующего тикета в багтрекере. После переноса правок ветвь исправления закрывается.

Ветви функциональности (feature branches)


Ветви функциональности именуются как feature/* и порождаются только от develop(master).
Ветви функциональностей (feature branches) используются для разработки новых функций, которые должны появиться в текущем или будущем релизах. Ветвь существует так долго, сколько продолжается разработка функциональности. По мере достижения промежуточных результатов ветвь публикуется в центральном репозитории. Когда работа в ветви завершена, последняя обязательно вливается в главную ветвь разработки (что означает, что функциональность будет добавлена в следующий релиз) и опционально — в релизные ветви. После переноса кода ветвь функциональности закрывается.

linflow


Стоит сказать несколько слов об инструментарии linflow, который упоминался несколько раз выше по тексту. Linflow предназначен для операций с модулями дерева исходных кодов, а также для поддержки нашей модели ветвления. Клиентская часть linflow — это форк проекта git-flow (https://github.com/nvie/gitflow), который был изменен для нашей стратегии и расширен для поддержки linmodules. Кроме того, нами была разработана и серверная часть, которая работает как расширение для gitolite (http://gitolite.com).

Функционал управления модулями в linflow позволяет:
  • регистрировать/удалять модули;
  • редактировать источник и целевую директорию существующего модуля;
  • производить первоначальную настройку рабочей копии;
  • отслеживать состояние модуля-контейнера и своевременно переключать и обновлять вложенные модули;
  • производить упаковку модулей;
  • осуществлять проверку на согласованность всего дерева проекта.

Функционал управления ветвями в linflow позволяет:
  • создавать/удалять/публиковать ветви всех разрешенных типов;
  • производить контроль над исполнением соглашения об именовании ветвей;
  • согласованно переключаться на ветви и теги во всех модулях вслед за модулем-контейнером;
  • осуществлять массовые операции над модулями;
  • переносить код с ветви на ветвь с использованием различных стратегий;
  • переносить код на ветви с измененной историей;
  • предупреждать ошибочное удаление ветвей.

Функционал серверной части позволяет:
  • осуществлять контроль над соблюдением правил именования;
  • разграничивать права пользователей по ролям;
  • управлять ветвями-ссылками;
  • производить рассылку уведомлений об изменениях по динамически формируемому списку потенциально заинтересованных участников;
  • производить полное резервное копирование.

Вопрос о возможности публикации полной технической документации на модель ветвления и средств linflow в настоящий момент обсуждается. Не последнюю роль в этом могут сыграть отклики (или их отсутствие) на эту публикацию.
Автор: @relexru
РЕЛЭКС
рейтинг 32,73
Разработка СУБД ЛИНТЕР и ПО на заказ

Комментарии (8)

  • +1
    Вы написали, что linflow позволяет
    согласованно переключаться на ветви и теги во всех модулях вслед за модулем-контейнером
    .
    Вопрос — кто принимает решение о переключении на новую версию подмодуля или на новую ветку подмодуля?
    У нас похожая схема (только через классические подмодули), но периодически вознимает проблема, что несколько человек передвигают указатели на подмодули, и, соответственно, возникает конфликты. Они решаемы, но присутствует некая головная боль, из-за которой все подозревают git в глупости.
    А так — весьма любопытно, но как мне показалось — слегка поверхностно про linmodules, явно из статьи не следует преимущество перед классической схемой.
    • +1
      Вопрос — кто принимает решение о переключении на новую версию подмодуля или на новую ветку подмодуля?

      В зависимости от ситуации: если мы находимся в главном модуле-контейнере, то если переходим на develop/master или релизную ветвь linflow обойдет все зарегистрированные подмодули и предпримет попытку переключить и их на одноименную ветвь. Если переключение идет на feature или hotfix, то обхода не будет. Это поведение по умолчанию, но оно может быть изменено при необходимости переданными ключами.

      но периодически вознимает проблема, что несколько человек передвигают указатели на подмодули

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

      linmodules обеспечивает возможность работать с каждым модулем независимо, обеспечивая при этом массовую обработку (переключение, update, pull/push и т.п.) при необходимости. Но самое важное для нашего случая — возможность просто извлекать и оперировать частью большого репозитория, так, например, для работы над ядром ЛИНТЕР-а можно извлечь ~10 подмодулей а не тащить весь репозиторий из 100+ подмодулей.
  • –11
    Баян
  • 0
    Ветви релизов не могут быть закрыты пока осуществляется сопровождение продукта, так на момент написания этих строк багфиксы и часть нового функционала вносятся на все версии выпущенные с начала 2009 года.

    Не понял момента с незакрываемостью веток релизов. Вы ведь когда-то делаете, собственно, релиз. Значит можно закончить ветку release/1.0.0, а в случае дополнение сделать из неё ветку release/1.0.1? А если вы не закрываете ветку – как вы помечаете факт релиза?
    • 0
      Прошу прощения, немного промахнулся с ответом — он в моем комментарии ниже.
  • 0
    Каждая релизная ветвь порождается от develop, а не от предыдущей. Факт релиза отмечается тегом на ветви.
    Допустим, что ваш пример (release/1.0.0 и release/1.0.1) отвечает порядку нумерации версии major.minor.maintenance, тогда репозиторий будет иметь одну релизную ветвь release/1.0 и два тега release_1.0.0 и release_1.0.1, которые указывают на соответствующие моменты выхода версий. Соответственно, следующая релизная ветвь будет необходима, только когда потребуется выпустить версию release/1.1.0
    Возможно, было бы правильнее называть релизные ветви ветвями версий, но терминология досталась «по наследству» от оригинальной работы Vincent Driessen-а и прижилась.
    • 0
      Ну то есть у вас под каждую версию по сути свой мастер, в который мы добавляете изменения, как я понял, патчами. И сразу же ставить тег релиза, так как, как было указано выше, в «релизных» ветках у вас код всегда готов поехать на бой. То есть это все-равно, что коммитить в мастер. Чем вам не нравится идея делать ветки следующий релизов от старых релизов (для того, чтобы не забирать изменения из мастера, а в мастер отправлять все изменения в старых релизах? Кстати, да, не совсем понятно, как изменения из релизных веток попадают в мастер. Только параллельно, патчами из фича-веток?
      • 0
        Ну то есть у вас под каждую версию по сути свой мастер, в который мы добавляете изменения, как я понял, патчами.


        В нашей ситуации все-таки релизные ветви не совсем можно назвать мастером, поскольку подавляющее большинство hotfix-ов глобальные, т. е. затрагивают все актуальные версии, поэтому они (hotfix branches ) чаще всего заводятся от develop, вливаются в него же и потом расходятся по актуальным release branches.
        Что касается переноса изменений, то разработчиков не ограничивают какими средствами это делать, но если использовать linflow, то там реализовано две стратегии: для feature — это простой merge, а для hotfix производится поиск родительской ветви и точки ветвления от нее, а затем диапазон коммитов от этой точки до HEAD переносятся cherry-pick -ом в целевую.

        Чем вам не нравится идея делать ветки следующий релизов от старых релизов (для того, чтобы не забирать изменения из мастера, а в мастер отправлять все изменения в старых релизах?


        Почему же не нравится, хорошая идея, единственное что смущает — потенциальное усложнение истории и структуры ветвей. Нам такой вариант не подойдет еще по специфической для проекта причине: ЛИНТЕР выпускается в четырех редакциях с разным функционалом, каждая из которых еще имеет несколько поддерживаемых релизов, соответственно как минимум 4 ветви (самые свежие версии в редакциях) регулярно получают одинаковые правки, логично, что они получают их с мастера, а не с «соседней» release baranch.

        Кстати, да, не совсем понятно, как изменения из релизных веток попадают в мастер. Только параллельно, патчами из фича-веток?


        Они из релизных ветвей в мастер не должны попадать: новый функционал (feature ветви) создаются только от develop(master) и, если и переносятся на уже вышедший релиз, то release branch в этом случае — репициент. Аналогичная ситуация с упомянутыми выше «глобальными» исправлениями — они тоже создаются от develop(master). В тех же случаях, когда hotfix затрагивает только одну версию/релиз, то ветвь заводится от release, в нее же вливается, а на develop(master) эти правки не нужны.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка