Git 🌙
Chapters ▾ 2nd Edition

10.7 Notranjost Gita - Vzdrževanje in obnovitev podatkov

Vzdrževanje in obnovitev podatkov

Včasih se lahko pojavi potreba po čiščenju repozitorija — ga narediti bolj kompaktnega, očistiti uvoženi repozitorij ali pa obnoviti izgubljeno delo. Ta razdelek bo pokril nekatere od teh scenarijev.

Vzdrževanje

Včasih Git samodejno zažene ukaz, imenovan »auto gc«. Večino časa ta ukaz ne naredi ničesar. Vendar pa, če je preveč šibkih objektov (objektov, ki niso v paketni datoteki) ali preveč paketnih datotek, Git zažene pravi ukaz git gc. »gc« pomeni sproščanje pomnilnika (angl. garbage collect), ukaz pa naredi nekaj stvari: zbere vse šibke objekte in jih postavi v paketne datoteke, konsolidira paketne datoteke v eno veliko paketno datoteko in odstrani objekte, do katerih ni mogoče dostopati iz nobene potrditve in so stari nekaj mesecev.

Ukaz auto gc lahko zaženete ročno na naslednji način:

$ git gc --auto

Ponovno, to običajno ne naredi ničesar. Morate imeti okoli 7000 šibkih objektov ali več kot 50 paketnih datotek za zagon pravega ukaza gc. Te omejitve lahko spremenite z nastavitvami konfiguracije gc.auto in gc.autopacklimit.

Druga stvar, ki jo bo gc naredil, je združiti vaše reference v eno datoteko. Predpostavimo, da vaš repozitorij vsebuje naslednje veje in oznake:

$ find .git/refs -type f
.git/refs/heads/experiment
.git/refs/heads/master
.git/refs/tags/v1.0
.git/refs/tags/v1.1

Če zaženete git gc, teh datotek več ne boste imeli v mapi refs. Git jih bo zaradi učinkovitosti premaknil v datoteko imenovano .git/packed-refs, ki bo videti takole:

$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled
cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment
ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master
cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0
9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1
^1a410efbd13591db07496601ebc7a059dd55cfe9

Če posodobite referenco, Git tega datotečnega sistema ne spreminja, ampak namesto tega ustvari novo datoteko v mapi refs/heads. Za ustrezni SHA-1 glede na dano referenco, Git najprej preveri, če obstaja ta referenca v mapi refs, nato pa, kot rezervno možnost, preveri datoteko packed-refs. Če torej ne morete najti reference v mapi refs, se verjetno nahaja v vaši datoteki packed-refs.

Pomembno je opaziti zadnjo vrstico datoteke, ki se začne s ^. To pomeni, da je oznaka nad to vrstico opisna oznaka in da je ta vrstica potrditev, na katero ta oznaka kaže.

Obnovitev podatkov

V neki točki vašega potovanja z Gitom se vam lahko zgodi, da izgubite potrditev. Navadno se to zgodi, ko prisilno izbrišete vejo, ki ima delo na sebi, in se izkaže, da ste vejo vseeno želeli obdržati, ali pa trdo ponastavite vejo, s čimer opustite potrditve, od katerih ste nekaj želeli. Kaj storiti, če se to zgodi in kako lahko dobite nazaj svoje potrditve?

Tu je primer, ki trdo ponastavi vejo master v vašem preskusnem repozitoriju na starejšo potrditev in nato obnovi izgubljene potrditve. Najprej poglejmo, kje je vaš repozitorij v tem trenutku:

$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b Modify repo.rb a bit
484a59275031909e19aadb7c92262719cfcdf19a Create repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit

Sedaj premaknite vejo master nazaj na sredinsko potrditev:

$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef Third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit

Dejansko ste izgubili zgornji dve potrditvi — nimate veje, iz katere bi bili dosegljivi. Morali boste najti zadnjo SHA-1 potrditve in dodati vejo, ki kaže nanjo. Trik se skriva v iskanju tega najnovejšega SHA-1 — saj si ga niste zapomnili, kajne?

Pogosto najhitrejši način je uporaba orodja, imenovanega git reflog. Kot delo sodelavcem Git tiho beleži, kje je vaš HEAD vsakič, ko ga spremenite. Vsakič, ko potrdite, ali spremenite veje, se posodobi reflog. Reflog se posodablja tudi z ukazom git update-ref, kar je še en razlog, da ga uporabite namesto da bi vrednosti SHA-1 zapisovali neposredno v vaše datoteke ref, kot smo opisali v Reference Git. Kjer ste bili kadarkoli, si lahko ogledate tako, da poženete git reflog:

$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: Modify repo.rb a bit
484a592 HEAD@{2}: commit: Create repo.rb

Tukaj lahko vidimo dve potrditvi, ki smo ju imeli izvlečeni, vendar tu ni veliko informacij. Če želite videti iste informacije na veliko bolj uporaben način, lahko zaženete git log -g, kar vam bo dalo običajen dnevniški izpis za vaš reflog.

