Git
Chapters ▾ 2nd Edition

10.7 Гит изнутра - Одржавање и опоравак податак

Одржавање и опоравак податак

Повремено може бити потребе да се обави мало чишћења – да се репозиторијум мало сажме, прочисти увезени репозиторијум, или опорави изгубљени рад. Овај одељак ће приказати неке од тих сценарија.

Одржавање

Програм Гит повремен о аутоматски покреће команду која се зове „auto gc”. Ова команда најчешће не ради ништа. Међутим, када има превише слободних објеката (оних који се не налазе у pack фајлу) или превише pack фајлова, програм Гит покреће темељну git gc команду. Овде „gc” замењује garbage collect (скупљање отпада) и та команда обавља више ствари: скупља се слободне објекте и поставља их у pack фајлове, консолидује pack фајлове у један велики pack фајл и уклања објекте до којих не може да се стигне из ниједног комита, а стари су барем неколико месеци.

Команду auto gc можете и ручно да покренете на следећи начин:

$ git gc --auto

Да поновимо, ово у општем случају не ради ништа. Морате имати око 7.000 слободних објеката, или више од 50 pack фајлова да би програм Гит покренуо праву gc команду. Ове границе можете променити конфигурационим подешавањима gc.auto и gc.autopacklimit респективно.

Друга ствар коју ће gc урадити је да ваше референце спакује у један фајл. Претпоставимо да репозиторијум садржи следеће гране и ознаке:

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

Ако извршите git gc, више нећете имати ове фајлове у refs директоријуму. Програм Гит ће их у циљу ефикасности преместити у фајл под именом .git/packed-refs који изгледа овако:

$ 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

Ако ажурирате референцу, програм Гит не уређује овај фајл, већ уписује нови фајл у refs/heads. Да би за дату референцу дошао до одговарајуће SHA-1 вредности, програм Гит тражи ту референцу у refs директоријуму, па онда проверава packed-refs фајл. Тако да ако референцу не нађе у refs директоријуму, она се највероватније налази у вашем packed-refs фајлу.

Приметите последњу линију фајла која почиње са ^. То значи да је ознака непосредно изнад обележена ознака и да је та линија комит на коју показује обележена ознака.

Опоравак података

У неком тренутку током вашег Гит пута, можете грешком да изгубите комит. У општем случају, ово се дешава јер сте насилно обрисали грану на којој се налазио рад, а испоставља се да вам је грана ипак била потребна; ули урадите hard-reset гране, чиме напуштате комитове из којих сте нешто хтели. Под претпоставком да се то догодило, постоји ли начин да комитове вратите?

Ево примера коју ради hard-reset master вашег тест репозиторијума на старији комит, па онда опоравља изгубљене комитове. Најпре, хајде да погледамо стање вашег репозиторијума у овом тренутку:

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

Померимо сада master грану на средњи комит:

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

Ефективно сте изгубили горња два комита – немате више грану из које се може стићи у те комитове. Морате да пронађете SHA-1 последњег комита, па да додате грану која показује на њега. Трик је пронаћи тај SHA-1 последњег комита – не сећате се баш како се то ради, зар не?

Често је најбржи начин да се употреби алат који се зове git reflog. Док радите, програм Гит у тишини чува шта је ваш HEAD сваки пут када га измените. Сваки пут када комитујете или пређете на другу грану, ажурира се reflog. Reflog се такође ажурира командом git update-ref и то је још један разлога да корисите њу, уместо да једноставно уписујете SHA-1 вредност у своје ref фајлове, као што смо показали у Гит референце. Ако извршите 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

Овде можемо видети два комита које смо одјавили, међутим нема много детаља. Ако желите да исте информације видите на много кориснији начин, треба да извршите git log -g, што вам приказује уобичајени лог излаз за ваш 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

Изгледа да је комит на дну онај који сте изгубили, тако да га опорављате креирајући нову грану на том комиту. На пример, на том комиту (ab1afef) можете да започнете нову грану са именом recover-branch:

$ 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

Фино – сада имате грану која се зове recover-branch на оном месту на којем се налазила ваша master грана, чиме поново може да се дође до прва два комита. Даље, претпоставимо да се из неког разлога ваш губитак не налази у reflog – то може да се симулира уклањањем recover-branch гране и брисањем reflog. Сада ништа не може да дође до прва два комита:

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

Пошто се reflog подаци чувају у .git/logs/ директоријуму, ефективно више немате reflog. Како сада да вратите те комитове? Један начин је да употребите алат git fsck који проверава интегритет базе података. Ако га покренете са опцијом --full, он вам приказује све објекте на које не указује ниједан други објекат:

$ 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

У овом случају, ваш комит који недостаје видите иза стринга „dangling commit”. Можете да га опоравите на исти начин, додавањем гране која показује на тај SHA-1.

Уклањање објеката

