W poprzedniej lekcji przygotowaliśmy Pull Request na GitHubie. Utworzyliśmy osobną gałąź, zapisaliśmy zmiany w commitach, poprawiliśmy skrypt po code review i połączyliśmy wszystko z master. Taka sytuacja jest banalnie prosta, w życiu często jest inaczej, bardzo często może pojawić się dodatkowy problem.
Przykładowo ktoś inny zdążył już dodać nowe commity do głównej gałęzi, a my nadal pracujemy na starszej wersji projektu. Innym razem zapisujemy commit i chwilę później zauważamy, że zapomnieliśmy o jednym pliku. Zdarza się też, że z kilku zmian przygotowanych na osobnej gałęzi potrzebujemy przenieść tylko jedną. Git ma do tego odpowiednie narzędzia.
W tej lekcji poznamy git rebase, git commit --amend i git cherry-pick. Wszystko przećwiczymy w naszym repozytorium devops-lab, wykorzystując skrypty i pliki konfiguracyjne podobne do tych, nad którymi pracowaliśmy wcześniej.
Git merge a git rebase – czym się różnią?
Zanim zaczniemy wykonywać polecenia, wróćmy na moment do pracy z gałęziami. Załóżmy, że utworzyliśmy feature branch z commita C. Dodaliśmy na nim dwa commity, D i E.
A---B---C master
\
D---E feature
W tym czasie ktoś inny wprowadził zmiany na master. Pojawiły się commity F i G.
Historia wygląda teraz tak:
A---B---C---F---G master
\
D---E feature
Nasz feature branch nadal zawiera potrzebne zmiany, ale powstał na starszej wersji projektu. Chcemy uwzględnić commity F i G, zanim dokończymy pracę.
Aktualizacja gałęzi przez merge
Jedną z metod poznaliśmy już w poprzednich artykułach. Będąc na feature branchu, możemy wykonać:
git merge master
Git połączy historie obu gałęzi. Jeśli nie będzie konfliktów, w tej sytuacji utworzy dodatkowy merge commit.
D---E-------M feature
/ /
A---B---C---F-------G master
Commit M ma dwóch rodziców: wcześniejszy commit naszej gałęzi oraz ostatni commit z master.
Historia zachowuje informację o tym, że rozwijaliśmy feature branch niezależnie, a później włączyliśmy do niego nowsze zmiany.
Aktualizacja gałęzi przez rebase
Możemy również wykonać:
git rebase master
Tym razem Git odtworzy nasze commity na nowszej podstawie. Zamiast poprzedniego układu otrzymamy:
A---B---C---F---G---D'---E' feature
↑
master
Zmiany z commitów D i E pojawiły się po commitach F i G. Zwróć uwagę na apostrofy przy nazwach D' i E'.
To nowe commity. Mogą zawierać takie same zmiany jak poprzednie, ale mają innych rodziców i nowe identyfikatory. Właśnie dlatego mówi się, że rebase przepisuje historię.
Przygotowanie laboratorium
Wracamy do repozytorium na naszej maszynie Debian:
cd ~/devops-lab-pr
Sprawdź stan projektu:
git status
git branch --show-current
Jeżeli jesteś na gałęzi z poprzedniego ćwiczenia, wróć na master:
git switch master
Możesz pobrać informacje z GitHuba:
git fetch origin
Jeśli lokalny master jest zgodny z historią zdalną albo pozostaje jedynie w tyle za nią, zaktualizuj go:
git pull
W tym artykule nie będziemy modyfikować master. Wystarczy nam czysty stan repozytorium, od którego utworzymy nowe gałęzie.
Laboratorium 1. Git rebase w praktyce
Żeby nie dodawać testowych commitów bezpośrednio do obecnej historii, przygotujemy dwie lokalne gałęzie:
lab/rebase-base– będzie udawać główną gałąź, na której ktoś inny dodał nowy commit,lab/rebase– posłuży do przygotowania skryptu i wykonania rebase.
Działanie będzie dokładnie takie samo jak przy aktualizowaniu własnego feature brancha względem master.
Tworzenie gałęzi laboratoryjnych
Będąc na master, wykonaj:
git switch -c lab/rebase-base
git switch -c lab/rebase
Teraz pracujemy na lab/rebase, możesz to sprawdzić poleceniem:
git branch --show-current
Pierwszy commit: skrypt sprawdzający usługi
W poprzednich lekcjach tworzyliśmy już skrypty monitorujące dysk i podstawowe parametry systemu. Dodajmy kolejny, który wyświetli informacje o usługach.
nano scripts/service-check.sh
Wklej do niego poniższą zawartość:
#!/usr/bin/env bash
echo "Service check"
systemctl --failed
Polecenie systemctl --failed pokazuje jednostki systemd znajdujące się w stanie błędu. Na poprawnie działającej maszynie wynik może informować, że nie ma żadnych jednostek w stanie failed. Jest to tylko przykładowy skrypt.
Zapisz plik, nadaj mu uprawnienia do wykonania i go uruchom:
chmod +x scripts/service-check.sh
./scripts/service-check.sh
Zapisz zmiany:
git add scripts/service-check.sh
git commit -m "Add service check script"
Mamy pierwszy commit na gałęzi lab/rebase.
Drugi commit: nazwa hosta
Rozbudujmy skrypt o informację, na jakiej maszynie został uruchomiony. Otwórz ponownie plik:
nano scripts/service-check.sh
Na końcu dopisz:
echo "Hostname: $(hostname)"
Następnie zapisz zmianę w osobnym commicie:
git add scripts/service-check.sh
git commit -m "Add hostname to service check"
Wyświetl historię:
git log --oneline --decorate --graph -5
Na gałęzi lab/rebase powinniśmy mieć dwa nowe commity. Zapisz sobie ich skrócone hashe. Za chwilę sprawdzimy, czy zmienią się po rebase.

