W systemie kontroli wersji Git głównym narzędziem jest polecenie git
, które
umożliwia wydawanie komend o szerokim spektrum funkcji: od zarządzania obiektami
w repozytorium i wyliczania sum kontrolnych po wykonywanie rozgałęzień
i scaleń. W tym odcinku dowiemy się, w jaki sposób posługiwać się Gitem w codziennej
pracy.
Tworzenie repozytorium
Objęcie projektu programistycznego kontrolą wersji z użyciem Gita polega na stworzeniu repozytorium, czyli lokalnej bazy danych zawierającej potrzebne obiekty, w których zapisywane będą informacje o zmianach.
Stwórzmy więc testowy projekt o nazwie Siup, w którym będą zawarte dwa katalogi: jeden przeznaczony do przechowywania dokumentacji, a drugi zawierający pliki z kodem źródłowym:
Efektem wydania powyższych poleceń będzie następująca struktura katalogowa:
Patrząc na nazwy, nietrudno się domyślić, że w ramach eksperymentu będziemy tworzyli moduł w języku Python.
Konfiguracja
Każde polecenie wydawane Gitowi będzie korzystało z ustawień konfiguracyjnych, które mogą być systemowe, globalne (dla użytkownika) bądź przypisane do konkretnego projektu. Istnieją trzy miejsca, w których Git poszuka informacji o opcjach konfiguracyjnych:
- w systemowym pliku konfiguracyjnym,
- w pliku konfiguracyjnym użytkownika,
- w pliku konfiguracyjnym projektu.
Konkretne lokalizacje plików konfiguracyjnych będą zależały od systemu operacyjnego,
dystrybucji pakietu Git i wartości pewnych zmiennych środowiskowych. Aby wyświetlić
aktualne ustawienia, które będą użyte, możemy skorzystać z polecenia git config
--list
. Sprawi ono, że zostaną wczytane wszystkie pliki w kolejności, przy czym
wartości opcji konfiguracyjnych pochodzące ze zbiorów wczytywanych później
przesłonią te załadowane wcześniej.
Możemy zobaczyć, w którym z plików zostały zdefiniowane konkretne opcje, wydając
polecenie git config --list --show-origin
.
Zmian ustawień możemy dokonywać ręcznie, edytując zawartość odpowiednich plików,
chociaż bardziej odpowiednią metodą będzie skorzystanie z narzędzia git config
,
podając najpierw nazwę konkretnej opcji konfiguracyjnej, a za nią jej
wartość.
W trybie zmiany ustawień wywołanie możemy wzbogacić o dodatkowy argument, który określi w jakim miejscu chcemy je zapisać:
--system
– w systemowym pliku konfiguracyjnym
(uwaga: musimy mieć uprawnienia do zapisywania),--global
– w globalnym pliku konfiguracyjnym użytkownika,--local
– w katalogu bieżącego projektu.
Jeżeli nie wyspecyfikujemy, o którą konfigurację chodzi (systemową, globalną czy
lokalną projektu), polecenie użyje konfiguracji lokalnej, czyli związanej
z aktualnym projektem. Przez aktualny projekt rozumiemy tu katalog systemu plikowego,
który zawiera bazę danych Gita (w podkatalogu .git
).
Domyślnym zachowaniem narzędzia git config
jest zastąpienie wartości danej
opcji tą, którą podamy. Zachowanie to można zmienić z użyciem opcji --add
. Sprawi
ona, że do istniejącego zbioru ustawień zostanie dodany kolejny składnik.
Informacje autorskie
Zanim przejdziemy do inicjowania repozytorium, warto skonfigurować Gita tak, aby
poprawnie dodawał informacje o autorze zmian, czyli o nas. Umożliwia to polecenie
git config
:
Przykładowe wpisy należy zastąpić własnymi danymi.
Kolorowanie komunikatów
Możemy wprowadzić nieco koloru w naszą skodowaną codzienność i poprosić Gita o to, aby narzędzie wiersza poleceń wyświetlało komunikaty w różnych barwach:
Aliasy poleceń w stylu SVN
Przyzwyczajeni do Subversion mogą stworzyć aliasy dla często używanych poleceń Gita, tak aby odpowiadały komendom SVN-a:
Ustawianie domyślnego edytora
Jeżeli nie odpowiada nam korzystanie z edytora, którego nazwa zapisana jest
w zmiennej środowiskowej EDITOR
lub GIT_EDITOR
, możemy wymusić korzystanie
z innego:
Miłośnicy Emacsa mogą stworzyć odpowiedni wrapper, czyli skrypt wywołujący edytor, gdy jeszcze nie jest on uruchomiony, a jeżeli jest, wywołujący specjalne narzędzie klienckie. (Emacs potrafi działać w modelu klient-serwer).
Poniższy kod skryptowy można zapisać w podkatalogu bin
w katalogu domowym, nadając
mu nazwę edit
. Ten akurat jest przystosowany zarówno do systemu GNU/Linux, jak
i Mac OS X:
Inicjowanie repozytorium
Jesteśmy gotowi do objęcia projektu Siup kontrolą wersji. Aby tego dokonać, musimy wejść do katalogu projektu i zainicjować bazę danych Gita:
Voilà! Naszym oczom powinien ukazać się następujący komunikat (w miescu wielokropka pojawi się nazwa ścieżkowa katalogu domowego):
Initialized empty Git repository in …/siup/.git/
Możemy zauważyć, że w katalogu siup
pojawił się podkatalog .git
. To właśnie tam
Git będzie przechowywał obiekty z zapisanymi migawkami i historią
zatwierdzanych zmian.
Zaraz po utworzeniu nasze repozytorium Gita jest puste i zawiera tylko minimalne obiekty potrzebne do tego, aby je utrzymać. Żeby śledzić zmiany w plikach i katalogach musimy po pierwsze dodać je do repozytorium, a po drugie zatwierdzić pierwszą zmianę.
Dodawanie plików do indeksu
Żeby dodać pliki do indeksu, zwanego również poczekalnią
(ang. staging area), użyjemy polecenia git add
:
Kropka oznacza tu katalog bieżący i informuje Gita, że naszą intencją jest objęcie kontrolą wersji katalogu projektu wraz ze wszystkimi zawartymi w nim plikami i katalogami.
Dla ciekawskich
Tak naprawdę w repozytorium Gita nie pojawiły się jeszcze żadne informacje o strukturze katalogowej projektu. Umieszczone w niej zostały następujące dwa zbiory:
W oryginale mieliśmy 4 pliki i 2 katalogi. Skąd więc różnica? Możemy sprawdzić, z jakimi obiektami mamy do czynienia:
W rezutacie otrzymamy:
77dff83b77ebac99e1918f6736c9ac655626b90d blob 13
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 blob 0
Pierwsza wartość to identyfikator obiektu, druga to jego typ, a ostatnia wielkość zawartości w bajtach. W przypadku ostatniego parametru wskazywany jest rozmiar danych przechowywanych w obiekcie, a nie rozmiar samego obiektu.
Widziemy więc, że po operacji git add
w bazie Gita znalazły się dwa obiekty typu
blob, które służą do przechowywania zawartości plików pochodzących z kopii
roboczej. Pierwszy odpowiada zawartości o rozmiarze 13 bajtów,
a drugi zawartości pustej (o rozmiarze 0 bajtów). Wyświetlmy zawartość tego
o niezerowej długości:
Naszym oczom ukaże się zawartość wpisana w obiekt:
Dokumentacja
Widzimy, że mamy do czynienia z obiektem typu blob, który odzwierciedla zawartość
pliku README
. Możemy też przyjąć, że drugi obiekt tego samego typu również
odzwierciedla treść dokumentu dodanego do poczekalni, jednak pozbawionego
treści. Skąd więc różnica w liczbie?
Zauważmy, że w katalogu naszego projektu mamy wiele plików, lecz tylko dwie
unikatowe zawartości. Pierwszą znajdziemy we wspomnianym zbiorze README
,
a drugiej należy szukać w pozostałych plikach (o zerowych
długościach). W repozytorium Gita na tym etapie nie znajdziemy nazw czy lokalizacji
plików, ale to, co można w nich znaleźć.
Sprawdzanie oczekujących
Więcej informacji o plikach i katalogach oczekujących na zatwierdzenie znajdziemy w indeksie. To w nim przechowywane są metadane dodawanych zasobów, takie jak nazwy, uprawnienia czy przynależność do ścieżek w strukturze katalogowej, a także układ struktury katalogowej oczekujących zasobów.
Istnieje kilka sposobów na sprawdzenie, jakie zmiany znajdują się
w poczekalni. Pierwszy to skorzystanie z polecenia git status
. Służy ono
do wyświetlania stanu drzewa roboczego, włączając w to porównywanie jego zawartości
ze zmianami, które zapisano w indeksie. Wywołane bez parametrów pokaże nam bieżącą
gałąź i informacje o operacjach skolejkowanych w indeksie:
W naszym przypadku otrzymamy komunikat, że bieżącą gałęzią jest master
, nie ma
jeszcze żadnych zatwierdzeń wersji, a w indeksie oczekują na zatwierdzenie wymienione
pliki:
Wyświetlanie różnic
Jeżeli chcemy być bardziej drobiazgowi i ciekawią nas konkretne różnice
w zawartościach, możemy skorzystać z opcji -v
poprzedniego polecenia lub użyć git
diff
:
Powinniśmy otrzymać komunikat w ujednoliconym formacie diff (ang. unified diff, skr. unidiff):
Opcja --cached
(lub --staged
) sprawia, że zawartości plików w drzewie
roboczym będą porównywane z zawartościami oraz atrybutami plików,
których metadane znajdują się w indeksie (oczekując na zatwierdzenie
zmian).
Warto wspomnieć, że uzyskane wyjście może nieznacznie różnić się między systemami
w miejscach określających słowa trybu dostępu do plików wyrażone ósemkowo (fragmenty
po napisach file mode
). Wynika to z różnic między systemami w kwestii ustawianej
wartości tzw. maski pliku. Parametr ten, jako jeden z atrubutów procesów w systemach
zgodnych ze standardem POSIX, określa uprawnienia dostępowe nowotworzonych zbiorów.
Graficzne obrazowanie różnic
W zależności od użytkowanego systemu operacyjnego i pakietu instalacyjnego Gita
będziemy mieli do czynienia z dostępnością różnych narzędzi, które służą
do graficznego obrazowania różnic w zawartościach zbiorów. Aby jakoś poradzić sobie
z tą różnorodnością, Git zawiera mały program pomocniczy, który wywoła domyślne lub
ustawione przez użytkownika narzędzie. Program ów nazywa się difftool
.
Spróbujmy więc użyć go w naszym środowisku:
Okno, które się ukaże, możemy zamknąć.
Warto pamiętać, że jesteśmy w stanie ustawiać własne narzędzie graficzne do wyświetlania różnic, korzystając z następujących opcji konfiguracyjnych:
dyrektywa | opis |
---|---|
diff.tool |
domyślnie używane narzędzie |
diff.guitool |
domyślnie używane narzędzie, gdy podano opcję --gui |
difftool.<narzędzie>.path |
zmiana ścieżki systemu plikowego dla narzędzia (pomocne, gdy nie ma go w ścieżka określonych zmienną środowiskową PATH ) |
difftool.<narzędzie>.cmd |
polecenie, które wywoła narzędzie |
difftool.prompt |
wyświetla monit przed każdym wywołaniem narzędzia |
difftool.trustExitCode |
kończy pracę programu pomocniczego, gdy wywołane narzędzie zwróci niezerowy kod wyjścia |
Zatwierdzanie zmian
Gdy zdecydujemy, że zawartości plików, które dodaliśmy do indeksu, mają być
zapamiętane jako zmiana, możemy taką operację zatwierdzić (ang. commit) z użyciem
polecenia git commit
:
Naszym oczom powinien ukazać się następujący komunikat diagnostyczny:
Opcja -m
pozwala nam określić komunikat, jakim będzie opatrzona zmiana. Gdybyśmy
jej nie podali, zostanie uruchomiony domyślny edytor tekstu, w którym będziemy mieli
okazję uzupełnić tę informację.
Usuwanie plików
Poza zatwierdzaniem zmian w plikach, możemy również zatwierdzać operację usuwania
plików z repozytorium. Żeby wyłączyć plik lub katalog (i wszystkie zawarte w nim
pliki) z aktualnie zatwierdzanej wersji, należy posłużyć się poleceniem git rm
:
Po wykonaniu git rm setup.py
plik ten zostanie usunięty z katalogu roboczego,
a w indeksie zapisana zostanie informacja o tym, że podczas zatwierdzania zmiany ma
on zniknąć w kolejnej wersji. Oczywiście w poprzednich (historycznych) wydaniach,
zbiór ten nadal będzie obecny.
Polecenia git rm
możemy również użyć, gdy przez przypadek (wykonując git add
)
wprowadziliśmy do indeksu plik, którego nie chcemy jednak umieszczać w historii
wersji podczas najbliższego zatwierdzenia zmian. Należy wtedy skorzystać
z przełącznika --cached
. Sprawi on, że informacja o pliku będzie usunięta
z poczekalni, ale sam zbiór nie zostanie skasowany z drzewa roboczego projektu.
Dla ciekawskich
Zmiany zatwierdzone stają się elementami repozytorium, którym odpowiadają odpowiednie obiekty Gita. W naszym przypadku w bazie Gita dojdzie do powstania następujących zbiorów:
Pomijając pliki obiektów, które już wcześniej znajdowały się w repozytorium:
Te pięć nowych plików, to dodatkowe informacje definiujące dwie ostatnie zmiany –
dodanie zbiorów do repozytorium, a następnie usunięcie jednego. Jeżeli pamiętamy
zasadę działania Gita z poprzedniej części, to będziemy mogli zlokalizować
najważniejszy z obiektów, czyli ostatni obiekt zatwierdzeniowy. Dokonamy tego
zaglądając do zawartości pliku .git/refs/heads/master
, która powinna być
identyfikatorem ostatnio dokonanej zmiany:
Naszym oczom powinna ukazać się linia:
1b9b584db83289e78155fc59cca3b13da9e21816
Widzimy więc, że plik .git/1b/9b584db83289e78155fc59cca3b13da9e21816
(lub inny, w
zależności od rezultatu polecenia cat
) jest tzw. obiektem
zatwierdzeniowym (ang. commit object). Zawiera on
wszystkie informacje, których Git potrzebuje, aby stwierdzić jaka jest zawartość
objętych kontrolą wersji plików z danego punktu w czasie. A co z pozostałymi czterema
zbiorami? Możemy je zbadać z użyciem polecenia git show
:
W efekcie otrzymamy:
tree 0e2da9c1380be2d00f0a5981ef1ce77274dde61f
README
siup/
tree 8f48403e5512311ab249c6a7c796f318267f801a
README
setup.py
siup/
tree 98bccf5690cc3053730329fa1b8c98dc0d52622a
__init__.py
main.py
commit 45764193954e472af3c198d750223d287080fbe8
Author: Imię Nazwisko <adres@emailowy>
Date: Wed Jun 19 21:54:51 2019 +0200
Pierwsze wydanie
Mamy więc do czynienia z trzema obiektami drzew (ang. tree objects), których
zadaniem jest przechowywanie informacji o strukturach katalogowych migawek w danej
chwili. To właśnie do tych obiektów znajdziemy odwołania wewnątrz obiektów
zatwierdzeniowych, które reprezentują zmiany. Z kolei zawartości plików z podanego
czasu odnajdziemy w dwóch pierwszych obiektach typu blob, które omawialiśmy na samym
początku (powstały w momencie wykonania operacji git add
).
Możemy więc podsumować, w jaki sposób Git odzwierciedlił operację zatwierdzenia zmian:
Operowanie na zmianach jest możliwe, ponieważ wszystkie powyżej zaprezentowane
obiekty Gita są ze sobą powiązane. Identyfikator ostatnio wprowadzonej zmiany
znajdziemy w pliku .git/refs/heads/master
i na tej podstawie ustalimy lokalizację
obiektu zatwierdzeniowego. Z kolei obiekt zatwierdzeniowy będzie zawierał odwołania
do dwóch obiektów drzew katalogowych wraz z atrybutami plików. Jeżeli zaś chodzi
o same zawartości plików, potrzebne do odtworzenia migawki, to będzie je można
znaleźć dzięki zawartym w obiektach drzew referencjom do obiektów typu blob –
w naszym przykładzie jeden z nich będzie obiektem definiującym zawartość pustą,
a drugi zawartość „Dokumentacja”.
Przeglądanie zmian
Spróbujmy wprowadzić do naszego projektu jeszcze jakąś zmianę i zatwierdzić ją, aby stworzyć bazę dla kolejnych przykładów:
Gdybyśmy zechcieli przejrzeć, jakie zmiany zostały wprowadzone w bieżącej gałęzi,
skorzystamy z polecenia git log
. Sprawi ono, że zostaną wyświetlone zmiany – od
najnowszej do najstarszej – a każda będzie opisana wprowadzonym wcześniej
komentarzem, a także informacją o jej autorze i czasem zatwierdzenia.
Narzędzia tekstowe i graficzne
Przeglądanie zmian i zarządzanie nimi z użyciem wbudowanego narzędzia jest wystarczające, jednak istnieją klienty Gita, które znacznie ułatwiają ten proces. Kilka wartych polecenia projektów to:
- gitg dla GNOME,
- tig dla GNU/Linuksa (konsola),
- GitX-Dev dla Mac OS-a X,
- TortoiseGit dla Windows.
Skrócona historia zmian
Aby wyświetlić skróconą historię zmian, możemy dostosować format wyświetlania raportu
– służy do tego m.in. opcja --pretty
polecenia git log
. Poza tym, jeżeli
zamierzamy często korzystać z takiego zestawienia, warto wpisać je w konfigurację
w postaci aliasu:
Od teraz możemy korzystać z polecenia git hist
:
Powinniśmy zobaczyć:
* 18c458a 2019-06-19 | informacje o wersji (HEAD -> refs/heads/master) [I… N…]
* 1b9b584 2019-06-19 | Usuwamy plik [Imię Nazwisko]
* 4576419 2019-06-19 | Pierwsze wydanie [Imię Nazwisko]
Często użytkowaną alternatywą w stosunku do definiowania własnego aliasu polecenia
log
ze zmienionym formatowaniem jest też korzystanie z opcji --oneline
:
Przełączanie między wersjami
Git umożliwia powrót do dowolnej, wcześniej zapamiętanej wersji zmian. Polega to na podmianie zawartości plików i katalogów drzewa roboczego (ang. working tree) na umieszczone w znadujących się w repozytorium obiektach typu tree (przechowujących struktury katalogowe i właściwości plików w tych katalogach) i blob (przechowujących zawartości plików).
Do przełączania się między wersjami służy polecenie git checkout
. Jako argument
należy mu przekazać nazwę gałęzi lub identyfikator zmiany (czyli obiektu
zatwierdzeniowego).
Gdy podamy nazwę gałęzi, zostanie ona wewnętrznie przekształcona w odpowiedni
identyfikator na podstawie zawartości obecnej w jednum z plików przechowujących
odwzorowania (np. refs/heads/gałązka
). Z kolei identyfikator zmiany może być pełną
sumą SHA-1 lub nawet jej częścią, lecz w tym drugim przypadku musi to być fragment
jednoznacznie wyróżniający go spośród innych.
Spróbujmy cofnąć się w czasie do pierwszej wersji naszego projektu. Aby zobrazować
zmiany wyświetlimy zawartość pliku README
przed i po dokonaniu tej operacji.
W rezultacie powinniśmy otrzymać:
Dokumentacja
wersja 1.0
A teraz cofnijmy się do poprzedniej zmiany przez podanie jej identyfikatora, który uzyskamy z użyciem zdefiniowanego wcześniej aliasu skróconej historii:
Widzimy, że zmianie oznaczonej jako „Pierwsze wydanie” odpowiada skrócony
identyfikator 4576419
(w praktyce będzie to inny identyfikator, ponieważ zależy on
m.in. od daty i czasu wprowadzania zmian). Możemy więc przełączyć się na tę wersję,
przepisując go, albo skorzystać z odpowiedniego idiomu, który powie Gitowi, że chodzi
nam właśnie o tamto zatwierdzenie.
Jednym ze sposobów wskazania, że chodzi nam o jedno zatwierdzenie wcześniej, niż
bieżące, jest użycie specyfikatora HEAD^
:
Symbol ^
oznacza tu pierwszego przodka podanego obiektu
zatwierdzeniowego. Wybieramy pierwszego w przypadkach, gdyby obiekt miał więcej, niż
jednego rodzica.
Naszym oczom ukaże się komunikat ostrzegawczy, który w angielskiej wersji językowej brzmi następująco:
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b <new-branch-name>
HEAD is now at 1b9b584 Usuwamy plik
Oznacza on, że wskaźnik HEAD znalazł się w tzw. oderwanym stanie, tzn. zamiast
wskazywać na pierwszą zmianę z gałęzi o podanej nazwie odnosi się bezpośrednio
do konkretnego obiektu zatwierdzeniowego. Możemy to sprawdzić wyświetlając zawartość
pliku .git/HEAD
. Zamiast nazwy gałęzi master
znajduje się w nim suma SHA-1
identyfikująca konkretną zmianę.
Czy to coś złego? Dopóki nie zaczniemy w tym momencie wprowadzać zmian i zatwierdzać ich, nic niebezpiecznego się nie stanie. Gdybyśmy jednak chcieli tworzyć nową linię historii zmian na bazie wcześniejszej wersji, powstałoby nowe rozgałęzienie, którego przodkiem stałby się wybrany obiekt typu commit. Kłopot polegałby jednak na tym, że tak wytworzona gałąź zmian nie byłaby śledzona, ponieważ formalnie jej nie stworzono.
Git obsługuje historię zmian i rozgałęzienia, lecz relacje między kolejnymi wydaniami
są reprezentowane acyklicznym, skierowanym grafem i śledzenie ich musi zacząć się
od najstarszego obiektu zatwierdzeniowego. Jeżeli nigdzie nie zapiszemy, że dany
obiekt jest początkiem rozgałęzienia, to po przełączeniu się, stracimy możliwość
śledzenia zmian, jeżeli nie zapamiętamy identyfikatora obiektu. Właśnie przed tym
przestrzega nas narzędzie w prezentowanym komunikacie. Zaleca nam też, że jeżeli
zamierzamy wprowadzać zmiany i tym samym stwarzać alternatywną ich linię, powinniśmy
sokrzystać z opcji -b
i nadać nowej gałęzi nazwę.
Wracając do naszego przykładu, wyświetlmy zawartość pliku z dokumentacją:
Tym razem otrzymamy:
Dokumentacja
Co się stało? Zawartość pliku README
została zmieniona, aby odpowiadała treści
wcześniej zarchiwizowanej w obiekcie typu blob, do którego odnosił się obiekt typu
tree (odpowiedzialny za przechowywanie struktury katalogu siup
). Ten ostatni
z kolei był pod lupą obiektu zatwierdzeniowego o wprowadzonym przez nas
identyfikatorze. Polecenie git checkout
sprawiło, że:
- zmienił się wskaźnik
HEAD
, - zmieniła się zawartość plików, aby odzwierciedlić zapamiętaną migawkę.
Mówiąc skrótowo: przełączyliśmy wersję, więc zawartość rzewa roboczego została zmieniona w taki sposób, aby odzwierciedlić zapamiętaną wtedy zawartość.
Wróćmy do ostatniej wersji głównej gałęzi:
Szybkie przełączenie z oderwaniem
Warto wspomnieć, że jeżeli chcemy na chwilę wrócić do jednej z poprzednich wersji
i znamy dokładną liczbę zatwierdzeń, możemy skorzystać z operatora ~
(znak tyldy)
w odniesieniu do wskaźnika HEAD. Składnia jest następująca:
git checkout HEAD~X
gdzie X
jest liczbą zatwierdzeń, o którą chcemy się cofnąć. W naszym przypadku
moglibyśmy wykonać:
Ten sposób nie wygeneruje ostrzeżenia, ponieważ jest to wyrażona wprost chęć przełączenia się z oderwanym wskaźnikiem HEAD.
Historia przełączeń
Gdybyśmy potrzebowali prześledzić wcześniejsze działania pod kątem przełączania się
między wersjami, możemy to uczynić wydając polecenie git reflog
. Komenda ta
wyświetli zestawienie zmian zapisywanych w tzw. raporcie referencyjnym
(ang. reference log, skr. reflog). Git zapisuje w nim każdą zmianę wewnętrznych
wskaźników służących do śledzenia zatwierdzeń.
Wycofywanie zmian
Cofanie wcześniej wprowadzonych zmian jest ważną funkcją systemów kontroli wersji. W przypadku Gita możemy bezproblemowo dokonać lokalnego wycofania i nadpisania ostatnio zatwierdzonej zmiany, wycofania konkretnej, wcześniejszej wersji, a także przywrócenia zawartości z dowolnego momentu przy zachowaniu pełnej historii.
Kasowanie zatwierdzenia
Jeżeli omyłkowo zatwierdziliśmy zmianę i chcemy ją skasować, możemy tego dokonać
z użyciem polecenia git reset
. Z jego pomocą jesteśmy w stanie manipulować
wartością wskaźnika HEAD, a więc również ustawiać go na dowolną wartość. Jest to
bardzo inwazyjna komenda, ponieważ umożliwia „przycinanie” drzewa historii wersji
w taki sposób, że wierzchołkiem staje się wskazany element gałęzi.
Wyobraźmy sobie, że w naszym przykładzie błędnie wpisaliśmy numer wersji w pliku
README
i chcemy, aby zatwierdzona już zmiana nie była częścią historii. Możemy
wykonać:
Ostatnie polecenie pokaże nam skróconą historię zmian, w której będą 2 wpisy:
* 1b9b584 2019-06-19 | Usuwamy plik (HEAD -> refs/heads/master) [I… N…]
* 4576419 2019-06-19 | Pierwsze wydanie [Imię Nazwisko]
Zauważmy jednak, że zawartość pliku README
nie zmieniła się. Wciąż zawiera zmianę,
lecz jest ona niezatwierdzona. To zasługa opcji --soft
, która sprawia, że nie są
modyfikowane zawartości indeksu czy drzewa roboczego, a jedynie wskaźnik HEAD.
Jeżeli zamiast opcji --soft
podalibyśmy --hard
, to podmienione zostałyby też
zawartości plików w drzewie roboczym.
Na potrzeby przykładu zatwierdzimy wcześniejszą zmianę jeszcze raz:
Uwaga: Ta metoda narusza integralność łańcucha zatwierdzeń i nie zadziała, gdy lokalne repozytorium Gita jest synchronizowane z innym, a ktoś w międzyczasie zatwierdził kolejne zmiany. Dojdzie wtedy do konfliktu podczas synchronizowania repozytoriów. W takich przypadkach należy korzystać z opisanego niżej przywracania wersji.
Korygowanie zatwierdzenia
Istnieje również skrócona wersja omawianej wyżej sekwencji poleceń, która polega na
użyciu komendy git commit
z opcją --amend
. Spróbujmy jeszcze raz dokonać
podmiany, ale tym razem zmodyfikujemy wcześniej plik README
:
Widzimy, że dokonaliśmy wyłącznie zmiany komentarza obiektu zatwierdzeniowego,
a nowa modyfikacja (dodana litera b
) wciąż jest nieuwzględniona:
Jeżeli chcemy dokonać korekty, biorąc pod uwagę również zawartości plików, musimy
najpierw z użyciem git add
umieścić zmienione zbiory w poczekalni:
Gdybyśmy chcieli pozostawić w niezmienionym stanie komentarz do poprzedniego
zatwierdzenia, możemy do git commit --amend
dodać opcję --no-edit
.
Uwaga: Ta metoda narusza integralność łańcucha zatwierdzeń i nie zadziała, gdy lokalne repozytorium Gita jest synchronizowane z innym, a ktoś w międzyczasie zatwierdził kolejne zmiany. Dojdzie wtedy do konfliktu podczas synchronizowania repozytoriów. W takich przypadkach należy korzystać z opisanego niżej przywracania wersji.
Wycofywanie wersji
Wycofać (ang. revert) konkretną, wcześniejszą wersję możemy z użyciem polecenia
git revert
. Sprawia ono, że w historii zmian pojawi się nowy obiekt
zatwierdzeniowy, który będzie zawierał migawkę będącą efektem takiej modyfikacji
drzewa, jakby wycofywana przez nas wersja nie istniała.
A teraz wycofajmy przedostatnią zmianę (dodaliśmy w niej numer wersji):
Pojawił się pewien problem, sygnalizowany komunikatem:
error: could not revert a8a29f8... Informacje o wersji po raz czwarty
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'
Oznacza on, że Git miał kłopoty z automatycznym utworzeniem łatki, która nałożona na pliki zmieniłaby je tak, aby ich nowa zawartość wykluczała istnienie pośredniej modyfikacji.
Dlaczego tak się stało? Git tworzy łatki uwzględniając m.in. konteksty zmian, czyli zestawy dodawanych lub usuwanych linii tekstu wraz z otaczającymi je sekwencjami specjalnymi, np. separatorami oddzielającymi bloki kodu źródłowego w formie pustych linii. W ten sposób w pewien podstawowy sposób chroni on przed automatycznym wprowadzaniem zmian, które z punktu widzenia gramatyki wielu języków programowania byłyby bezsensowne.
Gdy zajrzymy do pliku README
(wydając komendę cat README
), ujrzymy dodatkowe
znaczniki wprowadzone przez Gita:
Obszar między <<<<<<<
a =======
zawiera zmiany, których Git nie był w stanie
automatycznie włączyć w historię.
W przypadku naszego pliku tekstowego zmiany w sąsiadujących liniach są pożądane, dlatego możemy wymusić wprowadzenie zmian. Najpierw sprawdzimy, w jakim stanie jest repozytorium:
On branch master
You are currently reverting commit 41fc9f3.
(fix conflicts and run "git revert --continue")
(use "git revert --abort" to cancel the revert operation)
Unmerged paths:
(use "git reset HEAD <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: README
Komunikat mówi, że powinniśmy naprawić konflikty, a następnie użyć polecenia
git revert --continue
, aby dokończyć przywracanie. Gdybyśmy się rozmyślili, możemy
użyć git revert --abort
.
Dokonajmy więc edycji pliku README i złączmy w całość zmiany, aby zawartość wyglądała tak, jak poniżej:
Następnie wydajmy polecenie kontunuacji:
Ostatnie polecenie uruchomi nasz domyślny edytor, który pozwoli przejrzeć komentarz
towarzyszący zatwierdzeniu wycofania. Po wyjściu z edytora (z zapisaniem zmian),
wycofanie zmian będzie kompletne, a polecenie git hist
wyświetli:
* 4a8e346 2019-06-19 | Revert "Informacje o wersji po raz czwarty" [I… N…]
* e49b228 2019-06-19 | Dodatkowa zmiana 123 [Imię Nazwisko]
* a8a29f8 2019-06-19 | Informacje o wersji po raz czwarty [Imię Nazwisko]
* 1b9b584 2019-06-19 | Usuwamy plik [Imię Nazwisko]
* 4576419 2019-06-19 | Pierwsze wydanie [Imię Nazwisko]
Spróbujmy jeszcze czegoś prostszego, co na pewno nie doprowadzi do powstania konfliktu podczas automatycznego złączania. W naszej historii zmian figuruje zatwierdzenie usunięcia pliku. Spróbujmy więc wycofać tę wersję, podając jej skrócony identyfikator:
Po zapisaniu tymczasowego pliku w wywołanym przez Gita edytorze, ujrzymy zbiór
setup.py
w katalogu roboczym projektu, a historia zmian będzie prezentowała się
następująco:
* e624220 2019-06-19 | Revert "Usuwamy plik" (HEAD -> …
* 4a8e346 2019-06-19 | Revert "Informacje o wersji po raz czwarty" [I… N…]
* e49b228 2019-06-19 | Dodatkowa zmiana 123 [Imię Nazwisko]
* a8a29f8 2019-06-19 | Informacje o wersji po raz czwarty [Imię Nazwisko]
* 1b9b584 2019-06-19 | Usuwamy plik [Imię Nazwisko]
* 4576419 2019-06-19 | Pierwsze wydanie [Imię Nazwisko]
Przywracanie zawartości wybranych plików
Pracując nad projektem może zdarzyć się, że będziemy chcieli przywrócić historyczną zawartość wybranego pliku. W zależności od potrzeb możemy w Gicie skorzystać w tym celu z różnych poleceń.
Przywracanie ostatnio zatwierdzonego
Jeżeli w jakiś sposób „popsuliśmy” zawartość pliku i chcemy zapomnieć o lokalnych
zmianach, możemy tego dokonać z użyciem git checkout
. Wprowadźmy niechciane zmiany
do pliku README
:
A teraz przywróćmy ostatnią zatwierdzoną wersję tego zbioru z repozytorium:
Dwa znaki łącznika informują Gita, że chcemy operować na bieżącej gałęzi. Gdyby
istniała gałąź o nazwie takiej samej, jak plik (tu README
), polecenie dokonałoby
przełączenia między gałęziami, zamiast przywracać wersję pliku.
Istnieje również wariant polecenia checkout
, w którym możemy wyspecyfikować
konkretną wersję do przywrócenia. Należy wtedy zamiast dwóch znaków dywizu podać
identyfikator obiektu zatwierdzeniowego w pełnej lub skróconej formie, albo inny
specyfikator zmiany.
Kasowanie zmian
Historia zmian może być również nadpisana lub skrócona. Są to inwazyjne operacje, które mogą spowodować konflikty przy synchronizowaniu zmian z innymi repozytoriami, dlatego powinno się ich używać w ostateczności.
Uwaga: Te metody naruszają integralność łańcucha zatwierdzeń i sprawią kłopoty, gdy lokalne repozytorium Gita jest synchronizowane z innym, a ktoś w międzyczasie zatwierdzi kolejne zmiany. Dojdzie wtedy do konfliktu podczas synchronizowania repozytoriów.
Resetowanie wskaźnika
Jeżeli chcielibyśmy pozbyć się części historii zmian, możemy skorzystać
ze wspomnianego już polecenia git reset
. Jego użycie sprawia, że dochodzi
do pominięcia pewnej liczby ostatnich obiektów zatwierdzeniowych. Dzieje się to
w taki sposób, że wskaźnik HEAD, który odnosi się do najbardziej aktualnego
wierzchołka grafu zatwierdzeń, zostaje zmieniony, aby odwoływać się do wcześniejszego
obiektu zatwierdzeniowego, niż oryginalny. W efekcie dochodzi do pominięcia ostatnich
zatwierdzeń, a więc zmiany historii.
Istnieją dwa warianty operacji reset
: twardy i miękki. Miękki reset po prostu
zmienia wartość wskaźnika HEAD, natomiast twardy dokonuje dodatkowo zmian w drzewie
roboczym i w indeksie.
Spróbujmy permanentnie usunąć ostatnie dwie zmiany przez miękkie odcięcie historii:
Teraz nasza historia zmian przedstawia się następująco:
* b143bef 2019-06-20 | Dodatkowa zmiana 123 (HEAD -> …
* a8a29f8 2019-06-19 | Informacje o wersji po raz czwarty [Imię Nazwisko]
* 1b9b584 2019-06-19 | Usuwamy plik [Imię Nazwisko]
* 4576419 2019-06-19 | Pierwsze wydanie [Imię Nazwisko]
Warto zauważyć, że w pliku README
mamy teraz zawartość, która różni się od
najnowszej wersji zatwierdzonej, ponieważ ten sposób nie dokonuje zmian w drzewie
roboczym.
Cofnijmy się jeszcze o trzy wersje, ale tym razem wykonując twardy reset wskaźnika HEAD:
Tym sposobem cofnęliśmy stan naszego repozytorium do pierwszej zatwierdzonej wersji, a zawartość indeksu i drzewa roboczego odzwierciedlają ten fakt.
Przywracanie wskaźnika
Gdybyśmy przez nieuwagę skorzystali z git-reset
, to mamy szansę na odzyskanie
historii zmian, ponieważ musi upłynąć trochę czasu zanim osierocone obiekty zostaną
trwale usunięte z bazy Gita. Pomoże nam wspomniany wcześniej raport referencyjny,
zwany skrótowo reflogiem. Dzięki niemu będziemy w stanie namierzyć identyfikatory
obiektów zatwierdzeniowych, które stały się już niewidoczne w historii zmian,
ponieważ została przez nas zmniejszona.
Spójrzmy na pierwsze 3 wpisy w reflogu:
Naszym oczom powinny ukazać się wpisy podobne do poniższych:
4576419 (HEAD -> refs/heads/master) HEAD@{0}: reset: moving to HEAD~3
e49b228 HEAD@{1}: reset: moving to HEAD~2
e624220 HEAD@{2}: revert: Revert "Usuwamy plik"
Są to trzy ostatnie operacje na wskaźniku HEAD wraz z identyfikatorami obiektów, na które wskazywał po każdej z nich. Korzystając z tej wiedzy, możemy przywrócić wartość referencji i odzyskać pominiętą historię.
Spróbujmy przesunąć się do momentu, w którym cofnęliśmy się tylko o 2 wpisy:
Teraz historia zmian składa się z 4 zatwierdzeń:
* b143bef 2019-06-20 | Dodatkowa zmiana 123 (HEAD -> …
* a8a29f8 2019-06-19 | Informacje o wersji po raz czwarty [Imię Nazwisko]
* 1b9b584 2019-06-19 | Usuwamy plik [Imię Nazwisko]
* 4576419 2019-06-19 | Pierwsze wydanie [Imię Nazwisko]
Przepisywanie historii
Dzięki git reset
potrafimy selekcjonować wierzchołek gałęzi zmian, a z użyciem git
commit
możemy do tego wierzchołka dodawać nowe elementy. Jest więc teoretycznie
możliwe wykonanie sekwencji tych poleceń w taki sposób, aby przepisać historię
zmian od pewnego punktu, przy okazji modyfikując wybrane zatwierdzenia lub nawet
całkiem je pomijając. Operacja taka spowodowałaby powstanie nowej linii zmian, jednak
z zachowaniem pełnej integralności, ponieważ podczas ponownego zatwierdzania
utrzymywana byłaby ciągłość relacji obiektów zatwierdzeniowych, a także generowane
byłyby nowe, identyfikujące je sumy kontrolne.
System kontroli wersji Git wyposażono w odpowiednie narzędzie, które służy właśnie
do tego i ułatwia proces przepisywania historii. Jest ono reprezentowane poleceniem
git rebase
.
Operacji rebase
używa się do złączania gałęzi (zamiast merge
), a także do edycji
zatwierdzonych już komentarzy zatwierdzeń, jeżeli zachodzi taka
konieczność. Polecenie to będzie dokładniej omówione w dalszych częściach,
traktujących o rozgałęzieniach i złączeniach w projektach.
Specyfikatory
Wiele poleceń Gita wymaga podania pełnej lub skróconej sumy kontrolnej
identyfikującej obiekt, albo wyspecyfikowania wersji w inny, idiomatyczny
sposób. Spotkaliśmy się już z umieszczaniem nazwy wierzchołka gałęzi (np. master
)
bądź liczby zmian od pozycji bieżącej (np. HEAD~2
). Poniższa tabela przedstawia
możliwe specyfikatory wydań i ich składnię:
nazwa specyfikatora | składnia | przykład(-y) | opis |
---|---|---|---|
suma SHA-1 | <sha> |
5a9f57750c7bf4a90c0cf11c623e6e547dae18d0 |
identyfikuje obiekt zatwierdzeniowy |
skrócona suma SHA-1 | <krótka-sha> |
5a9f577 |
identyfikuje obiekt zatwierdzeniowy |
nazwa referencji | <ref> <dir>/<ref> |
master heads/master tags/nazwa remotes/nazwa HEAD |
symboliczna nazwa referencji (w pliku .git/[nazwa] lub .git/refs/[nazwa] ) |
referencja zdalna | <spec>@{upstream} <spec>@{u} @{u} |
master@{upstream} @{u} |
identyfikuje zdalną referencję towarzyszącą podanej (upstream i u to napisy stałe) |
czas | <spec>@{czas} @{czas} |
master@{1hour ago} HEAD@{12:00} @{yesterday} |
specyfikuje czas dla podanej referencji lub bieżącej linii |
instancja | <spec>@{<n>} |
master@{2} @{3} HEAD@{5} |
specyfikuje kolejną, licząc od ostatniej, instancję podanej referencji (nie wersję!) |
instancja przełączenia | @{<n>} |
@{-3} |
jw., ale oznacza n-tą gałąź wstecz względem bieżącej, na którą dokonywano przełączenia |
wybór przodka | <spec>^ <spec>^ … ^ <spec>^<n> |
HEAD^ HEAD^^ HEAD^2 5a9f577^ master^ |
określa przodka podanej specyfikacji, gdy obiekt ma więcej, niż jednego rodzica (powtórzenie symbolu ^ określa kolejnego przodka w linii) |
przodek z typem | <spec>^{<typ>} <spec>^ … ^<{typ>} <spec>^<n>{<typ>} |
HEAD^{commit} |
jw., lecz dokonuje rekursywnej dereferencji, jeżeli obiekt jest tagiem, aż do znalezienia obiektu podanego typu |
przodek typu commit | <spec>^{} <spec>^ … ^{} <spec>^<n>{} |
HEAD^{} |
jw., lecz typem poszukiwanego obiektu jest commit |
przodek z komentarzem | <spec>^{/<tekst>} <spec>^ … ^{/<tx>} |
master^{/Poprawka} HEAD^^{/plik} |
określa najmłodszego przodka, którego komentarz pasuje do podanego tekstu |
generacja przodka | ~<n> <spec>~ <spec>~<n> |
~3 HEAD~3 HEAD~ master~2 |
określa n-tego przodka wyspecyfikowanego obiektu lub bezpośredniego rodzica, gdy nie podano liczby |
komentarz | :/<tekst> |
:/Poprawka |
określa najmłodszy obiekt zatwierdzeniowy, którego komentarz pasuje do wprowadzonego tekstu lub fragmentu |
ścieżka | <spec>:<ścieżka> |
master:./plik.txt HEAD:plik.txt :plik.txt |
specyfikuje obiekt typu blob lub tree, którego nazwa ścieżkowa pasuje do podanej |
etap złączania | :<0…3>:<ścieżka> |
:plik.txt :0:plik.txt :3:plik.txt |
określa obiekt typu blob o podanej nazwie ścieżkowej w indeksie podczas złączania gałęzi (prefiks 0 lub brak oznacza obiekt, 1 – wspólnego przodka, 2 – docelową gałąź, 3 – gałąź scalaną) |
Aby sprawdzić poprawność danego specyfikatora, możemy skorzystać z polecenia git
rev-parse
. Zwraca on identyfikator obiektu, który pasuje do podanej jako argument
specyfikacji.
Warto mieć na względzie, że niektóre symbole specyfikujące (np. nawiasy klamrowe czy tylda), mogą mieć specjalne znaczenie w interpreterze interaktywnej powłoki. W takich przypadkach warto ująć argument w znaki maszynowego cudzysłowu.