Гілки та злиття

Про гілки

Щоб по-справжньому зрозуміти, як Git працює з розгалуженнями, нам потрібно зробити крок назад і розглянути, як Git зберігає свої дані. Git зберігає дані не у вигляді серії наборів змін або відмінностей, а у вигляді серії знімків. Коли ви робите коміт, Git зберігає об’єкт коміту, який містить вказівник на створений вами знімок вмісту. Цей об’єкт також містить ім’я та адресу електронної пошти автора, повідомлення, яке ви набрали, і вказівники на комміт або комміти, які безпосередньо передували цьому комміту (його батька або батьків): нульовий батько для початкового комміту, один батько для звичайного комміту і кілька батьків для комміту, який є результатом злиття двох або більше гілок.

%%{init: {'gitGraph': {'mainBranchName': 'master'}} }%% gitGraph: commit id: "Initial commit" commit id: "383a7630" type: HIGHLIGHT branch develop commit id: "1fe791ec" branch testing commit id: "01cda149" checkout master commit id: "1c060f79"

Commits tree

Гілка в Git’і - це просто легкий рухомий вказівник на одну з цих коммітів. Стандартна назва гілки у Git’і - master. Коли ви починаєте робити комміти, ви отримуєте головну гілку, яка вказує на останній зроблений вами комміт. Кожного разу, коли ви робите комміт, вказівник головної гілки автоматично пересувається вперед.

../_images/git-commits.svg

Управління гілками

Команда git branch робить більше, ніж просто створює і видаляє гілки. Якщо ви запустите її без аргументів, ви отримаєте простий список ваших поточних гілок:

$ git branch
  devel
  feature/contributing
  feature/legacy
  feature/lib-ms
  feature/libms
  feature/pdf-builder
* feature/vcs
  master

Щоб побачити всі гілки, які містять роботи, до яких ви ще не приєдналися, використовуйте опцію --no-merged:

$ git branch --no-merged
  feature/contributing
  feature/lib-ms
  feature/libms

У гілках, перелічених у виводі, є напрацювання, які не злито з поточною гілкою. Отже, їх видалення за допомогою git branch -d не спрацює.

Створення гілок

Існує декілька способів створити нову гілку в Git-сховищі. Найпоширеніший з них - використання команди git branch.

git branch <branch_name> [parent_commit]

Вказівник на батьківський коміт не є обов’язковим. За замовчуванням батьківський коміт встановлюється на найновіший доступний (вказівник HEAD) на момент створення гілки. Ви також можете переключитися на новостворену гілку одразу після її створення за допомогою:

git checkout -b <branch_name> [parent_commit]

Перемикання гілок

Щоб перейти до існуючої гілки, виконайте команду git checkout.

$ git branch
  devel
  feature/contributing
  feature/legacy
  feature/lib-ms
  feature/libms
  feature/pdf-builder
* feature/vcs
  master

$ git checkout devel
Switched to branch 'devel'
Your branch is up to date with 'origin/devel'.

Основи розгалуження та злиття

По-перше, припустимо, що ви працюєте над своїм проектом і вже маєте кілька комітів на гілці master. Ви вирішили, що будете працювати над feature #53 в будь-якій системі моніторінгу завдань, яку використовує ваша компанія. Щоб створити нову гілку і одночасно переключитися на неї, ви можете виконати команду git checkout з ключем -b:

$ git checkout -b feature/GH-53
Switched to a new branch "feature/GH-53"

Це скорочено означає:

$ git branch feature/GH-53
$ git checkout feature/GH-53

Насправді це створить новий покажчик гілки, спрямований на останню фіксацію, наявну у гілці master. Але відтепер ваша гілка називається feature/GH-53. Отже, створення коммітів пересуває вказівник гілки вперед, тому що ви її вилучили (тобто HEAD вказує на неї).

Тепер припустимо, що працю над «GH-53» завершено, і вам потрібно перенести коміти з гілки feature/GH-53 назад до master.

Злиття гілок

Вам потрібно повернутися до гілки master і скористатися командою git merge, щоб об’єднати зміни (коміти) з вихідної гілки в цільову.

$ git checkout master
$ git merge feature/GH-53
Updating f42c576..3a0874c
Fast-forward
    index.html | 2 ++
    1 file changed, 2 insertions(+)

Ви помітите фразу «fast-forward» у цьому злитті. Оскільки коміт, на який вказує гілка feature/GH-53, до якої ви приєдналися, був безпосередньо перед останнім комітом у гілці master, Git просто пересуне вказівник вперед.

Зміни з feature/GH-53 тепер містяться у знімку коміту, на який вказує гілка master.

%%{init: {'gitGraph': {'mainBranchName': 'master'}} }%% gitGraph commit id: "Initial commit" branch feature/GH-53 commit commit checkout master merge feature/GH-53

Fast-forward merge