Niezależny commit na drugiej gałęzi
Teraz zasymulujemy sytuację, w której ktoś inny kontynuuje pracę nad projektem. Przełącz się na gałąź przygotowaną wcześniej:
git switch lab/rebase-base
Otwórz README.md:
nano README.md
Na końcu dopisz:
## Service diagnostics
Repository contains Linux service diagnostic scripts.
Zapisz zmianę:
git add README.md
git commit -m "Document service diagnostics"
Mamy teraz dwie różne historie.
Na lab/rebase powstał skrypt i jego rozbudowana wersja. Na lab/rebase-base pojawił się niezależny commit dotyczący README.
Sprawdź graf:
git log --oneline --decorate --graph --all -12

Interesujący nas fragment można przedstawić tak:
D---E lab/rebase
/
A---B---C
\
F lab/rebase-base
Commit F nie jest częścią historii lab/rebase, ale za chwilę to zmienimy.
Wykonanie git rebase
Wróć na gałąź ze skryptem:
git switch lab/rebase
Następnie wykonaj:
git rebase lab/rebase-base
Git sprawdzi, które commity należą do naszej gałęzi, ale nie występują w historii wskazanej jako nowa podstawa.
W tym przypadku są to:
D = Add service check script
E = Add hostname to service check
Następnie odtworzy ich zmiany po commicie F. Jeżeli wszystko przebiegnie poprawnie, zobaczysz komunikat o pomyślnym zakończeniu rebase. Sprawdź historie:
git log --oneline --decorate --graph --all -12
Schemat powinien przypominać:
A---B---C---F---D'---E'
↑ ↑
lab/rebase-base lab/rebase
Wcześniejsze commity D i E zostały zastąpione nowymi commitami D' i E'. Porównaj ich hashe z tymi, które zapisałeś przed rebase, zobaczysz, że się zmieniły. Wiadomości commitów nadal brzmią tak samo, zmieniły się natomiast identyfikatory.

