Git
Chapters ▾ 2nd Edition

7.7 Orodja Git - Demistifikacija ponastavitve

Demistifikacija ponastavitve

Preden se premaknemo na bolj specializirana orodja, se pogovorimo o Gitovih ukazih reset in checkout. Ta ukaza sta ena izmed najbolj zmedenih delov Gita, ko se z njima prvič srečate. Naredita toliko stvari, da se zdi obupno poskusiti jih dejansko razumeti in pravilno uporabiti. Za to priporočamo preprosto metaforo.

Tri drevesa

Lažji način razmišljanja o reset in checkout je skozi mentalni okvir Gita kot vsebinskega upravitelja treh različnih dreves. S pojmom »drevo« tukaj dejansko mislimo na »zbirko datotek«, ne nujno na podatkovno strukturo. Obstajajo nekateri primeri, kjer indeks ne deluje točno kot drevo, vendar je za naše namene za zdaj lažje razmišljati o njem na ta način.

Git v svojem normalnem delovanju kot sistem upravlja in manipulira s tremi drevesi:

Drevo Vloga

HEAD

Zadnji posnetek potrditve, naslednja nadrejena

Indeks

Predlagan posnetek naslednje potrditve

Delovni direktorij

Peskovnik

HEAD

HEAD je kazalec na trenutno vejno referenco, ki pa je sama po sebi kazalec na zadnjo potrditev, narejeno na tej veji. To pomeni, da bo HEAD nadrejeni naslednje ustvarjene potrditve. Na splošno je najpreprosteje razmišljati o HEAD-u kot o posnetku vaše zadnje potrditve na tej veji.

Dejansko je precej enostavno videti, kako je ta posnetek videti. Tukaj je primer pridobivanja dejanskega seznama direktorijev in kontrolnih vsot SHA-1 za vsako datoteko v posnetku HEAD:

$ git cat-file -p HEAD
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
author Scott Chacon  1301511835 -0700
committer Scott Chacon  1301511835 -0700

initial commit

$ git ls-tree -r HEAD
100644 blob a906cb2a4a904a152...   README
100644 blob 8f94139338f9404f2...   Rakefile
040000 tree 99f1a6d12cb4b6f19...   lib

Gitova ukaza cat-file in ls-tree sta ukaza »napeljave«, ki se uporabljata za stvari nižje ravni in nista resnično uporabljena v vsakodnevnem delu, vendar nam pa pomagata videti, kaj se dogaja tukaj.

Indeks

Indeks je vaša naslednja predlagana potrditev. To zasnovo smo označevali tudi kot Gitovo »področje priprave«, saj si ga Git ogleda, ko zaženete ukaz git commit.

Git napolni ta indeks s seznamom vsebine datotek, ki so bile nazadnje izvlečene v vaš delovni direktorij, in kako so bile videti, ko so bile prvotno izvlečene. Nato nekatere od teh datotek nadomestite z novimi različicami in git commit to pretvori v drevo za novo potrditev.

$ git ls-files -s
100644 a906cb2a4a904a152e80877d4088654daad0c859 0	README
100644 8f94139338f9404f26296befa88755fc2598c289 0	Rakefile
100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0	lib/simplegit.rb

Tukaj ponovno uporabljamo git ls-files, ki je bolj ukaz v zakulisju in vam prikaže, kako je trenutno videti vaš indeks.

Indeks tehnično gledano ni drevesna struktura — dejansko je implementiran kot sploščen manifest — vendar za naše namene je dovolj blizu.

Delovni direktorij

Nazadnje imate vaš delovni direktorij (ki se pogosto imenuje tudi »delovno drevo«). Drugi dve drevesi shranjujeta svojo vsebino na učinkovit, vendar nepraktičen način v direktoriju .git. Delovni direktorij jih razpakira v dejanske datoteke, kar vam omogoča lažje urejanje. Delovni direktorij si lahko predstavljate kot peskovnik, kjer lahko preizkusite spremembe, preden jih potrdite v področje priprave (indeks) in nato v zgodovino.

