forked from tkrajina/uvod-u-git
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ispod-haube.tex
executable file
·278 lines (191 loc) · 16.2 KB
/
ispod-haube.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
\chapter*{Ispod haube}
\addcontentsline{toc}{chapter}{Ispod haube}
\section*{Kako biste vi\dots}
\addcontentsline{toc}{section}{Kako biste vi\dots}
Da kojim slučajem danas morate dizajnirati i implementirati sustav za verzioniranje koda, kako biste to napravili?
Kako biste čuvali povijest svake datoteke?
Prije nego što ste počeli koristiti takve sustave, vjerojatno ste radili sljedeće: kad bi zaključili da ste došli do nekog važnog stanja u projektu, kopirali bi cijeli projekt u direktorij naziva \verb+projekt_backup+ ili \verb+projekt_2012_04_05+ ili neko drugo slično ime.
Rezultat je da ste imali gomilu sličnih "backup" direktorija.
Svaki direktorij predstavlja neko stanje projekta (dakle to je \emph{commit}).
I to je nekakvo verzioniranje koda, ali s puno nedostataka.
Na primjer, nemate komentare uz \emph{commit}ove, ali to bi se moglo srediti tako da u svaki direktorij spremite datoteku naziva \verb+komentar.txt+.
Nemate niti graf, odnosno redoslijed nastajanja \emph{commit}ova.
I to bi se moglo riješit tako da u svakom direktoriju u nekoj posebnoj datoteci, npr. \verb+parents+ nabrojite nazive direktorija koji su "roditelji" trenutnom direktoriju.
Sve je to prilično neefikasno što se tiče diskovnog prostora.
Imate li u repozitoriju jednu datoteku veličine 100 kilobajta koju \textbf{nikad} ne mijenjate, njena kopija će opet zauzimati 100 kilobajta u svakoj kopiji projekta.
Čak i ako nije nikad mijenjana.
Zato bi možda bilo bolje da umjesto \textbf{kopije direktorija} za \emph{commit} napravimo novi u kojeg ćemo staviti \textbf{samo one datoteke koje su izmijenjene}.
Zahtijevalo bi malo više posla jer morate točno znati koje su datoteke izmijenjene, ali i to se može riješiti.
Mogli bi napraviti neku jednostavnu \emph{shell} skriptu koja bi to napravila za nas.
S time bi se problem diskovnog prostora drastično smanjio.
Rezultat bi mogli još malo poboljšati tako da datoteke kompresiramo.
Još jedna varijanta bi bila da ne čuvate izmijenjene datoteke, nego samo izmijenjene linije koda.
Tako bi vaše datoteke, umjesto cijelog sadržaja, imale nešto tipa "Peta linija izmijenjena iz 'def suma\_brojeva()' u 'def zbroj\_brojeva()'".
To su takozvane "delte".
Još jedna varijanta bi bila da ne radite kopije direktorija, nego sve snimate u jednom tako da za svaku datoteku čuvate originalnu verziju i nakon toga (u istoj datoteci) dodajete delte.
Onda će vam trebati nekakav pomoćni programčić kako iz povijesti izvući zadnju verziju bilo koje datoteke, jer on mora izračunati sve delte od početne verzije.
Sve te varijante imaju jedan suptilni, ali neugodan, problem.
Problem konzistencije.
Vratimo se na trenutak na ideju s direktorijima sa izmijenjenim datotekama (deltama).
Dakle, svaki novi direktorij sadrži samo datoteke koje su izmijenjene u odnosu na prethodni.
Ako svaki direktorij sadrži samo izmijenjene datoteke, onda prvi direktorij mora sadržavati \textbf{sve} datoteke.
Pretpostavite da imate jednu datoteku koja nije nikad izmijenjena od prve do zadnje verzije.
Ona će se nalaziti samo u prvom (originalnom) direktoriju.
Što ako neki zlonamjernik upadne u vaš sustav i izmijeni takvu datoteku?
Razmislimo malo, on je upravo izmijenio ne samo početno stanje takve datoteke nego je \textbf{promijenio cijelu njenu povijest}!
Kad bi vi svoj sustav pitali "daj mi zadnju verziju projekta", on bi protrčao kroz sva stanja projekta i dao bi vam "zlonamjernikovu" varijantu.
Jeste li sigurni da bi primijetili podvaljenu datoteku?
Možda biste kad bi se vaš projekt sastojao od dvije-tri datoteke, ali što ako se radi o stotinama ili tisućama?
Rješenje je da je vaš sustav nekako dizajniran tako da sam prepoznaje takve izmijenjene datoteke.
Odnosno, da je tako dizajniran da, ako bilo tko promijeni nešto u povijesti -- sam sustav prepozna da nešto s njime ne valja.
To se može na sljedeći način: neka jedinstveni identifikator svakog \emph{commit}a bude neki podatak koji je \textbf{izračunat} iz trenutnog sadržaja svih datoteka u projektu.
Takav jedinstveni identifikator će se nalaziti u grafu projekta, i sljedeći \emph{commit}ovi će znati da im je on prethodnik.
Ukoliko bilo tko promijeni sadržaj nekog \emph{commit}a, onda on više neće odgovarati tom identifikatoru.
Promijeni li i identifikator, onda graf više neće biti konzistentan -- sljedeći \emph{commit} će sadržavati identifikator koji više ne postoji.
Zlonamjernik bi trebao promijeniti sve \emph{commit}ove do zadnjeg.
U biti, trebao bi promijeniti previše stvari da bi mu cijeli poduhvat mogao proći nezapaženo.
Još ako je naš sustav distribuiran (dakle i drugi korisnici imaju povijest cijelog projekta) onda mu je još teže -- jer tu radnju mora ponoviti na računalima svih ljudi koji imaju kopije.
S distribuiranim sustavima, nitko sa sigurnošću ne zna tko sve ima kopije.
Svaka kopija repozitorija sadrži povijest projekta.
Ukoliko netko zlonamjerno manipulira poviješću projekta na jednom repozitoriju -- to će se primijetiti kad se taj repozitorij "sinkronizira" s ostalima.
Nakon ovog početnog razmatranja, idemo pogledati koje od tih ideja su programeri gita uzeli kad su krenuli dizajnirati svoj sustav.
Krenimo s problemom konzistentnosti.
\section*{SHA1}
\addcontentsline{toc}{section}{SHA1}
Znate li malo matematike čuli ste za jednosmerne funkcije.
Ako i niste, nije bitno.
To su funkcije koje je lako izračunati, ali je izuzetno teško iz rezultata zaključiti kakav je mogao biti početni argument.
Takve su, na primjer, \emph{hash} funkcije, a jedna od njih je SHA1.
SHA1 kao argument uzima string i iz njega izračunava drugi string duljine $40$ znakova.
Primjer takvog stringa je \verb+974ef0ad8351ba7b4d402b8ae3942c96d667e199+.
Izgleda poznato?
SHA1 ima sljedeća svojstva:
\begin{itemize}
\item \emph{Nije} jedinstvena. Dakle, sigurno postoje različiti ulazni stringovi koji daju isti rezultat, no \textbf{praktički ih je nemoguće naći}\footnote{Ovdje treba napomenuti kako SHA1 nije \emph{potpuno} siguran. Ukoliko se nađe algoritam s kojime je moguće naći različite stringove kojima je rezultat funkcije SHA1 isti -- tada cijela sigurnost potencijalno pada u vodu. Netko može podvaliti drukčiji string u povijest za isti SHA1. Postoje istraživanja koja naznačuju da se to može i zato je moguće da će git u budućnosti preći na SHA-256.}.
\item Kad dobijete rezultat funkcije (npr. \verb+974ef0ad8351ba7b4d402b8ae3942c96d667e199+) iz njega je \textbf{praktički nemoguće izračunati string iz kojeg je nastala}.
\end{itemize}
Takvih $40-$znamenkastih stringova ćete vidjeti cijelu gomilu u \verb+.git+ direktoriju.
Git nije ništa drugo nego graf SHA1 stringova, od kojih svaki jedinstveno identificira neko stanje projekta \textbf{i izračunati su iz tog stanja}.
Osim SHA1 identifikatora git uz svaki \emph{commit} čuva i neke metapodatke kao, na primjer:
\begin{itemize}
\item Datum i vrijeme kad je nastao.
\item Komentar
\item SHA1 \emph{commit}a koji mu je prethodio
\item SHA1 \emph{commit}a iz kojeg su preuzete izmjene za \emph{merge} (\textbf{ako} je taj \emph{commit} rezultat \emph{merge}a).
\item \dots
\end{itemize}
Budući da je svaki \emph{commit} SHA1 sadržaja projekta u nekom trenutku, kad bi netko htio neopaženo promijeniti povijest projekta, morao bi promijeniti i njegov SHA1 identifikator.
Onda mora promijeniti i SHA1 njegovog sljedbenika, i sljedbenika njegovog sljedbenika, i\dots
Sve da je to i napravio na jednom repozitoriju -- tu radnju mora ponoviti na svim ostalim \textbf{distribuiranim} repozitorijima istog projekta.
\section*{Grane}
\addcontentsline{toc}{section}{Grane}
Razmislimo o još jednom detalju, uz poznati graf:
\input{graphs/primjer_s_imenovanim_granama_i_spajanjima}
Takve grafove matematičari zovu "usmjereni grafovi" jer veze između čvorova imaju svoj smjer.
To jest, veze između čvorova nisu obične relacije (crte) nego \textbf{usmjerene} relacije, odnosno strelice u jednom smjeru: $\vec{ab}$, $\vec{bc}$, itd.
Znamo već da svaki čvor tog grafa predstavlja stanje nekog projekta, a svaka strelica neku izmjenu u novo stanje.
Sad kad znamo ovo malo pozadine oko toga kako git interno pamti podatke, idemo korak dalje.
Prethodni graf ćemo ovaj put prikazati malo drukčije:
\input{graphs/primjer_s_imenovanim_granama_i_spajanjima_suprotne_strelice}
Sve strelice su ovdje usmjerene suprotno nego što su bile u grafovima kakve smo do sada imali.
Radi se o tome da git upravo tako i "pamti" veze između čvorova.
Naime, čvor \emph h ima referencu na \emph g, \emph g ima reference na \emph f i na \emph q, itd.
Uočite da nam uopće nije potrebno znati da se grana \verb+novi-feature+ sastoji od \emph x, \emph y, \emph z, \emph q i \emph w.
Dovoljan nam je $w$.
Iz njega možemo, prateći reference "unazad" (suprotno od redoslijeda njihovog nastajanja) doći sve do mjesta gdje je grana nastala.
Tako, na osnovu samo jednog čvora (\emph{commit}a) možemo saznati cijelu povijest neke grane.
Dakle, dovoljno nam je imati reference na zadnje \emph{commit}ove svih grana u repozitoriju, da bi mogli saznati povijest cijelog projekta.
Zato \textbf{gitu grane i nisu ništa drugo nego reference na njihove zadnje \emph{commit}ove}.
\section*{Reference}
\addcontentsline{toc}{section}{Reference}
SHA1 stringovi su računalu praktični, no ljudima su nezgodni za pamćenje.
Zbog toga git ima par korisnih sitnica vezanih uz reference.
Pogledajmo SHA1 string \verb+974ef0ad8351ba7b4d402b8ae3942c96d667e199+.
Takav string je teško namjerno ponoviti. I vjerojatno je mala vjerojatnost da postoji neki drugi string koji započinje s \verb+974ef0a+ ili \verb+974e+.
Zbog toga se u gitu može slobodno koristiti i samo prvih nekoliko znakova SHA1 referenci umjesto cijelog $40$-znamenkastog stringa.
Dakle,
\gitoutputcommand{git cherry-pick 974ef0ad8351ba7b4d402b8ae3942c96d667e199}
\dots{}je isto što i:
\gitoutputcommand{git cherry-pick 974ef0}
Dogodi li se, kojim slučajem, da postoje dvije reference koje počinju s \verb+974ef0a+, git će vam javiti grešku da ne zna na koju od njih se naredba odnosi.
Tada samo dodajte jedan ili dva znaka više (\verb+974ef0ad+ ili \verb+974ef0ad8+), sve dok nova skraćenica reference ne postane jedinstvena.
\tocSection{HEAD}
\verb+HEAD+ je referenca na trenutni \emph{commit}.
Obično je to zadnji $commit$ u grani u kojoj se nalazimo, ali može biti i bilo koji drugi.
Ukoliko pokazuje na $commit$ koji \textbf{nije zadnji u grani} -- onda se kaže da je repozitorij u \verb+detached HEAD+ stanju.
Ukoliko nam treba referenca na predzadnji \emph{commit}, mogli bi pogledati \verb+git log+ i tamo naći njegov SHA1.
Postoji i lakši način: \verb+HEAD~1+.
Pred-predzadnji commit je \verb+HEAD~2+, i tako dalje\dots
Na primjer, ako se prebacimo na stanje u predzadnjem $commit$u, to možemo napraviti s:
\gitoutputcommand{git checkout HEAD\textasciitilde{}1}
\dots{}i za git smo sada u \verb+detached HEAD+ stanju.
Na spisku grana -- dobiti ćemo:
\input{git_output/git_branch_detached_state}
U ovakvoj situaciji smijemo čak i $commit$ati, ali te izmjene neće biti dio ni jedne grane.
Ukoliko ne pripazimo -- lako ćemo te izmjene izgubiti.
Trebamo li ih sačuvati, jednostavno kreiramo novu granu s \verb+git branch+ i repozitorij više neće biti u \verb+detached HEAD+ stanju, a izmjene koje smo napraviti su sačuvane u novoj grani.
Želimo li pogledati koje su se izmjene dogodile između sadašnjeg stanja grana i stanja od prije $10$ \emph{commit}ova, to će ići ovako:
\gitoutputcommand{git diff HEAD\textasciitilde{}10 HEAD}
Notacija kojom dodajemo \verb+~1+, \verb+~2+, \dots vrijedi i za reference na grane i na SHA1 identifikatore $commit$ova.
Imate li granu \verb+test+ -- već znamo da je to referenca samo na njen zadnji \emph{commit}, a referenca na predzadnji je \verb+test~1+.
Analogno, \verb+974ef0a~11+ je referenca na $11$-ti \emph{commit} prije \verb+974ef0ad8351ba7b4d402b8ae3942c96d667e199+.
\section*{.git direktorij}
\addcontentsline{toc}{section}{.git direktorij}
Pogledajmo na trenutak \verb+.git+ direktorij.
Vjerojatno ste to već učinili, i vjerojatno ste otkrili da je njegov sadržaj otprilike ovakav:
\input{git_output/dot_git_folder}
Ukratko ćemo ovdje opisati neke važne dijelove: \verb+.git/config+, \verb+.git/objects+, \verb+.git/refs+, \verb+HEAD+ i \verb+.git/hooks+
\subsection*{.git/config}
\addcontentsline{toc}{subsection}{.git/config}
U datoteci \verb+.git/config+ se spremaju sve lokalne postavke.
To jest, tu su sve one postavke koje smo snimili s \verb+git config <naziv> <vrijednost>+, i to su konfiguracije koje se odnose samo za tekući repozitorij.
Za razliku od toga, \textbf{globalne} postavke (one koje se snime s \verb+git config --global+) se snimaju u korisnikov direktorij u datoteci \verb+~/.gitconfig+.
\subsection*{.git/objects}
\addcontentsline{toc}{subsection}{.git/objects}
Sadržaj direktorija \verb+.git/objects+ izgleda ovako nekako:
\input{git_output/git_objects}
To je direktorij koji sadrži sve verzije svih datoteka i svih \emph{commit}ova našeg projekta.
Dakle, to git koristi umjesto onih višestrukih direktorija koje smo spomenuli u našem hipotetskom sustavu za verzioniranje na početku ovog poglavlja.
Apsolutno sve se ovdje čuva.
Uočite, na primjer, datoteku \verb+d7/3aabba2b969b2ff2cbff18f1dc4e254d2a2af+.
Ona se odnosi na git objekt s referencom \verb+d73aabba2b969b2ff2cbff18f1dc4e254d2a2af+.
Sadržaj tog objekta se može pogledati koristeći \verb+git cat-file <referenca>+.
Na primjer, tip objekta se može pogledati s:
\input{git_output/git_cat_file_type}
\dots{}što znači da je to objekt tipa \emph{commit}.
Zamijenite li \verb+-t+ s \verb+-p+ dobiti ćete točan sadržaj te datoteke.
Postoje četiri vrste objekata: \emph{commit}, \emph{tag}, \emph{tree} i \emph{blob}.
\emph{Commit} i \emph{tag} sadrže metapodatke vezane uz ono što im sam naziv kaže.
\emph{Blob} sadrži binarni sadržaj neke datoteke, dok je \emph{tree} popis datoteka.
Poigrate li se malo s \verb+git cat-file -p <referenca>+ otkriti ćete da \emph{commit} objekti sadrže:
\begin{itemize}
\item Referencu na prethodni \emph{commit},
\item Referencu na \emph{commit} grane koju smo \emph{merge}ali (ako je dotični \emph{commit} rezultat \emph{merge}a),
\item Datum i vrijeme kad je nastao,
\item Autora,
\item Referencu na jedan objekt tipa \emph{tree} koji sadrži popis svih datoteka koje su sudjelovale u tom \emph{commit}u.
\end{itemize}
Drugim riječima, tu imamo sve potrebno da bi znali od čega se \emph{commit} sastojao i da bi znali gdje je njegovo mjesto na grafu povijesti projekta.
Stvar se može zakomplicirati kad broj objekata poraste.
Tada se u jedan \emph{blob} objekt može zapakirati više datoteka ili samo dijelova datoteka posebnim algoritmom za pakiranje (\emph{pack}).
\subsection*{.git/refs}
\addcontentsline{toc}{subsection}{.git/refs}
Bacimo pogled kako izgleda direktorij \verb+.git/refs+:
\input{git_output/find_dot_git_refs}
Pogledajmo i sadržaj tih datoteka:
\input{git_output/cat_git_refs_tag}
Svaka od tih datoteka sarži referencu na jedan od objekata iz \verb+.git/objects+.
Poigrajte se s \verb+git cat-file+ i otkriti ćete da su to uvijek \emph{commit} objekti.
Zaključak se sam nameće -- u \verb+.git/refs+ se nalaze reference na sve grane, tagove i grane udaljenih repozitorija koji se nalaze u \verb+.git/objects+.
To je implementacija one priče da je gitu grana samo referenca na njen zadnji \emph{commit}.
\subsection*{HEAD}
\addcontentsline{toc}{subsection}{HEAD}
Datoteka \verb+.git/HEAD+ u stvari nije obična datoteka nego samo simbolički link na neku od datoteka unutar \verb+git/refs+.
I to na onu od tih datoteka koja sadrži referencu na granu u kojoj se trenutno nalazimo.
Na primjer, u trenutku dok pišem ove retke \verb+HEAD+ je kod mene \verb+refs/heads/master+, što znači da se moj repozitorij nalazi na \verb+master+ grani.
\subsection*{.git/hooks}
\addcontentsline{toc}{subsection}{.git/hooks}
Ovaj direktorij sadrži \emph{shell} skripte koje želimo izvršiti u trenutku kad se dogode neki važni događaji na našem repozitoriju.
Svaki git repozitorij već sadrži primjere takvih skripti s ekstenzijom \verb+.sample+.
Ukoliko taj sample maknete, one će se početi izvršavati na tim događajima (\emph{event}ima).
Na primjer, želite li da se prije svakog \emph{commit}a izvrše \emph{unit} testovi i pošalje mejl s rezultatima, napraviti ćete skriptu \verb+pre-commit+ koja to radi.