Co właściwie zrobił rebase?
Możemy rozłożyć całą operację na kilka prostych czynności. Git znalazł dwa commity należące do lab/rebase. Następnie odtworzył ich zmiany po najnowszym commicie gałęzi lab/rebase-base.
Dlatego po rebase mamy jedną linię historii:
F---D'---E'
Bez dodatkowego merge commita. Nie oznacza to jednak, że wcześniejsze commity zostały fizycznie przesunięte. Git utworzył ich nowe odpowiedniki. Ta różnica ma znaczenie, zwłaszcza gdy pracujemy z gałęzią opublikowaną już na GitHubie.
Co zrobić, gdy podczas rebase wystąpi konflikt?
W naszym ćwiczeniu zmienialiśmy różne pliki. lab/rebase zawierał skrypt, a lab/rebase-base modyfikował README. Dlatego Git mógł odtworzyć commity bez konfliktu.
Przy pracy z tym samym fragmentem pliku sytuacja wygląda inaczej. Załóżmy, że na jednej gałęzi zmieniliśmy:
port=8080
na:
port=8081
A na drugiej:
port=9090
Podczas rebase Git może zatrzymać się na commicie, którego zmian nie potrafi automatycznie zastosować. Zobaczymy komunikat CONFLICT. Dalsza praca przypomina rozwiązywanie konfliktów z wcześniejszej lekcji. Najpierw sprawdzamy:
git status
Git wskaże plik, którego nie udało się połączyć. Otwieramy go, analizujemy obie wersje i przygotowujemy poprawną zawartość. Usuwamy oznaczenia konfliktu:
<<<<<<<
=======
>>>>>>>
Następnie dodajemy poprawiony plik:
git add NAZWA_PLIKU
I kontynuujemy:
git rebase --continue
Git spróbuje odtworzyć kolejne commity. Jeśli pojawi się następny konflikt, trzeba go rozwiązać w ten sam sposób.
Przerwanie rebase
Jeśli podczas operacji coś pójdzie nie tak albo nie chcesz jeszcze rozwiązywać konfliktów, możesz przerwać rebase:
git rebase --abort
Git wróci do stanu gałęzi sprzed rozpoczęcia tej operacji. Zapamiętaj różnicę:
git rebase --continue
kontynuuje rebase po rozwiązaniu problemu.
git rebase --abort
anuluje trwającą operację.
To podobny mechanizm do git merge --continue i git merge --abort, które poznaliśmy podczas ćwiczenia konfliktów.
Dlaczego nie wykonujemy rebase na współdzielonej historii?
Wyobraź sobie, że pracujesz na gałęzi, z której korzystają również inne osoby. Jej historia wygląda tak:
A---B---C---D
Ktoś z zespołu pobrał już commity C i D.
Jeśli przebudujesz tę historię przez rebase, możesz otrzymać nowe commity:
A---B---C'---D'
Dla Gita C i C' to różne commity. Podobnie D i D'. Osoby pracujące na starej historii nadal mają wcześniejsze identyfikatory. Kiedy zaczniecie ponownie synchronizować zmiany, może pojawić się niepotrzebne zamieszanie.
Dlatego rebase najlepiej ćwiczyć na własnych, lokalnych gałęziach. W pracy zespołowej trzeba dodatkowo wiedzieć, czy dana gałąź jest już opublikowana i czy ktoś inny opiera na niej swoją pracę.
Laboratorium 2. Git commit –amend
Przejdźmy do innego przypadku.
Załóżmy, że przygotowujesz konfigurację usługi. Tworzysz plik, zapisujesz commit i dopiero po chwili zauważasz, że zapomniałeś o jednym parametrze. Możesz oczywiście utworzyć kolejny commit z wiadomością:
Fix previous commit
Ale jeśli poprzedni commit nie został jeszcze opublikowany, możesz poprawić go bez dokładania następnego zapisu do historii. Do tego służy git commit --amend.
Osobna gałąź do ćwiczenia
Wróć na master:
git switch master
Utwórz nową gałąź:
git switch -c lab/amend
Przygotuj katalog na konfigurację:
mkdir -p configs
Następnie utwórz plik:
nano configs/cache.conf
Wklej do niego poniższą zawartość:
enabled=true
ttl=300
To uproszczona konfiguracja mechanizmu cache. Zapisz plik i wykonaj commit:
git add configs/cache.conf
git commit -m "Add cache config"
Sprawdź jego hash i zapisz sobie jego identyfikator. Za chwilę będziemy go porównywać.
git log -1 --oneline