$ tree
.
├── README
├── Rakefile
└── lib
    └── simplegit.rb

1 directory, 3 files

Potek dela

Običajni način dela z Gitom je zabeležiti posnetke vašega projekta v zaporedno boljša stanja z manipulacijo teh treh dreves.

Gitov običajni potek dela
Slika 137. Gitov običajni potek dela

Predstavljajmo si ta proces: recimo, da vstopite v nov direktorij, ki vsebuje eno samo datoteko. To imenujemo v1 datoteke in jo bomo označili z modro barvo. Sedaj zaženemo git init, kar bo ustvarilo repozitorij Git z referenco glave (HEAD), ki kaže na nerojeno vejo master.

Novo inicializirani repozitorij Git z datoteko izven področja priprave v delovnem direktoriju
Slika 138. Novo inicializirani repozitorij Git z datoteko izven področja priprave v delovnem direktoriju

V tem trenutku ima vsebino le drevo delovnega direktorija.

Zdaj želimo potrditi to datoteko, zato uporabimo git add, da vsebino v delovnem direktoriju kopiramo v indeks.

`git add` kopira datoteko v indeks
Slika 139. git add kopira datoteko v indeks

Nato zaženemo git commit, ki sprejme vsebino indeksa in jo shrani kot trajni posnetek, ustvari objekt potrditve, ki kaže na ta posnetek, in posodobi master, da kaže na to potrditev.

Korak `git commit`
Slika 140. Korak git commit

Če zaženemo git status, ne bomo videli sprememb, saj so vsa tri drevesa enaka.

Sedaj želimo spremeniti to datoteko in jo potrditi. Gremo skozi enak postopek; najprej spremenimo datoteko v svojem delovnem direktoriju. Imenujmo jo v2 datoteke in jo označimo z rdečo barvo.

Repozitorij Git s spremenjeno datoteko v delovnem direktoriju
Slika 141. Repozitorij Git s spremenjeno datoteko v delovnem direktoriju

Če zdaj zaženemo git status, bomo videli datoteko v rdeči barvi z napisom »Changes not staged for commit«, ker se ta vnos razlikuje med indeksom in delovnim direktorijem. Nato zaženemo git add, da jo damo v področje priprave (v naš indeks).

Dodajanje spremembe v področje priprave (indeks)
Slika 142. Dodajanje spremembe v področje priprave (indeks)

Če zaženemo git status, bomo trenutno videli datoteko v zeleni barvi pod »Changes to be committed«, ker se indeks in HEAD razlikujeta — to pomeni, da se naša naslednja predlagana potrditev razlikuje od naše zadnje potrditve. Na koncu zaženemo git commit, da zaključimo potrjevanje.

Korak `git commit` s spremenjeno datoteko
Slika 143. Korak git commit s spremenjeno datoteko

Zdaj nam git status ne bo dal nobenega izpisa, saj so vsa tri drevesa spet enaka.

Preklapljanje vej in kloniranje gresta skozi podoben proces. Ko preklopite na vejo, se spremeni HEAD, da kaže na novo referenco veje, vaš indeks se napolni s posnetkom te potrditve, nato pa se vsebina indeksa kopira v vaš delovni direktorij.

Vloga ponastavitve

Ukaz reset ima več smisla, če ga gledamo v tem kontekstu.

Za namene teh primerov recimo, da smo spet spremenili datoteko file.txt in jo tretjič potrdili. Tako je sedaj videti naša zgodovina:

Repozitorij Git s tremi potrditvami
Slika 144. Repozitorij Git s tremi potrditvami

Sedaj si poglejmo, kaj reset točno naredi, ko ga pokličemo. Neposredno spreminja ta tri drevesa na preprost in predvidljiv način. Izvede do tri osnovne operacije.

1. korak: Premikanje HEAD

Prva stvar, ki jo reset naredi, je premik tistega, na kar kaže HEAD. To ni enako spreminjanju samega HEAD (to naredi ukaz checkout); reset premakne vejo, na katero kaže HEAD. To pomeni, da če je HEAD nastavljen na vejo master (torej se trenutno nahajate na veji master), bo izvajanje ukaza git reset 9e5e6a4 najprej spremenilo master, da kaže na 9e5e6a4.

Mehka ponastavitev
Slika 145. Mehka ponastavitev

Ne glede na to, katero obliko reset z določeno potrditvijo uporabite, je to prva stvar, ki jo bo vedno poskusil narediti. Z uporabo reset --soft se bo postopek tam preprosto ustavil.

Sedaj si vzemite trenutek in si oglejte ta diagram ter ugotovite, kaj se je zgodilo: v bistvu se je preklical zadnji ukaz git commit. Ko zaženete git commit, Git ustvari novo potrditev in nanjo premakne vejo, na katero kaže HEAD. Ko se z ukazom reset vrnete na HEAD~ (na nadrejeno od HEAD), premaknete vejo nazaj na prejšnje mesto, pri tem pa ne spremenite indeksa ali delovnega direktorija. Sedaj lahko posodobite indeks in znova zaženete git commit, da dosežete to, kar bi naredil git commit --amend (glejte Spreminjanje zadnje potrditve).

2. korak: Posodobitev indeksa (--mixed)

Opazite lahko, da boste z zagonom ukaza git status sedaj videli v zeleni barvi razliko med indeksom in tem, kam kaže novi HEAD.

Naslednja stvar, ki jo bo reset naredil, je posodobitev indeksa z vsebino posnetka, na katerega sedaj kaže HEAD.

Mešana ponastavitev
Slika 146. Mešana ponastavitev

Če določite možnost --mixed, se bo postopek ukaza reset ustavil na tem koraku. To je tudi privzeta možnost, zato če ne navedete nobene možnosti (v tem primeru le git reset HEAD~), se bo ukaz končal tukaj.

Sedaj si vzemite še eno sekundo, da si ogledate ta diagram in ugotovite, kaj se je zgodilo: še vedno ste preklicali zadnji ukaz commit, vendar ste tudi premaknili vse spremembe iz indeksa v delovni direktorij. Vrnili ste se na stanje pred zagonom ukazov git add in git commit.

3. korak: Posodobitev delovnega direktorija (--hard)

Tretja stvar, ki jo bo reset naredil, je, da bo delovni direktorij videti tako kot indeks. Če uporabite možnost --hard, se bo nadaljeval na tej stopnji.

Trda ponastavitev
Slika 147. Trda ponastavitev

Razmislimo o tem, kaj se je pravkar zgodilo. Preklicali ste zadnjo potrditev, ukaza git add in git commit ter vso delo, ki ste ga opravili v svojem delovnem direktoriju.

Pomembno je opozoriti, da je ta zastavica (--hard) edini način, da je ukaz reset nevaren, in eden izmed zelo redkih primerov, ko Git dejansko uniči podatke. Katerakoli drugačna uporaba ukaza reset se lahko precej enostavno razveljavi, ne pa možnost --hard, saj silovito prepiše datoteke v delovnem direktoriju. V tem posebnem primeru imamo še vedno različico v3 naše datoteke v potrditvi v naši bazi podatkov Git in jo lahko dobimo nazaj s pogledom v naš reflog, vendar bi jo Git še vedno prepisal, če je ne bi potrdili, in postala bi nepopravljivo izgubljena.

Povzetek

Ukaz reset prepisuje ta tri drevesa v določenem vrstnem redu in se ustavi, ko mu to sporočite:

  1. Premakne vejo, na katero kaže HEAD (ustavi se tu, če je izbrana možnost --soft).

  2. Posodobi indeks, da ustreza trenutnemu stanju HEAD-a (ustavi se tu, razen če je izbrana možnost --hard).

  3. Posodobi delovni direktorij tako, da ustreza indeksu.

Ponastavitev s potjo

