Конфлікти
Останнє оновлення 2025-01-28 | Редагувати цю сторінку
Огляд
Питання
- Що робити, коли мої зміни конфліктують зі змінами інших?
Цілі
- Що таке конфлікти і коли вони можуть виникати.
- Вирішення конфліктів, що виникають внаслідок злиття змін.
Як тільки люди починають працювати паралельно, вони, швидше за все, “наступають один одному на ноги”. Це навіть може статися з однією людиною: якщо ми працюємо над програмою на нашому ноутбуці і на сервері в лабораторії водночас, ми можемо внести різні зміни в кожну копію. Контроль версій допомагає нам вирішувати ці конфлікти, надаючи інструменти для узгодження змін, які накладаються одна на одну.
Щоб побачити, як ми можемо розвʼязувати конфлікти, спочатку ми
повинні їх створити. Наразі файл guacamole.md
виглядає
однаково у клонах обох партнерів нашого репозиторію
recipes
:
ВИХІД
# Guacamole
## Ingredients
* avocado
* lime
* salt
## Instructions
Тепер додамо один рядок до копії співавтора:
ВИХІД
# Guacamole
## Ingredients
* avocado
* lime
* salt
## Instructions
* put one avocado into a bowl.
а потім відправимо наші зміни на GitHub:
ВИХІД
[main 5ae9631] First step on the instructions
1 file changed, 1 insertion(+)
ВИХІД
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 331 bytes | 331.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To https://github.com/alflin/recipes.git
29aba7c..dabb4c8 main -> main
Тепер нехай власник зробить іншу зміну у своїй копії без отримання нових змін з GitHub:
ВИХІД
# Guacamole
## Ingredients
* avocado
* lime
* salt
## Instructions
* peel the avocados
Ми можемо зробити коміт наших змін локально:
ВИХІД
[main 07ebc69] Add first step
1 file changed, 1 insertion(+)
але Git не дозволить нам відправити зміни на GitHub:
ВИХІД
To https://github.com/alflin/recipes.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'https://github.com/alflin/recipes.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Git не дозволяє цю операцію, оскільки виявляє, що віддалений репозиторій має нові оновлення, які не були включені до локальної гілки. Що ми повинні зробити - це отримати зміни з GitHub, обʼєднати їх з копією репозиторію, у якій ми зараз працюємо, а тільки потім надіслати їх на GitHub. Давайте почнемо з отримання змін:
ВИХІД
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 3 (delta 2), reused 3 (delta 2), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/alflin/recipes
* branch main -> FETCH_HEAD
29aba7c..dabb4c8 main -> origin/main
Auto-merging guacamole.md
CONFLICT (content): Merge conflict in guacamole.md
Automatic merge failed; fix conflicts and then commit the result.
Можливо, вам доведеться надати Git додаткові інструкції
Якщо у виведених даних з’являється наступне, Git запитує вас про вказівки.
ВИХІД
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint: git config pull.rebase false # merge (the default strategy)
hint: git config pull.rebase true # rebase
hint: git config pull.ff only # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
У новіших версіях Git ви можете обрати різні стратегії поведінки у
випадку, коли git pull
призводить до злиття розбіжних
гілок. У нашому випадку нам потрібна стандартна стратегія. Щоб
використовувати її, виконайте наступну команду, яка встановить її як дію
за замовчуванням.
Потім спробуйте отримати зміни ще раз.
Команда git pull
оновлює локальний репозиторій, щоб
додати до нього ті зміни, які вже містяться у віддаленому репозиторії.
Після отримання змін із віддаленої гілки Git визначає, що зміни, внесені
до локальної копії, конфліктують зі змінами, зробленими у віддаленому
репозиторії. Тому, щоб запобігти перезапису нашої роботи, Git
відмовляється об’єднувати дві версії. У файлі, де є конфлікт, він
позначається наступним чином:
ВИХІД
# Guacamole
## Ingredients
* avocado
* lime
* salt
## Instructions
<<<<<<< HEAD
* peel the avocados
=======
* put one avocado into a bowl.
>>>>>>> dabb4c8c450e8475aee9b14b4383acc99f42af1d
Нашим змінам передує <<<<<<< HEAD
.
Потім Git вставив =======
як роздільник між суперечливими
змінами, та позначив кінець вмісту, завантаженого з GitHub, за допомогою
>>>>>>>
. (Рядок літер і цифр після
цього маркера ідентифікує щойно завантажений коміт.)
Тепер ми маємо відредагувати цей файл, щоб видалити ці маркери та узгодити зміни. Ми можемо зробити все, що бажаємо: зберегти зміни з локального чи віддаленого репозиторію, написати щось нове та замінити обидві версії, або позбутися змін повністю. Замінимо обидві версії так, щоб файл виглядав наступним чином:
BASH
Enumerating objects: 10, done.
Counting objects: 100% (10/10), done.
Delta compression using up to 8 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 645 bytes | 645.00 KiB/s, done.
Total 6 (delta 4), reused 0 (delta 0)
remote: Resolving deltas: 100% (4/4), completed with 2 local objects.
To https://github.com/vlad/planets.git
dabb4c8..2abf2b1 main -> main
ВИХІД
# Guacamole
## Ingredients
* avocado
* lime
* salt
## Instructions
* peel the avocados and put them into a bowl.
Щоб закінчити злиття, додамо guacamole.md
до зони
стейджингу, а потім зробимо коміт:
BASH
remote: Enumerating objects: 10, done.
remote: Counting objects: 100% (10/10), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 6 (delta 4), reused 6 (delta 4), pack-reused 0
Unpacking objects: 100% (6/6), done.
From https://github.com/vlad/planets
* branch main -> FETCH_HEAD
dabb4c8..2abf2b1 main -> origin/main
Updating dabb4c8..2abf2b1
Fast-forward
mars.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
ВИХІД
On branch main
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: guacamole.md
ВИХІД
[main 2abf2b1] Merge changes from GitHub
Тепер ми можемо відправити наші зміни на GitHub:
ВИХІД
Enumerating objects: 10, done.
Counting objects: 100% (10/10), done.
Delta compression using up to 8 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 645 bytes | 645.00 KiB/s, done.
Total 6 (delta 4), reused 0 (delta 0)
remote: Resolving deltas: 100% (4/4), completed with 2 local objects.
To https://github.com/alflin/recipes.git
dabb4c8..2abf2b1 main -> main
Git відстежує, що з чим було об’єднано, тому нам не потрібно знову
виправляти конфлікти вручну коли співавтор, який зробив першу зміну,
знову виконує git pull
:
ВИХІД
remote: Enumerating objects: 10, done.
remote: Counting objects: 100% (10/10), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 6 (delta 4), reused 6 (delta 4), pack-reused 0
Unpacking objects: 100% (6/6), done.
From https://github.com/alflin/recipes
* branch main -> FETCH_HEAD
dabb4c8..2abf2b1 main -> origin/main
Updating dabb4c8..2abf2b1
Fast-forward
guacamole.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
Ми отримуємо файл з об’єднаними змінами:
ВИХІД
# Guacamole
## Ingredients
* avocado
* lime
* salt
## Instructions
* peel the avocados and put them into a bowl.
Нам не потрібно знову робити злиття змін, оскільки Git знає, об’єднання вже завершено.
Здатність Git вирішувати конфлікти є дуже корисною, але вона вимагає часу та зусиль, і може призвести до помилок, якщо конфлікти не вирішуються належним чином. Якщо ви часто стикаєтеся з конфліктами в вашому проекті, розгляньте ці технічні підходи щоб мінімізувати їх:
- Частіше “витягуйте” (за допомогою
git pull
) зміни, особливо перед початком нової роботи - Використовуйте тематичні гілки (feature branches) для розділення
роботи, з їх подальшим об’єднанням до гілки
main
після завершення роботи - Створюйте менші, більш цілеспрямовані (атомарні) коміти
- Надсилайте (за допомогою
git push
) свою роботу після її завершення до спільного репозиторію та заохочуйте свою команду робити те саме. Це зменшує обсяг незавершеної роботи та, відповідно, ймовірність конфліктів - Там, де логічно доречно, розбивайте великі файли на менші, щоб зменшити ймовірність того, що кілька авторів редагуватимуть один і той самий файл одночасно
Конфлікти також можна мінімізувати за допомогою дотримання деяких стратегій управління проєктами:
- Чітко визначте обов’язки кожного співавтора, хто відповідає за які аспекти проєкту
- Обговоріть зі своїми співавторами порядок виконання задач, щоб уникнути одночасної роботи над тими самими рядками
- Якщо конфлікти є стилістичними (наприклад, табуляції або пробіли),
домовтеся про стиль коду, яким ви будете керуватися, та використовуйте
за необхідністю інструменти форматування (наприклад,
htmltidy
,perltidy
,rubocop
, та ін.) для забезпечення єдиного стилю
Вправа: створення та вирішення конфліктів
Клонуйте репозиторій, створений вашим інструктором. Додайте до нього новий файл та змініть наявний файл (ваш інструктор скаже, який саме). Потім, на запит вашого інструктора, отримайте нові зміни з цього репозиторію, щоб створити конфлікт, а потім вирішіть його.
Конфлікти у бінарних файлах
Як Git вирішує конфлікти в зображеннях або інших бінарних файлах, що зберігаються в системі контролю версій?
Спробуймо це дослідити. Припустимо, Альфредо сфотографував свій гуакамоле та зберіг фото у файлі `guacamole.jpg’.
Якщо у вас немає файлу із зображенням гуакамоле, ви можете створити фіктивний бінарний файл наступним чином:
ВИХІД
-rw-r--r-- 1 alflin 57095 1.0K Mar 8 20:24 guacamole.jpg
ls
показує, що було створено файл розміром 1 кілобайт.
Він містить випадкові байти, які були зчитані зі спеціального файлу
/dev/urandom
.
Тепер припустимо, що Альфредо додає guacamole.jpg
до
свого репозиторію:
ВИХІД
[main 8e4115c] Add picture of guacamole
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 guacamole.jpg
Припустимо, що Джиммі тим часом додав схожу фотографію. На його фотографії зображено гуакамоле з начос, але воно також називається guacamole.jpg. Коли Альфредо намагається відправити зміни до віддаленого репозиторію, він отримує вже знайоме повідомлення:
ВИХІД
To https://github.com/alflin/recipes.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'https://github.com/alflin/recipes.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Як ми вже дізналися раніше, ми спочатку маємо отримати зміни та вирішити усі конфлікти:
Коли конфлікт виникає у зображенні або іншому бінарному файлі, git друкує таке повідомлення:
ВИХІД
$ git pull origin main
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://github.com/alflin/recipes.git
* branch main -> FETCH_HEAD
6a67967..439dc8c main -> origin/main
warning: Cannot merge binary files: guacamole.jpg (HEAD vs. 439dc8c08869c342438f6dc4a2b615b05b93c76e)
Auto-merging guacamole.jpg
CONFLICT (add/add): Merge conflict in guacamole.jpg
Automatic merge failed; fix conflicts and then commit the result.
Повідомлення про конфлікт здебільшого ідентичне повідомленням для
guacamole.md
, але містить один додатковий рядок:
ВИХІД
warning: Cannot merge binary files: guacamole.jpg (HEAD vs. 439dc8c08869c342438f6dc4a2b615b05b93c76e)
Git не може автоматично вставляти маркери конфлікту у зображення, як
він це робить для текстових файлів. Тому замість того, щоб редагувати
файл зображення, нам потрібно викликати з історії змін ту його версію,
яку ми хочемо зберегти. Після цього ми можемо виконати відповідні
команди git add
та git commit
, щоб зберегти цю
версію.
В додатковому рядку вище, Git зручно надав нам ідентифікатори коміту
для обох версій guacamole.jpg
. Наша версія -
HEAD
, а Джиммі зберіг версію 439dc8c0...
. Якщо
ми хочемо використовувати нашу версію, ми можемо застосувати
git checkout
:
BASH
$ git checkout HEAD guacamole.jpg
$ git add guacamole.jpg
$ git commit -m "Use image of just guacamole instead of with nachos"
ВИХІД
[main 21032c3] Use image of just guacamole instead of with nachos
Якщо замість цього ми хочемо використовувати версію Джиммі, ми можемо
застосувати git checkout
з його ідентифікатором коміту,
тобто 439dc8c0
:
BASH
$ git checkout 439dc8c0 guacamole.jpg
$ git add guacamole.jpg
$ git commit -m "Use image of guacamole with nachos instead of just guacamole"
ВИХІД
[main da21b34] Use image of guacamole with nachos instead of just guacamole
Ми також можемо зберегти обидва зображення. Але ж проблема
полягає в тому, що ми не зможемо зберегти їх з однаковими назвами. Проте
ми можемо викликати з історії кожну версію послідовно та
перейменувати її, а потім додати перейменовані версії до
репозиторію за допомогою git add
. Спочатку “дістаньте” з
історії змін кожне зображення та перейменуйте його:
BASH
$ git checkout HEAD guacamole.jpg
$ git mv guacamole.jpg guacamole-only.jpg
$ git checkout 439dc8c0 guacamole.jpg
$ mv guacamole.jpg guacamole-nachos.jpg
Потім видаліть стару версію файлу guacamole.jpg
та
додайте два нових файли:
BASH
$ git rm guacamole.jpg
$ git add guacamole-only.jpg
$ git add guacamole-nachos.jpg
$ git commit -m "Use two images: just guacamole and with nachos"
ВИХІД
[main 94ae08c] Use two images: just guacamole and with nachos
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 guacamole-nachos.jpg
rename guacamole.jpg => guacamole-only.jpg (100%)
Тепер обидва зображення гуакамоле містяться у репозиторії, а файл
guacamole.jpg
більше не існує.
Звичайна робоча сесія
Ви сідаєте за комп’ютер, щоб працювати над спільним проєктом, який відстежується у віддаленому репозиторії Git. Під час робочого сеансу ви виконуєте наступні дії, хоча не обов’язково в такому порядку:
-
Зробити зміни, додавши число ‘100’ до текстового файлу
numbers.txt
- Оновити віддалений репозиторій, щоб він відповідав локальному репозиторію
- Відсвяткувати свій успіх
- Оновити локальний репозиторій, щоб він відповідав віддаленому репозиторію
- Додати зміни до зони стейджингу
- Зробити коміт у локальному репозиторії
В якому порядку слід виконувати ці дії, щоб мінімізувати ймовірність конфліктів? Розташуйте їх у порядку виконання в стовпці “дія” в таблиці нижче. Коли ви розташуєте їх у відповідному порядку, спробуйте написати відповідні команди в стовпці “команда”. Кілька кроків вже заповнені, щоб допомогти вам розпочати.
крок | дія . . . . . . . . . . | команда . . . . . . . . . . |
---|---|---|
1 | ||
2 | echo 100 >> numbers.txt |
|
3 | ||
4 | ||
5 | ||
6 | Відсвяткувати! |
крок | дія . . . . . . | команда . . . . . . . . . . . . . . . . . . . |
---|---|---|
1 | Оновити локальний репозиторій | git pull origin main |
2 | Зробити зміни | echo 100 >> numbers.txt |
3 | Додати зміни до зони стейджингу | git add numbers.txt |
4 | Зробити коміт | git commit -m "Add 100 to numbers.txt" |
5 | Оновити віддалений репозиторій | git push origin main |
6 | Відсвяткувати! |
Ключові моменти
- Конфлікти виникають, коли двоє або більше людей змінюють ті самі рядки в одному файлі.
- Система контролю версій не дозволяє користувачам перезаписувати зміни один одного наосліп, але виділяє конфлікти, щоб їх можна було вирішити.