Poprawienie ostatniego commita
Po sprawdzeniu konfiguracji zauważamy, że zabrakło parametru określającego maksymalną liczbę przechowywanych elementów. Dopiszmy go do pliku:
echo "max_items=1000" >> configs/cache.conf
Dodaj zmianę do staging area:
git add configs/cache.conf
Tym razem zamiast zwykłego git commit wykonaj:
git commit --amend --no-edit
Opcja --amend zastępuje ostatni commit nowym, zawierającym przygotowane zmiany. --no-edit oznacza, że chcemy zachować jego dotychczasową wiadomość:
Add cache config
Sprawdź ponownie:
git log -1 --oneline
Wiadomość commita będzie taka sama jak wcześniej, ale hash powinien być inny.

Dlaczego amend zmienia hash?
git commit --amend nie dopisuje zawartości do istniejącego commita , tylko tworzy nowy commit, który zastępuje poprzedni na końcu bieżącej gałęzi.
Przed operacją:
A---B---C lab/amend
Po operacji:
A---B---C' lab/amend
Commit C' zawiera już poprawioną konfigurację. To kolejna forma przepisywania historii, dlatego najlepiej korzystać z niej przed wysłaniem commita na GitHuba.
Poprawianie wiadomości commita
--amend przydaje się również wtedy, gdy popełnisz literówkę w wiadomości ostatniego commita.
Załóżmy, że zapisałeś:
git commit -m "Add ngnix config"
A miało być:
Add nginx config
Jeżeli to ostatni, nieopublikowany commit, możesz zmienić jego wiadomość:
git commit --amend -m "Add nginx config"
Git utworzy nowy commit z poprawioną wiadomością. Nie musisz dodawać osobnego commita tylko po to, żeby poprawić literówkę. Pamiętaj jednak, że --amend uwzględnia również aktualną zawartość staging area. Przed jego wykonaniem warto sprawdzić status, dzięki temu nie dołączysz przypadkiem innych zmian.
git status
git diff --staged
Laboratorium 3. Git cherry-pick
Zostawmy na chwilę poprawianie historii i zajmijmy się przenoszeniem pojedynczych zmian. Wyobraź sobie, że na osobnej gałęzi przygotowujesz trzy poprawki:
D = nowa konfiguracja aplikacji A
E = nowa konfiguracja aplikacji B
F = nowa konfiguracja aplikacji C
Wszystkie znajdują się na jednym branchu. Okazuje się jednak, że do innej gałęzi potrzebujesz wyłącznie zmiany z commita E. Nie chcesz przenosić całej historii zawierającej D, E i F.
Możesz wtedy użyć:
git cherry-pick HASH_COMMITA
Git odtworzy zmianę z wybranego commita na aktualnej gałęzi i zapisze ją jako nowy commit.
Przygotowanie gałęzi źródłowej
Wróć na master:
git switch master
Utwórz gałąź:
git switch -c lab/cherry-source
Przygotuj katalog:
mkdir -p configs
Dodaj pierwszą konfigurację:
echo "app=A" > configs/app-a.conf
git add configs/app-a.conf
git commit -m "Add application A configuration"
Teraz drugą:
echo "app=B" > configs/app-b.conf
git add configs/app-b.conf
git commit -m "Add application B configuration"
I trzecią:
echo "app=C" > configs/app-c.conf
git add configs/app-c.conf
git commit -m "Add application C configuration"
Sprawdź historię:
git log --oneline -4
Na początku powinny znajdować się trzy nowe commity:
Add application C configuration
Add application B configuration
Add application A configuration

Zapisz hash commita B, ten commit za chwilę przeniesiemy.
Add application B configuration
Przeniesienie jednego commita
Wróć do master:
git switch master
Utwórz gałąź docelową:
git switch -c lab/cherry-target
Nie ma tu jeszcze plików app-a.conf, app-b.conf ani app-c.conf z naszej gałęzi źródłowej. Teraz wykonaj:
git cherry-pick HASH_B
Zastąp HASH_B identyfikatorem commita Add application B configuration, który zapisałeś wcześniej.
Jeśli wszystko przebiegnie poprawnie, Git utworzy commit na aktualnej gałęzi lab/cherry-target.
git log -1 --oneline
Powinieneś zobaczyć wiadomość:
Add application B configuration
Otwórz plik:
cat configs/app-b.conf
Wynik:
app=B
Na gałęzi docelowej pojawiła się konfiguracja aplikacji B. Nie przenieśliśmy natomiast konfiguracji A ani C, ponieważ należą do innych commitów.

