1 == Uchylenie tajemnicy ==
3 Rzućmy spojrzenie pod maskę silnika i wytłumaczymy w jaki sposób Git realizuje swoje cuda. Nie będę wchodził w szczegóły. Dla pogłębienia tematu odsyłam na http://www.kernel.org/pub/software/scm/git/docs/user-manual.html[anielskojęzychny podręcznik użytkownika].
7 Jak to możliwe, że Git jest taki niepostrzeżony? Zapominając na chwilę o sporadycznych 'commits' i 'merges', możesz pracować w sposób, jakby kontrola wersji w ogóle nie istniała. Chciałem powiedzieć, do czasu aż będzie ci potrzebna. A oto chodzi, byś był zadowolony z tego, że Git cały czas czuwa nad twoją pracą.
9 Inne systemy kontroli wersji ciągle zmuszają cię do ciągłego borykania się z zagadnieniem samej kontroli i związanej z tym biurokracji. Pliki mogą być zabezpieczone przed zapisem, aż do momentu gdy uda ci się poinformować centralny serwer o tym, że chciałabyś nad nimi popracować. Przy wzroście liczby użytkowników nawet najprostsze polecenia stają się wolne jak ślimak. Gdy tylko zniknie sieć lub centralny serwer praca staje.
11 W przeciwieństwie do tego, Git posiada kronikę całej swojej historii w podkatalogu .git twojego katalogu roboczego. Jest to twoja własna kopie całej historii, z którą mogłabyś pracować offline, aż do momentu gdy zechcesz wymienić dane z innymi. Posiadasz absolutną kontrolę nad losem twoich danych, ponieważ Git potrafi dla ciebie w każdej chwili odtworzyć zapamiętany poprzednio stan z właśnie podkatalogu .git.
15 Z kryptografią przez większość ludzi łączona jest poufność informacji, jednak równie ważnym jej celem jest zabezpieczenie danych. Właściwe zastosowanie kryptograficznych funkcji hashujących (funkcji skrótu) może uchronić przed nieumyślnym lub celowym zniszczeniem danych.
17 Klucz hashujący SHA1 mogłabyś wyobrazić sobie jako składający się ze 160 bitów numer identyfikacyjny jednoznacznie opisujący dowolny łańcuch znaków, i który spotkasz w sowim życiu jeden jedyny raz. Nawet i więcej niż to: wszystkie łańcuchy znaków, jakie ludzkość przez wiele generacji stworzyła.
19 Sama suma konreolna SHA1 też jest łańcuchem znaków w formie bajtów. Możemy generować hashe SHA1 z łańcuchów samych zawierających inne hashe SHA1. Ta prosta obserwacja okazała się niesamowicie pożyteczna: jeśli cię to zainteresowało poszukaj informacji na temat 'hash chains'. Zobaczymy później w jaki sposób wykorzystuje je Git dla zapewnienia produktywności i integralności danych.
21 Krótko mówiąc, Git przechowuje twoje dane w podkatalogu `.git/objects`, gdzie zamiast nazw plików znajdziesz numery identyfikacyjne. Poprzez wykorzystanie tych numerów identyfikacyjnych jako nazwy plików razem z kilkoma innymi trikami związanymi z plikami blokującymi i znacznikami czasu, Git zamienia twój prosty system plików na produktywną i solidną bazę danych.
25 Skąd Git wie o tym, że zmieniłaś nazwę jakiegoś pliku, jeśli nigdy go o tym wyraźnie nie poinformowałaś? Oczywiście, być może użyłaś polecenia *git mv*, jest to jednak to samo jakbyś użyła *git rm*, a następnie *git add*.
27 Git poszukuje heurystycznie zmian nazw w następujących po sobie wersjach kopii. Dodatkowo potrafi czasami nawet znaleźć całe bloki z kodem przenoszonym tam i z powrotem między plikami! Mimo iż wykonuje kawał dobrej roboty, a ta właściwość staje się coraz lepsza, nie potrafi niestety jeszcze poradzić sobie z wszystkimi możliwymi przypadkami. Jeśli to u ciebie nie działa, spróbuj poszukać opcji rozszerzonego rozpoznawania kopii, aktualizacja samego Gita, też może pomóc.
31 Dla każdego kontrolowanego pliku, Git zapamiętuje informacje o jego wielkości, czasie utworzenia i czasie ostatniej edycji w pliku znanym nam jako indeks. By ustalić, czy nastąpiła jakaś zmiana, Git porównuje stan aktualny ze stanem zapamiętanym w indeksie. Jeśli dane te nie różnią się, Git może pominąć czytanie zawartości pliku.
33 Ponieważ sprawdzenie statusu pliku trwa dużo krócej niż jego całkowite wczytanie, to jeśli dokonałaś zmian tylko na kilku plikach Git zaktualizuje swój stan w mgnieniu oka.
35 Stwierdziliśmy już wcześniej, że indeks jest przechowalnią (ang. staging area). Jak to możliwe, że stos informacji o statusie danych może być przechowalnią? Ponieważ polecenie 'add' transportuje pliki do bazy danych Git i aktualizuje informacje o ich statusie, podczas gdy polecenie 'commit' (bez opcji) tworzy commit tylko wyłącznie na podstawie informacji o statusie plików, ponieważ pliki te już się w tej bazie znajdują.
39 Ten http://lkml.org/lkml/2005/4/6/121['Linux Kernel Mailing List' post] opisuje cały łańcuch zdarzeń, które inicjowały powstanie Git. Cały post jest archeologicznie fascynującą stroną dla historyków zajmujących się Gitem.
41 === Obiektowa baza danych ===
43 Każda wersja twoich danych jest przechowywana w obiektowej bazie danych, która znajduje się w podkatalogu `.git/objects`. Inne miejsca w `.git/` posiadają mniej ważne dane, jak indeks, nazwy gałęzi ('branch'), tagi, logi, konfigurację, aktualną pozycję HEAD i tak dalej. Obiektowa baza danych jest prosta, mimo to jednak elegancka i jest źródłem siły Gita.
45 Każdy plik w `.git/objects` jest obiektem. Istnieją trzy rodzaje obiektów, które nas interesują: 'blob', 'tree' i 'commit'.
49 Na początek magiczna sztuczka. Wymyśl jakąś nazwę pliku, jakąkolwiek. W pustym katalogu:
52 $ echo sweet > TWOJA_NAZWA
55 $ find .git/objects -type f
56 $ find .git/objects -type f
58 Zobaczysz coś takiego: +.git/objects/aa/823728ea7d592acc69b36875a482cdf3fd5c8d+.
60 Skąd mogłem to wiedzieć, mimo iż nie znałem nazwy pliku? Ponieważ suma kontrolna SHA1 dla:
62 "blob" SP "6" NUL "sweet" LF
64 wynosi właśnie: aa823728ea7d592acc69b36875a482cdf3fd5c8d. Przy czym SP to spacja, NUL - to bajt zerowy, a LF to znak nowej linii ('newline'). Możesz to skontrolować wpisując:
66 $ printf "blob 6\000sweet\n" | sha1sum
68 Git pracuje asocjacyjnie (skojarzeniowo): dane nie są zapamiętywane na podstawie ich nazwy, tylko wartości ich własnego hasha SHA1 w pliku, który określamy mianem obiektu 'blob'. Sumę kontrolną SHA1 możemy sobie wyobrazić jako niepowtarzalny numer identyfikacyjny zawartości pliku, co oznacza, że pliki adresowane są na podstawie ich zawartości. Początkowe `blob 6`, to jedynie adnotacja, która określa tylko rodzaj obiektu i jego wielkość w bajtach, pozwala to na uproszczenie zarządzania wewnętrznego.
70 Przez to właśnie mogłem 'przepowiedzieć' wynik. Nazwa pliku nie ma znaczenia, jedynie jego zawartość służy do utworzenia obiektu 'blob'.
72 Pytasz się, a co w przypadku identycznych plików? Spróbuj dodać kopie twojej danej pod jakąkolwiek nazwą. Zawartość +.git/objects+ nie zmieni się, niezależnie ile kopii dodałaś. Git zapamięta zawartość pliku wyłącznie jeden raz.
74 Na marginesie, dane w +.git/objects+ są spakowane poprzez 'zlib', nie powinieneś otwierać ich bezpośrednio. Przefiltruj je najpierw przez http://www.zlib.net/zpipe.c[zpipe -d], albo wpisz:
76 $ git cat-file -p aa823728ea7d592acc69b36875a482cdf3fd5c8d
78 polecenie to pokaże ci zawartość obiektu jako tekst.
82 Gdzie są więc nazwy plików? Przecież muszą być gdzieś zapisane. Podczas wykonywania 'commit' Git troszczy się o nazwy plików:
84 $ git commit # dodaj jakiś opis.
85 $ find .git/objects -type f
86 $ find .git/objects -type f
88 Powinieneś ujrzeć teraz 3 obiekty. Tym razem nie jestem w stanie powiedzieć, jak nazywają się te dwa nowe pliki, ponieważ częściowo są zależne od nazwy jaką nadałaś plikom. Pójdźmy dalej, zakładając, że jedną z tych danych nazwałaś ``rose''. Jeśli nie, możesz zmienić opis, by wyglądał jakby był twój:
90 $ git filter-branch --tree-filter 'mv TWOJA_NAZWA rose'
91 $ find .git/objects -type f
93 Powinnaś zobaczyć teraz plik +.git/objects/05/b217bb859794d08bb9e4f7f04cbda4b207fbe9+, ponieważ jest to suma kontrolna SHA1 jego zawartości.
95 "tree" SP "32" NUL "100644 rose" NUL 0xaa823728ea7d592acc69b36875a482cdf3fd5c8d
97 Sprawdź, czy plik na prawdę odpowiada powyższej zawartości przez polecenie:
99 $ echo 05b217bb859794d08bb9e4f7f04cbda4b207fbe9 | git cat-file --batch
101 Za pomocą 'zpipe' łatwo sprawdzić hash SHA1:
104 $ zpipe -d < .git/objects/05/b217bb859794d08bb9e4f7f04cbda4b207fbe9 | sha1sum
106 Sprawdzanie za pomocą 'cat-file' jest troszeczkę kłopotliwe, bo jego 'output' zawiera więcej niż tylko nieskomprymowany obiekt pliku.
108 Nasz plik to tak zwany obiekt 'tree': lista wyrażeń, na którą składają się rodzaj pliku, jego nazwa i jego suma kontrolna SHA1. W naszym przykładzie typ pliku to 100644, co oznacza, że `rose` jest plikiem zwykłym, natomiast hash SHA1 odpowiada sumie kontrolnej SHA1 obiektu 'blob' zawierającego zawartość `rose`. Inne możliwe rodzaje plików to programy, linki symboliczne i katalogi. W ostatnim przypadku hash SHA1 wskazuje na obiekt 'tree'.
110 Jeśli użyjesz polecenia 'filter-branch', otrzymasz stare objekty, które nie są już używane. Mimo iż automatycznie zostaną usunięte po upłynięciu okresu karencji, chcemy się ich pozbyć od zaraz, aby lepiej prześledzić następne przykłady.
112 $ rm -r .git/refs/original
113 $ git reflog expire --expire=now --all
116 W prawdziwych projektach powinnaś unikać takich komend, ponieważ zniszczą zabezpieczone dane. Jeśli chcesz posiadać czyste repozytorium, to najlepiej załóż nowy klon. Bądź też ostrożna przy bezpośredniej manipulacji +.git+: gdy równocześnie wykonywane jest polecenie Git i zgaśnie światło? Generalnie do kasowania referencji powinnaś używać *git update-ref -d*, nawet gdy ręczne usunięcie +ref/original+ jest dość bezpieczne.
120 Wytłumaczyliśmy dwa z trzech obiektów. Ten trzeci to obiekt 'commit' Jego zawartość jest zależna od opisu 'commit' jak i czasu jego wykonania. By wszystko do naszego przykładu pasowało, musimy trochę pokombinować.
122 $ git commit --amend -m Shakespeare # Zmień ten opis.
123 $ git filter-branch --env-filter 'export GIT_AUTHOR_DATE="Fri 13 Feb 2009 15:31:30 -0800" GIT_AUTHOR_NAME="Alice" GIT_AUTHOR_EMAIL="alice@example.com" GIT_COMMITTER_DATE="Fri, 13 Feb 2009 15:31:30 -0800" GIT_COMMITTER_NAME="Bob" GIT_COMMITTER_EMAIL="bob@example.com"' # Zmanipuluj znacznik czasowy i nazwę autora.
124 $ find .git/objects -type f
125 $ find .git/objects -type f
127 Powinieneś znaleźć +.git/objects/49/993fe130c4b3bf24857a15d7969c396b7bc187+, co odpowiada sumie kontrolnej SHA1 jego zawartości:
129 "commit 158" NUL "tree 05b217bb859794d08bb9e4f7f04cbda4b207fbe9" LF "author Alice <alice@example.com> 1234567890 -0800" LF "committer Bob <bob@example.com> 1234567890 -0800" LF LF "Shakespeare" LF
131 Jak i w poprzednich przykładach możesz użyć 'zpipe' albo 'cat-file' by to sprawdzić.
133 To jest pierwszy 'commit', przez to nie posiada matczynych 'commits'. Następujące 'commits' będą zawsze zawierać przynajmniej jedną linikę identyfikującą rodzica.
135 === Nie do odróżnienia od magii ===
137 Tajemnice Gita wydają się być proste. Wygląda to jak połączenie kilku skryptów, troszeczkę kodu C i w przeciągu kilku godzin jesteśmy gotowi: zmiksowanie podstawowych operacji na systemie danych, obliczenia SHA1, przyprawienie plikami blokującymy i synchronizacją dla stabilności. W sumie można by tak opisać najwcześniejsze wersje Gita. Tym niemniej, abstrahując od udanych trików pakujących, by oszczędnie odnosić się z pamięcią i udanych trików indeksujących by zaoszczędzić czas, wiemy jak Git sprawnie przemienia system danych w objektową bazę danych, co jest optymalne dla kontroli wersji.
139 Przyjmijmy, gdy jakikolwiek plik w obiektowej bazie danych ulegnie zniszczeniu poprzez błąd nośnika, to jego SHA1 nie będzie zgadzać się z jego zawartością, co od razu wskaże nam problem. Poprzez tworzenie kluczy SHA1 z kluczy SHA1 innych objektów, osiągniemy integralność danych na wszystkich poziomach. 'Commits' są elementarne, to znaczy, 'commit' nie potrafi zapamiętać jedynie części zmian: hash SHA1 'commit' możemy obliczyć i zapamiętać dopiero po tym gdy zapamiętane zostały wszystkie obiekty 'tree', 'blob' i rodziców 'commit'. Obiektowa baza dynch jest odporna na nieoczekiwane przerwy, jak na przykład przerwanie dostawy prądu.
141 Możemy przetrwać nawet podstępnego przeciwnika. Wyobraź sobie, ktoś ma zamiar zmienić treść jakiegoś pliku, która leży w jakiejś starszej wersji projektu. By sprawić pozory, że baza danych wygląda nienaruszona musiałby zmienić sumy kontrolne SHA1 korespondujących obiektów, ponieważ plik zawiera teraz zmieniony sznur znaków. To znaczy również, że musiałby zmienić każdy hash obiektu 'tree', które ją referują oraz w wyniku tego wszystkie sumy kontrolne 'commits' zawierające obiekty 'tree' dodatkowo do pochodnych tych 'commits'. Oznacza to również, że suma kontrolna oficjalnego HEAD różni się od sumy kontrolnej HEAD manipulowanego repozytorium. Wystarczy teraz prześledzić ścieżkę różniących się hashy SHA1, odnaleźć okaleczony plik, jak i 'commit' w którym po raz pierwszy wystąpił.
143 Krótko mówiąc, dopuki reprezentujące ostatni commit 20 bajtów są zabezpieczone, sfałszowanie repozytorium Gita nie jest możliwe.
145 A co ze sławnymi możliwościami Gita?
146 'Branching'? 'Merging'? 'Tags'? To szczegół. Aktualny HEAD przetrzymywany jest w pliku +.git/HEAD+, która posiada hash SHA1 ostatniego 'commit'. Hash SHA1 zostaje aktualizowany podczas wykonania 'commit', tak samo jak i przy wielu innych poleceniach. 'branches' to prawie to samo, są plikami zapamiętanymi w +.git/refs/heads+. 'Tags' również, znajdziemy je w +.git/refs/tags+, są one jednak aktualizowane poprzez serię innych poleceń.