Як вирішувати merge конфлікти в Git?
Merge конфлікт - стан, коли Git не може автоматично об'єднати дві гілки, бо обидві змінили ті самі рядки по-різному, і він зупиняється, чекаючи на твоє рішення.
Теорія
TL;DR
- Двоє кухарів редагують один рядок рецепту: один пише "1 ч.л. солі", інший "2 ч.л.". Git зупиняється і просить тебе вибрати або об'єднати.
- Git позначає конфлікти маркерами
<<<<<<<,=======,>>>>>>>і чекає, поки ти їх виправиш вручну. - Після редагування:
git add <файл>, потімgit commit(абоgit rebase --continueпри rebase). git merge --abortскасовує merge посередині і відновлює обидві гілки до попереднього стану.git mergetoolвідкриває візуальний редактор з трьома панелями, якщо ти надаєш перевагу графіці перед ручним редагуванням.
Швидкий приклад
# main: price: 10
# feature гілка змінила на: price: 12
# main також змінив на: price: 15
git checkout main
git merge feature
# CONFLICT (content): Merge conflict in file.txt
# file.txt тепер виглядає так:
<<<<<<< HEAD
price: 15
=======
price: 12
>>>>>>> feature
# Редагуємо file.txt: видаляємо всі три маркери, пишемо правильне значення:
price: 15 + tax (12 base)
git add file.txt
git commit -m "Merge with tax adjustment"Ти видалив усі три маркери і написав чистий код. Це весь цикл.
Як Git знаходить конфлікт
Git виконує тривимірний merge (three-way merge): порівнює твою гілку (HEAD), вхідну гілку і їхній спільний предок через git merge-base. Якщо тільки одна сторона змінила рядок, Git бере ту зміну автоматично. Якщо обидві змінили однаковий рядок по-різному, Git не може вирішити. Тому зупиняється, записує маркери в файл і чекає.
Маркери показують рівно те, що написала кожна сторона:
- від
<<<<<<< HEADдо=======- це твоя поточна гілка - від
=======до>>>>>>> branch-name- це вхідна гілка
Після того як ти відредагував і додав файл у стейдж, індекс зберігає розв'язану версію. Коміт її фіналізує.
Коли який підхід використовувати
- Один файл, кілька конфліктів: відкрий в редакторі, виправ вручну,
git add,git commit. - Багато файлів або складні зміни:
git mergetoolз візуальним інструментом на зразок Meld або VS Code. Три панелі поруч: базова (предок), наша версія, їхня версія. - Передумав посередині:
git merge --abortскидає все назад. Безпечно запускати будь-коли до фінального коміту. - Бінарні файли (зображення, скомпільовані артефакти): Git їх не злиє. Використай
git checkout --ours file.pngабоgit checkout --theirs file.pngдля явного вибору версії. - Конфлікти при git rebase: ті самі маркери, але розв'язуєш per-commit і запускаєш
git rebase --continueпісля кожного замістьgit commit.
Налаштування diff3 для більшого контексту
За замовчуванням блок конфлікту показує лише дві сторони. Один раз запусти:
git config --global merge.conflictstyle diff3Тепер у блоці конфлікту з'явиться третя секція з тим, що мав спільний предок. Цей контекст часто одразу пояснює, яка сторона правіша, без читання всієї історії гілки.
Часті помилки
Коміт без попереднього стейджингу.
# Неправильно - після редагування conflicted.js:
git commit -m "fixed"
# Помилка: nothing to commit
# Правильно:
git add conflicted.js
git commit -m "Resolve price conflict"Git потребує staged-версії, щоб зрозуміти, що конфлікт вирішено. Без git add він не знає, що ти закінчив.
Вибрати одну сторону, не читаючи обидві.
Якщо колега виправив баг у своїй гілці, а ти сліпо береш git checkout --ours, ти втрачаєш його виправлення. Читай обидві сторони. git diff HEAD file.js покаже, що є у твоїй гілці.
Залишити символи маркерів у коді.
<<<<<<< HEAD - це звичайний текст. Якщо він потрапить у JavaScript або JSX, застосунок впаде з синтаксичною помилкою. Після розв'язання зроби пошук <<<<<<< у файлі, щоб переконатись, що маркерів немає. Я бачив, як це проходить через code review частіше, ніж можна очікувати.
Забути git status після merge кількох файлів.
Git перелічує всі невирішені файли під "both modified". Якщо виправив один файл і закомітив без перевірки, інші залишаться невирішеними і заблокують подальшу роботу.
git merge --continue без стейджингу.
--continue перевіряє індекс на наявність staged-змін. Якщо нічого не додано, видає помилку. Спочатку git add, потім continue.
Де зустрічається на практиці
- React / Next.js: конфлікти в
package.jsonзалежностях при merge PR - найпоширеніший випадок у командах. - Node/Express: двоє розробників додають route-обробники до
app.jsв одному місці. - Kubernetes:
deployment.yaml, де ops редагує ліміти ресурсів, а dev - тег образу на одних рядках. - Монорепозиторії: lock-файли (
package-lock.json,yarn.lock) конфліктують майже щоразу. Більшість команд не розв'язують їх вручну, а просто регенерують файл після злиття.
На спільних гілках використовуй git merge для збереження історії. Якщо працюєш сам на feature-гілці, git rebase main дає чисту лінійну історію без merge-коміту.
Follow-up питання
Q: Яка різниця між конфліктом при merge і конфліктом при rebase?
A: Маркери конфлікту однакові, але відрізняється момент. Merge розв'язує всі конфлікти в одному коміті. Rebase переграє кожен коміт поверх цільової гілки, тому конфлікти виникають по одному, і після кожного запускаєш git rebase --continue.
Q: Як запобігти конфліктам у команді?
A: Короткі гілки, які часто зливаються. git pull --rebase щодня, щоб гілка залишалась близькою до main. Trunk-based development прибирає довгоживучі гілки взагалі - це найефективніший підхід із усіх.
Q: Що дає git mergetool порівняно з ручним редагуванням?
A: Відкриває візуальний diff (Meld, vimdiff, VS Code) з трьома панелями: предок, твоя версія, вхідна версія. Вибираєш hunks або пишеш результат у четвертій панелі. Налаштовується через git config merge.tool meld.
Q: Як вирішити конфлікт у бінарному файлі, наприклад зображенні?
A: Git позначає його як unmergeable. Вибираєш одну версію явно: git checkout --ours logo.png або git checkout --theirs logo.png, потім git add logo.png і коміт.
Q: (Senior) Як merge.conflictstyle=diff3 допомагає, і коли визначення перейменувань не спрацьовує?
A: diff3 додає версію предка всередину блоку конфлікту, і ти одразу бачиш контекст. Визначення перейменувань (rename detection) не спрацьовує, коли схожість файлу падає нижче 50% після правок. Git вважає файл видаленим на одній гілці і новим на іншій, і ти отримуєш "modify/delete" конфлікт замість звичайного. Рішення: git rm file1.txt, перенеси зміни в новий файл, git add file2.txt, коміт.
Приклади
Базовий: конфлікт в одному рядку
# main гілка має:
echo "version: 1.0" > app.txt
git commit -am "initial version"
# feature гілка змінює на 2.0
git checkout -b feature
echo "version: 2.0" > app.txt
git commit -am "update version"
# main незалежно змінює на 1.5
git checkout main
echo "version: 1.5" > app.txt
git commit -am "patch version"
git merge feature
# CONFLICT (content): Merge conflict in app.txt
# app.txt показує:
<<<<<<< HEAD
version: 1.5
=======
version: 2.0
>>>>>>> feature
# Обираємо правильне, видаляємо всі маркери:
echo "version: 2.0" > app.txt
git add app.txt
git commit -m "Resolve version conflict: take 2.0"Обидві гілки змінили один рядок. Читаєш обидва варіанти, обираєш 2.0, видаляєш маркери, стейджиш, комітиш.
Проміжний: React компонент з кількома пропсами
// src/App.js на main - додали відображення ролі:
const App = () => <div>{user.name} ({user.role})</div>;
// feature гілка додала email:
const App = () => <div>{user.name} - {user.email}</div>;Після git merge feature:
<<<<<<< HEAD
const App = () => <div>{user.name} ({user.role})</div>;
=======
const App = () => <div>{user.name} - {user.email}</div>;
>>>>>>> featureОбидві зміни потрібні. Об'єднуємо:
const App = () => (
<div>{user.name} ({user.role}) - {user.email}</div>
);git add src/App.js
git commit -m "Merge feature: show role and email"Запусти застосунок після коміту. Синтаксична помилка в JSX на цьому етапі майже завжди означає, що маркер залишився в коді.
Просунутий: конфлікт перейменування плюс редагування
# main перейменовує file1.txt на file2.txt
git mv file1.txt file2.txt
git commit -m "rename file1 to file2"
# feature гілка редагувала file1.txt ще до перейменування
git checkout -b feature main~1
echo "new content" >> file1.txt
git commit -am "update file1"
git checkout main
git merge feature
# CONFLICT (modify/delete): file1.txt deleted in HEAD and modified in feature.Визначення перейменувань Git використовує поріг схожості у 50%. Якщо file1.txt зазнав великих правок на feature-гілці, Git може побачити його як видалений на main і змінений на feature, і замість звичайного конфлікту вмісту ти отримуєш "modify/delete".
# Приймаємо перейменування, застосовуємо зміни до нового файлу:
git rm file1.txt
# Вручну переносимо нові зміни в file2.txt, потім:
git add file2.txt
git commit -m "Resolve rename + edit conflict"Розробники, які бачать "modify/delete", часто думають, що щось зламалось. Нічого не зламалось. Файл просто переїхав, і Git потребує твоєї допомоги, щоб пов'язати зміни з новим місцем.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.