Git
Chapters ▾ 2nd Edition

10.7 Git на ниско ниво - Поддръжка и възстановяване на данни

Поддръжка и възстановяване на данни

От време на време може да се налага почистване на хранилището — за да стане то по-компактно, за да се почисти импортирано такова, или за да се възстанови загубена информация. В тази секция ще разгледаме няколко сценария от този сорт.

Поддръжка

В дадени моменти, Git автоматично изпълнява команда наречена “auto gc”. През повечето време, тя не прави нищо. Обаче, ако има твърде много loose обекти (такива, които са извън packfile) или пък твърде много packfiles, Git стартира пълнокръвна git gc. Фразата “gc” идва от garbage collect и командата прави много неща: тя събира всички loose обекти и ги поставя в packfiles, консолидира packfiles в един по-голям packfile, и премахва обекти, които са недостъпни през никой къмит и са по-стари от няколко месеца.

Можете да пуснете auto gc ръчно:

$ git gc --auto

Отново, това в повечето случаи не прави нищо. Трябва да имате приблизително 7,000 loose обекта или повече от 50 packfiles, за да се извърши реално изпълнение на 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

При обновяване на референция, Git не редактира този файл, а създава нов в refs/heads. За да вземе SHA-1 чексума за дадена референция, Git първо проверява в директорията refs и след това проверява packed-refs файла като резервен вариант. Обаче, ако не можете да намерите референция в refs директорията, тя вероятно е в packed-refs файла.

Обърнете внимание на последния ред от файла, който започва с ^. Това показва, че тагът на реда отгоре е анотиран таг и че този ред е къмита, към който сочи анотирания таг.

Възстановяване на данни

В някой хубав момент при работата си с Git може по невнимание да загубите къмит. Обикновено това се случва, ако направите форсирано изтриване на клон с работа в него или ако направите hard-reset на клон. Ако това се случи, как да си върнете къмитите?

Ето един пример, който прави hard-rest на клона master в тестово хранилище до по-стар къмит и след това възстановява загубените къмити. Първо, нека разгледаме хранилището в момента:

$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added 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 чексумата на последния и след това ще добавим клон сочещ към него. Работата е да намерим въпросната чексума — едва ли я запомнихте само от примера, нали?

Често най-бързият начин за това е инструмента git reflog. Докато работите, Git задкулисно записва къде сочи HEAD при всяка промяна. Всеки път, когато къмитвате или сменяте клонове, reflog-ът се опреснява. Това става с командата git update-ref, което е още една причина да я използваме, вместо да пишем SHA-1 стойностите в ref файловете, както видяхме в Git референции. Можете да видите къде сте били по всяко време с git reflog:

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

Тук виждаме двата къмита, които бяхме разпакетирали, но отделно от това няма много друга информация. За да видим същата информация в много по-полезен вид, може да изпълним git log -g, която ни показва това:

$ 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

       modified repo.rb a bit

Изглежда сякаш долният къмит е този, който сме загубили, така че можем да го върнем създавайки нов клон базиран на него. Създаваме клон с име recover-branch от този къмит (ab1afef):

$ git branch recover-branch ab1afef
$ git log --pretty=oneline recover-branch
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added 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”. След това можем да възстановим къмита по гореописания начин, добавяйки нов базиран на него клон.

Изтриване на обекти

Една от особеностите на Git, която би могла да доведе до проблеми, е фактът, че при клониране на хранилище git clone издърпва цялата история на проекта, включително всяка версия на всеки файл. В това няма нищо лошо, ако става дума само за сорс код, защото Git е силно оптимизиран да компресира ефективно подобен тип данни. Обаче, ако някой в произволен момент от историята на проекта е добавил единичен голям файл, тогава всяко клониране ще трябва да изтегля този файл дори и той да е бил премахнат от проекта още със следващия го къмит. Файлът е достъпен през историята, следователно винаги ще е тук.

Това може да се превърне в огромен проблем, когато конвертирате Subversion или Perforce хранилища в Git. Понеже при тези системи не изтегляте цялата история, такъв тип добавка има последици. Ако сте импортирали от друга система или по друг начин установите, че хранилището ви е много по-голямо отколкото би следвало, ето как да намерите и премахнете големи обекти.

Предупреждаваме: тази техника е деструктивна по отношение на историята на къмитите. Тя пренаписва всеки къмит обект от най-ранното необходимо дърво натам. Ако го направите веднага след импорта, преди никой друг да е базирал работата си на даден къмит, тогава няма проблем — в противен случай следва да уведомите всички колеги, че трябва да пребазират работата си върху вашите нови къмити.

За пример ще добавим един голям файл в тестовото хранилище, ще го премахнем в следващ къмит, след което ще го намерим и ще го изтрием перманентно. Първо го добавяме:

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

Осъзнаваме, че не искаме този огромен архив в проекта и трябва да се отървем от него:

$ git rm git.tgz
rm 'git.tgz'
$ git commit -m 'oops - removed large tarball'
[master dadf725] oops - removed 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 показва размера на вашите packfiles в килобайти, така че в случая се използват почти 5MB. Преди последния къмит използвахме около 2K — очевидно изтриването на файла от предишния къмит не го е извадило от историята. В това състояние на нещата, всеки път когато някой клонира това хранилище ще трябва да изтегли всичките 5MB, въпреки че проектът е мъничък. И това само защото по невнимание добавихме този голям файл. Нека видим как да го разкараме.

Първо, трябва да го намерим. В този конкретен пример знаем какъв е този файл. Но това може и да не е така и следователно се нуждаем от начин по който да идентифицираме големите файлове в проекта си. Ако изпълните git gc всички обекти отиват в packfile и може да използвате друга plumbing команда за да идентифицирате големите. Командата е 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

Големият обект се показва най-отдолу: 5MB. За да разберете на кой файл отговаря, използвайте командата rev-list, която бегло видяхме в Налагане на специфичен Commit-Message формат. Ако ѝ подадете параметъра --objects, rev-list ще отпечата SHA-1 стойностите на blob обектите с асоциираните към тях пътища на файлове. Може да изпълните нещо такова за да намерите името на файла за този blob:

$ 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 file, трябва да го извадим от индекса с git rm --cached. Трием от индексната област, не от диска. Причината да правим нещата по този начин е скоростта — Git не трябва да разпакетира всяка версия на диска преди да приложи филтъра ни и целият процес е много по-бърз. Разбра се, същата цел може да постигнем и с --tree-filter, ако желаем. Флагът --ignore-unmatch към git rm указва да не се печатат грешки, ако подадения в израза обект не е намерен. Последно, указваме на filter-branch да пренапише историята от къмита 7b30847 натам, понеже знаем, че там е възникнал проблема. В противен случай командата започва отначало и ще отнеме ненужно дълго време.

Сега историята не съдържа референции към този файл. Обаче, reflog информацията и новото множество референции, които Git създаде при изпълнението на filter-branch все още пазят данни за него, така че за да е цялостно завършен процеса, трябва да премахнем и тях, след което да препакетираме базата данни. Трябва да се освободим от всичко, което има указатели към тези стари къмити преди препакетирането:

$ 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

Пакетираното хранилище сега е сведено до 8K, което е доста по-малко от предишните 5MB. От реда size се вижда, че големият обект е все още в loose обектите, така че не е изчезнал съвсем, но по-важното е, че той няма да се изпраща при публикуване или последващо клониране, което е целта ни. Ако окончателно искаме да го изтрием, може да изпълним 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