Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як вирішувати merge конфлікти в Git?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Merge конфлікт** - стан, коли Git не може автоматично об'єднати дві гілки, бо обидві змінили ті самі рядки по-різному. Git позначає кожен конфлікт маркерами `<<<<<<<`, `=======`, `>>>>>>>`. Відредагуй файл, видали всі три маркери, напиши чистий код, потім `git add <файл>` і `git commit`. **Ключове:** перевіряй `git status` після злиття.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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](/questions/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](/questions/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 потребує твоєї допомоги, щоб пов'язати зміни з новим місцем.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.