To zajema obnašanje ukaza reset v njegovi osnovni obliki, vendar pa mu lahko podate tudi pot, na kateri naj deluje. Če navedete pot, bo reset preskočil korak 1 in omejil preostanek svojih dejanj na določeno datoteko ali niz datotek. To dejansko ima smisel — HEAD je le kazalec in ne morete kazati na del ene potrditve in del druge. Vendar pa se lahko indeks in delovno okolje delno posodobita, zato reset nadaljuje s korakoma 2 in 3.

Zato predpostavimo, da zaženemo git reset file.txt. Ta oblika (ker niste navedli SHA-1 ali veje in niste navedli --soft ali --hard) je kratka oblika git reset --mixed HEAD file.txt, ki bo:

  1. Premaknila vejo, na katero kaže HEAD (preskočeno).

  2. Naredi, da indeks izgleda kot HEAD (ustavi se tukaj).

Tako preprosto kopira file.txt iz HEAD-a v indeks.

Mešana ponastavitev s potjo
Slika 148. Mešana ponastavitev s potjo

To ima praktični učinek dajanja datoteke izven področja priprave. Če si ogledamo diagram za ta ukaz in razmislimo o tem, kaj naredi git add, sta si natančno nasprotna.

Dodajanje datoteke v področje priprave (indeks)
Slika 149. Dodajanje datoteke v področje priprave (indeks)

To je razlog, zakaj nam izhod ukaza git status predlaga, naj za preklic dajanja datoteke v področje priprave uporabimo ta ukaz (za več informacij glejte razdelek Povrnitev datoteke iz področja priprave).

Enako lahko dosežemo tudi tako, da Gitu ne dovolimo, da smo mislili »povleci podatke iz HEAD«, tako da navedemo specifično potrditev, od koder želimo povleči datoteko. Tako bi lahko zagnali nekaj podobnega git reset eb43bf file.txt.

Mehka ponastavitev s potjo na določeno potrditev
Slika 150. Mehka ponastavitev s potjo na določeno potrditev

To dejansko naredi enako, kot če bi v delovnem direktoriju vsebino datoteke pripeljali nazaj na v1, na njej uporabili git add in jo nato spet vrnili na v3 (ne da bi dejansko šli skozi vse te korake). Če zdaj poženemo git commit, bo zabeležil spremembo, ki vrne datoteko nazaj na v1, čeprav je dejansko nikoli nismo imeli v svojem delovnem direktoriju.

Zanimivo je tudi omeniti, da kot pri git add, lahko tudi ukaz reset sprejme možnost --patch, da premakne vsebine iz področja priprave po kosih. Tako lahko izbirate katero vsebino želite pustiti izven področja priprave, ali jo obrniti.

Stiskanje skupaj

Poglejmo, kako lahko s tem novo pridobljenim orodjem naredimo nekaj zanimivega — stisnemo potrdive skupaj (angl. squashing).

Recimo, da imate vrsto potrditev s sporočili, kot so »ups.«, »WIP« in »forgot this file«. Uporabite lahko reset, da jih hitro in enostavno združite v eno samo potrditev, kar vas bo naredilo pametnejšega. V razdelku Stiskanje potrditev skupaj je prikazan še en način, kako to storiti, vendar bo v tem primeru lažje uporabiti reset.

Recimo, da imate projekt, kjer prva potrditev vsebuje eno datoteko, druga pa je dodala novo datoteko in spremenila prvo, tretja potrditev pa je ponovno spremenila prvo datoteko. Druga potrditev je bilo delo v napredku in ga želite stisniti skupaj.

Repozitorij Git
Slika 151. Repozitorij Git

Za premikanje glavne veje na starejšo potrditev (zadnjo potrditev, ki jo želite obdržati), lahko zaženete ukaz git reset --soft HEAD~2:

Premik HEAD-a pri mehki ponastavitvi
Slika 152. Premik HEAD-a pri mehki ponastavitvi

In nato enostavno ponovno poženete git commit:

Repozitorij Git s stisnjeno potrditvijo
Slika 153. Repozitorij Git s stisnjeno potrditvijo

Sedaj lahko vidite, da vaša dosegljiva zgodovina, zgodovina, ki bi jo potisnili, zdaj kaže, kot da imate eno potrditev s file-a.txt v1, nato drugo, ki je spremenila file-a.txt na v3 in dodala file-b.txt. Potrditve z različico datoteke v2 ni več v zgodovini.

Izvlečenje

Končno se morda sprašujete, kakšna je razlika med ukazoma checkout in reset. Podobno kot reset, checkout manipulira s tremi drevesi, in je nekoliko drugačen glede na to, ali mu podate pot do datoteke ali ne.

Brez poti

Izvedba git checkout [branch] je precej podobna izvedbi git reset --hard [branch], saj posodobi vsa tri drevesa tako, da so videti kot [branch], vendar obstajata dve pomembni razliki.

Prvič, glede na reset --hard je checkout varnejši glede delovnega direktorija; preveri, da ne briše datotek, ki so že spremenjene. Pravzaprav je malce pametnejši od tega — poskuša narediti trivialno združevanje v delovnem direktoriju, zato bodo vse datoteke, ki jih niste spremenili, posodobljene. reset --hard pa preprosto nadomesti vse preko celega nabora brez preverjanja.

Druga pomembna razlika je, kako checkout posodobi HEAD. Kjer reset premakne vejo, na katero kaže HEAD, checkout premakne sam HEAD, da kaže na drugo vejo.

Na primer, recimo, da imamo veji master in develop, ki kažeta na različni potrditvi, in smo trenutno na veji develop (tako da kaže HEAD nanjo). Če izvedemo git reset master, se bo develop sama premaknila na isto potrditev kot master. Če pa izvedemo git checkout master, se develop ne premakne, premakne se sam HEAD. HEAD bo sedaj kazal na master.

V obeh primerih premikamo HEAD, da kaže na potrditev A, vendar je način, kako to storimo, zelo drugačen. reset premakne vejo, na katero kaže HEAD, checkout pa premakne sam HEAD.

`git checkout` in `git reset`
Slika 154. git checkout in git reset

S potmi

Drugi način izvedbe ukaza checkout je s potjo do datoteke, kar, tako kot pri reset, ne premakne HEAD-a. Gre za isto kot git reset [branch] file, saj posodobi indeks z datoteko v tej potrditvi in prepiše datoteko v delovnem direktoriju. Gre za popolnoma enako reč, kot je git reset --hard [branch] file (če bi lahko to izvedli z reset) — ni varno za delovni direktorij in ne premakne HEAD-a.

Podobno kot pri git reset in git add, tudi checkout sprejme možnost --patch, s katero lahko izberete, katere vsebine datoteke želite selektivno povrniti po koščkih.

Povzetek

Upamo, da zdaj razumete ukaz reset in se z njim počutite bolj domače, vendar ste pa verjetno še vedno malo zmedeni glede tega, kako se točno razlikuje od checkout in si ne morete zapomniti vseh pravil za različne klice.

Tu je plonk listek za ukaze, ki vplivajo na drevesa. Stolpec HEAD prebere REF, če ta ukaz premakne referenco (vejo), na katero kaže HEAD, in HEAD, če premakne sam HEAD. Posebno pozornost posvetite stolpcu »Varno za delovni direktorij?« — če piše NE, premislite še enkrat, preden zaženete ta ukaz.

HEAD Indeks Delovni direktorij Varno za delovni direktorij?

Nivo potrditve

reset --soft [commit]

REF

NE

NE

DA

reset [commit]

REF

DA

NE

DA

reset --hard [commit]

REF

DA

DA

NE

checkout <commit>

HEAD

DA

DA

DA

Nivo datoteke

reset [commit] <paths>

NE

DA

NE

DA

checkout [commit] <paths>

NE

DA

DA

NE

scroll-to-top