Konflikty w Git – jak rozwiązać merge conflict

by Patryk

W poprzednim artykule przećwiczyłeś pracę na osobnych gałęziach i łączyłeś zmiany za pomocą git merge. Wszystko przebiegało bez problemów, ponieważ na jednej gałęzi modyfikowaliśmy skrypt, a na drugiej plik README. Co jednak się stanie, gdy dwie gałęzie zmienią ten sam fragment pliku, ale każda w inny sposób? W tym artykule celowo wywołamy kilka konfliktów, nauczymy się odczytywać komunikaty i samodzielnie rozwiązywać konflikty w git. Sprawdzimy również, jak przerwać merge, gdy nie chcemy jeszcze łączyć zmian.

Czym jest merge conflict w Git?


Konflikt podczas merge pojawia się wtedy, gdy Git nie potrafi automatycznie połączyć zmian z dwóch gałęzi.

Wyobraź sobie, że pracujesz nad konfiguracją aplikacji. Na gałęzi master zmieniasz port z 8080 na 8081. W tym samym czasie na gałęzi feature/change-port ktoś zmienia tę samą wartość na 9090.

Obie zmiany zostały zapisane w osobnych commitach.

                     C  feature/change-port
/
A -------- B
\
D master

W commicie C mamy:

port=9090

Natomiast w commicie D:

port=8081

Kiedy spróbujemy połączyć obie gałęzie, Git napotka dwie różne wartości tego samego parametru. Nie wie, czy aplikacja powinna korzystać z portu 8081, czy 9090. Nie zna przecież wymagań projektu. Zamiast wybierać jedną z wartości, zatrzyma merge i poprosi nas o rozstrzygnięcie konfliktu.

Przygotowanie repozytorium do ćwiczeń


Wracamy do repozytorium devops-lab, na którym pracowaliśmy w poprzednich lekcjach.

Powinniśmy mieć w nim plik README.md oraz dwa utworzone wcześniej skrypty:

devops-lab/
├── README.md
└── scripts/
    ├── monitoring.sh
    └── system-info.sh

Przejdź do repozytorium i upewnij się, że jesteś na gałęzi master. Upewnij się, że drzewo robocze jest czyste. To dobra praktyka przed rozpoczęciem każdej pracy z projektem.

cd ~/devops-lab
git switch master
git status

Możesz również wyświetlić ostatnie commity:

git log --oneline --decorate --graph --all -10

Dzięki temu zobaczysz, z jakiego miejsca w historii rozpoczynamy pracę. Wszystkie konflikty będziemy wywoływać lokalnie. Nie musisz wysyłać ćwiczebnych gałęzi na GitHuba.

Pierwszy konflikt: dwie różne wartości tego samego parametru


Zaczniemy od prostego pliku konfiguracyjnego. Utworzymy dwie gałęzie, zmienimy na nich ten sam parametr, a następnie spróbujemy połączyć ich historię.

Tworzenie pliku konfiguracyjnego

Będąc na master, utwórz katalog configs:

mkdir -p configs

Następnie utwórz plik web.conf:

cat > configs/web.conf <<'EOF'
environment=development
port=8080
logging=enabled
EOF

Mamy trzy parametry: nazwę środowiska, port aplikacji i ustawienie logowania. Dodaj plik do staging area i utwórz commit:

git add configs/web.conf
git commit -m "Add web application configuration"

Teraz obie gałęzie, które za chwilę utworzymy, będą miały wspólny punkt startowy.

Zmiana portu na feature branch

Utwórz gałąź feature/change-port:

git switch -c feature/change-port

Na tej gałęzi chcemy zmienić port aplikacji z 8080 na 9090. Możesz otworzyć plik w edytorze albo skorzystać z polecenia sed, które wyszuka wiersz zawierający dokładnie port=8080 i zastąpi go nową wartością:

sed -i 's/^port=8080$/port=9090/' configs/web.conf

Sprawdź różnicę:

git diff

W wyniku zobaczysz między innymi:

-port=8080
+port=9090

Zapisz zmianę:

git add configs/web.conf
git commit -m "Change application port to 9090"

Od tej chwili gałąź feature/change-port zawiera commit, w którym aplikacja korzysta z portu 9090.

Niezależna zmiana na master

Wróć do głównej gałęzi:

git switch master

Sprawdź plik:

cat configs/web.conf

Port ponownie ma wartość 8080. To prawidłowe zachowanie, ponieważ zmiana na 9090 znajduje się na osobnej gałęzi.

Teraz na master zmień ten sam parametr na 8081:

sed -i 's/^port=8080$/port=8081/' configs/web.conf

Zapisz zmianę:

git add configs/web.conf
git commit -m "Change application port to 8081"

W historii mamy teraz dwa niezależne commity. Jeden ustawia port 9090, a drugi 8081. Żaden z nich nie zawiera zmian drugiej gałęzi.

Wywołanie konfliktu

Będąc na master, spróbuj połączyć zmiany:

git merge feature/change-port

Tym razem Git nie zakończy operacji automatycznie. Zobaczysz komunikat podobny do tego:

Git próbował połączyć zawartość web.conf, ale znalazł sprzeczne zmiany. Merge został zatrzymany. Żeby go dokończyć, musimy zdecydować, jak ma wyglądać plik.

Jak sprawdzić, gdzie wystąpił konflikt?

Gdy merge kończy się konfliktem, zacznij od sprawdzenia statusu repozytorium:

git status

Wśród komunikatów znajdziesz informację o nierozwiązanych zmianach:

oba zmienione oznacza, że obie strony zmodyfikowały plik i Git nie zdołał połączyć tych zmian.

Możemy też uruchomić:

git diff

Podczas nierozwiązanego konfliktu Git może wyświetlić specjalny format różnic, pokazujący zmiany pochodzące z obu gałęzi.

Na razie otwórzmy plik, w którym wystąpił problem.

cat configs/web.conf

Zobaczysz:

environment=development
<<<<<<< HEAD
port=8081
=======
port=9090
>>>>>>> feature/change-port
logging=enabled

Git dodał do pliku specjalne oznaczenia, nazywane conflict markers. Pozwalają one rozpoznać fragmenty, których Git nie potrafił połączyć.

Co oznaczają conflict markers?

Przyjrzyjmy się początkowi:

<<<<<<< HEAD
port=8081

W tym ćwiczeniu HEAD wskazuje gałąź master, na której rozpoczęliśmy merge, dlatego w tej części widzimy wartość 8081.

Następnie pojawia się zapis, który oddziela obie konkurencyjne wersje:

=======

Poniżej mamy wartość pochodząca z gałęzi feature/change-port, którą próbujemy włączyć do master:

port=9090
>>>>>>> feature/change-port

Git pokazał nam oba warianty. Teraz trzeba zdecydować, co powinno znaleźć się w ostatecznej konfiguracji.

Ręczne rozwiązanie konfliktu

Załóżmy, że ustaliliśmy, iż aplikacja ma korzystać z portu 9090. Musimy więc usunąć oznaczenia konfliktu i pozostawić poprawną wartość. Otwórz plik:

nano configs/web.conf

Usuń fragmenty zaczynające się od <<<<<<<, ======= i >>>>>>>, a następnie pozostaw taką zawartość:

environment=development
port=9090
logging=enabled

W edytorze Nano zapiszesz zmiany przez Ctrl+O, a następnie Enter. Wyjście z edytora to Ctrl+X.

Sprawdź plik:

cat configs/web.conf

W tym momencie usunęliśmy konfliktującą zawartość z pliku. Git nadal musi jednak otrzymać informację, że zaakceptowaliśmy przygotowaną wersję.

Oznaczenie konfliktu jako rozwiązanego

Do tego służy znane nam już polecenie git add:

git add configs/web.conf

W pierwszej lekcji używaliśmy go do przygotowywania zmian przed commitem. Tutaj działa na tej samej zasadzie: zapisuje do staging area aktualną, poprawioną zawartość pliku. Dodatkowo oznacza skonfliktowany plik jako rozwiązany.

Sprawdź status:

git status

Git powinien poinformować, że wszystkie konflikty zostały rozwiązane, ale operacja merge nadal wymaga zakończenia. Przed jej dokończeniem możesz sprawdzić przygotowaną zawartość:

git diff --staged

Następnie zakończ merge:

git merge --continue

Git może otworzyć edytor z proponowaną wiadomością merge commita. Możesz ją zaakceptować, zapisując i zamykając edytor. Alternatywnie po rozwiązaniu wszystkich konfliktów możesz zakończyć merge poleceniem git commit.

Sprawdź historię:

git log --oneline --decorate --graph --all -10

Widzimy osobny merge commit, który łączy dwie historie. Git zachował oba wcześniejsze commity, natomiast w końcowej konfiguracji znajduje się wybrany przez nas port 9090.

Drugi konflikt: połączenie zmian z obu gałęzi


W poprzednim przykładzie wybraliśmy jedną z dwóch wartości portu. Nie zawsze jest to najlepsze rozwiązanie. Czasami poprawna konfiguracja musi zawierać część zmian z jednej gałęzi i część z drugiej.

Przygotowanie konfiguracji usługi

Upewnij się, że jesteś na master i nie masz niezapisanych zmian:

git switch master
git status

Utwórz plik service.conf:

cat > configs/service.conf <<'EOF'
service=web
workers=2
loglevel=info
EOF

Parametr workers będzie oznaczał liczbę procesów roboczych, natomiast loglevel poziom szczegółowości logów.

Zapisz plik:

git add configs/service.conf
git commit -m "Add service configuration"

Zmiany na feature/performance

Utwórz gałąź:

git switch -c feature/performance

Na tej gałęzi chcemy zwiększyć liczbę procesów roboczych do 8 i zmienić poziom logowania na warning. Edytuj plik:

nano configs/service.conf

Zmień jego zawartość na:

service=web
workers=8
loglevel=warning

Zapisz i sprawdź zmiany:

git diff

Następnie utwórz commit:

git add configs/service.conf
git commit -m "Tune service performance"

Zmiany na master

Przełącz się z powrotem na gałąź master:

git switch master

Plik wróci do początkowej konfiguracji z workers=2 i loglevel=info. Na master chcemy zwiększyć liczbę procesów tylko do 4. Poziom logowania pozostawimy bez zmian.

Otwórz:

nano configs/service.conf

Ustaw:

service=web
workers=4
loglevel=info

Zapisz commit:

git add configs/service.conf
git commit -m "Adjust production worker count"

Kolejny merge conflict

Spróbuj włączyć zmiany z gałęzi feature/performance:

git merge feature/performance

Git ponownie zgłosi konflikt w pliku konfiguracyjnym.

Sprawdź:

git status
git diff

Otwórz plik:

cat configs/service.conf

Zobaczysz fragment podobny do tego:

service=web
<<<<<<< HEAD
workers=4
loglevel=info
=======
workers=8
loglevel=warning
>>>>>>> feature/performance

Tym razem konflikt obejmuje dwa sąsiadujące parametry.

Na master zwiększyliśmy liczbę procesów roboczych do 4. Na feature/performance ustawiliśmy 8 i zmieniliśmy poziom logowania.

Załóżmy, że po sprawdzeniu wymagań zdecydowaliśmy się na cztery procesy robocze, ale chcemy również ograniczyć szczegółowość logowania.

Poprawna konfiguracja będzie wyglądać tak:

service=web
workers=4
loglevel=warning

Nie jest to pełna wersja z żadnej gałęzi. Wartość workers=4 pochodzi z master, a loglevel=warning z feature/performance.

Zapisanie rozwiązania

Otwórz plik:

nano configs/service.conf

Usuń oznaczenia konfliktu i pozostaw wybrane wartości. Następnie przygotuj rozwiązanie:

git add configs/service.conf
git status

Przed zakończeniem merge możesz jeszcze sprawdzić zawartość staging area:

git diff --staged

Jeżeli wszystko się zgadza, wykonaj:

git merge --continue

Po zakończeniu sprawdź stan repozytorium:

git status

Oraz historię:

git log --oneline --decorate --graph --all -12

Utworzyliśmy drugi merge commit. Tym razem jego końcowa zawartość łączy wybrane zmiany z dwóch gałęzi.

Jak przerwać merge przez git merge –abort?


Nie każdy konflikt musimy rozwiązywać od razu. Może się okazać, że nie wiemy, która konfiguracja powinna zostać, musimy skonsultować zmianę z inną osobą albo po prostu nie chcemy teraz łączyć gałęzi. W takim przypadku możemy przerwać rozpoczęty merge.

Przygotowanie kolejnego konfliktu

Będąc na master, upewnij się, że drzewo robocze jest czyste:

git switch master
git status

Wykorzystamy istniejący plik configs/web.conf. Utwórz gałąź:

git switch -c feature/environment

Na tej gałęzi zmień nazwę środowiska z development na staging.

Możesz zrobić to poleceniem:

sed -i 's/^environment=development$/environment=staging/' configs/web.conf

Zapisz commit:

git add configs/web.conf
git commit -m "Set staging environment"

Teraz przejdź na master:

git switch master

Na tej gałęzi zmień ten sam parametr na production:

sed -i 's/^environment=development$/environment=production/' configs/web.conf

Zapisz zmianę:

git add configs/web.conf
git commit -m "Set production environment"

Obie gałęzie mają teraz własne commity modyfikujące ten sam wiersz.

Przerywanie konfliktu

Spróbuj wykonać merge:

git merge feature/environment

Git zgłosi konflikt. Sprawdź:

git status
cat configs/web.conf

W pliku zobaczysz oznaczenia konfliktu między wartościami production i staging. Tym razem nie będziemy ich usuwać. Zamiast tego wykonaj:

git merge --abort

Polecenie anuluje trwającą operację merge i próbuje przywrócić stan sprzed jej rozpoczęcia.

Sprawdź:

git status
cat configs/web.conf

W pliku powinna pozostać konfiguracja z gałęzi master:

environment=production
port=9090
logging=enabled

Nie ma już oznaczeń konfliktu, a historia master nie zawiera merge commita z tej próby.

**git merge --abort** nie usuwa zmian zapisanych wcześniej na feature branch. Gałąź feature/environment nadal istnieje i zawiera commit ze środowiskiem staging. Możemy wrócić do niej później, przeanalizować konfigurację i ponownie spróbować połączyć zmiany.

Pamiętaj też, że merge --abort nie służy do cofania merge, który został już zakończony i zapisany. Anuluje operację będącą w toku. Najbezpieczniej rozpoczynać merge z czystym drzewem roboczym. Jeśli przed scalaniem miałeś niezapisane modyfikacje, Git nie zawsze będzie w stanie odtworzyć ich pierwotny stan.

Konflikty Git w praktyce DevOps


Podczas pracy z infrastrukturą konflikt może dotyczyć plików takich jak:

nginx.conf
Dockerfile
terraform/main.tf
deployment.yaml
pipeline.yml

Załóżmy, że dwie osoby zmieniają konfigurację usługi Nginx. Jedna modyfikuje port nasłuchiwania, druga wprowadza własne ustawienia w tym samym fragmencie pliku. Git może zgłosić konflikt, ale nie sprawdzi za nas, czy ostateczna konfiguracja jest właściwa dla danego środowiska. Po ręcznym połączeniu zmian trzeba jeszcze upewnić się, że plik jest poprawny.

W zależności od rodzaju konfiguracji może to oznaczać uruchomienie odpowiedniego polecenia walidacyjnego, testów albo sprawdzenie działania aplikacji.

Na przykład przy konfiguracji Nginx możemy użyć:

nginx -t

Polecenie sprawdza składnię i możliwość załadowania konfiguracji Nginx w środowisku, w którym je uruchomimy.

W projektach Terraform przydatne jest:

terraform validate

Wykonujemy je w odpowiednio zainicjalizowanym katalogu konfiguracji Terraform. Takie testy nie zastępują oceny, czy wybrane ustawienia odpowiadają wymaganiom projektu, ale pomagają wykryć błędy techniczne.

W naszych ćwiczeniach pliki web.conf i service.conf są uproszczone, dlatego ich poprawność sprawdzamy przez porównanie zawartości z założeniami danego scenariusza.

Najczęściej używane polecenia podczas rozwiązywania konfliktów

PolecenieDo czego służy
git merge NAZWAPróbuje połączyć wskazaną gałąź z aktualną
git statusPokazuje stan merge i skonfliktowane pliki
git diffPozwala sprawdzić nierozwiązane różnice
git add PLIKDodaje poprawioną wersję pliku i oznacza konflikt jako rozwiązany
git diff --stagedPokazuje przygotowane zmiany
git diff --staged --checkSprawdza problemy w przygotowanych zmianach
git merge --continueKończy merge po rozwiązaniu konfliktów
git commitMoże zakończyć merge po rozwiązaniu wszystkich konfliktów
git merge --abortPróbuje przywrócić stan sprzed rozpoczętego merge
git log --oneline --decorate --graph --allWyświetla graf historii commitów i gałęzi

Krótkie zadanie na koniec


Na koniec spróbuj samodzielnie wywołać i rozwiązać kolejny konflikt. Tym razem bez gotowego zestawu poleceń.mWybierz jeden z utworzonych wcześniej plików konfiguracyjnych. Rozpocznij pracę z czystego stanu gałęzi master, utwórz nowy feature branch i zmień wybrany parametr. Zapisz zmianę w commicie.

Następnie wróć na master, zmodyfikuj ten sam fragment w inny sposób i również wykonaj commit.

Spróbuj połączyć obie gałęzie. Kiedy Git zgłosi konflikt, sprawdź status, przeczytaj jego oznaczenia i przygotuj poprawną wersję pliku. Nie wybieraj automatycznie całej wersji z jednej strony. Przygotuj rozwiązanie, które ma sens dla przyjętego scenariusza, nawet jeśli będzie wymagało połączenia kilku zmian.

Po zakończeniu wykonaj:

git status
git log --oneline --decorate --graph --all -15

Jeśli potrafisz wyjaśnić, dlaczego konflikt powstał i skąd pochodzi każda z konkurencyjnych zmian, rozumiesz mechanizm jego rozwiązywania.


Następny krok: Wiesz już, jak rozpoznawać i rozwiązywać konflikty podczas łączenia gałęzi. W kolejnej lekcji zajmiemy się cofaniem zmian w Git. Poznasz różnice między git restore, git reset i git revert oraz sprawdzisz, jak odzyskać wcześniejszą wersję pliku lub cofnąć błędny commit.

Kolejna lekcja: Cofanie zmian w Git – restore, reset i revert.

← Poprzednia lekcja: Git i GitHub – zdalne repozytorium, gałęzie i merge · Wróć do spisu kursu

Może Cię również zainteresować

Dodaj komentarz