Перейти до змісту
Увійти

Теорія

Конфлікти злиття (merge conflict): чому виникають і що робити

Що таке merge conflict на GitHub і чому дві гілки можуть зіткнутися на тому самому рядку, що означає «This branch has conflicts that must be resolved», як читати маркери конфлікту й розв'язати простий конфлікт у браузері і чому вибір правильного тексту це продуктове рішення.

~10 хв читання · 11 вправ · 250 балів

Перед початком

Сіра кнопка Emma

Звичайний день у продуктовій команді Котокорму, аж поки Emma, яка пише тексти для магазину, не надсилає панічне повідомлення. Її pull request #132 повідомляє, що в гілці є конфлікти, які треба розв'язати, кнопка merge сіра, і Emma думає, що зламала сайт.

У цьому уроці ти з'ясуєш, чому дві гілки можуть зіткнутися на одному рядку, прочитаєш маркери конфлікту, розв'яжеш простий конфлікт у браузері й побачиш, чому вибір правильного тексту — продуктове рішення.

Конфлікт блокує один мердж, і більше нічого

Контент-маркетологиня Котокорму Emma відкриває свій pull request #132, «Rewrite delivery page copy», і бачить «This branch has conflicts that must be resolved». Кнопка мерджу сіра. Перша думка: вона зламала сайт.

Pull requests про доставку в catchow/website
  • Rewrite delivery page copy#132

    This branch has conflicts that must be resolved: мердж заблоковано. Конфліктний файл: pages/delivery.md

  • Raise free-delivery threshold to 1000 UAH#133

    Змерджено вчора як коміт 6f2c8a1

Ні. Конфлікт (merge conflict) блокує один мердж, і більше нічого. main, живий сайт і робота Emma в її гілці лишаються такими, як були. Pull request просто чекає, доки хтось ухвалить рішення.

Що сталося? Pull request #133 від Daniel, уже змерджений, підняв поріг безкоштовної доставки й змінив одне речення на сторінці доставки. Emma переписала всю сторінку, і це речення теж. Git отримав дві різні версії одного рядка й не став угадувати, яка має перемогти.

Двоє людей тягнуться різними шматочками пазла до того самого порожнього місця й зупиняються, дивлячись одне на одного.
Конфлікт — не помилка: це означає, що обидві сторони змінили ті самі рядки.

Повідомлення ще й називає файл, тут pages/delivery.md, тож відомо, куди дивитися. Для продакта правильна перша реакція — спокій: на сайті нічого не змінилося, нічого не втрачено, а розв'язання — це розмова про те, що має бути на сторінці.

  • Так
    «Нічого не зламано; #132 просто чекає рішення щодо рядка 12».
  • Не так
    «Конфлікт, відкочуйте сайт!»

    Конфліктна зміна на сайт навіть не потрапила.

Коли Git зупиняється й питає

Щоб змерджити, Git знаходить коміт, де дві лінії роботи розійшлися, — їхню спільну точку відліку — і порівнює з нею кожну сторону.

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

Конфлікт виникає, лише коли обидві сторони по-різному змінили ті самі рядки або сусідні, або коли одна сторона видалила файл, який інша редагувала. Для такого випадку правила немає: яка версія переможе — це рішення, а рішення Git залишає людям.

Зміни в різних файлах чи частинах Git об'єднує сам; конфліктом стають лише ті самі рядки, змінені по-різному, — питання, яке вирішує людина.

У #132 спільна точка казала «Free delivery on orders from 800 UAH.» main змінила це на «...from 1000 UAH.», а гілка Emma — на «Orders from 800 UAH ship free, right to your door.» Той самий рядок, дві різні правки: конфлікт. Ні Daniel, ні Emma нічого не зробили неправильно; вони просто торкнулися того самого речення того самого тижня.