$ git log -g
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Reflog: HEAD@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:22:37 2009 -0700

		Third commit

commit ab1afef80fac8e34258ff41fc1b867c702daa24b
Reflog: HEAD@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:15:24 2009 -0700

       Modify repo.rb a bit

Videti je, da je spodnja potrditev tista, ki ste jo izgubili, zato jo lahko obnovite tako, da ustvarite novo vejo na tej potrditvi. Na primer, lahko začnete novo vejo z imenom recover-branch na tej potrditvi (ab1afef):

$ git branch recover-branch ab1afef
$ git log --pretty=oneline recover-branch
ab1afef80fac8e34258ff41fc1b867c702daa24b Modify repo.rb a bit
484a59275031909e19aadb7c92262719cfcdf19a Create repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit

Odlično — zdaj imate vejo z imenom recover-branch, ki je tam, kjer je bila vaša veja master, tako da sta prvi dve potrditvi ponovno dosegljivi. Naslednjič, recimo, da vaša izguba iz nekega razloga ni v reflogu — to lahko simulirate tako, da odstranite recover-branch in izbrišete reflog. Zdaj prvi dve potrditvi nista dosegljivi prek nobene veje:

$ git branch -D recover-branch
$ rm -Rf .git/logs/

Ker so podatki o reflogu shranjeni v mapi .git/logs/, dejansko nimate nobenega refloga. Kako lahko sedaj obnovite to potrditev? En način je uporaba orodja git fsck, ki preveri vašo podatkovno zbirko za integriteto. Če ga zaženete z možnostjo --full, vam pokaže vse objekte, na katere ni usmerjen noben drug objekt:

$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (18/18), done.
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293

V tem primeru lahko vidite vašo manjkajočo potrditev za nizom »dangling commit«. Enako jo lahko obnovite tako, da dodate vejo, ki kaže na ta SHA-1.

Odstranjevanje objektov

Git ima veliko dobrih lastnosti, vendar pa lahko ena lastnost povzroča težave, in sicer ta, da git clone prenese celotno zgodovino projekta, vključno z vsako različico vsake datoteke. To je v redu, če gre za celotno izvorno kodo, saj je Git visoko optimiziran za učinkovito stiskanje teh podatkov. Če pa je nekdo v zgodovini projekta dodal eno ogromno datoteko, bo vsak klon za vse čase prisiljen prenesti to veliko datoteko, tudi če je bila iz projekta odstranjena že v naslednji potrditvi. Ker je dostopna iz zgodovine, bo vedno tam.

To lahko predstavlja velik problem, ko pretvarjate repozitorije Subversion ali Perforce v Git. Ker v teh sistemih ne prenesete celotne zgodovine, ta vrsta dodatka ne povzroča veliko posledic. Če ste uvozili iz drugega sistema ali našli drug način, da je vaš repozitorij veliko večji, kot bi moral biti, vam predstavljamo, kako najti in odstraniti velike objekte.

Opozorilo: ta tehnika lahko uniči vašo zgodovino potrditev. Prepiše vsak objekt potrditve od najstarejšega drevesa naprej, da odstrani sklice na veliko datoteko. Če to storite takoj po uvozu, preden je kdo začel delati na tej potrditvi, ste v redu — v nasprotnem primeru morate obvestiti vse sodelavce, da morajo svoje delo prenesti na vaše nove potrditve.

Da bi to demonstrirali, boste v testnem repozitoriju dodali veliko datoteko, jo v naslednji potrditvi odstranili, jo našli in trajno odstranili iz repozitorija. Najprej dodajte velik objekt v svojo zgodovino:

$ curl -L https://www.kernel.org/pub/software/scm/git/git-2.1.0.tar.gz > git.tgz
$ git add git.tgz
$ git commit -m 'Add git tarball'
[master 7b30847] Add git tarball
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 git.tgz

Ups — niste želeli dodati velike arhivske datoteke tar v svoj projekt. Bolje, da se je znebite:

$ git rm git.tgz
rm 'git.tgz'
$ git commit -m 'Oops - remove large tarball'
[master dadf725] Oops - remove large tarball
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 git.tgz

Zdaj pa počistite svojo bazo podatkov z ukazom gc in preverite, koliko prostora uporabljate:

$ git gc
Counting objects: 17, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (13/13), done.
Writing objects: 100% (17/17), done.
Total 17 (delta 1), reused 10 (delta 0)

Za hitro preverjanje, koliko prostora uporabljate, lahko zaženete ukaz count-objects:

$ git count-objects -v
count: 7
size: 32
in-pack: 17
packs: 1
size-pack: 4868
prune-packable: 0
garbage: 0
size-garbage: 0

Vnos size-pack predstavlja velikost vaših paketnih datotek v kilobajtih, torej uporabljate skoraj 5 MB. Pred zadnjo potrditvijo ste uporabljali manj kot 2 K — očitno odstranjevanje datoteke iz prejšnje potrditve ni odstranilo datoteke iz zgodovine. Vsakič, ko nekdo klonira ta repozitorij, bo moral klonirati vseh 5 MB, da bo dobil ta mali projekt, ker ste slučajno dodali veliko datoteko. Znebimo se ga.

