Лекция
Привет, Вы узнаете о том , что такое контроля версий, Разберем основные их виды и особенности использования. Еще будет много подробных примеров и описаний. Для того чтобы лучше понимать что такое контроля версий, ветка в git, git, системы управления версиями, стратегии слияния веток, слияние веток , настоятельно рекомендую прочитать все из категории Разработка программного обеспечения и информационных систем.
Ядро модели разработки не отличается от большинства существующих моделей. Центральный репозиторий содержит две главные ветки, существующие все время.
Ветвь master создается при инициализации репозитория, что должно быть знакомо каждому пользователю Git. Параллельно ей также мы создаем ветку для разработки под названием develop.
Мы считаем ветку origin/master главной. То есть, исходный код в ней должен находиться в состоянии production-ready в любой произвольный момент времени.
Ветвь origin/develop мы считаем главной ветвью для разработки. Хранящийся в ней код в любой момент времени должен содержать самые последние изданные изменения, необходимые для следующего релиза. Эту ветку также можно назвать «интеграционной». Она служит источником для сборки автоматических ночных билдов.
Когда исходный код в ветви разработки (develop) достигает стабильного состояния и готов к релизу, все изменения должны быть определенным способом влиты в главную ветвь (master) и помечены тегом с номером релиза. Ниже мы рассмотрим этот процесс в деталях.
Следовательно, каждый раз, когда изменения вливаются в главную ветвь (master), мы по определениюполучаем новый релиз. Мы стараемся относиться к этому правилу очень строго, так что, в принципе, мы могли бы использовать хуки Git, чтобы автоматически собирать наши продукты и выкладывать их на рабочие сервера при каждом коммите в главную ветвь (master).
Помимо главных ветвей master и develop, наша модель разработки содержит некоторое количество типов вспомогательных ветвей, которые используются для распараллеливания разработки между членами команды, для упрощения внедрения нового функционала (features), для подготовки релизов и для быстрого исправления проблем в производственной версии приложения. В отличие от главный ветвей, эти ветви всегда имеют ограниченный срок жизни. Каждая из них в конечном итоге рано или поздно удаляется.
Мы используем следующие типы ветвей:
У каждого типа ветвей есть свое специфическое назначение и строгий набор правил, от каких ветвей они могут порождаться, и в какие должны вливаться. Сейчас мы рассмотрим их по очереди.
Конечно же, с технической точки зрения, у этих ветвей нет ничего «специфического». Разбиение ветвей на категории существует только с точки зрения того, как они используются. А во всем остальном это старые добрые ветви Git.
Могут порождаться от: develop
Должны вливаться в: develop
Соглашение о наименовании: все, за исключением master, develop, release-* или hotfix-*
Ветви функциональностей (feature branches), также называемые иногда тематическими ветвями (topic branches), используются для разработки новых функций, которые должны появиться в текущем или будущем релизах. При начале работы над функциональностью (фичей) может быть еще неизвестно, в какой именно релиз она будет добавлена. Смысл существования ветви функциональности (feature branch) состоит в том, что она живет так долго, сколько продолжается разработка данной функциональности (фичи). Когда работа в ветви завершена, последняя вливается обратно в главную ветвь разработки (что означает, что функциональность будет добавлена в грядущий релиз) или же удаляется (в случае неудачного эксперимента).
Ветви функциональностей (feature branches) обычно существуют в репозиториях разработчиков, но не в главном репозитории (origin).
Создание ветви функциональности (feature branch)
При начале работы над новой функциональностью делается ответвление от ветви разработки (develop).
$ git checkout -b myfeature develop
Switched to a new branch "myfeature"
Добавление завершенной функциональности в develop
Завершенная функциональность (фича) вливается обратно в ветвь разработки (develop) и попадает в следующий релиз.
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff myfeature
Updating ea1b82a..05e9557
(Отчет об изменениях)
$ git branch -d myfeature
Deleted branch myfeature (was 05e9557).
$ git push origin develop
Флаг --no-ff вынуждает Git всегда создавать новый объект коммита при слиянии, даже если слияние может быть осуществлено алгоритмом fast-forward. Это позволяет не терять информацию о том, что ветка существовала, и группирует вместе все внесенные изменения. Сравните:
Во втором случае невозможно увидеть в истории изменений, какие именно объекты коммитов совместно образуют функциональность, — для этого придется вручную читать все сообщения в коммитах. Отменить функциональность целиком (т.е., группу коммитов) в таком случае невозможно без головной боли, а с флагом --no-ff это делается элементарно.
Конечно, такой подход создает некоторое дополнительное количество (пустых) объектов коммитов, но получаемая выгода более чем оправдывает подобную цену.
К сожалению, я еще не нашел, как можно настроить Git так, чтобы --no-ff было поведением по-умолчанию при слияниях. Но этот способ должен быть реализован.
Могут порождаться от: develop
Должны вливаться в: develop и master
Соглашение о наименовании: release-*
Ветви релизов (release branches) используются для подготовки к выпуску новых версий продукта. Они позволяют расставить финальные точки над i перед выпуском новой версии. Кроме того, в них можно добавлять минорные исправления, а также подготавливать метаданные для очередного релиза (номер версии, дата сборки и т.д.). Когда вся эта работа выносится в ветвь релизов, главная ветвь разработки (develop) очищается для добавления последующих фич (которые войдут в следующий большой релиз).
Новую ветку релиза (release branch) надо порождать в тот момент, когда состояние ветви разработки полностью или почти полностью соответствует требованиям, соответствующим новому релизу. По крайней мере, вся необходимая функциональность, предназначенная к этому релизу, уже влита в ветвь разработки (develop). Функциональность, предназначенная к следующим релизам, может быть и не влита. Даже лучше, если ветки для этих функциональностей подождут, пока текущая ветвь релиза не отпочкуется от ветви разработки (develop).
Очередной релиз получает свой номер версии только в тот момент, когда для него создается новая ветвь, но ни в коем случае не раньше. Вплоть до этого момента ветвь разработки содержит изменения для «нового релиза», но пока ветка релиза не отделилась, точно неизвестно, будет ли этот релиз иметь версию 0.3, или 1.0, или какую-то другую. Решение принимается при создании новой ветви релиза и зависит от принятых на проекте правил нумерации версий проекта.
Создание ветви релиза (release branch)
Ветвь релиза создается из ветви разработки (develop). Пускай, например, текущий изданный релиз имеет версию 1.1.5, а на подходе новый большой релиз, полный изменений. Ветвь разработки (develop) готова к «следующему релизу», и мы решаем, что этот релиз будет иметь версию 1.2 (а не 1.1.6 или 2.0). В таком случае мы создаем новую ветвь и даем ей имя, соответствующее новой версии проекта:
$ git checkout -b release-1.2 develop
Switched to a new branch "release-1.2"
$ ./bump-version.sh 1.2
Files modified successfully, version bumped to 1.2.
$ git commit -a -m "Bumped version number to 1.2"
[release-1.2 74d9424] Bumped version number to 1.2
1 files changed, 1 insertions(+), 1 deletions(-)
Мы создали новую ветку, переключились в нее, а затем выставили номер версии (bump version number). В нашем примере bump-version.sh — это вымышленный скрипт, который изменяет некоторые файлы в рабочей копии, записывая в них новую версию. (Разумеется, эти изменения можно внести и вручную; я просто обращаю Ваше внимание на то, что некоторые файлы изменяются.) Затем мы делаем коммит с указанием новой версии проекта.
Эта новая ветвь может существовать еще некоторое время, до тех пор, пока новый релиз окончательно не будет готов к выпуску. Об этом говорит сайт https://intellect.icu . В течение этого времени к этой ветви (а не к develop) могут быть добавлены исправления найденных багов. Но добавление крупных новых изменений в эту ветвь строго запрещено. Они всегда должны вливаться в ветвь разработки (develop) и ждать следующего большого релиза.
Закрытие ветви релиза
Когда мы решаем, что ветвь релиза (release branch) окончательно готова для выпуска, нужно проделать несколько действий. В первую очередь ветвь релиза вливается в главную ветвь (напоминаю, каждый коммит в master — это по определению новый релиз). Далее, этот коммит в master должен быть помечен тегом, чтобы в дальнейшем можно было легко обратиться к любой существовавшей версии продукта. И наконец, изменения, сделанные в ветви релиза (release branch), должны быть добавлены обратно в разработку (ветвь develop), чтобы будущие релизы также содержали внесенные исправления багов.
Первые два шага в Git:
$ git checkout master
Switched to branch 'master'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Отчет об изменениях)
$ git tag -a 1.2
Теперь релиз издан и помечен тегом.
Замечание: при желании, Вы также можете использовать флаги -s или -u <ключ>, чтобы криптографически подписать тег.
Чтобы сохранить изменения и в последующих релизах, мы должны влить эти изменения обратно в разработку. Делаем это так:
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Отчет об изменениях)
Этот шаг, в принципе, может привести к конфликту слияния (нередко бывает, что к причиной конфликта является изменение номера версии проекта). Если это произошло, исправьте их и издайте коммит.
Теперь мы окончательно разделались с веткой релиза. Можно ее удалять, потому что она нам больше не понадобится:
$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).
Могут порождаться от: master
Должны вливаться в: develop и master
Соглашение о наименовании: hotfix-*
Ветви для исправлений (hotfix branches) весьма похожи на ветви релизов (release branches), так как они тоже используются для подготовки новых выпусков продукта, разве лишь незапланированных. Они порождаются необходимостью немедленно исправить нежелательное поведение производственной версии продукта. Когда в производственной версии находится баг, требующий немедленного исправления, из соответствующего данной версии тега главной ветви (master) порождается новая ветвь для работы над исправлением.
Смысл ее существования состоит в том, что работа команды над ветвью разработки (develop) может спокойно продолжаться, в то время как кто-то один готовит быстрое исправление производственной версии.
Создание ветви исправлений (hotfix branch)
Ветви исправлений (hotfix branches) создаются из главной (master) ветви. Пускай, например, текущий производственный релиз имеет версию 1.2, и в нем (внезапно!) обнаруживается серьезный баг. А изменения в ветви разработки (develop) еще недостаточно стабильны, чтобы их издавать в новый релиз. Но мы можем создать новую ветвь исправлений и начать работать над решением проблемы:
$ git checkout -b hotfix-1.2.1 master
Switched to a new branch "hotfix-1.2.1"
$ ./bump-version.sh 1.2.1
Files modified successfully, version bumped to 1.2.1.
$ git commit -a -m "Bumped version number to 1.2.1"
[hotfix-1.2.1 41e61bb] Bumped version number to 1.2.1
1 files changed, 1 insertions(+), 1 deletions(-)
Не забывайте обновлять номер версии после создания ветви!
Теперь можно исправлять баг, а изменения издавать хоть одним коммитом, хоть несколькими.
$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbed6] Fixed severe production problem
5 files changed, 32 insertions(+), 17 deletions(-)
Закрытие ветви исправлений
Когда баг исправлен, изменения надо влить обратно в главную ветвь (master), а также в ветвь разработки (develop), чтобы гарантировать, что это исправление окажется и в следующем релизе. Это очень похоже на то, как закрывается ветвь релиза (release branch).
Прежде всего надо обновить главную ветвь (master) и пометить новую версию тегом.
$ git checkout master
Switched to branch 'master'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Отчет об изменениях)
$ git tag -a 1.2.1
Замечание: при желании, Вы также можете использовать флаги -s или -u <ключ>, чтобы криптографически подписать тэг.
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Отчет об изменениях)
У этого правила есть одно исключение: если в данный момент существует ветвь релиза (release branch), то ветвь исправления (hotfix branch) должна вливаться в нее, а не в ветвь разработки (develop). В этом случае исправления войдут в ветвь разработки вместе со всей ветвью релиза, когда та будет закрыта. (Хотя, если работа в develop требует немедленного исправления бага и не может ждать, пока будет завершено издание текущего релиза, Вы все же можете влить исправления (bugfix) в ветвь разработки (develop), и это будет вполне безопасно).
И наконец, удаляем временную ветвь:
$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbed6).
Считается, что «киллер фичей» СКВ Git является легковесное ветвление. Я ощутил это преимущество в полной мере, ведь я перешел на Git с SVN, где ветвление было достаточно дорогим процессом: для создания ветки нужно было скопировать весь рабочий каталог. В Git все проще: создание ветки подразумевает лишь создание нового указателя на определенный коммит в папке .git/refs/heads, который является файлом с 40 байтами текста, хешем коммита.
Основными командами пользовательского уровня для ветвления в Git являются git-branch, git-checkout, git-rebase, git-log и, конечно же, git-merge. Для себя я считаю git-merge зоной наибольшей ответственности, точкой огромной магической энергии и больших возможностей. Но это достаточно сложная команда, и даже достаточно длительный опыт работы с Git порой бывает недостаточным для освоение всех ее тонкостей и умения применить ее наиболее эффективно в какой-либо нестандартной ситуации.
Попробуем же разобраться в тонкостях git-merge и приручить эту великую магию.
Здесь я хочу рассмотреть только случай хорошего слияния, под которым я понимаю слияние без конфликтов. Обработка и разрешение конфликтов — отдельная интересная тема, достойная отдельной статьи.
Если верить мануалу, команда имеет следующий синтаксис:
git merge [-n] [--stat] [--no-commit] [--squash] [--[no-]edit] [-s <strategy>] [-X <strategy-option>] [--[no-]rerere-autoupdate] [-m <msg>] [<commit>...] git merge <msg> HEAD <commit>... git merge --abort
По большому счету, в Git есть два вида слияния: перемотка (fast-forward merge) и «истинное» слияние (true merge). Рассмотрим несколько примеров обоих случаев.
«Истинное» слияние (true merge)
Мы отклоняемся от ветки master, чтобы внести несколько багов улучшений. История коммитов у нас получилась следующая:
master: A - B - C - D \ feature: X - Y
Выполним на ветке master git merge feature:
master: A - B - C - D - (M) \ / feature: X - Y
Это наиболее частый паттерн слияния. В данном случае в ветке master создается новый коммит (M), который будет ссылаться на двух родителей: коммит D и коммит Y; а указатель master установится на коммит (M). Таким образом Git будет понимать, какие изменения соответствуют коммиту (M) и какой коммит последний в ветке master. Обычно коммит слияния делается с сообщением вроде «Merge branch 'feature'», но можно определить и свое сообщение коммита с помощью ключа -m.
Посмотрим историю коммитов в тестовом репозитории, который я создал специально для этого случая:
$ git log --oneline 9384bd (M) beb5a4 D 5ce5b1 Y 7f13e7 X d920dc C 35c217 B 84af94 A
А теперь посмотрим информацию о коммите (M):
$ git cat-file -p 9384bd tree 25c78f9086384bd86a2ab9d00c7e41a56f01d04 parent beb5a4ad88e80467404473b94c3e0758dd8e0be parent 5ce5b1edef64bd0d4e1039061a77be4d7182678 author Adre <adrey.pro@gmail.com> 1980475972 +0400 committer Adre <adrey.pro@gmail.com> 1980475972 +0400 (M)
Мы видим двух родителей, объект-дерево, соответствующее данному состоянию файлов репозитория, а так же информацию о том, кто виновен в коммите.
Посмотрим, куда ссылается указатель master:
$ cat .git/refs/heads/master 9384bd77304c09b81dcc4485da165923b96ed5f
Действительно, он теперь передвинут на коммит (M).
Squash и no-commit
Но что делать, если за содержимое ветки feature вас могут побить? К примеру, улучшение было небольшим, и вполне могло уместиться в один логичный коммит, но так вышло, что посреди работы вам было нужно убегать на электричку, а продолжать уже дома? В таком случае есть два выхода: экспорт репозитория с последующим импортом на другой машине, либо (особенно когда до электрички 10 минут, а до вокзала около километра) — сделать push origin feature.
Заливать незаконченные коммиты в основную ветку плохо, и с этим нужно что-то делать. Одним из способов, и, пожалуй самым простым, является опция --squash.
git merge feature --squash объединит изменения всех коммитов ветки feature, перенесет их в ветку master и добавит в индекс. При этом коммит слияния не будет создан, вам нужно будет сделать его вручную.
Такого же поведения без параметра squash можно добиться, передав при слиянии параметр --no-commit.
В случае применения такого слияния коммиты ветки feature не будут включены в нашу историю, но коммит Sq будет содержать все их изменения:
master: A - B - C - D - Sq \ feature: X - Y
Позже, в случае выполнения «классического» git merge feature можно исправить это. Тогда история примет следующий вид:
master: A - B - C - D - Sq - (M) \ / feature: X - Y
В случае, если вы выполнили слияние без коммита, а потом поняли, что совершили фатальную ошибку, все можно отменить простой командой: git merge --abort. Эта же команда может быть применена, если во время слияния произошли конфликты, а разрешать их в данный момент не хочется.
Перемотка (fast-forward merge)
Рассмотрим другой случай истории коммитов:
master: A - B - C \ feature: X - Y
Все как и в прошлый раз, но теперь в ветке master нет коммитов после ответвления. В этом случае происходит слияние fast-forward (перемотка). В этом случае отсутствует коммит слияния, указатель (ветка) master просто устанавливается на коммит Y, туда же указывает и ветка feature:
master, feature: A - B - C - X - Y
Чтобы предотвратить перемотку, можно использовать параметр --no-ff.
В случае, если мы выполним git merge feature --no-ff -m '(M)', мы получим уже такую картину:
master: A - B - C - (M) \ / feature: X - Y
Если же для нас единственным приемлемым поведением является fast-forward, мы можем указать опцию --ff-only. В этом случае, если к слиянию не применима перемотка, будет выведено сообщение о невозможности совершить слияние. Именно так было бы, если бы мы добавили опцию --ff-only в самом первом примере, где после ответвления feature в ветке master был сделано коммит C.
Можно добавить, что при выполнении git pull origin branch_name применяется как раз что-то вроде --ff-only. То есть, в случае, если при слиянии с веткой origin/branch_name не приемлема перемотка, операция отменяется и выводится сообщении о невозможности выполнения.
У команды git-merge есть интересный параметр, --strategy, стратегия. Git поддерживает следующие стратегии слияния:
Стратегия resolve — классическое трехсторонее слияние (three-way merge). Стандартный алгоритм трехстороннего слияния применяется для двух файлов с общим предком. Условно этот алгоритм можно представить в виде следующих шагов:
Эта стратегия имеет один недостаток: в качестве общего предка двух веток всегда выбирается наиболее ранний общий коммит. Для случая из нашего первого примера это не страшно, можно смело применять git merge feature -s resolve, и результат будет ожидаемым:
master: A - B - C - D - (M) \ / feature: X - Y
Здесь C — общий коммит двух веток, дерево файлов, соответствующее этому коммиту, принимается за общего предка. Анализируются изменения, произведенные в ветках master и feature со времен этого коммита, после чего для коммита (M) создается новая версия дерева файлов в соответствии с пунктами 4 и 5 нашего условного алгоритма.
В каком же случае проявляется недостаток стратегии resolve? Он проявляется в том случае, если для коммита (M) нам пришлось разрешить конфликты, после чего мы продолжили разработку и еще раз хотим выполнить git merge feature -s resolve. В этом случае в качестве общего предка снова будет использован коммит C, и конфликты произойдут снова и будут нуждаться в нашем вмешательстве.
Данная стратегия решает проблемы стратегии resolve. Она так же реализует трехстороннее слияние, но в качестве предка используется не реальный, а «виртуальный» предок, который конструируется по следующему условному алгоритму:
Результат этого действия принимается за общего предка и проводится трехсторонее слияние.
Для иллюстрации этой стратегии позаимствуем пример из статьи Merge recursive strategy из блога «The plasticscm blog»:
Итак, у нас есть две ветки: main и task 001. И так вышло, что наши разработчики знают толк в извращениях: они слили коммит 15 из ветки main с коммитом 12 из ветки task 001, а так же коммит 16 с коммитом 11. Когда нам понадобилось слить ветки, оказалось, что поиск реального предка — дело неблагодарное, но стратегия recursive с ее конструированием «виртуального» предка нам поможет. В результате мы получим следующую картину:
Стратегия recursive имеет множество опций, которые передаются команде git-merge с помощью ключа -X:
Эта стратегия используется для слияние более чем двух веток. Получившийся в итоге коммит будет иметь, соответственно, больше двух родителей.
Данная стратегия предполагает большую осторожность относительно потенциальных конфликтов. В связи с этим порой можно получить отказ в слиянии при применении стратегии octopus.
Не следует путать стратегию ours и опцию ours стратегии recursive.
Выполняя git merge -s ours obsolete, вы как бы говорите: я хочу слить истории веток, но проигнорировать все изменения, которые произошли в ветке obsolete. Иногда рекомендуют вместо стратегии ours использовать следующий вариант:
$ git checkout obsolete $ git merge -s recursive -Xtheirs master
Стратегия ours — более радикальное средство.
Стратегия subtree
Для иллюстрации данной стратегии возьмем пример из главы Слияние поддеревьев книги «Pro Git».
Добавим в наш проект новые удаленный репозиторий, rack:
$ git remote add rack_remote git@github.com:shacon/rack.git $ git fetch rack_remote warning: no common commits remote: Counting objects: 384, done. remote: Compressing objects: 100% (165/165), done. remote: Total 3184 (delta 152), reused 270 (delta 175) Receiving objects: 100% (384/384), 67.42 KiB | 4 KiB/s, done. Resolving deltas: 100% (152/152), done. From git@github.com:schacon/rack * [new branch] build -> rack_remote/build * [new branch] master -> rack_remote/master * [new branch] rack-0.4 -> rack_remote/rack-0.4 * [new branch] rack-0.9 -> rack_remote/rack-0.9 $ git checkout -b rack_branch rack_remote/master Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master. Switched to a new branch "rack_branch"
Ясно, что ветки master и rack_branch имеют абсолютно разные рабочие каталоги. Добавим файлы из rack_branch в master с использованием squash, чтобы избежать засорения истории ненужными нам фактами:
$ git checkout master $ git merge --squash -s subtree --no-commit rack_branch Squash commit -- not updating HEAD Automatic merge went well; stopped before committing as requested
Теперь файлы проекта rack у нас в рабочем каталоге.
Хотя в этой модели ветвления совершенно нет ничего принципиально нового, «большая картинка», с которой начинается эта статья, зарекомендовала себя в наших проектах с самой лучшей стороны. Она формирует элегантную мысленную модель, которую легко полностью охватить одним взглядом, и которая позволяет сформировать у команды совместное понимание процессов ветвления и слияния, действующих на проекте.
Итак, я собрал вместе все знания, которые я получил за время работы с Git относительно благополучного git-merge. Я буду рад, если кому-то это поможет, но так же я буду рад, если кто-то поможет мне дополнить материал или исправить неточности и ошибки, если вдруг я допустил такие.
Анализ данных, представленных в статье про контроля версий, подтверждает эффективность применения современных технологий для обеспечения инновационного развития и улучшения качества жизни в различных сферах. Надеюсь, что теперь ты понял что такое контроля версий, ветка в git, git, системы управления версиями, стратегии слияния веток, слияние веток и для чего все это нужно, а если не понял, или есть замечания, то не стесняйся, пиши или спрашивай в комментариях, с удовольствием отвечу. Для того чтобы глубже понять настоятельно рекомендую изучить всю информацию из категории Разработка программного обеспечения и информационных систем
Комментарии
Оставить комментарий
Разработка программного обеспечения и информационных систем
Термины: Разработка программного обеспечения и информационных систем