Тепер припустимо, що інший розробник почав працювати над feature/GH-54 в той самий час. І у цій гілці мало комітів. Припустимо, що розробник вирішив, що робота над GH-54 завершена і він хоче об’єднати свою гілку з master.

$ git branch
  master
* feature/GH-54
$ git checkout master
$ git merge feature/GH-54
Merge made by the 'recursive' strategy.
    index.html | 1 +
    1 file changed, 1 insertion(+)

Це виглядає дещо інакше, ніж злиття feature/GH-53. У цьому випадку історія розробки розійшлася з якоїсь давнішої точки. Оскільки коміт на гілці, в якій ви перебуваєте, не є прямим предком гілки, в яку ви виконуєте злиття, Git’у доведеться виконати деяку роботу. У цьому випадку Git виконує просте тристороннє злиття, використовуючи два знімки, на які вказують кінчики гілок, і їхнього спільного предка.

Замість того, щоб просто перемістити вказівник гілки вперед, Git створює новий знімок, який є результатом цього тристороннього злиття, і автоматично створює новий коміт, який вказує на нього. Це називається коммітом злиття, і він особливий тим, що має більше одного батька.

%%{init: {'gitGraph': {'mainBranchName': 'master'}} }%% gitGraph commit id: "Initial commit" branch feature/GH-53 branch feature/GH-54 checkout feature/GH-53 commit commit checkout master merge feature/GH-53 checkout feature/GH-54 commit checkout master commit merge feature/GH-54 type:NORMAL id: "merge commit"

Merge commit

Конфлікти об’єднання

Іноді процес злиття відбувається не зовсім гладко. Якщо у двох гілках, які ви об’єднуєте, однакова частина одного і того ж файлу змінена по-різному, Git не зможе об’єднати їх чисто.

$ git merge vcs
Auto-merging branches.txt
CONFLICT (content): Merge conflict in branches.txt
Automatic merge failed; fix conflicts and then commit the result

Git не створив автоматично новий коміт злиття. Він призупинив процес, поки ви вирішуєте конфлікт. Якщо ви хочете побачити, які файли було від’єднано після конфлікту злиття, ви можете запустити git status:

$ git status
On branch master

You have unmerged paths.
    (fix conflicts and run "git commit")

Unmerged paths:
    (use "git add <file>..." to mark resolution)

    both modified: branches.txt

no changes added to commit (use "git add" and/or "git commit -a")

Все, що має конфлікти злиття, які не було вирішено, позначається як не об’єднане. Git додає стандартні маркери вирішення конфліктів до файлів, які мають конфлікти, щоб ви могли відкрити їх вручну і вирішити ці конфлікти. Ваш файл містить розділ, який виглядає приблизно так:

<<<<<<< HEAD:branches.txt
Anything that has merge conflicts and has not been resolved is listed as
=======
Anything that has merge conflicts
and hasn't been resolved is listed as
</div>
>>>>>>> vcs:branches.txt

Це означає, що версія у гілці HEAD (ваша гілка master, тому що саме її ви перевірили, коли виконували команду merge) - це верхня частина блоку (все, що вище «=======»), тоді як версія у гілці vcs виглядає як все, що знаходиться у нижній частині. Для того, щоб вирішити конфлікт, вам доведеться або стати на чийсь бік, або об’єднати вміст самостійно.

Коли конфлікти буде вирішено, ви можете знову запустити команду git status:

$ git status
On branch master

All conflicts fixed but you are still merging.
    (use "git commit" to conclude merge)

Changes to be committed:
    modified: branches.txt

Якщо вас все влаштовує, і ви переконалися, що все, що викликало конфлікти, було усунуто, ви можете запустити git commit для остаточного завершення злиття.

Перебазування

У Git’і існує два основних способи інтеграції змін з однієї гілки в іншу: merge (злиття) і rebase (перебазування).

Раніше ми вже наводили приклад розбіжностей у роботі та зобов’язаннях, зроблених на двох різних гілках.

%%{init: {'gitGraph': {'mainBranchName': 'master'}} }%% gitGraph commit id: "Initial commit" branch feature checkout master commit id: "Second commit" checkout feature commit id: "1st commit on feature" commit id: "2nd commit on feature"

Before rebase

Найпростішим способом об’єднання гілок, як ми вже розглядали, є команда merge. Вона виконує тристороннє злиття між двома останніми знімками гілок і останнім спільним предком, створюючи новий знімок (і фіксацію).

Втім, є й інший спосіб: ви можете взяти патч зі змінами, які було внесено до master, і повторно застосувати його поверх feature. У Git’і це називається rebasing. За допомогою команди rebase ви можете взяти всі зміни, які були зафіксовані на одній гілці, і відтворити їх на іншій гілці.

$ git checkout feature
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

