Нові можливості git rebase з опцією ‑‑onto
Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.
Усім привіт, мене звати Сергій Бойко, я працюю Software Engineer в Railsware. У цій статті поговоримо про Git rebase. Це чудовий інструмент, що дає практично необмежений контроль над історією комітів. Та користуючись ним тривалий час, я зібрав низку випадків, які було важко елегантно вирішити за його допомогою.
Саме тоді я натрапив на маловідому опцію --onto
, яка суттєво змінила мій стиль роботи з rebase
.
Якщо коротко, git rebase --onto
дозволяє однією командою переміщувати пачку комітів з однієї гілки в іншу. Можна сказати, що це такий собі git cherry-pick
на стероїдах. Подивімося, як це виглядає.
Що не так з rebase
Нехай є гілка feature1
, яку створив ваш колега. Ваша мета — додати до неї щось нове. Ви відгалужуєтесь від feature1
і реалізуєте новий функціонал у комітах C
і D
. Ось як виглядає історія комітів в Git:
A --- B feature1 \ C --- D feature2
Поки ви працювали над своєю задачею, колега додав новий код у feature1
. Але він також любить модифікувати історію, і тому трохи змінив коміт B
. На діаграмі нижче модифікований коміт позначений B*
. Ось як змінилось робоче дерево:
A --- B* --- E --- F feature1 \ B --- C --- D feature2
Вам потрібно підтягнути його зміни у свою гілку і ви змінюєте базовий коміт:
git rebase feature1
І ось момент болю. Вам потрібно владнати конфлікт між комітами B
і B*
. Знайомо, чи не так?
Розв’язуємо проблему за допомогою git rebase —onto
Перш ніж звинувачувати колегу, потрібно усвідомити, що справа не в самій зміні. Проблема в тому, як саме rebase
виконує свою роботу. У наведеному вище прикладі rebase
бере останній коміт feature1
як початкову точку, а потім намагається накласти всі нові коміти на нього.
Інакше кажучи, він обирає коміт F
як нову початкову точку. Потім шукає місце, де обидві гілки розійшлися. У нашому випадку це коміт A
. Отже, rebase
бере коміти B
, C
, D
і намагається додати їх по черзі за комітом F
. Зміни в коміті B
конфліктують з існуючим кодом і саме тому виникає конфлікт.
Ідеальним варіантом було б уникнути конфлікту і просто отримати наступне дерево історії:
A --- B* --- E --- F feature1 \ C' --- D' feature2
(Зверніть увагу: C'
та D'
містять ті ж самі зміни, що і C
та D
, але ми оновили назви, щоб показати зміни в SHA
)
Отже, ми хочемо взяти коміти C
та D
і приєднати їх до feature1
. Для цього можна використати git cherry-pick
, але це досить муторна робота, і тут легко помилитися.
Натомість можна використати git rebase --onto
. Для цього нам потрібно переключитися на feature2
та виконати наступну команду:
git checkout feature2 git rebase --onto feature1 B
Остання команда значить: візьми всі коміти після коміту B
(у нашому випадку це C
і D
) і розмісти їх поверх feature1
.
Таким чином ми отримуємо бажане робоче дерево без необхідності вирішувати конфлікт між B
і B*
. Все ще існує ймовірність колізії, якщо коміти C
або D
конфліктують із B*
, E
, або F
, але це вже нормальний випадок.
Замість коміта B
як аргумента, можна використати відносну адресацію у вигляді HEAD~N
. Для нашого прикладу це буде так:
git checkout feature2 git rebase --onto feature1 HEAD~2
Що можна інтерпретувати як: візьми два останні коміти з моєї гілки та помісти їх поверх feature1
.
Розв’язання проблеми відгалуження з неправильного місця
Бувають випадки, коли робочу гілку створили не з того місця, або, можливо, з якоїсь причини вам потрібно перенести свої коміти в інше місце.
Ось поширений приклад. Ви відгалузились від гілки main
, але раптом продакт-менеджер каже, що ваші зміни потрібно швидко закинути на продакшн. А це вимагає змінити базу на гілку hotfix
. Ось історія комітів:
E --- F --- G feature / A --- B main \ C --- D hotfix
Тобто вам потрібно отримати наступне дерево:
A --- B main \ C --- D hotfix \ E' --- F' --- G' feature
(Зверніть увагу: E'
, F'
, та G'
містять ті ж самі зміни, що й E
, F
, та G
, але ми оновили назви, щоб показати зміни в SHA
)
Якщо ви скористаєтесь звичайним rebase
, небажаний коміт B
проскочить у гілку hotfix
. На практиці main
може мати десятки нових комітів і всі вони потраплять в hotfix
.
Але git rebase --onto
не має такої проблеми. Ось рішення:
git checkout feature git rebase --onto hotfix B
або те ж саме з відносною адресацією:
git checkout feature git rebase --onto hotfix HEAD~3
Фактично, майже всі сценарії переміщення комітів можна реалізувати за допомогою опції --onto
.
Видалення комітів
Для повноти картини нам залишилось розглянути як можна видаляти коміти, використовуючи опцію --onto
. Ось приклад робочого дерева:
A --- B --- C --- D --- E feature
Ми хочемо видалити коміти C
і D
, щоб отримати наступну історію комітів:
A --- B --- E' feature
(Зверніть увагу: E
та E'
мають однаковий набір змін, але E'
отримав новий SHA
через модифікації дерева комітів)
Ось рішення:
git checkout feature git rebase --onto B HEAD~1
Техніка безпеки
Необережне використання rebase
може призвести до втрати комітів. Якщо ви ще не знайомі з цим інструментом, обов’язково дотримуйтесь звичайних заходів безпеки: зберігайте SHA
з HEAD
вашої гілки перед тим, як застосувати rebase
і навчіться використовувати git reflog
.
На завершення
Як вже було сказано, git rebase --onto
сильно спростив ряд повсякденних задач для мене. Попри відносну простоту опції --onto
, мені знадобився певний час, щоб знайти правильний спосіб і практичне розуміння того, як її використовувати в різних сценаріях. Тож, сподіваюсь, що ця стаття заощадить ваш час і поповнить колекцію прийомів Git.
33 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів