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
| Polecenie | Do czego służy |
|---|---|
git merge NAZWA | Próbuje połączyć wskazaną gałąź z aktualną |
git status | Pokazuje stan merge i skonfliktowane pliki |
git diff | Pozwala sprawdzić nierozwiązane różnice |
git add PLIK | Dodaje poprawioną wersję pliku i oznacza konflikt jako rozwiązany |
git diff --staged | Pokazuje przygotowane zmiany |
git diff --staged --check | Sprawdza problemy w przygotowanych zmianach |
git merge --continue | Kończy merge po rozwiązaniu konfliktów |
git commit | Może zakończyć merge po rozwiązaniu wszystkich konfliktów |
git merge --abort | Próbuje przywrócić stan sprzed rozpoczętego merge |
git log --oneline --decorate --graph --all | Wyś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