Chapters ▾ 2nd Edition

10.7 Git bakom kulisserna - Underhåll och dataåterställning

Underhåll och dataåterställning

Ibland behöver du städa upp – komprimera ett kodförråd, rensa ett importerat kodförråd eller återställa förlorat arbete. Det här avsnittet går igenom några av dessa scenarier.

Underhåll

Ibland kör Git automatiskt ett kommando som kallas “auto gc”. För det mesta gör det ingenting. Men om det finns för många lösa objekt (objekt som inte ligger i en packfil) eller för många packfiler startar Git ett fullständigt git gc‑kommando. “gc” står för skräpinsamling och kommandot gör ett antal saker: det samlar ihop alla lösa objekt och placerar dem i packfiler, det konsoliderar packfiler till en stor packfil, och det tar bort objekt som inte kan nås från någon incheckning och som är några månader gamla.

Du kan köra auto gc manuellt så här:

$ git gc --auto

Återigen gör detta i regel ingenting. Du måste ha runt 7 000 lösa objekt eller fler än 50 packfiler för att Git ska starta ett riktigt gc‑kommando. Du kan justera dessa gränser med konfigurationsinställningarna gc.auto respektive gc.autopacklimit.

Det andra som gc gör är att packa ihop dina referenser till en enda fil. Anta att ditt kodförråd innehåller följande grenar och taggar:

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

Om du kör git gc kommer du inte längre att ha dessa filer i katalogen refs. Git flyttar dem för effektivitetens skull till en fil som heter .git/packed-refs, som ser ut så här:

$ 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

Om du uppdaterar en referens redigerar Git inte denna fil utan skriver i stället en ny fil till refs/heads. För att få rätt SHA‑1 för en given referens kontrollerar Git den referensen i katalogen refs och kontrollerar sedan filen packed-refs som reserv. Så om du inte hittar en referens i katalogen refs finns den troligen i din packed-refs‑fil.

Observera den sista raden i filen, som börjar med ett ^. Det betyder att taggen direkt ovanför är en annoterad tagg och att den raden är incheckningen som den annoterade taggen pekar på.

Dataåterställning

Vid något tillfälle i din Git‑resa kan du av misstag tappa en incheckning. Vanligtvis händer detta eftersom du tvångsraderar en gren som hade arbete på sig, och så visar det sig att du ville ha grenen ändå; eller så gör du en hård återställning av en gren och överger därmed incheckningar som du ville ha något från. Om detta händer, hur får du tillbaka dina incheckningar?

Här är ett exempel som gör en hård återställning av grenen master i ditt testkodförråd till en äldre incheckning och sedan återhämtar de förlorade incheckningarna. Först tittar vi på var ditt kodförråd befinner sig just nu:

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

Flytta nu grenen master tillbaka till den mellersta incheckningen:

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

Du har i praktiken förlorat de två senaste incheckningarna – du har ingen gren från vilken dessa incheckningar är nåbara. Du behöver hitta SHA‑1:an för den senaste incheckningen och sedan lägga till en gren som pekar på den. Tricket är att hitta den senaste SHA‑1:an – det är inte direkt så att du har den i huvudet, eller hur?

Ofta är det snabbast att använda verktyget git reflog. När du arbetar registrerar Git tyst vad HEAD pekar på varje gång du ändrar den. Varje gång du checkar in eller byter gren uppdateras refloggen. Refloggen uppdateras också av kommandot git update-ref, vilket är ännu en anledning att använda det i stället för att bara skriva SHA‑1‑värdet till dina ref‑filer, som vi tog upp i Git-referenser. Du kan se var du har varit när som helst genom att köra 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

Här kan vi se de två incheckningar vi har haft utpekade, men det finns inte mycket information här. För att se samma information på ett mycket mer användbart sätt kan vi köra git log -g, vilket ger dig vanliga loggutdata för refloggen.

$ 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

