forked from tkrajina/uvod-u-git
-
Notifications
You must be signed in to change notification settings - Fork 0
/
git-log.tex
executable file
·345 lines (220 loc) · 18 KB
/
git-log.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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
\chapter*{Povijest}
\addcontentsline{toc}{chapter}{Povijest}
Već smo se poznali s naredbom \verb+git log+ s kojom se može vidjeti povijest \emph{commit}ova grane u kojoj se trenutno nalazimo, no ona nije dovoljna za proučavanje povijesti projekta.
Posebno s git projektima, čija povijest zna biti dosta kompleksna (puno grana, \emph{merge}anja, i sl.).
Sigurno će vam se desiti da želite vidjeti koje su se izmjene desile između predzadnjeg i pred-predzanjeg \emph{commit}a ili da vratite neku neku datoteku u stanje kakvo je bilo prije mjesec dana ili da proučite tko je zadnji napravio izmjenu na trinaestoj liniji nekog programa ili tko je prvi uveo funkciju koja se naziva \verb+get_image_x_size+ u projektu\dots
Čak i ako vam se neki od navedenih scenarija čine malo vjerojatnim, vjerujte mi -- trebati će vam.
U ovom poglavlju ćemo proći neke često korištene naredbe za proučavanje povijesti projekta.
\section*{\emph{Diff}}
\addcontentsline{toc}{section}{\emph{Diff}}
S \verb+git diff+ smo se već sreli.
Ukoliko ju pozovemo bez ikakvog dodatnog argumenta, ona će nam ispisati razlike između radne verzije repozitorija (tj. stanja projekta kakvo je trenutno na našem računalu) i zadnje verzije u repozitoriju.
Drugi način korištenja naredbe je da provjeravamo razlike između dva \emph{commit}a ili dvije grane (podsjetimo se da su grane u biti samo reference na zadnje \emph{commit}ove).
Na primjer:
\gitoutputcommand{git diff master tesna-grana}
\dots{}će nam ispisati razliku između te dvije grane.
Treba paziti na redosljed jer je ovdje bitan.
Ukoliko isprobamo s:
\gitoutputcommand{git diff tesna-grana master}
\dots{}dobiti ćemo suprotan ispis.
Ako smo u \verb+testna-grana+ jedan redak dodali -- u jednom slučaju će \verb+diff+ ispisati da smo ga \textbf{dodali}, a u drugom da smo ga \textbf{oduzeli}.
Želimo li provjeriti koje su izmjene dogodile između predzadnjeg i pred-predzadnjeg \emph{commit}a:
\gitoutputcommand{git diff HEAD\textasciitilde{}2 HEAD\textasciitilde{}1}
\dots{}ili između pred-predzadnjeg i sadašnjeg:
\gitoutputcommand{git diff HEAD\textasciitilde{}2}
\dots{}ili izmjene između \verb+974ef0ad8351ba7b4d402b8ae3942c96d667e199+ i \verb+testna-grana+:
\gitoutputcommand{git diff 974ef testna-grana}
\section*{\emph{Log}}
\addcontentsline{toc}{section}{\emph{Log}}
Standardno s naredbom \verb+git log <naziv_grane>+ dobijemo kratak ispis povijesti te grane.
Sad kad znamo da je grana u biti samo referenca na zadnji \emph{commit}, znamo i da bi bilo preciznije kazati da je ispravna sintaksa \verb+git log <referenca_na_commit>+.
Za git nije previše bitno jeste li mu dali naziv grane ili referencu na \emph{commit} -- naziv grane je ionako samo alias za zadnji \emph{commit} u toj grani.
Ukoliko mu damo neki proizvoljan \emph{commit}, on će jednostavno krenuti "unazad" po grafu i dati vam povijest koju na taj način nađe.
Dakle, ako želimo povijest trenutne grane, ali bez zadnjih pet unosa -- treba nam referenca na peti \emph{commit} unazad:
\gitoutputcommand{git log HEAD\textasciitilde{}5}
Ili, ako želimo povijest grane \verb+testna-grana+ bez zadnjih 10 unosa:
\gitoutputcommand{git log testna-grana\textasciitilde{}10}
Želimo li povijest sa \emph{samo} nekoliko zadnjih unosa, koristimo \\\verb+git log -<broj_ispisa>+ sintaksu.
Na primjer, ako nam treba samo 10 rezultata iz povijesti:
\gitoutputcommand{git log -10 testna-grana}
\dots{}ili, ako to želimo za trenutnu granu, jednostavno:
\gitoutputcommand{git log -10}
\subsection*{\emph{Whatchanged}}
\addcontentsline{toc}{subsection}{\emph{Whatchanged}}
Naredba \verb+git whatchanged+ je vrlo slična \verb+git log+, jedino što uz svaki \emph{commit} ispisuje i popis svih datoteka koje su se tada promijenile:
\input{git_output/git_whatchanged}
\subsection*{Pretraživanje povijesti}
\addcontentsline{toc}{subsection}{Pretraživanje povijesti}
Vrlo često će vam se dogoditi da tražite neki \emph{commit} iz povijesti po nekom kriteriju.
U nastavku ćemo obraditi dva najčešća načina pretraživanja.
Prvi je kad pretražujemo prema tekstu komentara uz \emph{commit}ove, tada se koristi \verb+git log --grep=<regularni_izraz>+.
Na primjer, tražimo li sve \emph{commit}ove koji \textbf{u \emph{commit} komentarima sadrže riječ} \verb+graph+:
\gitoutputcommand{git log --grep=graph}
Drugi česti scenarij je odgovor na pitanje "Kad se \textbf{u kodu} prvi put spomenuo neki string?". Tada se koristi \verb+git log -S<string>+.
Dakle, ne u komentaru \emph{commit}a nego \textbf{u sadržaju datoteka}.
Recimo da tražimo tko je prvi napisao funkciju \verb+get_image_x_size+:
\gitoutputcommand{git log -Sget\_image\_x\_size}
Treba li pretraživati za string s razmacima:
\gitoutputcommand{git log -S"get image width"}
Zapamtite, ovo će vam samo naći \emph{commit}ove.
Kad ih nađemo, htjeti ćemo vjerojatno pogledati koje su točno bile izmjene.
Ako nam pretraživanje nađe da je \emph{commit} \\ \verb+76cf802d23834bc74473370ca81993c5b07c2e35+, detalji izmjena koje su se njime dogodile su:
\gitoutputcommand{git diff 76cf8 76cf8\textasciitilde{}1}
Podsjetimo se \verb+76cf8+ je kratica za \emph{commit}, a \verb+76cf8~1+ je referenca na njegovog \textbf{prethodnika} (zbog \verb+~1+).
\section*{\emph{Blame}}
\addcontentsline{toc}{section}{\emph{Blame}}
S \verb+git blame <datoteka>+ ćemo dobiti ispis neke datoteke s detaljima o tome \textbf{tko}, \textbf{kad} i u \textbf{kojem \emph{commit}u} je napravio (ili izmijenio) pojedinu liniju u toj datoteci\footnote{Nažalost, linije su preduge da bi se ovdje ispravno vidjelo.}:
\input{git_output/git_blame}
Nađete li liniju koja započinje znakom \verb+^+ -- to znači da je ta linija bila tu i u prvoj verziji datoteke.
U svakom projektu datoteke mijenjaju imena.
Tako da kod koji je pisan u jednoj datoteci ponekad završi u datoteci nekog trećeg imena.
Ukoliko želimo znati i u kojoj su datoteci linije naše trenutke datoteke prvi put pojavile, to se može s:
\gitoutputcommand{git blame -C <datoteka>}
\section*{Digresija o premještanju datoteka}
\addcontentsline{toc}{section}{Digresija o premještanju datoteka}
Vezano uz \verb+git blame -C+, napraviti ćemo jednu malu digresiju.
Pretpostavimo da Mujo i Haso zajedno pišu zbirku pjesama.
Svaku pjesmu spreme u posebnu datoteku, a da bi lakše pratili tko je napisao koju pjesmu -- koriste git.
Mujo je napisao pjesmu \verb+proljeće.txt+.
Nakon toga je Haso tu datoteku preimenovao u \verb+proljeće-u-mom-gradu.txt+.
U trenutku kad je Haso \emph{commit}ao svoju izmjenu -- sustav za verzioniranje je te izmjene vidio kao da je \verb+proljeće.txt+ obrisana, a nova pjesma \verb+proljeće-u-mom-gradu.txt+ dodana.
Problem je što je novu datoteku \verb+proljeće-u-mom-gradu.txt+ dodao Haso.
Ispada kao da je on \textbf{napisao} tu pjesmu, iako je samo preimenovao datoteku.
Zbog ovakve situacije neki sustavi za verzioniranje (kao subversion ili mercurial) zahtijevaju od korisnika da \textbf{preko njihovih naredbi} radi preimenovanje datoteke ili njeno micanje u neki drugi repozitorij\footnote{Na primjer, u mercuriralu morate upisati \texttt{hg mv početna\_datoteka krajnja\_datoteka}, a ni slučajno \texttt{mv početna\_datoteka krajnja\_datoteka} ili \texttt{move početna\_datoteka krajnja\_datoteka}}.
Tako oni znaju da je Haso \textbf{preimenovao} postojeću datoteku, ali sadržaj je napisao Mujo.
Ukoliko to ne učinite preko njegovih naredbi -- nego datoteke preimenujete standardnim putem (naredbe \verb+mv+ ili \verb+move+) -- sustav za verzioniranje neće imati informaciju o tome da je preimenovana.
To je problem, jer ljudi to često zaborave, a kad se to desi -- gubi se povijest \textbf{sadržaja} datoteke.
Kad to zaboravite -- problem je i kod \emph{merge}a.
Ako je u jednoj grani datoteka \textbf{preimenovana} a u drugoj samo \textbf{izmijenjena} -- sustav neće znati da se u stvari radi o istoj datoteci koja je promijenjena, on će to percipirati kao dvije različite datoteke.
Rezultat \emph{merge}a može biti neočekivan.
Spomenuto nije problem i u gitu.
Git ima ugrađenu heuristiku koja prati je li datoteka u nekom \emph{commit}u preimenovana.
Ukoliko u novom \emph{commit}u on nađe da je jedna datoteka \textbf{obrisana} -- proučiti će koje datoteke su u istom \emph{commit}u \textbf{nastale}.
Ako se sadržaj poklapa u dovoljno velikom postotku linija, git će sam zaključiti da se radi o preimenovanju datoteke -- a ne o datoteci koja se prvi put pojavljuje u repozitoriju.
Isto vrijedi ako datoteku nismo preimenovali nego premjestili (i, eventualno, preimenovali) na novu lokaciju u repozitorij.
To je princip na osnovu kojeg \verb+git blame -C+ "zna" u kojoj datoteci se neka linija prvi put pojavila.
Zato bez straha možemo koristiti naredbe \verb+mv+, \verb+move+ ili manipulirati ih preko nekog IDE-a.
Nemojte da vas zbuni to što git \textbf{ima} naredbu:
\gitoutputcommand{git mv <stara\_datoteka> <nova\_datoteka>}
\dots{}za preimenovanje/micanje datoteka.
Ta naredba ne radi ništa posebno \textbf{u gitu}, ona je samo \emph{alias} za standardnu naredbu operativnog sustava (\verb+mv+ ili \verb+move+).
Zato git ispravno \emph{merge}a čak i ako u jednoj grani datoteku preimenujemo i izmijenimo, a u drugoj samo izmijenimo -- git će sam zaključiti da se radi o istoj datoteci različitog imena.
\section*{Preuzimanje datoteke iz povijesti}
\addcontentsline{toc}{section}{Preuzimanje datoteke iz povijesti}
Svima nam se ponekad desilo da smo izmijenili datoteku i kasnije shvatili da ta izmjena ne valja.
Na primjer zaključili smo da je verzija od prije 5 \emph{commit}ova je bila bolja nego ova trenutno.
Kako da ju vratimo iz povijesti i \emph{commit}amo u novo stanje projekta?
Znamo već da s \verb+git checkout <naziv_grane> -- <datoteka1> <datoteka2>...+ možemo lokalno dobiti stanje datoteke iz te grane.
Odnedavno znamo i da naziv grane nije ništa drugo nego referenca na njen zadnji \emph{commit}.
Ako umjesto grane, tamo stavimo referencu na neki drugi \emph{commit} dobiti ćemo stanje datoteke iz tog trenutka u povijesti.
Dakle, \verb+git checkout+ se, osim za prebacivanje s grane na granu, može koristiti i za preuzimanje neke datoteke iz prošlosti:
\gitoutputcommand{git checkout HEAD\textasciitilde{5} -- pjesma.txt}
To će nam u trenutni direktorij vratiti točno stanje datoteke \verb+pjesma.txt+ od prije $5$ \emph{commit}ova.
I sad smo slobodni tu datoteku opet \emph{commit}ati ili ju promijeniti i \emph{commit}ati.
Treba li nam da datoteka kakva je bila u predzadnjem \emph{commit}u grane \verb+test+?
\gitoutputcommand{git checkout test\textasciitilde{}1 -- pjesma.txt}
Isto tako i s bilo kojom drugom referencom na neki \emph{commit} iz povijesti.
\section*{"Teleportiranje" u povijest}
\addcontentsline{toc}{section}{"Teleportiranje" u povijest}
Isto razmatranje kao u prethodnom odjeljku vrijedi i za vraćanje stanja cijelog repozitorija u neki trenutak iz prošlosti.
Na primjer, otkrili smo bug, ne znamo gdje točno u kodu, ali znamo da se taj bug nije prije manifestirao.
Međutim, ne znamo točno \textbf{kada} je bug zepočeo.
Bilo bi zgodno kad bismo cijeli projekt mogli teleportirati na neko stanje kakvo je bilo prije $n$ \emph{commit}ova.
Ako se tamo bug i dalje manifestira -- vratiti ćemo se još malo dalje u povijest.
Ako se tamo manifestirao -- prebaciti ćemo se u malo bližu povijest.
I tako -- mic po mic po povijesti, sve dok ne nađemo trenutak (\emph{commit}) u povijesti repozitorija u kojem je bug stvoren.
Ništa lakše:
\gitoutputcommand{git checkout HEAD\textasciitilde{}10}
\dots{}i za čas imamo stanje kakvo je bilo prije $10$ \emph{commit}ova. Sad tu možemo s \verb+git branch+ kreirati novu granu ili vratiti se na najnovije stanje s \verb+git checkout HEAD+. Ili možemo provjeriti je li bug i dalje tu. Ako jest, idemo probati s\dots
\gitoutputcommand{git checkout HEAD\textasciitilde{}20}
\dots{}ako buga sad nemamo, znamo da se pojavio negdje između \textbf{dvadesetog} i \textbf{desetog} zadnjeg \emph{commit}a.
I tako, mic po mic, sve dok ne nađemo onaj trenutak u povijesti kad se greška pojavila.
\section*{\emph{Reset}}
\addcontentsline{toc}{section}{\emph{Reset}}
Uzmimo ovakvu hipotetsku situaciju: Radimo na nekoj grani, a u jednom trenutku stanje je:
\input{graphs/linearni_model_za_reset}
I sad zaključimo kako tu nešto ne valja -- svi ovi \emph{commit}ovi od $f$ pa na dalje su krenuli krivim smjerom.
Htjeli bi se nekako vratiti na:
\input{graphs/linearni_model_za_reset_2}
\dots{}i od tamo nastaviti cijeli posao, ali ovaj put drukčije.
E sad, lako je "teleportirati se" s \verb+git checkout ...+, to to ne mijenja stanje grafa -- $f$, $g$, $h$ i $i$ bi i dalje ostali u grafu.
Mi bi ovdje htjeli baš obrisati dio grafa, odnosno zadnjih nekoliko \emph{commit}ova.
Naravno da se i to može, a naredba je \verb+git reset --hard <referenca_na_commit>+.
Na primjer, ako se želimo vratiti na predzadnje stanje i potpuno obrisati zadnji \emph{commit}:
\gitoutputcommand{git reset --hard HEAD\textasciitilde{}1}
Želimo li se vratiti na \verb+974ef0ad8351ba7b4d402b8ae3942c96d667e199+ i maknuti sve \emph{commit}ove koji su se desili nakon njega.
Isto:
\gitoutputcommand{git reset --hard 974ef0a}
Postoji i \verb+git reset --soft <referenca>+.
S tom varijantom se isto brišu \emph{commit}ovi iz povijesti repozitorija, ali stanje datoteka u našem radnom direktoriju ostaje kakvo jest.
Cijela poanta naredbe \verb+git reset+ je da \textbf{pomiče} \verb+HEAD+.
Kao što znamo, \verb+HEAD+ je referenca na zadnji \emph{commit} u trenutnoj grani.
Za razliku od nje, kad se "vratimo u povijest" s \verb+git checkout HEAD~2+ -- mi \textbf{nismo} dirali \verb+HEAD+.
Git i dalje zna koji mu je \emph{commit} \verb+HEAD+ i, posljedično, i dalje zna kako izgleda cijela grana.
U našem primjeru od početka, mi želimo maknuti \textcolor{red}{crvene} \emph{commit}ove.
Ako prikažemo graf prema načinu kako git čuva podatke -- svaki \emph{commit} ima referencu na prethodnika, dakle strelice su suprotne od smjera nastajanja:
\input{graphs/linearni_model_za_reset_HEAD_1}
Uopće se ne trebamo truditi \textbf{brisati} čvorove/\emph{commit}ove $f$, $g$, $h$ i $i$.
Dovoljno je reći "Pomakni \verb+HEAD+ tako da pokazuje na $e$.
I to je upravo ono što git čini s \verb+git reset+:
\input{graphs/linearni_model_za_reset_HEAD_2}
Iako su ovi čvorovi ovdje prikazani na grafu, git do njih više ne može doći.
Da bi rekonstruirao svoj graf, on uvijek kreće od \verb+HEAD+, dakle $f$, $g$, $h$ i $i$ su izgubljeni.
\section*{\emph{Revert}}
\addcontentsline{toc}{section}{\emph{Revert}}
Svaki \emph{commit} mijenja povijest projekta, no on to čini tako da \textbf{dodaje na kraju grane}.
Treba imati na umu jednu stvar -- \verb+git reset+ \textbf{mijenja povijest projekta retrogradno}.
Ne dodaje samo čvor na kraj grane, on mijenja postojeću granu tako da obriše nekoliko njegovih zadnjih čvorova.
To može biti problem, a posebno u situacijama kad radite s drugim ljudima, o tome više u sljedećem poglavlju.
To se može riješiti s \verb+git revert+. Uzmimo, na primjer, ovakvu situaciju:
\input{graphs/linearni_model_za_revert_1}
Došli smo do zaključka da je \emph{commit} $f$ neispravan i treba vratiti na stanje kakvo je bilo u $e$.
Znamo već da bi \verb+git reset+ jednostavno maknuo $f$ s grafa, ali recimo da to ne želimo.
Tada se može napraviti sljedeće \verb+git revert <commit>+.
Na primjer, ako želimo \emph{revert}ati samo zadnji \emph{commit}:
\gitoutputcommand{git revert HEAD}
Rezultat će biti da je dosadašnji graf ostao potpuno isti, no \textbf{dodan je novi commit} koji miče sve izmjene koje su uvedene u $f$:
\input{graphs/linearni_model_za_revert_1_revertano}
Dakle, stanje u $g$ će biti isto kao i u $e$.
To se može i sa \emph{commit}om koji nije zadnji u grani:
\input{graphs/linearni_model_za_revert_2}
Ako je SHA1 referenca \emph{commit}a $f$ \verb+402b8ae3942c96d667e199974ef0ad8351ba7b4d+, onda ćemo s:
\gitoutputcommand{git revert 402b8ae39}
\dots{}dobiti:
\input{graphs/linearni_model_za_revert_2_revertano}
\dots{}gdje \emph{commit} $j$ ima stanje kao u $i$, ali bez izmjena koje su uvedene u $f$.
Naravno, ako malo o tome razmislite, složiti ćete se da sve to radi idealno samo ako $g$, $h$ i $i$ \textbf{nisu dirali isti kod kao i $f$}.
\verb+git revert+ uredno radi ako revertamo \emph{commit}ove koji su bliži kraju grane.
Teško će, ako ikako, \emph{revert}ati stvari koje smo mijenjali prije dvije godine u kodu koji je, nakon toga, puno puta bio mijenjan.
Ukoliko to i pokušamo, git će javiti grešku i tražiti nas da mu sami damo do znanja kako bi \emph{revert} trebao izgledati.
\section*{Gitk}
\addcontentsline{toc}{section}{Gitk}
U standardnoj instalaciji gita dolazi i pomoćni programčić koji nam grafički prikazuje povijest trenutne grane.
Pokreće se s:
\gitoutputcommand{gitk}
\dots{}a izgleda ovako:
\gitgraphics{images/gitk.png}
Sučelje ima pet osnovnih dijelova.
Ovdje opisani redom s lijeva na desno od gore prema dolje:
\begin{itemize}
\item grafički prikaz povijesti grane i \emph{commit}ova iz drugih grana koji su imali veze s tom granom (bilo zbog grananja, \emph{merge}anja ili \emph{cherry-pick} \emph{merge}),
\item popis ljudi koji su \emph{commit}ali,
\item datum i vrijeme,
\item pregled svih izmjena,
\item pregled datoteka koje su izmijenjene.
\end{itemize}
Kad odaberete \emph{commit} dobiti ćete sve izmjene i datoteke koje su sudjelovale u izmjenama.
Kad odaberete datoteku, \verb+gitk+ će u dijelu sa izmjenama "skočiti" na izmjene koje su se dogodile na toj datoteci.
Gitk vam može i pregledati povijest samo do nekog \emph{commit}a u povijesti:
\gitoutputcommand{gitk HEAD\textasciitilde{}10}
\dots{}ili:
\gitoutputcommand{gitk 974ef0ad8351ba7b4d402b8ae3942c96d667e199}
\dots{}ili određene grane:
\gitoutputcommand{gitk testna-grana}
Gitk prikazuje povijest samo trenutne grane, odnosno samo onih \emph{commit}ova koji su na neki način sudjelovali u njoj.
Želimo li povijest svih grana -- treba ga pokrenuti su:
\gitoutputcommand{gitk --all}
Kao i \verb+git gui+, tako i \verb+gitk+ na prvi pogled možda baš i nije jako intuitivan.
No, brz je i praktičan za rad kad se naučite na njegovo sučelje.
%\section*{}
%\addcontentsline{toc}{section}{}