Ця операція виконується шляхом переходу до спільного предка двох гілок, отримання відмінностей, внесених кожним коммітом гілки, на якій ви перебуваєте, збереження цих відмінностей у тимчасових файлах, скидання поточної гілки до того ж комміту, що і гілка, на яку ви перебазуєтесь, і, нарешті, застосування кожної зміни по черзі.

%%{init: {'gitGraph': {'mainBranchName': 'master'}} }%% gitGraph commit id: "Initial commit" commit id: "Second commit" branch feature checkout feature commit id: "1st commit on feature" commit id: "2nd commit on feature"

After rebase

Примітка

Інші супроводжувачі надають перевагу перебазуванню (або вилученню) внесених напрацювань на кінці своєї master гілки, замість того, щоб об’єднувати їх, щоб зберегти переважно лінійну історію.

Cherry-picking

Інший спосіб перенесення внесеної роботи з однієї гілки в іншу - це збирання коммітів (cherry-pick). Збирання вишень у Git’і - це відновлення бази для одного коміту. Він бере патч, який було внесено в комміті, і намагається повторно застосувати його на гілці, на якій ви зараз перебуваєте. Це корисно, якщо ви маєте декілька коммітів у гілці теми і хочете інтегрувати лише один з них, або якщо ви маєте лише один комміт у гілці теми і вважаєте за краще вибрати його замість того, щоб запускати rebase.

%%{init: {'gitGraph': {'mainBranchName': 'master'}} }%% gitGraph commit id: "Initial commit" branch develop checkout master commit checkout develop commit checkout master commit checkout develop commit id: "Important commit" checkout master commit checkout develop commit checkout master cherry-pick id: "Important commit" commit

Cherry-pick a commit

$ git cherry-pick e43a6
[master 0288270] Important commit
 Date: Thu Aug 17 20:30:05 2023 +0300
 3 files changed, 17 insertions(+), 3 deletions(-)

Стратегії розгалуження

Стратегія розгалуження в системах контролю версій визначає, як створюються, управляються та використовуються гілки в процесі розробки проекту. Вона допомагає командам ефективно співпрацювати, ізолювати зміни та організовувати зусилля з розробки. Хороша стратегія розгалуження забезпечує чіткість у роботі з гілками, мінімізує конфлікти та уможливлює плавний і структурований робочий процес розробки.

GitFlow - це популярна стратегія розгалуження, яка надає чітко визначену модель для управління гілками в Git-репозиторії. Вона була представлена Вінсентом Дріссеном (Vincent Driessen) і базується на ідеї використання двох основних гілок: «master» і «develop».

  • Майстер-гілка: «Майстер-гілка» представляє стабільну версію кодової бази. Вона завжди повинна містити готовий до використання код і не містити жодних серйозних проблем. Майстер-гілка захищена, і в неї зливаються тільки релізні версії.

  • Гілка розробки: Гілка «develop» - це місце, де відбувається постійна розробка та інтеграція функцій. Вона слугує інтеграційною гілкою для різних функціональних гілок, а також повинна містити стабільну версію коду.

  • Гілки функцій: Для кожної нової функції або виправлення помилки створюється окрема гілка «feature» у гілці «develop». Ці гілки недовговічні і існують лише на час розробки функції.

  • Гілки релізу: Коли розробка на гілці «develop» готова до релізу, створюється гілка «release». Релізна гілка використовується для тестування, виправлення помилок і підготовки до розгортання.

  • Гілки виправлень: Якщо у виробничій версії виявлено критичну проблему, на основі основної гілки створюється гілка «hotfix». Це дозволяє швидко виправити проблему, не впливаючи на поточну розробку на гілці «develop».

GitFlow забезпечує структурований підхід до управління розгалуженням у середовищі спільної розробки. Він гарантує, що основні гілки залишаються стабільними, а нові функції та виправлення помилок плавно інтегруються перед випуском. Ця стратегія особливо корисна для проектів з регулярними випусками і командою, яка працює над декількома функціями одночасно.

%%{init: {'gitGraph': {'mainBranchName': 'master'}} }%% gitGraph %% initialize repository commit id: "Initial commit" %% define development branch branch develop commit %% define hotfix branch checkout master commit id: "Buggy commit" type: REVERSE branch hotfix %% define feature branches checkout develop branch feature/A branch feature/B checkout develop commit branch feature/C commit %% working with hotfix branch checkout hotfix commit id: "Fix bugs" type:HIGHLIGHT %% merge hotfix commits into main and develop checkout master merge hotfix checkout develop merge hotfix %% working with feature-b branch checkout feature/A commit checkout develop merge feature/A %% work with feature-a branch checkout feature/B commit checkout develop %% define release branch checkout develop branch release/v1.0 commit id: "Work on release" tag: "v1.0" checkout master merge release/v1.0 checkout develop commit merge release/v1.0 %% merging previous feature branch commit merge feature/B

GitFlow diagram