Постоји много одличних ствари у вези програма Гит, али једна од могућности која може да буде узрок проблема је чињеница да git clone преузима целокупну историју пројекта, укључујући сваку верзију сваког фајла. Ово није тако лоше ако је у репозиторијуму само изворни кôд, јер је програм Гит одлично оптимизован да ефикасно компресује податке. Међутим, ако је неко у неком тренутку историје вашег пројекта додао један огроман фајл, свако клонирање ће увек бити принуђено да преузима тај велики фајл, чак и ако је одмах био уклоњен у наредном комиту. Пошто до њега може да се стигне из историје, он ће тамо заувек да остане.

Ово може да представља велики проблем када Subversion или Перфорс репозиторијуме конвертујете у Гит. Пошто у тим системима не преузимате комплетну историју, ова врста додавања има мало последица. Ако сте обавили увоз из неког другога система, или откријете да вам је репозиторијум много већи него што би требало да буде, ево начина на који можете да пронађете и уклоните велике објекте.

Упозоравамо вас: ова техника може да оштети вашу историју комитова. Она поново исписује сваки комит објекат почевши од најранијег стабла које морате да измените како би уклонили референцу на велики фајл. Ако то урадите непосредно након увоза, пре него што је било ко свој рад почео да базира на комиту, онда нема проблема – у супротном, све сараднике морате обавестити да свој рад морају ребазирати преко ваших нових комитова.

Да бисмо показали ово, додаћете велики фајл у свој тест репозиторијум, уклонићете га у наредном комиту, пронаћи га и трајно уклонити из репозиторијума. Најпре, додајте велики објекат у своју историју:

$ 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

Ууупс – нисте хтели да у свој пројекат додате огромну tarball архиву. Боље да је се решите:

$ 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

А сада, извршите gc над својом базом да видите колико простора заузима:

$ 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)

Можете да извршите и команду 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

Ставка size-pack је величина ваших pack фајлова у килобајтима, тако да користите скоро 5МБ. Пре последњег комита, трошило се близу 2K – очито, уклањање фајла из претходног комита га није уклонило из историје. Сваки пут када неко клонира овај репозиторијум, мораће да клонирају свих 5МБ само да би преузели овај малецки пројекат, јер сге грешком додали велики фајл. Хајде да га се решимо.

Најпре морате да га пронађете. У овом случају већ знате који је то фајл. Али претпоставимо да не знате; како можете да откријете који фајл или фајлови заузимају толики простор? Ако извршите git gc, сви објекти се налазе у pack фајлу; велике објекте можете да препознате извршавањем друге водоводне команде која се зове git verify-pack и сортирањем по трећем пољу излаза, које представља величину. Такође можете и да преусмерите излаз кроз команду tail јер вас интересује само неколико највећих фајлова на крају:

$ 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

Велики објекат је на дну: 5МБ. Да бисте пронашли у ком фајлу се налази, употребићете команду rev-list коју сте накратко употребили у Спровођење одређеног формата за комит поруке. Ако команди rev-list проследите --objects, она ће приказати све SHA-1 вредности комитова заједно са придруженим путањама фајлова. Да бисте пронашли име вашег блоба, употребите следеће:

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

Сада треба да уклоните овај фајл из свих стабала у вашој прошлости. Лако можете да видите који комитови су мењали овај фајл:

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

Да бисте овај фајлу потпуно уклонили из Гит историје, морате поново да испишете све комитове низводно од 7b30847. За то ћете употребити команду filter-branch, коју сте већ користили у Поновно исписивање историје:

$ 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

Опција --index-filter је слична опцији --tree-filter која је употребљена у Поновно исписивање историје, осим што уместо да прослеђивања команде која мења одјављене фајлове на диску, она сваки пут мења стејџ или индекс.

Уместо да одређени фајл уклоните са нечим као што је rm фајл, морате да га уклоните са git rm --cached – морате да га уклоните из индекса, а не са диска. Разлог због којег се овако ради је брзина – пошто програм Гит не мора да одјављује сваку ревизију на диск пре него што изврши ваш филтер, процес може да буде много, много бржи. Ако желите, исти задатак можете да извршите са --tree-filter. --ignore-unmatch опција команди git rm говори да не прекине извршавање са грешком ако не наиђе на шаблон који покушавате да уклоните. Коначно, тражите од команде filter-branch да поново испише вашу историју почевши од комита 7b30847 на овамо, јер знате да је проблем настао на том месту. У супротном, она ће да крене од почетак и без потребе ће продужити извршавање.

Ваш историја више нема референцу на тај фајл. Међутим, ваш reflog и нови скуп референци које је програм Гит додао када сте извршили filter-branch под .git/refs/original још увек садрже, тако их морате уклонити и препаковати базу података. Пре поновног паковања, морате да се решите свега што има показивач на те старе комитове:

$ 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)

Хајде да видимо колико простора сте уштедели.

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

Величина спакованог репозиторијума је спала на 8К, што је много боље од 5МБ. Из size вредности видите да се велики објекат још увек налази у слободним објектима, тако да није нестао; али се приликом гурања или накнадног клонирања неће преносити, а те јо оно што је битно. Ако то заиста желите, могли бисте потпуно да уклоните објекат ако извршите git prune са --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