Нові можливості 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.

👍ПодобаєтьсяСподобалось30
До обраногоВ обраному6
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

отличная статья, спасибо. Полезно, но и в то же время просто как на пальцах

Вам потрібно владнати конфлікт між комітами B і B*. Знайомо, чи не так?

если это коллега так делает в своей локальной ветке, то пожалуйста, сколько угодно. А если в мастере такое проворачивают при совместной работе, то это серьезное нарушение рабочих процессов

раптом продакт-менеджер каже, що ваші зміни потрібно швидко закинути на продакшн

без должного QA я бы остудил такого продакт-менеджера, после «быстро закинуть» в прод новые фичи можно потом очень долго тушить пожар. Я в последние годы максимум соглашался закидывал быстро только минималистические хотфиксы на прод, если вас так новые фичи заливают побыренькому прод то у вас проблемный проект и неправильный ПМ

если у вас на проекте нормально построенный CI/CD и хорошо написанные тесты то подобный хотфикс релиз более чем рабочая схема
плюс не все проекты это кровавый ынтырпрайз, в стартапе такое более чем допустимо

Дякую за корисну статтю 💯/💯

Основную проблему обычно составляет осознание на каком коммите пощло разветвление и он он должен быть базой для рибейза. А после этого ключик -i и минута в vim позволяют полностью контролировать процесс. Маловероятно что буду пользоваться —onto.

Основную проблему обычно составляет осознание на каком коммите пощло разветвление

`git merge-base` недостаточно?

А после этого ключик -i и минута в vim позволяют полностью контролировать процесс.

Interactive режим не даёт ни onto, ни cherry-pick. Непонятно сравнение.

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

`git rebase -i` дає більше можливостей і набагато простіший для розуміння

Як це не дає? Абсолютно все, що описано в пості можна робити в інтерактивному режимі.

Ще раз: інтерактивний режим сам по собі не дає ніякого onto. Скомбінувати — можна. Але просто інтерактивним режимом... ну так, ви можете взяти список комітів зі старої гілки і наробити pick команд для них. Але це вже буде як раз «вираховувати правильний хеш коміта».
Інтерактивний rebase з onto — це onto і інтерактивний rebase. Без onto — тільки друге. Ось і все.

`git rebase -i` дає більше можливостей і набагато простіший для розуміння

Інтерактивність і onto ортогональні і незалежні одне від одного.

Щось мене зтригирело і я записав відос в якому всі приклади з статі вирішуються через git rebase -i:

youtu.be/kAQuSGRGYoU

Я не говорю, що --onto це погано. Просто interactive значно гнучкіший (можна змінювати порядок комітів, їх текст, об’єднувати і т.д.). Те саме і в порівнянні з cherry-pick.

Хоча, cherry-pick я використовую у випадках, коли треба пікнути один коміт, або коли треба забрати декілька комітів з середини іншої гілки. Тут дійсно з rebase буде не зручно працювати (хоча знову ж таки — це можливо, бо interactive режим дуже потужний).

Так як rebase -i закриває усі потреби, то я не бачу сенсу забивати голову додатковими командами. Навіть якщо --onto можна використовувати паралельно з --interactive. Це просто зайве когнітивне навантаження. Кому воно потрібно? 😅

Але основна мета мого коменту — захистити інших користувачів від стверджень типу, що «Interactive режим не даёт» і що «Непонятно сравнение». І те, що «інтерактивний режим сам по собі не дає ніякого onto» це теж не важливо, якщо твоя задача змінити дерево комітів, а не використати --onto. Сподіваюсь, що мій відос достатньо наглядно демонструє, що тут є, що порівнювати. А далі нехай кожний обирає сам, що йому підходить більше. Я вже свій вибір зробив. :)

Подивився. Да, такий варіант у простому випадку дійсно спрацює — якщо виконуються умови:
1) У дерев комітів є спільний базовий коміт (у >99% випадків це так, але не обовʼязково).
2) Людина згодна видалити з отриманого списку із, у загальному випадку, 100500 комітів, всі крім кінцевого невеличкого набору.
3) Набір комітів, що переносяться, лінійний. (Я ще не впевнений, що не треба лінійности попередніх комітів. У вашому прикладі це не перевірялось.)
4) Замість «Не треба вираховувати правильний хеш коміта» все одно треба вираховувати, який коміт состанній на видалення — по повідомленню чи знову хешу.

Тоді — згоден. Для практичного використання у такому випадку — придатно і може бути навіть зручно.

(PS: Нездорова звичка — використання «відосу» там, де достатньо текста. Ну нема тут нічого такого що потребує кількох мегабайтів того відео.)

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

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

rebase тот еще гемор, конечно. но какая красивая линейная история main получается, просто закачаешься!

У цій фічі найскладніше, мені здається, запамʼятати зміну ролі аргументів. Звичайний rebase має один аргумент new_base. А з onto отримуємо «—onto new_base old_base_previous», тобто роль позиційного аргумента змінюється на протилежну. Не розумію, чому не зробили з самого початку якийсь «—from», щоб не було таких несподіванок.
Це не єдина така проблема в Git і за це його часто лають. Нещодавно створені команди «restore» і «switch» — приклади того, як намагаються покращити його UX. Тут треба було б зробити щось подібне.