Kiedy cherry-pick przydaje się w pracy DevOps?
Dobrym przykładem jest poprawka bezpieczeństwa albo naprawa błędu, która musi trafić do kilku wersji aplikacji. Załóżmy, że na master mamy nowy commit poprawiający konfigurację usługi. Jednocześnie utrzymujemy starszą wersję aplikacji na gałęzi release/1.5.
Nie chcemy przenosić do niej wszystkich zmian z master. Potrzebujemy tylko konkretnej poprawki. Możemy wtedy wykonać:
git switch release/1.5
git cherry-pick HASH_POPRAWKI
Git spróbuje zastosować wskazaną zmianę w historii gałęzi release/1.5. Oczywiście trzeba sprawdzić, czy poprawka nadal ma sens w starszej wersji projektu. Sam fakt, że Git potrafi zastosować commit, nie gwarantuje poprawnego działania aplikacji.
Rebase, cherry-pick i amend – kiedy którego użyć?
Mamy już kilka poleceń, które wpływają na historię repozytorium. Warto uporządkować ich zastosowania.
| Polecenie | Kiedy się przydaje |
|---|---|
git merge | Gdy chcemy połączyć historie gałęzi |
git rebase | Gdy chcemy odtworzyć własne commity na nowszej podstawie |
git commit --amend | Gdy chcemy poprawić ostatni commit |
git cherry-pick | Gdy potrzebujemy zmiany z konkretnego commita na innej gałęzi |
Zadanie samodzielne
Po wykonaniu trzech laboratoriów spróbuj jeszcze raz wykorzystać poznane polecenia na własnym przykładzie.
Zadanie 1. Aktualizacja feature brancha
Utwórz gałąź feature/log-analysis i przygotuj na niej dwa commity:
Add log analysis script
Document log analysis script
Następnie utwórz niezależną zmianę na osobnej gałęzi bazowej i wykonaj rebase feature/log-analysis na tę nowszą podstawę. Nie dodawaj ćwiczebnego commita bezpośrednio na master.
Sprawdź historię przed operacją i po niej. Porównaj hashe commitów.
Zadanie 2. Poprawienie commita
Na gałęzi feature/log-analysis dodaj zmienną przechowującą nazwę pliku z logami.
Zapisz zmianę w commicie:
Add log file variable
Następnie dopisz komentarz wyjaśniający przeznaczenie tej zmiennej. Tym razem nie twórz kolejnego commita Fix comment. Popraw ostatni commit przez --amend. Sprawdź, czy jego hash się zmienił.
Zadanie 3. Przeniesienie wybranej zmiany
Utwórz gałąź feature/utility-scripts i zapisz na niej trzy niezależne commity:
Add utility script A
Add utility script B
Add utility script C
Następnie utwórz z master gałąź release/test i przenieś na nią wyłącznie commit Add utility script B. Wykorzystaj git cherry-pick.
Sprawdź, czy na gałęzi docelowej znajdują się tylko pliki i zmiany pochodzące z wybranego commita.
Sprawdzenie repozytorium po ćwiczeniach
Wróć na główną gałąź:
git switch master
Sprawdź stan projektu:
git status
git branch -a
Następnie wyświetl graf historii:
git log --oneline --decorate --graph --all -25
Powinieneś zobaczyć osobne gałęzie laboratoryjne oraz główną gałąź master, której historia nie została zmieniona przez nasze eksperymenty.
Następny krok: Kończymy część kursu poświęconą Gitowi. Potrafisz już pracować na gałęziach, rozwiązywać konflikty, przygotowywać Pull Requesty i zarządzać historią commitów. Teraz przejdziemy do Dockera. Zobaczymy, czym są kontenery, jak działają i dlaczego pomagają uniknąć sytuacji, w której aplikacja działa na jednej maszynie, a na drugiej już nie.
Przejdź do następnej lekcji: Docker dla początkujących – jak rozwiązuje problem „u mnie działa”? →
← Poprzednia lekcja: Pull Request w GitHub – feature branch i code review · Wróć do spisu kursu DevOps