content/delivery-copy і main сьогодніОбидві лінії починаються від e7c3b19. Кожен виділений коміт по-своєму змінив рядок 12 у pages/delivery.md. Тепер гілка на 1 коміт ahead і на 3 behind.
  1. e7c3b19Add summer promo banner
  2. f02a6d4Update FAQ answers
  3. e19c3b7Rewrite delivery page copycontent/delivery-copy
  4. 8be2f70Add cat treats bundle
  5. 6f2c8a1Raise free-delivery threshold to 1000 UAH (#133)main

Граф комітів, від найстарішого: content/delivery-copy і main сьогодні 1. коміт e7c3b19 у main "Add summer promo banner" 2. гілка content/delivery-copy починається від коміту e7c3b19 3. коміт f02a6d4 у main "Update FAQ answers" 4. коміт e19c3b7 у content/delivery-copy "Rewrite delivery page copy" (виділено) 5. коміт 8be2f70 у main "Add cat treats bundle" 6. коміт 6f2c8a1 у main "Raise free-delivery threshold to 1000 UAH (#133)" (виділено)

  • Так
    Чекати на конфлікт, коли двоє по-різному редагують те саме речення
  • Не так
    Чекати на конфлікт лише тому, що двоє редагували той самий файл

    Різні частини одного файлу зливаються самі.

Немає конфлікту — ще не означає правильно

Git — уважний поєднувач тексту, а не тестувальник. Він порівнює рядки й зупиняється, лише коли ті самі рядки змінили двома способами. Він не знає, який вигляд має сторінка, що рахує кошик і чи не заважають одна одній дві фічі.

Згадай історію Daniel про кнопку. Його гілка з редизайном і панель cookie від колеги змінювали різні файли, тож Git змерджив їх мовчки. На телефонах панель лягла на нову кнопку, натиснути її не міг ніхто, і за десять хвилин мердж відкотили. Конфлікту не було, а результат зламаний.

Тримай у голові обидві половини:

  • Відсутність конфлікту не означає, що результат правильний. Після будь-якого мерджу, який поєднує роботу різних людей, перевір сторінку, бажано й на телефоні.
  • Конфлікт не означає, що хтось помилився. Він лише означає, що двоє змінили той самий текст, і рішення має ухвалити людина.

Для продакта це корисний розподіл довіри: текст хай поєднує Git, а продукт перевіряй сам.

  • Так
    «Злилося чисто. Перевірмо оформлення на телефоні, перш ніж оголошувати».
  • Не так
    «GitHub не показав конфліктів, отже, точно працює».

    Git перевіряє рядки тексту, а не поведінку.

Читаємо маркери конфлікту

Під повідомленням про конфлікт у #132 є кнопка Resolve conflicts («розв'язати конфлікти»). Вона відкриває простий редактор просто на GitHub, уже з конфліктним файлом. Схоже на звичайний текст із дивними стрілками посередині:

## Delivery

<<<<<<< content/delivery-copy
Orders from 800 UAH ship free, right to your door.
=======
Free delivery on orders from 1000 UAH.
>>>>>>> main

Couriers deliver within one day in Kyiv.

Це маркери конфлікту:

  • <<<<<<< відкриває конфлікт і називає гілку, з якої прийшла верхня версія, тут content/delivery-copy від Emma;
  • ======= розділяє дві версії;
  • >>>>>>> закриває конфлікт і називає другу гілку, тут main, чия версія стоїть одразу над ним.

Усе поза маркерами вже узгоджено: заголовок і рядок про кур'єрів рішення не потребують. На людину чекає лише частина між стрілками. А маркери — не текст для сторінки: усі три рядки з ними треба прибрати. Розробники бачать точнісінько такі маркери у своєму редакторі коду після git merge у терміналі.

  • Так
    Вирішувати лише між <<<<<<< і >>>>>>>; решту лишити як є
  • Не так
    Заодно переписати в редакторі заголовок і рядок про кур'єрів

    Рядки поза маркерами вже узгоджено.

Конфлікт — це питання про намір

Перший порив Emma — залишити свою версію; зрештою, це її pull request. Але тоді сторінка обіцятиме безкоштовну доставку від 800 грн, а кошик після #133 робитиме її безкоштовною лише від 1000. Клієнти платитимуть за доставку, яку сторінка назвала безкоштовною. А якщо залишити рядок Daniel, пропаде новий текст Emma, який погодив маркетинг.

Тож правильна відповідь — жодна зі сторін: слова Emma з числом Daniel, «Orders from 1000 UAH ship free, right to your door.»

Двоє колег за столом: одна тримає маленький помаранчевий цінник, інший щойно переписану сторінку, і разом вони прикріплюють цінник до нової сторінки.

У цьому суть розв'язання конфліктів. Конфлікт — не помилка, а питання: що мав на увазі кожен і що тепер має бути на сторінці? Git на це не відповість. Розробники технічно розв'яжуть будь-який конфлікт, але часто не знають, чи фінвідділ знову не передумав і яке формулювання погодив маркетинг. Тут і потрібен продакт.

У редакторі можна залишити одну сторону, обидві або написати щось нове. Вибирай за наміром, а не за тим, чий це pull request, і переконайся, що в результаті сторінка читається як одне ціле.

  • Так
    «Orders from 1000 UAH ship free, right to your door.»

    Погоджені слова плюс чинний поріг.

  • Не так
    Залишити свою сторону, бо це твій pull request

    Сторінка суперечитиме кошику.

Розв'язуємо конфлікт на GitHub

Для конфлікту в текстовому файлі вистачає вебредактора GitHub. Кроки по порядку:

  1. У pull request натисни Resolve conflicts.
  2. Знайди блок між <<<<<<< і >>>>>>>.
  3. Заміни весь блок погодженим текстом і видали всі три рядки з маркерами. Тут: «Orders from 1000 UAH ship free, right to your door.»
  4. Натисни Mark as resolved («позначити як розв'язане»). Це каже GitHub, що з файлом усе вирішено. Якщо конфліктних файлів кілька, вибери наступний у списку ліворуч і повтори.
  5. Натисни Commit merge («закомітити мердж»).
  6. Перевір вкладку Files changed у pull request так, як це зробив би рев'юер: там має бути лише переписаний текст Emma, з 1000 у рядку 12 і без жодного сліду 800. Тоді — рев'ю і мердж pull request, як завжди.

Порядок важливий: спершу редагування, потім Mark as resolved, і лише тоді Commit merge. І розв'язання — ще не фініш: воно оновлює тільки гілку. У main і на сайт зміна потрапить, коли сам pull request пройде рев'ю і його змерджать.

  • Так
    Редагування → Mark as resolved → Commit merge → перевірка Files changed → рев'ю і мердж
  • Не так
    Оголосити новий текст, щойно натиснув Commit merge

    Pull request ще треба переглянути й змерджити.

Забуті маркери потрапляють на сторінку

Коли комітять розв'язаний мердж, Git довіряє людині. Те, що лежить у файлі в цю мить, і є розв'язанням. Git за тебе не прибирає: забутий <<<<<<<, ======= чи >>>>>>> — просто текст, він комітиться, як будь-який інший рядок, і після мерджу може з'явитися на живій сторінці доставки перед кожним клієнтом.

Тому Daniel і каже «перевір двічі». Вебредактор GitHub трохи допомагає, бо чекає, що маркери зникнуть, перш ніж файл позначать як розв'язаний. У терміналі ніщо нікого не зупинить. Розробники страхуються командою git diff --check, яка помічає забуті маркери конфлікту, або шукають у файлі <<<<<<< перед комітом.

Для продакта страхувальна сітка — рев'ю. Після розв'язання відкрий Files changed і проглянь конфліктний файл: там має бути погоджений текст, без стрілок і рядків зі знаків рівності. Якщо помітиш маркер, залиш коментар до цього рядка, перш ніж хтось змерджить.

  • Так
    Перед схваленням проглянути Files changed на <<<<<<<, ======= і >>>>>>>
  • Не так
    «Git сам прибере маркери під час мерджу».

    Маркери комітяться як звичайний текст.

Куди потрапляє розв'язання

Що саме комітить Commit merge і куди? Мердж-коміт у гілці pull request, а не в main. Щоб розв'язати конфлікт, GitHub зливає main у content/delivery-copy, і твоє рішення всередині. Станом на 2026 рік його повідомлення таке: «Merge branch 'main' into content/delivery-copy».

На графі стрілка мерджу йде з main у гілку. Після цього в гілці є все з main плюс переписаний текст Emma, тож вона на нуль behind, а повідомлення про конфлікт зникає. Сама main не змінилася. Попередні коміти Emma всі на місці; нічого не викинуто.

Після Commit mergeМердж іде з main у гілку. Тепер у content/delivery-copy є все з main плюс новий текст Emma; сама main не змінилася.
  1. e7c3b19Add summer promo banner
  2. f02a6d4Update FAQ answers
  3. e19c3b7Rewrite delivery page copy
  4. 8be2f70Add cat treats bundle
  5. 6f2c8a1Raise free-delivery threshold to 1000 UAH (#133)main
  6. c90d4e7Merge branch 'main' into content/delivery-copycontent/delivery-copy

Граф комітів, від найстарішого: Після Commit merge 1. коміт e7c3b19 у main "Add summer promo banner" 2. гілка content/delivery-copy починається від коміту e7c3b19 3. коміт f02a6d4 у main "Update FAQ answers" 4. коміт e19c3b7 у content/delivery-copy "Rewrite delivery page copy" 5. коміт 8be2f70 у main "Add cat treats bundle" 6. коміт 6f2c8a1 у main "Raise free-delivery threshold to 1000 UAH (#133)" 7. мердж-коміт c90d4e7: main змерджено в content/delivery-copy "Merge branch 'main' into content/delivery-copy" (виділено)

Кнопка мерджу повертається й чекає на рев'ю. Оскільки Котокорм робить squash, цей додатковий мердж-коміт в історію main не потрапить: main отримає один чистий коміт для #132.

Тож щоразу, бачачи мердж на графі, читай напрямок. Мердж у гілку означає, що гілку оновили чи розв'язали в ній конфлікт; мердж у main — що робота приземлилася. Сайт змінюється лише від другого.

  • Так
    «Merge branch 'main' into content/about-page» → гілку оновили; main не змінилася
  • Не так
    Читати мердж у гілку як «уже на сайті»

    На сайт потрапляє лише мердж у main.

Хто розв'язує і що робить продакт

Вебредактор GitHub уміє розв'язувати один вид конфліктів: різні правки тих самих рядків текстового файлу. Цього вистачає для більшості конфліктів у текстах. Інші види потребують командного рядка, наприклад коли одна гілка видалила файл, який інша редагувала, або обидві замінили те саме зображення. Тоді GitHub вимикає Resolve conflicts і пише, що конфлікт надто складний для вебредактора; це робота розробника.

Хто розв'язує? Зазвичай автор заблокованого pull request разом з автором зміни, з якою сталося зіткнення. Продакт приносить відповідь на питання «що тут має бути написано?». Ця частина роботи така:

  • з'ясувати, що мала на увазі кожна сторона, наприклад з іншого pull request і його issue;
  • погодити результат із причетними людьми;
  • записати погоджений текст і причину в коментарі до pull request;
  • потім перевірити Files changed.

А чого не робити: вибирати сторону, аби кнопка стала зеленою, розв'язувати конфлікти в коді чи конфігурації, яких не розумієш, і редагувати чужу гілку, не спитавши.

  • Так
    «Погоджено з Emma і Daniel: її формулювання, 1000 грн із #133 (запит фінвідділу)».

    Намір, рішення й причина — записані.

  • Не так
    Розв'язати конфлікт у JavaScript оформлення замовлення, бо редактор дозволяє

    Конфлікти в коді — справа розробників, які за цей код відповідають.

Як мати менше конфліктів

Чому конфлікт Emma взагалі стався? Її гілка жила понад тиждень і переписала цілу сторінку. А main тим часом пішла вперед без неї: коли Emma відкрила pull request, гілка вже відставала на три коміти. Кожен із них був шансом на зіткнення.

Ризик росте з трьома речами, які видно просто зі списку pull requests: скільки гілка живе, скільки файлів зачіпає і наскільки відстає від main. Виправлення в одному файлі, відкрите вчора, зіштовхується рідко. Шеститижневий редизайн на 38 файлів, що відстає на 41 коміт, майже напевно зіштовхнеться, і конфлікти будуть великі.

Звички, що цьому запобігають:

  • маленькі гілки, одна задача на гілку, мердж за кілька днів;
  • у довшу гілку рано підтягувати свіжу main, поки конфлікти ще дрібні;
  • розмовляти: один рядок у командному чаті, «цього тижня я переписую сторінку доставки», — і Daniel написав би Emma, перш ніж чіпати те речення.
За сусідніми столами колега нахиляється й коротко щось каже, показуючи на ту саму сторінку на обох екранах.

Останнє — теж робота продакта: знати, що фінвідділ і маркетинг змінюють ту саму сторінку того самого тижня.

  • Так
    «Попереджаю: цього тижня переписую сторінку доставки».

    Один рядок у чаті запобігає зіткненню.

  • Не так
    Дати гілці з редизайном на 38 файлів відстати від main на 41 коміт

    Кожен пропущений коміт — шанс на зіткнення.

Готові потренуватися?

Виконайте вправи уроку й отримайте 250 балів, коли пройдете його вперше.

Розпочати вправи