Det ser ut som att den nedersta incheckningen är den du tappade bort, så du kan återställa den genom att skapa en ny gren vid den incheckningen. Du kan till exempel starta en gren som heter recover-branch vid den incheckningen (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

Bra – nu har du en gren som heter recover-branch där din master‑gren brukade vara, vilket gör de första två incheckningarna nåbara igen. Anta nu att din förlust av någon anledning inte finns i refloggen – du kan simulera det genom att ta bort recover-branch och radera refloggen. Nu är de två första incheckningarna inte nåbara från något:

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

Eftersom reflogdata lagras i katalogen .git/logs/ har du i praktiken ingen reflogg. Hur kan du återställa den incheckningen vid det här laget? Ett sätt är att använda verktyget git fsck, som kontrollerar din databas för integritet. Om du kör det med alternativet --full visar det alla objekt som inte pekas på av ett annat 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

I det här fallet kan du se din saknade incheckning efter strängen “dangling commit”. Du kan återställa den på samma sätt, genom att lägga till en gren som pekar på den SHA‑1:an.

Ta bort objekt

Det finns många bra saker med Git, men en funktion som kan ställa till det är att en git clone laddar ner hela projektets historik, inklusive varje version av varje fil. Det är bra om allt är källkod, eftersom Git är mycket optimerat för att komprimera den datan effektivt. Men om någon vid någon tidpunkt i projektets historik lade till en enda enorm fil kommer varje klon för all framtid att tvingas ladda ner den stora filen, även om den togs bort från projektet i nästa incheckning. Eftersom den är nåbar från historiken kommer den alltid att finnas där.

Det kan vara ett enormt problem när du konverterar Subversion‑ eller Perforce‑kodförråd till Git. Eftersom du inte laddar ner hela historiken i de systemen får sådana tillägg små konsekvenser. Om du har importerat från ett annat system eller på annat sätt upptäcker att ditt kodförråd är mycket större än det borde vara, så här hittar och tar du bort stora objekt.

Varning: den här tekniken är destruktiv för din incheckningshistorik. Den skriver om varje incheckningsobjekt sedan det tidigaste träd du måste ändra för att ta bort referensen till en stor fil. Om du gör detta direkt efter en import, innan någon har börjat basera arbete på incheckningen, är det okej – annars måste du meddela alla bidragsgivare att de måste ombasera sitt arbete på dina nya incheckningar.

Som demonstration lägger du till en stor fil i ditt testkodförråd, tar bort den i nästa incheckning, hittar den och tar bort den permanent ur kodförrådet. Först lägger du till ett stort objekt i historiken:

$ 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

Oj – du ville inte lägga till ett enormt tar‑arkiv i projektet. Bäst att ta bort den:

$ 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

Kör nu gc på din databas och se hur mycket utrymme du använder:

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

Du kan köra kommandot count-objects för att snabbt se hur mycket utrymme du använder:

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

Posten size-pack är storleken på dina packfiler i kilobyte, så du använder nästan 5 MB. Före den senaste incheckningen använde du närmare 2K – det är tydligt att borttagningen av filen i den föregående incheckningen inte tog bort den ur historiken. Varje gång någon klonar detta kodförråd måste de klona hela 5 MB bara för att få detta lilla projekt, eftersom du av misstag lade till en stor fil. Låt oss bli av med den.

Först måste du hitta den. I det här fallet vet du redan vilken fil det är. Men anta att du inte gjorde det; hur skulle du identifiera vilken fil eller vilka filer som tar upp så mycket utrymme? Om du kör git gc ligger alla objekt i en packfil; du kan identifiera stora objekt genom att köra ett annat lågnivåkommando som heter git verify-pack och sortera på tredje fältet i utdata, som är filstorleken. Du kan också skicka det genom kommandot tail eftersom du bara är intresserad av de sista största filerna:

$ 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

Det stora objektet är längst ner: 5 MB. För att ta reda på vilken fil det är använder du kommandot rev-list, som du kort använde i Upprätthålla ett specifikt incheckningsmeddelandeformat. Om du skickar --objects till rev-list listar den alla inchecknings‑SHA‑1:or och även blob‑SHA‑1:or med filvägarna som hör till dem. Du kan använda detta för att hitta blobens namn:

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

Nu behöver du ta bort filen från alla träd i historiken. Du kan enkelt se vilka incheckningar som ändrade filen:

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

Du måste skriva om alla incheckningar nedströms från 7b30847 för att helt ta bort filen ur din Git‑historik. För att göra det använder du filter-branch, som du använde i Skriva om historik:

$ 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

Alternativet --index-filter liknar alternativet --tree-filter som användes i Skriva om historik, förutom att du i stället för att skicka ett kommando som ändrar filer som är utlagda på disk, ändrar din köyta eller index varje gång.

I stället för att ta bort en specifik fil med något som rm file måste du ta bort den med git rm --cached – du måste ta bort den från indexet, inte från disk. Anledningen till att göra det så här är hastighet – eftersom Git inte behöver lägga ut varje revision till disk innan filtret körs kan processen gå mycket, mycket snabbare. Du kan åstadkomma samma uppgift med --tree-filter om du vill. Alternativet --ignore-unmatch till git rm säger åt det att inte ge fel om mönstret du försöker ta bort inte finns där. Slutligen ber du filter-branch att skriva om historiken bara från incheckningen 7b30847 och uppåt, eftersom du vet att det var där problemet började. Annars kommer den att börja från början och ta onödigt lång tid.

Din historik innehåller inte längre en referens till den filen. Däremot gör din reflogg och en ny uppsättning refs som Git lade till när du körde filter-branch under .git/refs/original det fortfarande, så du måste ta bort dem och sedan packa om databasen. Du behöver bli av med allt som har en pekare till de gamla incheckningarna innan du packar om:

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

Låt oss se hur mycket utrymme du sparade.

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

Den packade kodförrådsstorleken är nere på 8K, vilket är mycket bättre än 5 MB. Du kan se av storleksvärdet att det stora objektet fortfarande finns bland dina lösa objekt, så det är inte borta; men det kommer inte att överföras vid en uppskickning eller en senare klon, vilket är det viktiga. Om du verkligen vill kan du ta bort objektet helt genom att köra git prune med alternativet --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