System kontroli wersji Git, cz. 2

Wstęp do użytkowania

Grafika

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:

SH
cd
mkdir siup siup/docs siup/siup
echo "Dokumentacja" >> siup/README
touch siup/setup.py siup/siup/__init__.py siup/siup/main.py
cd mkdir siup siup/docs siup/siup echo "Dokumentacja" >> siup/README touch siup/setup.py siup/siup/__init__.py siup/siup/main.py

Efektem wydania powyższych poleceń będzie następująca struktura katalogowa:

siup
├── README
├── docs
├── setup.py
└── siup
    ├── __init__.py
    └── main.py
siup ├── README ├── docs ├── setup.py └── siup ├── __init__.py └── main.py

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:

SH
git config --global user.name "Imię Nazwisko"
git config --global user.email [email protected]
git config --global user.name "Imię Nazwisko" git config --global user.email [email protected]

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:

SH
git config --global color.interactive auto
git config --global color.status auto
git config --global color.branch auto
git config --global color.diff auto
git config --global color.interactive auto git config --global color.status auto git config --global color.branch auto git config --global color.diff auto

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:

SH
git config --global alias.st status
git config --global alias.ci commit
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.st status git config --global alias.ci commit git config --global alias.co checkout git config --global alias.br branch

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:

git config --global core.editor vim
git config --global core.editor vim

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:

~/bin/edit
#!/bin/sh

# Emacs launcher by Paweł Wilk <pw-at-gnu-dot-org>
# License: GNU AGPL

GDK_RGBA=0
export GDK_RGBA

if [ "$1" = "--no-wait" ]; then
    shift
    nowait="-n"
else
    nowait=""
fi

type xdotool 2>/dev/null >/dev/null
if [ "$?" != "0" ]; then
    havedotool=""
else
    havedotool="1"
fi

if [ -n "${havedotool}" ]; then
    xdotool search --onlyvisible --class emacs 2>/dev/null >/dev/null
    if [ "$?" = "1" ]; then
	    # there is no frame, create one
	    exec emacsclient ${nowait} -c -F "((fullscreen . maximized))" \
                 -a "" "[email protected]" || exit $?
    else
	    # there is a frame - use it
	    xdotool search --onlyvisible --class emacs windowactivate \
                    2>/dev/null >/dev/null
	    exec emacsclient ${nowait} -a "" "[email protected]" || exit $?
    fi
else
    case "$(uname -s)" in
        Darwin*)    open /Applications/Emacs.app || exit $? ;;
        *)          exec emacsclient ${nowait} -a "" "[email protected]"  || exit $?
    esac
fi

x=10
while [ $x -ge 0 ];
do
    emacsclient -ca false -e '(delete-frame)' \
                >/dev/null 2>/dev/null && break
    sleep 1
done

exec emacsclient ${nowait} -a vim "[email protected]" || exit $?
#!/bin/sh # Emacs launcher by Paweł Wilk &lt;pw-at-gnu-dot-org&gt; # License: GNU AGPL GDK_RGBA=0 export GDK_RGBA if [ &#34;$1&#34; = &#34;--no-wait&#34; ]; then shift nowait=&#34;-n&#34; else nowait=&#34;&#34; fi type xdotool 2&gt;/dev/null &gt;/dev/null if [ &#34;$?&#34; != &#34;0&#34; ]; then havedotool=&#34;&#34; else havedotool=&#34;1&#34; fi if [ -n &#34;${havedotool}&#34; ]; then xdotool search --onlyvisible --class emacs 2&gt;/dev/null &gt;/dev/null if [ &#34;$?&#34; = &#34;1&#34; ]; then # there is no frame, create one exec emacsclient ${nowait} -c -F &#34;((fullscreen . maximized))&#34; \ -a &#34;&#34; &#34;[email protected]&#34; || exit $? else # there is a frame - use it xdotool search --onlyvisible --class emacs windowactivate \ 2&gt;/dev/null &gt;/dev/null exec emacsclient ${nowait} -a &#34;&#34; &#34;[email protected]&#34; || exit $? fi else case &#34;$(uname -s)&#34; in Darwin*) open /Applications/Emacs.app || exit $? ;; *) exec emacsclient ${nowait} -a &#34;&#34; &#34;[email protected]&#34; || exit $? esac fi x=10 while [ $x -ge 0 ]; do emacsclient -ca false -e &#39;(delete-frame)&#39; \ &gt;/dev/null 2&gt;/dev/null &amp;&amp; break sleep 1 done exec emacsclient ${nowait} -a vim &#34;[email protected]&#34; || exit $?

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:

SH
cd ~/siup
git init
cd ~/siup git init

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:

SH
cd ~/siup
git add .
cd ~/siup 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:

.git
└── objects
    ├── 77
    │   └── dff83b77ebac99e1918f6736c9ac655626b90d
    └── e6
        └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
.git └── objects     ├── 77     │   └── dff83b77ebac99e1918f6736c9ac655626b90d     └── e6         └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391

W oryginale mieliśmy 4 pliki2 katalogi. Skąd więc różnica? Możemy sprawdzić, z jakimi obiektami mamy do czynienia:

SH
cd ~/siup
git cat-file --batch-all-objects --batch-check
cd ~/siup git cat-file --batch-all-objects --batch-check

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:

SH
cd ~/siup
git show 77dff83b77ebac99e1918f6736c9ac655626b90d
cd ~/siup git show 77dff83b77ebac99e1918f6736c9ac655626b90d

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:

SH
cd ~/siup
git status
cd ~/siup git status

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:

On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

	new file:   README
	new file:   setup.py
	new file:   siup/__init__.py
	new file:   siup/main.py
On branch master No commits yet Changes to be committed: (use &#34;git rm --cached &lt;file&gt;...&#34; to unstage) new file: README new file: setup.py new file: siup/__init__.py new file: siup/main.py

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:

SH
cd ~/siup
git diff --cached
cd ~/siup git diff --cached

Powinniśmy otrzymać komunikat w ujednoliconym formacie diff (ang. unified diff, skr. unidiff):

diff --git a/README b/README
new file mode 100644
index 0000000..630fcdc
--- /dev/null
+++ b/README
@@ -0,0 +1 @@
+Dokumentacja 2
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..e69de29
diff --git a/siup/__init__.py b/siup/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/siup/main.py b/siup/main.py
new file mode 100644
index 0000000..e69de29
diff --git a/README b/README new file mode 100644 index 0000000..630fcdc --- /dev/null +++ b/README @@ -0,0 +1 @@ +Dokumentacja 2 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e69de29 diff --git a/siup/__init__.py b/siup/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/siup/main.py b/siup/main.py new file mode 100644 index 0000000..e69de29

Opcja --cached (lub --staged) sprawia, że zawartości plików drzewie roboczym będą porównywane z zawartościami oraz atrybutami plików, których metadane znajdują się 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:

SH
cd ~/siup
git difftool --staged
cd ~/siup git difftool --staged

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:

SH
cd ~/siup
git commit -m 'Pierwsze wydanie'
cd ~/siup git commit -m &#39;Pierwsze wydanie&#39;

Naszym oczom powinien ukazać się następujący komunikat diagnostyczny:

[master (root-commit) 9e468ad] Pierwsze wydanie
 4 files changed, 1 insertion(+)
 create mode 100644 README
 create mode 100644 setup.py
 create mode 100644 siup/__init__.py
 create mode 100644 siup/main.py
[master (root-commit) 9e468ad] Pierwsze wydanie 4 files changed, 1 insertion(+) create mode 100644 README create mode 100644 setup.py create mode 100644 siup/__init__.py create mode 100644 siup/main.py

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:

SH
cd ~/siup
git rm setup.py
git commit -m 'Usuwamy plik'
cd ~/siup git rm setup.py git commit -m &#39;Usuwamy plik&#39;

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:

.git
└── objects
    ├── 0e
    │   └── 2da9c1380be2d00f0a5981ef1ce77274dde61f
    ├── 1b
    │   └── 9b584db83289e78155fc59cca3b13da9e21816
    ├── 45
    │   └── 764193954e472af3c198d750223d287080fbe8
    ├── 77
    │   └── dff83b77ebac99e1918f6736c9ac655626b90d
    ├── 8f
    │   └── 48403e5512311ab249c6a7c796f318267f801a
    ├── 98
    │   └── bccf5690cc3053730329fa1b8c98dc0d52622a
    └── e6
        └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
.git └── objects     ├── 0e     │   └── 2da9c1380be2d00f0a5981ef1ce77274dde61f     ├── 1b     │   └── 9b584db83289e78155fc59cca3b13da9e21816     ├── 45     │   └── 764193954e472af3c198d750223d287080fbe8     ├── 77     │   └── dff83b77ebac99e1918f6736c9ac655626b90d     ├── 8f     │   └── 48403e5512311ab249c6a7c796f318267f801a     ├── 98     │   └── bccf5690cc3053730329fa1b8c98dc0d52622a     └── e6         └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391

Pomijając pliki obiektów, które już wcześniej znajdowały się w repozytorium:

.git
└── objects
    ├── 0e
    │   └── 2da9c1380be2d00f0a5981ef1ce77274dde61f
    ├── 1b
    │   └── 9b584db83289e78155fc59cca3b13da9e21816
    ├── 45
    │   └── 764193954e472af3c198d750223d287080fbe8
    ├── 8f
    │   └── 48403e5512311ab249c6a7c796f318267f801a
    └── 98
        └── bccf5690cc3053730329fa1b8c98dc0d52622a
.git └── objects     ├── 0e     │   └── 2da9c1380be2d00f0a5981ef1ce77274dde61f     ├── 1b     │   └── 9b584db83289e78155fc59cca3b13da9e21816     ├── 45     │   └── 764193954e472af3c198d750223d287080fbe8     ├── 8f     │   └── 48403e5512311ab249c6a7c796f318267f801a     └── 98         └── bccf5690cc3053730329fa1b8c98dc0d52622a

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:

SH
cat ~/siup/.git/refs/heads/master
cat ~/siup/.git/refs/heads/master

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:

SH
cd ~/siup
git show 0e2da9c1380be2d00f0a5981ef1ce77274dde61f
git show 8f48403e5512311ab249c6a7c796f318267f801a
git show 98bccf5690cc3053730329fa1b8c98dc0d52622a
git show 45764193954e472af3c198d750223d287080fbe8
cd ~/siup git show 0e2da9c1380be2d00f0a5981ef1ce77274dde61f git show 8f48403e5512311ab249c6a7c796f318267f801a git show 98bccf5690cc3053730329fa1b8c98dc0d52622a git show 45764193954e472af3c198d750223d287080fbe8

W efekcie otrzymamy:

tree 0e2da9c1380be2d00f0a5981ef1ce77274dde61f

README
siup/

tree 8f48403e5512311ab249c6a7c796f318267f801a

README
setup.py
siup/

tree 98bccf5690cc3053730329fa1b8c98dc0d52622a

__init__.py
main.py

commit 45764193954e472af3c198d750223d287080fbe8
Author: Imię Nazwisko <[email protected]>
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:

.git
└── objects
    ├── 0e
    │   └── 2da9c1380be2d00f0a5981ef1ce77274dde61f (katalog siup – wersja 2)
    ├── 1b
    │   └── 9b584db83289e78155fc59cca3b13da9e21816 (zmiana nr 2)
    ├── 45
    │   └── 764193954e472af3c198d750223d287080fbe8 (zmiana nr 1)
    ├── 77
    │   └── dff83b77ebac99e1918f6736c9ac655626b90d (zawartość "Dokumentacja")
    ├── 8f
    │   └── 48403e5512311ab249c6a7c796f318267f801a (katalog siup – wersja 1)
    ├── 98
    │   └── bccf5690cc3053730329fa1b8c98dc0d52622a (katalog siup/siup)
    └── e6
        └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 (zawartość pusta)
.git └── objects     ├── 0e     │   └── 2da9c1380be2d00f0a5981ef1ce77274dde61f (katalog siup – wersja 2)     ├── 1b     │   └── 9b584db83289e78155fc59cca3b13da9e21816 (zmiana nr 2)     ├── 45     │   └── 764193954e472af3c198d750223d287080fbe8 (zmiana nr 1)     ├── 77     │   └── dff83b77ebac99e1918f6736c9ac655626b90d (zawartość &#34;Dokumentacja&#34;)     ├── 8f     │   └── 48403e5512311ab249c6a7c796f318267f801a (katalog siup – wersja 1)     ├── 98     │   └── bccf5690cc3053730329fa1b8c98dc0d52622a (katalog siup/siup)     └── e6         └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 (zawartość pusta)

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:

SH
cd ~/siup
echo "wersja 1.0" >> ./README
git add ./README
git commit -m 'informacje o wersji'
cd ~/siup echo &#34;wersja 1.0&#34; &gt;&gt; ./README git add ./README git commit -m &#39;informacje o wersji&#39;

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:

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:

SH
git config --global alias.hist \
 "log --pretty=format:'%C(yellow)%h%C(reset) %ad |\
 %s%d [%an]' --graph --date=short"
git config --global alias.hist \ &#34;log --pretty=format:&#39;%C(yellow)%h%C(reset) %ad |\ %s%d [%an]&#39; --graph --date=short&#34;

Od teraz możemy korzystać z polecenia git hist:

SH
cd ~/siup
git hist
cd ~/siup 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:

SH
cd ~/siup
git log --oneline
cd ~/siup git log --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.

SH
cd ~/siup
cat ./README
cd ~/siup cat ./README

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:

SH
cd ~/siup
git hist
cd ~/siup git hist

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^:

SH
cd ~/siup
git log HEAD^
git checkout HEAD^
cd ~/siup git log HEAD^ git checkout 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ą:

SH
cd ~/siup
cat ./README
cd ~/siup cat ./README

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:

SH
cd ~/siup
git checkout master
cd ~/siup git checkout master

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ć:

SH
cd ~/siup
git checkout HEAD~1
git checkout master  # powracamy
cd ~/siup git checkout HEAD~1 git checkout master # powracamy

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ć:

SH
cd ~/siup
git reset --soft HEAD~1
git hist
cd ~/siup git reset --soft HEAD~1 git hist

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:

SH
cd ~/siup
git commit -m 'Informacje o wersji raz jeszcze'
git hist
cd ~/siup git commit -m &#39;Informacje o wersji raz jeszcze&#39; git hist

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:

SH
cd ~/siup
echo "b" >> README
git commit --amend -m 'Informacje o wersji po raz trzeci'
git hist
cd ~/siup echo &#34;b&#34; &gt;&gt; README git commit --amend -m &#39;Informacje o wersji po raz trzeci&#39; git hist

Widzimy, że dokonaliśmy wyłącznie zmiany komentarza obiektu zatwierdzeniowego, a nowa modyfikacja (dodana litera b) wciąż jest nieuwzględniona:

SH
cd ~/siup
git diff
cd ~/siup git diff

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:

SH
cd ~/siup
git add README
git commit --amend -m 'Informacje o wersji po raz czwarty'
git hist
cd ~/siup git add README git commit --amend -m &#39;Informacje o wersji po raz czwarty&#39; git hist

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.

SH
cd ~/siup
echo       >> README
echo "123" >> README
echo       >> README
git add README
git commit -m 'Dodatkowa zmiana 123'
cd ~/siup echo &gt;&gt; README echo &#34;123&#34; &gt;&gt; README echo &gt;&gt; README git add README git commit -m &#39;Dodatkowa zmiana 123&#39;

A teraz wycofajmy przedostatnią zmianę (dodaliśmy w niej numer wersji):

SH
git revert HEAD~1
git revert HEAD~1

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:

~/siup/README
Dokumentacja
<<<<<<< HEAD
wersja 1.0
b

123

=======
>>>>>>> parent of a8a29f8... Informacje o wersji po raz czwarty
Dokumentacja &lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD wersja 1.0 b 123 ======= &gt;&gt;&gt;&gt;&gt;&gt;&gt; parent of a8a29f8... Informacje o wersji po raz czwarty

Obszar między <<<<<<<======= 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:

SH
cd ~/siup
git status
cd ~/siup git status
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:

~/siup/README
Dokumentacja
b

123
 
Dokumentacja b 123

Następnie wydajmy polecenie kontunuacji:

SH
cd ~/siup
git add README
git revert --continue
cd ~/siup git add README git revert --continue

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:

SH
cd ~/siup
git revert 1b9b584
cd ~/siup git revert 1b9b584

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:

SH
cd ~/siup
echo "NIECHCIANA ZMIANA" >> README
cd ~/siup echo &#34;NIECHCIANA ZMIANA&#34; &gt;&gt; README

A teraz przywróćmy ostatnią zatwierdzoną wersję tego zbioru z repozytorium:

SH
cd ~/siup
git checkout -- README
cd ~/siup git checkout -- README

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:

SH
cd ~/siup
git reset HEAD~2
git hist
cd ~/siup git reset HEAD~2 git hist

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:

SH
cd ~/siup
git reset --hard HEAD~3
git hist
cd ~/siup git reset --hard HEAD~3 git hist

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:

SH
cd ~/siup
git reflog show -3
cd ~/siup git reflog show -3

Naszym oczom powinny ukazać się wpisy podobne do poniższych:

4576419 (HEAD -> refs/heads/master) [email protected]{0}: reset: moving to HEAD~3
e49b228 [email protected]{1}: reset: moving to HEAD~2
e624220 [email protected]{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:

SH
cd ~/siup
git reset --hard e49b228
git hist
cd ~/siup git reset --hard e49b228 git hist

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}
[email protected]{upstream}
@{u}
identyfikuje zdalną referencję towarzyszącą podanej (upstreamu to napisy stałe)
czas <spec>@{czas}
@{czas}
[email protected]{1hour ago}
[email protected]{12:00}
@{yesterday}
specyfikuje czas dla podanej referencji lub bieżącej linii
instancja <spec>@{<n>} [email protected]{2}
@{3}
[email protected]{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.


comments powered by Disqus