Skip to main content

Як вирішувати merge конфлікти в Git?

Merge конфлікт - стан, коли Git не може автоматично об'єднати дві гілки, бо обидві змінили ті самі рядки по-різному, і він зупиняється, чекаючи на твоє рішення.

Теорія

TL;DR

  • Двоє кухарів редагують один рядок рецепту: один пише "1 ч.л. солі", інший "2 ч.л.". Git зупиняється і просить тебе вибрати або об'єднати.
  • Git позначає конфлікти маркерами <<<<<<<, =======, >>>>>>> і чекає, поки ти їх виправиш вручну.
  • Після редагування: git add <файл>, потім git commit (або git rebase --continue при rebase).
  • git merge --abort скасовує merge посередині і відновлює обидві гілки до попереднього стану.
  • git mergetool відкриває візуальний редактор з трьома панелями, якщо ти надаєш перевагу графіці перед ручним редагуванням.

Швидкий приклад

bash
# 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 для більшого контексту

За замовчуванням блок конфлікту показує лише дві сторони. Один раз запусти:

bash
git config --global merge.conflictstyle diff3

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

Часті помилки

Коміт без попереднього стейджингу.

bash
# Неправильно - після редагування 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, коміт.

Приклади

Базовий: конфлікт в одному рядку

bash
# 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 компонент з кількома пропсами

jsx
// 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:

jsx
<<<<<<< HEAD const App = () => <div>{user.name} ({user.role})</div>; ======= const App = () => <div>{user.name} - {user.email}</div>; >>>>>>> feature

Обидві зміни потрібні. Об'єднуємо:

jsx
const App = () => ( <div>{user.name} ({user.role}) - {user.email}</div> );
bash
git add src/App.js git commit -m "Merge feature: show role and email"

Запусти застосунок після коміту. Синтаксична помилка в JSX на цьому етапі майже завжди означає, що маркер залишився в коді.

Просунутий: конфлікт перейменування плюс редагування

bash
# 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".

bash
# Приймаємо перейменування, застосовуємо зміни до нового файлу: git rm file1.txt # Вручну переносимо нові зміни в file2.txt, потім: git add file2.txt git commit -m "Resolve rename + edit conflict"

Розробники, які бачать "modify/delete", часто думають, що щось зламалось. Нічого не зламалось. Файл просто переїхав, і Git потребує твоєї допомоги, щоб пов'язати зміни з новим місцем.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?