Git
Chapters ▾ 2nd Edition

3.6 Галуження в git - Перебазовування

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

Git має два основні способи інтегрувати зміни з гілки в гілку: merge (зливання) та rebase(перебазовування). У цій секції вивчатимете що таке перебазовування, як його виконувати, чому воно вважається таким чудовим інструментом та випадки, коли його не варто застосовувати.

Просте перебазовування

Поверніться до попереднього прикладу з Основи зливання, де видно, що ваша робота розгалузилася в комітах по двох різних гілках.

Просте розгалуження історії.
Рисунок 35. Просте розгалуження історії

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

Інтеграція розгалуженої історії за допомогою зливання.
Рисунок 36. Інтеграція розгалуженої історії за допомогою зливання

Проте, існує інший спосіб: можете взяти латку (patch) змін з C4 і накласти її поверху C3. У Git це називають перебазовуванням (rebasing). За допомогою rebase, ви можете взяти всі зміни, що були в комітах одної гілки та відтворити їх на іншій гілці.

Для цього прикладу, виконайте наступне:

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

Працює це так: шукається спільний предок двох гілок (поточної та тої, на котру перебазовуєтесь); отримуються відмінності (diff), спричинені кожним комітом поточної гілки; відмінності зберігаються в тимчасові файли; поточна гілка встановлюється (reset) на коміт гілки, на яку перебазовуєтесь; та нарешті, застосовується кожна зміна.

Перебазовування зміни з `C4` на `C3`.
Рисунок 37. Перебазовування зміни з C4 на C3

Тепер ви можете повернутися на master та зробити злиття перемотуванням (fast-forward merge).

$ git checkout master
$ git merge experiment
Злиття перемотуванням гілки master.
Рисунок 38. Злиття перемотуванням гілки master

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

Ви часто використовуватимите це, щоб переконатися в тому, що ваші коміти накладалися на віддалені гілки чисто — наприклад, якщо хочете зробити внесок до проекту, яким самі не керуєте. У цьому випадку, ви працюватимете в гілці й потім перебазуєтеся на origin/master, коли будете готові надіслати свої латки до основного проекту. У свою чергу, керуючий проектом не змушений робити ніякої додаткової інтеграційної роботи — просто перемотати гілку або чисто накласти зміни.

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

Цікавіше перебазовування

Під час перебазовування ви можете програвати зміни не лише на цільовій гілці. Для прикладу візьміть історію Історія з тематичною гілкою, відгалуженою від іншої тематичної гілки. Ви створили тематичну гілку (server) для того, щоб додати певну серверну функціональність до свого проекту та додали туди коміт. Потім створили гілку для змін на клієнтській стороні (client), де встигли зробити кілька комітів. Нарешті, повернулися до гілки з серверними змінами та додали кілька комітів там.

Історія з тематичною гілкою
Рисунок 39. Історія з тематичною гілкою, відгалуженою від іншої тематичної гілки

Допустимо, ви вирішили злити зміни клієнтської сторони до головної лінії коду для релізу, але хочете зачекати з серверними, доки вони будуть більше відтестовані. Для цього можете взяти клієнтські зміни, які ще не є на сервері (C8 і C9), та відтворити їх на master, користуючись опцією --onto команди git rebase:

$ git rebase --onto master server client

Що означає `Візьми гілку `client, знайди всі латки, відколи вона відгалузилася від гілки server, та відтвори їх у гілці client, ніби вона насправді базувалася на гілці master.'' Це трохи складно, але результат доволі класний.

Перебазовування тематичної гілки
Рисунок 40. Перебазування тематичної гілки, що відгалужена від іншої гілки

Тепер можете перемотати свою основну гілку master (дивіться Перемотування гілки master, для залучення в неї змін з клієнтської гілки):

$ git checkout master
$ git merge client
Перемотування гілки master
Рисунок 41. Перемотування гілки master, для залучення в неї змін з клієнтської гілки

Скажімо, тепер ви також вирішили прийняти зміни з серверної гілки. Ви можете перебазувати серверну гілку на master без переходу на неї, користуючись командою git rebase <базова гілка> <тематична гілка>, що перейде на тематичну гілку (в нашому випадку server) та застосує її зміни на базовій гілці (master):

$ git rebase master server

Це відтворить вашу зроблену роботу з server поверху master, як відображено в Перебазовування серверної гілки поверху master.

