Монорепозиторий, 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 в настоящий момент обсуждается. Не последнюю роль в этом могут сыграть отклики (или их отсутствие) на эту публикацию.
Комментарии (8)
coylOne
25.05.2015 18:22Ветви релизов не могут быть закрыты пока осуществляется сопровождение продукта, так на момент написания этих строк багфиксы и часть нового функционала вносятся на все версии выпущенные с начала 2009 года.
Не понял момента с незакрываемостью веток релизов. Вы ведь когда-то делаете, собственно, релиз. Значит можно закончить ветку release/1.0.0, а в случае дополнение сделать из неё ветку release/1.0.1? А если вы не закрываете ветку – как вы помечаете факт релиза?npechenkin
25.05.2015 19:16Прошу прощения, немного промахнулся с ответом — он в моем комментарии ниже.
npechenkin
25.05.2015 19:10Каждая релизная ветвь порождается от 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-а и прижилась.coylOne
27.05.2015 13:33Ну то есть у вас под каждую версию по сути свой мастер, в который мы добавляете изменения, как я понял, патчами. И сразу же ставить тег релиза, так как, как было указано выше, в «релизных» ветках у вас код всегда готов поехать на бой. То есть это все-равно, что коммитить в мастер. Чем вам не нравится идея делать ветки следующий релизов от старых релизов (для того, чтобы не забирать изменения из мастера, а в мастер отправлять все изменения в старых релизах? Кстати, да, не совсем понятно, как изменения из релизных веток попадают в мастер. Только параллельно, патчами из фича-веток?
npechenkin
27.05.2015 15:20Ну то есть у вас под каждую версию по сути свой мастер, в который мы добавляете изменения, как я понял, патчами.
В нашей ситуации все-таки релизные ветви не совсем можно назвать мастером, поскольку подавляющее большинство 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) эти правки не нужны.
yse
Вы написали, что linflow позволяет
.Вопрос — кто принимает решение о переключении на новую версию подмодуля или на новую ветку подмодуля?
У нас похожая схема (только через классические подмодули), но периодически вознимает проблема, что несколько человек передвигают указатели на подмодули, и, соответственно, возникает конфликты. Они решаемы, но присутствует некая головная боль, из-за которой все подозревают git в глупости.
А так — весьма любопытно, но как мне показалось — слегка поверхностно про linmodules, явно из статьи не следует преимущество перед классической схемой.
npechenkin
В зависимости от ситуации: если мы находимся в главном модуле-контейнере, то если переходим на develop/master или релизную ветвь linflow обойдет все зарегистрированные подмодули и предпримет попытку переключить и их на одноименную ветвь. Если переключение идет на feature или hotfix, то обхода не будет. Это поведение по умолчанию, но оно может быть изменено при необходимости переданными ключами.
В нашей практике это случалось довольно часто, поскольку модулей около сотни и над ними работают несколько десятков человек. Дополнительные проблемы были еще с тем, что главный модуль содержит состояние подмодуля ссылаясь на коммит, а не на ветвь. Возможно это и логично, но куда как удобнее, когда главный модуль указывает на ветвь, которая может «жить своей жизнью» без необходимости коммитов в родительский репозиторий своих новых публичных состояний.
linmodules обеспечивает возможность работать с каждым модулем независимо, обеспечивая при этом массовую обработку (переключение, update, pull/push и т.п.) при необходимости. Но самое важное для нашего случая — возможность просто извлекать и оперировать частью большого репозитория, так, например, для работы над ядром ЛИНТЕР-а можно извлечь ~10 подмодулей а не тащить весь репозиторий из 100+ подмодулей.