Git 🌙
Chapters ▾ 2nd Edition

10.7 Mechanizmy wewnętrzne w Git - Konserwacja i odzyskiwanie danych

Konserwacja i odzyskiwanie danych

Czasami będziesz musiał zrobić jakieś porządki – sprawić, aby repozytorium zajmowało mniej miejsca, oczyścić zaimportowane repozytorium, lub odtworzyć utracone zmiany. Ten rozdział zawiera opis postępowania w tych scenariuszach.

Konserwacja

Sporadycznie Git uruchamia automatycznie komendę auto gc`. Najczęściej ta komenda nic nie robi. Jednak, jeżeli istnieje za dużo luźnych obiektów (obiektów które nie są w plikach packfile), lub za dużo plików packfile, Git uruchamia pełną komendę git gc. Komenda gc (od ang. garbage collect) wykonuje różne operacje: gromadzi ona wszystkie luźne obiekty i umieszcza je w plikach packfile, łączy pliki packfile w jeden duży, oraz usuwa obiekty które nie są osiągalne przez żaden z commitów i są starsze niż kilka miesięcy.

Możesz uruchomić auto gc` ręcznie w ten sposób:

$ git gc --auto

I znowu, ona generalnie nic nie robi. Musisz mieć około 7000 luźnych obiektów, lub więcej niż 50 plików packfile, aby Git odpalił pełną komendę gc. Możesz zmienić te limity za pomocą ustawień konfiguracyjnych gc.auto oraz gc.autopacklimit.

Inną rzeczą którą komenda gc zrobi, jest spakowanie referencji do pojedynczego pliku. Załóżmy, że Twoje repozytorium zawiera następujące gałęzie i etykiety:

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

Jeżeli uruchomisz git gc, nie będziesz miał już tych plików w katalogu refs. Git przeniesie je, w celu poprawienia wydajności do pliku .git/packed-refs, który wygląda tak:

$ 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

Jeżeli zaktualizujesz referencje, Git nie będzie zmieniał tego pliku, ale zamiast tego stworzy nowy plik w refs/heads. Aby pobrać właściwą sumę SHA-1 dla danej referencji, Git sprawdzi czy istnieje ona w katalogu refs, a następnie sprawdzi plik packed-refs. Jeżeli nie możesz znaleźć referencji w katalogu refs, jest ona prawdopodobnie w pliku packed-refs.

Zauważ, że ostatnia linia w tym pliku zaczyna się od ^. Oznacza to, że dana etykieta jest etykietą opisaną, a ta linia jest commitem na który on wskazuje.

Odzyskiwanie danych

W pewnym momencie swojej pracy z Git, możesz czasami przez przypadek stracić commit. Zazwyczaj dzieje się tak dlatego, ponieważ wymusisz usunięcie gałęzi która miała w sobie zmiany, a okazuje się że jednak ją potrzebowałeś; lub wykonujesz na gałęzi hard-reset, porzucając zmiany które teraz potrzebujesz. Zakładając że tak się stało, w jaki sposób możesz odzyskać swoje zmiany?

Mamy tutaj przykład, na którym zobaczymy odzyskiwanie danych z testowego repozytorium na którym wykonano hard-reset na gałęzi master. Na początek, zobaczmy jak wygląda repozytorium w takiej sytuacji:

$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

Teraz cofnij gałąź master do środkowej zmiany:

$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

W ten sposób, skutecznie utraciłeś dwa najnowsze commity – nie masz gałęzi z której można by się dostać do nich. Musisz znaleźć najnowszą sumę SHA, a potem dodać gałąź wskazującą na nią. Najtrudniejsze jest znalezienie ostatniej sumy SHA-1 – przecież nie zapamiętałeś jej, prawda?

Często, najszybszym sposobem jest użycie narzędzia git reflog. W czasie pracy, Git w tle zapisuje na co wskazuje HEAD po każdej zmianie. Za każdym razem gdy wykonujesz commit lub zmieniasz gałęzie, reflog jest aktualizowany. Reflog jest również aktualizowany przez komendę git update-ref, co jest kolejnym argumentem za tym, aby jej używać zamiast zapisywać bezpośrednio wartości SHA-1 do plików ref, tak jak zostało to opisane wcześniej w sekcji Referencje w Git. Możesz zobaczyć na jakim etapie był projekt w każdym momencie za pomocą komendy 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