Перебазовування серверної гілки поверху `master`.
Рисунок 42. Перебазовування серверної гілки поверху master

Тоді, можете перемотати базову гілку (master):

$ git checkout master
$ git merge server

Гілки client та server можна вилучати, оскільки вся робота з них вже інтегрована і вам вони більше не знадобляться, що зробить вашу історію такою як Остаточна історія комітів:

$ git branch -d client
$ git branch -d server
Остаточна історія комітів.
Рисунок 43. Остаточна історія комітів

Небезпеки перебазовування

Ох, але все блаженство перебазовування не позбавлене й своїх недоліків, які можна підсумувати одним рядком:

Не перебазовуйте коміти, що існують в інших репозиторіях.

Якщо будете слідувати цьому правилу, то все буде гаразд. Якщо ні, то люди ненавидітимуть вас, а сім’я та друзі зневажатимуть.

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

Погляньмо на приклад того, як перебазовування змін, які вже зробили публічними, може створювати проблеми. Нехай ви зклонували репозиторій з центрального сервера і трохи в ньому попрацювали. Історія комітів виглядає десь так:

Склонували репозиторій та трохи в ньому попрацювали.
Рисунок 44. Склонували репозиторій та трохи в ньому попрацювали

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

Отримали ще коміти та злили їх із своєю роботою.
Рисунок 45. Отримали ще коміти та злили їх із своєю роботою

Далі, ті, хто викладали та зливали зміни, вирішили повернутися та перебазувати зроблену роботу, виконавши git push --force для того, щоб переписати історію на сервері. Коли ви оновитися (fetch), то отримаєте з сервера нові коміти.

Хтось виклав перебазовані коміти, зневажаючи коміти, на яких ви базували свою роботу.
Рисунок 46. Хтось виклав перебазовані коміти, зневажаючи коміти, на яких ви базували свою роботу

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

Ви знову виконали зливання тої самої роботи в новий коміт зливання.
Рисунок 47. Ви знову виконали зливання тої самої роботи в новий коміт зливання

Коли ви виконаєте git log для такої історії, то побачите два коміти, що мають того ж автора, дату та повідомлення, що є досить заплутаним. Більше того, коли ви відправите (push) таку історію, ви відновите всі перебазовані коміти на центральному сервері, що й надалі заплутуватиме інших. Мабуть, інші розробники не хочуть мати C4 та C6 в історії, власне чому вони й ініціювали перебазовування.

Перебазовуйся коли перебазовуєшся

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

Виявляється, що крім контрольної SHA-1 суми коміту Git також вираховує контрольну суму, що базується на латці (patch), спричиненою комітом. Вона називається ``patch-id''.

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

Скажімо, для попереднього прикладу, якщо, будучи на Хтось виклав перебазовані коміти, зневажаючи коміти, на яких ви базували свою роботу, ми, замість зливання, виконаємо git rebase teamone/master, Git зробить таке: * Визначить які зміни є унікальні для нашої гілки (C2, C3, C4, C6, C7) * Визначить що не є комітами злиття (C2, C3, C4) * Визначить які коміти не були переписані в цільовій гілці (лише C2 та C3, оскільки C4 має таку ж латку як і C4') * Застосує ті коміти поверху teamone/master

Перебазовування поверх змушено надісланих (force-pushed) змін.
Рисунок 48. Перебазовування поверх примусово надісланих (force-pushed) змін.

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

Ви також можете спростити процес виконуючи git pull --rebase замість звичайного git pull. Або "вручну" виконати послідовно git fetch та git rebase teamone/master.

Якщо ви використовуєте git pull та бажаєте щоб --rebase виконувався за замовчуванням, можете відповідно встановити конфігураційну опцію pull.rebase, виконавши щось типу git config --global pull.rebase true.

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

Якщо ж ви, чи ваші колеги, все ж мають таку необхідність, з тих чи інших причин, упевніться що інші виконують git pull --rebase для того, щоб трохи полегшити неприємну ситуацію.

Перебазовування чи зливання

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

Одна з точок зору, що історія репозиторія, це — запис того, що власне трапилося. Це історичний документ сам по собі, та його не можна підробляти. Під цим кутом, переписування історії є майже блюзнірством; ви брешете про те, що ж відбулося насправді. Ну то й що, як там серія заплутаних комітів зливання? Саме так все сталося, нехай репозиторій тримає це для прозорості нащадків.

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

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

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

scroll-to-top