Яка різниця між git merge та git rebase?
git merge інтегрує гілку, створюючи новий коміт з двома батьками і зберігаючи повну записану історію обох гілок. git rebase відтворює твої коміти поверх іншої гілки, переписуючи їхні SHA і формуючи лінійну послідовність.
Теорія
TL;DR
- Аналогія: merge це зшивання двох звітів разом, обидва шляхи залишаються видимими. Rebase це переписування своїх нотаток у кінець основного звіту так, щоб виглядало як один документ.
- Головна різниця: merge додає один коміт з двома батьками; rebase створює нові коміти з новими SHA для кожного відтвореного.
- Merge зберігає точки розходження і злиття гілок. Rebase робить так, наче робота йшла послідовно від початку.
- Правило вибору: спільна або публічна гілка? Merge. Локальна feature-гілка перед PR? Rebase.
- Після push на спільний remote: тільки merge. Rebase після push ламає клони команди.
Швидкий приклад
# main має коміт B, feature має D і E
git checkout main && echo "B" >> app.js && git commit -m "B"
git checkout -b feature
echo "D" >> app.js && git commit -m "D"
echo "E" >> app.js && git commit -m "E"
# Варіант 1: merge
git checkout main && git merge feature
# Історія: A-B-M (M має двох батьків: B і E)
# Варіант 2: rebase (на feature-гілці, перед мержем)
git checkout feature && git rebase main
# Історія: A-B-D'-E' (D' і E' мають нові SHA)Після merge в логу видно розгалуження і з'єднання. Після rebase пряма лінія. Код однаковий, але записана версія подій різна.
Ключова різниця
Merge знаходить спільного предка двох гілок, обчислює three-way diff і записує новий коміт з обома верхівками як батьками. Оригінальні коміти не змінюються. Rebase бере кожен коміт між точкою розгалуження і верхівкою feature-гілки, відв'язує його від оригінального батька і по черзі відтворює на новій базі через cherry-pick. Оскільки змінюється SHA батька, змінюється і SHA коміту, навіть якщо diff ідентичний. Саме тому не можна безпечно робити rebase на гілці, яку вже клонували колеги.
Коли що використовувати
- Спільна гілка (
main,develop): тільки merge. Rebase на гілці, яку вже підтягнули інші, створює дублікати комітів і змушує команду вручну виправляти локальний стан. - Локальна feature-гілка перед PR: зробити rebase на main спочатку. Reviewer отримає чисту послідовну історію.
- Публічна історія вже запушена і підтягнута колегою: merge. Навіть якщо це твоя власна гілка.
- Інтерактивний rebase (
git rebase -i): підходить для squash або редагування комітів до першого push.
Таблиця порівняння
| Аспект | git merge | git rebase |
|---|---|---|
| Форма історії | Розгалужена, показує відхилення | Лінійна, коміти послідовно |
| Нові коміти | Один merge commit (два батьки) | Новий коміт для кожного відтвореного, нові SHA |
| Переписує історію? | Ні | Так |
| Безпечний після push? | Так | Ні |
| Вирішення конфліктів | Один раз, в точці злиття | По одному разу на кожен відтворюваний коміт |
| Коли використовувати | Спільні гілки, аудит | Локальні feature-гілки, чисті PR |
Як це працює всередині
Merge обчислює three-way diff: спільний предок плюс дві верхівки гілок. Без конфліктів Git записує один коміт з двома батьками автоматично. Rebase йде по комітах між точкою розгалуження і верхівкою feature-гілки, робить cherry-pick кожного на нову базу і записує нові коміти. Оскільки батьківський SHA змінився, змінюється і SHA дочірнього коміту, навіть якщо код у ньому байт-у-байт однаковий.
Типові помилки
Rebase спільної гілки.
# Неправильно
git checkout main && git rebase feature # Переписує публічний mainКожен, хто вже підтягнув main, тепер має розбіжну історію. git pull впаде. Їм потрібно git reset --hard origin/main щоб відновитись. Для гілок, якими активно користується команда, тільки git merge.
Очікування одного кроку вирішення конфліктів під час rebase.
git rebase main
# Конфлікт на D -> виправляємо -> git rebase --continue
# Конфлікт на E -> виправляємо -> git rebase --continueMerge видає всі конфлікти одразу. Rebase зупиняється на кожному конфліктному коміті. Ті, хто чекає одного кроку, перериваються думаючи що щось зламалось. Це нормально: git rebase --continue після кожного конфлікту, або git rebase --abort щоб повернутись до початкового стану.
Force-push після rebase коли колега вже підтягнув гілку.
git push origin feature # Колега підтягує
git rebase main # Нові SHA на feature
git push --force-with-lease # Перезаписує remote
# Локальна feature-гілка колеги тепер осиротіла--force-with-lease перевіряє що верхівка remote збігається з останнім SHA, отриманим при fetch. Але він не захищає колегу, який підтягнув між твоїм fetch і push. На спільній гілці це завжди проблема.
Merge після rebase без розуміння fast-forward.
Після rebase feature-гілки на main, верхівка feature напряму попереду main. git merge feature зробить fast-forward автоматично і не додасть merge commit. Якщо потрібен merge commit щоб позначити межу PR, треба явно вказати --no-ff:
git checkout main && git merge --no-ff featureРеальне використання
- React (facebook/react): контрибьютори роблять rebase feature-гілок локально перед PR. Main залишається лінійним.
- Node.js core (nodejs/node): merge commits для стабільних гілок (
v20.x), інтерактивний rebase для особистого WIP. - Linux kernel (torvalds/linux): суворий rebase перед злиттям у Linus. Лінійна історія потрібна щоб
git bisectкоректно працював на тисячах комітів. - Express.js: rebase +
merge --no-ffпри злитті PR, щоб межа PR залишалась видною в логу.
Більшість команд зупиняється десь між цими підходами. "Rebase перед PR, merge в main" вирішує переважну більшість ситуацій і тримається без зайвого контролю.
Питання для поглиблення
Q: Як перевірити що rebase створив нові коміти?
A: git reflog одразу після rebase покаже старі SHA до операції і нові після. git log --oneline --graph також показує лінійний результат.
Q: Що робить --force-with-lease і коли це безпечно?
A: Push відбудеться тільки якщо верхівка remote збігається з останнім SHA, отриманим при fetch. Безпечно коли ти єдиний на гілці і ніхто не підтягував її після твого останнього fetch.
Q: Чим вирішення конфліктів при rebase відрізняється від merge?
A: Rebase зупиняється на кожному коміті що викликає конфлікт. Виправляєш і запускаєш git rebase --continue. Merge збирає всі конфлікти і вирішуєш їх за один раз.
Q: Що таке git pull --rebase origin main і чим відрізняється від git rebase main?
A: git pull --rebase спочатку отримує нові коміти з remote, потім робить rebase локальної гілки на оновлену верхівку. git rebase main робить rebase тільки на локальний main, який може відставати від remote.
Q: Чому в команді з багатьма контрибьюторами merge масштабується краще ніж rebase?
A: Відтворення сотень комітів від багатьох розробників генерує часті конфлікти на кожен коміт окремо. Merge це один коміт на інтеграцію. До того ж merge зберігає хронологію контрибьюторів, що важливо для git blame і аудиту.
Приклади
Merge в Node.js Express додатку
git checkout main
echo "app.get('/', (req,res) => res.send('Home'));" > server.js
git add . && git commit -m "Initial Express server"
git checkout -b auth-feature
echo "app.use('/admin', authMiddleware);" >> server.js
git add . && git commit -m "Add auth route guard"
echo "function authMiddleware(req,res,next){ next(); }" >> server.js
git add . && git commit -m "Implement auth check"
git checkout main
git merge auth-feature
# Результат: merge commit з двома батьками
# git log --graph показує точку розгалуження і злиттяОбидва оригінальні коміти залишились з оригінальними SHA. В логу видно коли auth-feature відгалузилась від main і коли потрапила назад.
Rebase перед відкриттям PR
# Поки ти був на auth-feature, main отримав нові коміти
git checkout auth-feature
git rebase main
# Коміти auth-feature тепер відтворені поверх останнього main
# git log --graph: лінійно - Initial -> нові коміти main -> auth коміти
git push origin auth-feature
git push --force-with-lease # Тільки якщо гілку вже пушили ранішеКод той самий що і після merge. Але історія читається так, наче auth-коміти написані після останніх змін в main. PR стає простіше ревьюити.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.