Widzimy tutaj dwa commity które pobraliśmy, jednak nie mamy za dużo informacji. Aby zobaczyć te same informacje w bardziej użytecznej formie, możemy uruchomić git log -g, która pokaże normalny wynik działania komendy log dla refloga:

$ 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

Wygląda na to, że dolny commit to jeden z tych które utraciłeś, możesz go odzyskać przez stworzenie nowej gałęzi wskazującej na niego. Na przykład, możesz dodać gałąź recover-branch wskazującą na ten commit (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

Świetnie – masz teraz gałąź recover-branch, która wskazuje na miejsce w którym był master, pozwalając tym samym na dostęp do pierwszych dwóch commitów. Następnie, załóżmy że utracone zmiany z jakiegoś powodu nie były w reflogu – możesz to zasymulować poprzez usunięcie recover-branch i usunięcie refloga. Teraz pierwsze dwa commity nie są dostępne w żaden sposób:

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

Ponieważ dane reflog są przechowywane w katalogu .git/logs/, w rzeczywistości nie masz refloga. W jaki sposób odtworzyć ten commit w tym momencie? Jednym ze sposobów jest użycie narzędzia git fsck, które sprawdza zawartość bazy pod względem integralności danych. Jeżeli uruchomisz go z opcją --full, pokaże on wszystkie obiekty do których nie da się dotrzeć przez inne:

$ 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

W tym przypadku, możesz zobaczyć brakujący commit oznaczony jako "opuszczony" (ang. dangling). Możesz odtworzyć go w ten sam sposób, poprzez dodanie gałęzi wskazującej na jego SHA-1.

Usuwanie obiektów

Można powiedzieć dużo dobrego o Gitcie, ale jedną z funkcjonalności która może powodować problemy jest fakt, że git clone pobiera całą historię projektu, włącznie z każdą wersją wszystkich plików. Jest to dobre rozwiązanie, jeżeli całość to kod źródłowy, ponieważ Git został przygotowany do tego aby efektywnie kompresować takie dane. Jednak, jeżeli w jakimś momencie trwania projektu, ktoś dodał pojedynczy duży plik, podczas klonowania repozytorium zawsze będzie on pobierany, nawet jeżeli został usunięty z projektu w następnym commicie. Ze względu na to, że można do niego dostać się przez historię projektu, zawsze tam będzie.

Może to być dużym problemem podczas konwersji repozytoriów Subversion lub Perforce do Gita. Ponieważ nie pobierasz w nich całej historii projektu, dodanie tak dużego pliku będzie powodowało pewne konsekwencje. Jeżeli wykonałeś import z innego systemu lub zobaczyłeś, że Twoje repozytorium jest dużo większej niż być powinno, poniżej prezentuję sposób na usunięcie dużych obiektów.

Ostrzegamy: ta technika działa destrukcyjnie na Twoją historię zmian. Nadpisuje ona każdy obiekt, począwszy od najwcześniejszego który trzeba zmodyfikować aby usunąć odwołanie do pliku. Jeżeli wykonasz to od razu po zaimportowaniu, zanim ktokolwiek rozpoczął pracę bazującą na nich, wszystko będzie w porządku – w przeciwnym wypadku, będziesz musiał poinformować wszystkich współpracowników o tym, że muszą wykonać rebase na nowe commity.

W celach demonstracyjnych, dodasz duży plik do swojego testowego repozytorium, usuniesz go w kolejnym commicie, odszukasz go i następnie usuniesz na stałe z repozytorium. Najpierw dodaj duży plik do repozytorium:

$ 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

Ojej – nie chciałeś dodać tego dużego pliku do projekt. Najlepiej go usuń:

$ 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

Teraz, uruchom gc na bazie danych i zobacz jak dużo miejsca jest zajmowane:

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

Możesz uruchomić komendę count-objects, aby szybko zobaczyć jak dużo miejsca jest zajmowane:

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

Wpis size-pack pokazuje wielkość plików packfile wyrażonych w kilobajtach, więc używasz 5MB. Przed ostatnim commitem, używałeś blisko 2KB – a więc jasno widać, że usunięcie pliku w poprzednim commitcie nie usunęło go z historii. Za każdym razem, gdy ktoś sklonuje to repozytorium, będzie musiał pobrać całe 5MB aby pobrać ten malutki projekt, tylko dlatego że pochopnie dodałeś duży plik. Naprawmy to.

Najpierw będzie musiał go znaleźć. W naszym wypadku, wiesz jaki plik to był. Ale załóżmy że nie wiesz; w jaki sposób dowiesz się jaki plik lub pliki zajmują tyle miejsca? Po uruchomieniu git gc, wszystkie obiekty są w plikach packfile; ale możesz zidentyfikować duże obiekty przez uruchomienie komendy git verify-pack i posortowanie wyniku po trzeciej kolumnie, oznaczającej rozmiar pliku. Możesz również przekazać wynik do komendy tail ponieważ jesteś zainteresowany tylko kilkoma największymi plikami:

$ 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

Duży obiekt jest na samym dole: 5MB. Aby dowiedzieć się jaki to jest plik, użyjesz komendy rev-list, której miałeś okazję już poznać w sekcji Enforcing a Specific Commit-Message Format. Jeżeli przekażesz opcję --objects do rev-list, w wyniku pokazane zostaną sumy SHA-1 commitów oraz obiektów blob z przyporządkowanymi do nich nazwami plików. Możesz użyć tej komendy, aby odnaleźć nazwę obiektu blob:

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

Musisz teraz usunąć ten plik ze wszystkich starszych rewizji. Możesz łatwo zobaczyć, które commity zmodyfikowały ten plik:

$ git log --oneline --branches -- git.tgz
dadf725 oops - removed large tarball
7b30847 add git tarball

Musisz nadpisać wszystkie commity starsze niż 7b30847, aby w pełni usunąć ten plik z historii projektu w Git. Aby to zrobić, użyjesz komendy filter-branch, poznanej w sekcji Przepisywanie historii:

$ 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

Opcja --index-filter jest podobna do opcji --tree-filter opisanej w sekcji Przepisywanie historii, z tą różnicą, że zamiast przekazywać komendę, która modyfikuje pobrane pliki na dysku, modyfikuje przechowalnię lub indeks za każdym razem.

Zamiast usuwać konkretny plik za pomocą rm file, musisz usunąć go za pomocą git rm --cached – musisz usunąć go z indeksu, nie z dysku. Powodem do takiego zachowania jest szybkość – ponieważ Git nie musi pobrać każdej rewizji na dysk przed uruchomieniem filtra, cały proces może być dużo szybszy. Możesz osiągnąć taki sam efekt za pomocą --tree-filter, jeżeli chcesz. Opcja --ignore-unmatch do git rm wskazuje, aby nie pokazywać błędu w przypadku, gdy szukana ścieżka nie istnieje. Na koniec, wskazujesz filter-branch, aby przepisana została historia począwszy od 7b30847, ponieważ wiesz że właśnie tam problem powstał. W przeciwnym razie, rozpocznie ona działanie od początku i przez to będzie trwała niepotrzebnie dłużej.

Twoja historia nie zawiera już odwołań do tego pliku. Ale reflog i nowe referencje które zostały dodane, wtedy gdy uruchomiłeś filter-branch w .git/refs/original nadal tak, musisz więc je usunąć i przepakować bazę danych. Musisz pozbyć się wszystkiego co wskazuje na te stare commity przed przepakowaniem:

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

Zobaczmy ile miejsca udało się zaoszczędzić.

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

Wielkość spakowanego repozytorium to teraz 8K, co jest dużo lepszym wynikiem niż 5MB. Możesz odczytać z wartości "size", że ten duży obiekt nadal znajduje się w repozytorium, nie został więc całkowicie usunięty; jednak co najważniejsze, nie będzie już przesyłany podczas wykonywania push lub klonowania. Jeżeli mocno chcesz, możesz usunąć ten obiekt całkowicie przez uruchomienie komendy 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
scroll-to-top