Najprej ga morate najti. V tem primeru že veste, katera datoteka je. Toda predpostavimo, da ne veste; kako bi ugotovili, katera datoteka ali datoteke zavzemajo toliko prostora? Če zaženete git gc, so vsi objekti v paketni datoteki; velike objekte lahko identificirate tako, da zaženete še eno orodje za pomožne dejavnosti z imenom git verify-pack in razvrstite po tretjem polju v izhodu, ki predstavlja velikost datoteke. Lahko ga preusmerite tudi skozi ukaz tail, ker vas zanimajo le zadnje največje datoteke:

$ git verify-pack -v .git/objects/pack/pack-29…69.idx \
  | sort -k 3 -n \
  | tail -3
dadf7258d699da2c8d89b09ef6670edb7d5f91b4 commit 229 159 12
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob   22044 5792 4977696
82c99a3e86bb1267b236a4b6eff7868d97489af1 blob   4975916 4976258 1438

Velik objekt se nahaja na dnu: 5 MB. Da ugotovite, katera datoteka je to, boste uporabili ukaz rev-list, ki ste ga uporabili v Uveljavljanje določene oblike sporočila potrditve. Če ukazu rev-list podate --objects, izpiše vse vrednosti SHA-1 potrditev in tudi SHA-1 blobov s potmi datotek, ki so z njimi povezane. To lahko uporabite, da poiščete ime svojega bloba:

$ git rev-list --objects --all | grep 82c99a3
82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz

Zdaj morate odstraniti to datoteko iz vseh dreves v svoji preteklosti. Enostavno lahko vidite, katere potrditve so spremenile to datoteko:

$ git log --oneline --branches -- git.tgz
dadf725 Oops - remove large tarball
7b30847 Add git tarball

Prepisati morate vse potrditve navzdol, katere izvirajo iz 7b30847, da bi popolnoma odstranili to datoteko iz vaše zgodovine Gita. To storite tako, da uporabite filter-branch, kakor ste storili v Prepisovanje zgodovine:

$ git filter-branch --index-filter \
  'git rm --ignore-unmatch --cached git.tgz' -- 7b30847^..
Rewrite 7b30847d080183a1ab7d18fb202473b3096e9f34 (1/2)rm 'git.tgz'
Rewrite dadf7258d699da2c8d89b09ef6670edb7d5f91b4 (2/2)
Ref 'refs/heads/master' was rewritten

Možnost --index-filter je podobna možnosti --tree-filter uporabljeni v Prepisovanje zgodovine, razlika je v tem, da namesto podajanja ukaza, ki spreminja datoteke izvlečene na disk, vsakič spreminjate svoje področje priprave ali indeks.

Namesto da bi z nečim, kot je rm file, odstranili določeno datoteko, jo morate odstraniti z git rm --cached — morate jo odstraniti iz indeksa, ne z diska. Razlog za to je hitrost — ker Git ne mora preveriti vsake revizije na disku preden zažene vaš filter, lahko postopek poteka veliko, veliko hitreje. Enako nalogo lahko opravite s --tree-filter, če želite. Možnost --ignore-unmatch pove ukazu git rm, naj ne vrne napake, če vzorca, ki ga poskušate odstraniti, ni tam. Nazadnje zahtevate, da filter-branch ponovno napiše vašo zgodovino le od potrditve 7b30847 naprej, ker veste, da je tam ta problem nastal. Sicer se bo začelo od začetka in bo po nepotrebnem trajalo dlje.

Vaša zgodovina ne vsebuje več sklicevanja na to datoteko. Vendar pa ga vaš reflog in nov nabor referenc, ki jih je Git dodal, ko ste izvedli filter-branch pod .git/refs/original, še vedno vsebujeta, zato jih morate odstraniti in nato ponovno zapakirati bazo podatkov. Pred ponovnim zapakiranjem morate odstraniti karkoli, kar kaže na te stare potrditve:

$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc
Counting objects: 15, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (11/11), done.
Writing objects: 100% (15/15), done.
Total 15 (delta 1), reused 12 (delta 0)

Poglejmo, koliko prostora ste prihranili.

$ git count-objects -v
count: 11
size: 4904
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0

Velikost zapakiranega repozitorija je zmanjšana na 8 K, kar je veliko boljše kot 5 MB. Vidite lahko iz velikostne vrednosti, da je velik objekt še vedno v vaših šibkih objektih, torej ni izbrisan; vendar se ne bo prenesel pri potiskanju ali kasnejšem kloniranju, kar je pomembno. Če bi res želeli, bi objekt lahko popolnoma odstranili tako, da bi zagnali git prune z možnostjo --expire:

$ git prune --expire now
$ git count-objects -v
count: 0
size: 0
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0
scroll-to-top