Намагаюся не використовувати rebase, якшо це можливо. Особисто мені легше створити локальний патч зі всіма моїми змінами, оновити гілку до актуального стану, а потім зверху його застосувати, шоб відправити «чистий» і красивий коміт.

Впевнений, бувають випадки, коли цей прийом не вийде застосувати, але на моїй практиці таких історій поки не було.

Ну ось ведете ви нову фічу, де не один коміт, а 10-20-30. Не задовбетесь чері-пікати постійно всі свої коміти поверх мейна перед пушем фічі назад?

А так, регулярний ребейз на актуальний апстрім-бранч дозволяє фіксити конфлікти в своїй гілці вчасно, не накопичуючи їх понад міру. І потім все, що треба для злиття із апстрімом — звичаний fast-forward pull request. І мейн залишається чистенький і красивим.

Ребейз очень опасная штука, без правильного понимания как оно работает и без договоренностей может привести к печальным последствиям (например, к затиранию кода). Безопаснее в больших командах при совместной работе использовать git pull --no-ff для слияния изменений из репозитория, когда работу над брачным ведут несколько человек, конфликты решать локально, каждое рабочею утро мерджить свежий мастер в свой бранч через git merge --no-ff master. Код мерджить в мастер через pull request.

Мержи (в смысле — коммиты с несколькими родителями) точно так же могут привести к затиранию кода, но, в отличие от rebase, там получается уже много предков, дифф с ними не рисуется явно и разрабы постоянно забывают и возможность, и необходимость сверить изменения. В итоге спасают только тесты.

Да, тут скорее больше организационная проблема, чем техническая. Надо наблюдать, что делают девелоперы и чтобы в репозитории был порядок. К сожалению, silver bullet не существует, но на моей практике ребейз обычно приводит (при неумелом использовании) к куда худшим последствиям. Хотя вопрос rebase vs merge такого же рода как vim vs emacs.

но на моей практике ребейз обычно приводит (при неумелом использовании) к куда худшим последствиям.

При нём таки легко видеть дифф изменений, а если кто его не видит... ну да, можно намешать 100500 разных правок в один коммит и дальше не понимать, что происходит. Но тогда проблема того, кто намешал, или его начальника (за принудительный squash надо расстреливать).
Поэтому, если соблюдать минимальную дисциплину использования, то rebase для джуна/миддла или такой команды будет таки проще.
Проблема возникает там, где идёт конфликт, который неудобно разбирать. Но 1) есть продвинутые (включая графические) merge tools, 2) его нагляднее решать с одним предком, чем с двумя. И нельзя забывать, что rebase это тот же merge, но с линеаризацией. Если это помнить, то 90% проблем от непонимания просто уходит.

Хотя вопрос rebase vs merge такого же рода как vim vs emacs.

И да, и нет. По крайней мере для меня само по себе это чисто практика, в каком случае что удобнее и эффективнее. Религия возникает в других местах.
Например, я однажды жёстко сцепился с челом, который настаивал на том, что любые правки должны идти в следующем порядке: если, например, в разработке версия 9, а отфоркнуты в релизные ветки 8 и 7, и правка касается их всех, то надо 1) коммитить в v7, 2) мержить целиком v7 в v8 (при этом эта правка, по его мнению, естественным образом «втечёт» в v8), 3) после этого из v8 мержить в транк таким же образом.
Подход прикольный, но никак не масштабировался на случаи, например, когда надо распространить в 8.1, 8.2, 7.1, 7.2. У меня для того же предполагались черипики.

Модифікація комміту мається на увазі —amend?

Дуже повчальна стаття, побільше б таких. Дякую!

Дуже корисна фіча, не знав. Дякую автору!

Саме тоді я натрапив на маловідому опцію —onto

Не виключено, що без цієї маловідомої опції користь від rebase десь біля 0.
Взагалі ніякого там ребейсу для того що вже запушили. Хіба що є 110% впевненість що більше над гілкою ніхто не працює. Годиться тільки для локальної роботи, чи фінального вливання гілки в мастер.

Не виключено, що без цієї маловідомої опції користь від rebase десь біля 0.

Чого це? Іноді краще таки зробити ланцюжок cherry-pickʼів. А rebase використовувати або в простішому варіанті, або interactive з усіма його можливостями.
«Onto» це по суті скорочення переміщення разом цього ланцюжка.

У першому випадку модифікувати історію на спільних гілках повинно бути заборонено

Хто сказав, що гілки були спільні?

якщо вони не спільні — не варто робити свою гілку з чужої приватної гілки

Не «не варто», а типова ситуація, що наприклад Петро пішов у відпустку, перед цим пушнувши свої проміжні результати у якусь feature/TRN4-count-all-cows, а Івану тімлід сказав підібрати їх, поновити і відправити. Має право.

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

тімлід навіть може зробити так щоб Петро фізично не мав цього права.

Може. А може і навпаки. У моєму попередньому проекті така передача результатів і відповідальности між колегами була нормою.

в такій ситуації гілка стала спільна і Петро не має права модифікувати її історію.

Та ось модифікують. Хоча звичайно робили таки окремі гілки, помічаючи ще своїм логіном або суфіксом типу −2, −3... але це вже більше для історії ніж для вказаної мети.

Проще не использовать ребейз)

Простіше не писати коментар

Підписатись на коментарі