Git
Chapters ▾ 2nd Edition

10.7 Εσωτερική λειτουργία του Git - Διατήρηση και ανάκτηση δεδομένων

Διατήρηση και ανάκτηση δεδομένων

Περιστασιακά, ίσως χρειαστεί να κάνουμε κάποια εκκαθάριση —να καταστήσουμε ένα αποθετήριο πιο συμπαγές, να καθαρίσουμε ένα εισαγόμενο αποθετήριο ή να ανακτήσουμε χαμένη εργασία. Αυτή η ενότητα καλύπτει ορισμένα από αυτά τα σενάρια.

Συντήρηση

Περιστασιακά, το Git εκτελεί αυτόματα μια εντολή που ονομάζεται auto gc. Τις περισσότερες φορές, αυτή η εντολή δεν κάνει τίποτα. Ωστόσο, αν υπάρχουν πάρα πολλά πακέτα (packfiles) ή πάρα πολλά χαλαρά αντικείμενα (αντικείμενα που δεν περιέχονται σε πακέτα), το Git εξαπολύει μια πλήρως εξοπλισμένη git gc. Το “gc” σημαίνει συλλογή σκουπιδιών (garbage collection) και η εντολή κάνει πολλά πράγματα: συγκεντρώνει όλα τα χαλαρά αντικείμενα και τα τοποθετεί σε πακέτα, ενοποιεί πακέτα σε ένα μεγάλο πακέτο και αφαιρεί αντικείμενα που δεν είναι προσπελάσιμα από καμία υποβολή και έχουν ηλικία λίγων μηνών.

Μπορούμε να εκτελέσουμε την auto gc αυτόματα ως εξής:

$ git gc --auto

Επαναλαμβάνουμε ότι αυτή η εντολή γενικά δεν κάνει τίποτα. Πρέπει να έχουμε περίπου 7.000 χαλαρά αντικείμενα ή περισσότερα από 50 πακέτα ώστε το Git να εξαπολύσει μια πραγματική εντολή 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 θα τα μετακινήσει για λόγους αποδοτικότητας σε ένα αρχείο με όνομα .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, κάποια στιγμή ενδέχεται να χάσουμε κάποια υποβολή κατά λάθος. Γενικά, αυτό συμβαίνει όταν επιβάλλουμε τη διαγραφή (force-delete) ενός κλάδου που είχε κάποια δουλειά που τελικά θέλαμε τον κλάδο· ή κάναμε hard-reset σε έναν κλάδο, εγκαταλείποντας έτσι υποβολές από τις οποίες θέλαμε κάτι. Αν υποθέσουμε ότι έχει συμβεί κάτι τέτοιο, τότε πώς μπορούμε να ανακτήσουμε τις υποβολές μας;

Ακολουθεί ένα παράδειγμα στο οποίο κάνουμε hard-reset στον κύριο κλάδο του αποθετηρίου μας σε μια παλαιότερη υποβολή και στη συνέχεια ανακτά τις χαμένες υποβολές. Αρχικά, ας θυμηθούμε πού βρίσκεται το αποθετήριό μας:

$ 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 και στη συνέχεια να προσθέσουμε έναν κλάδο που να δείχνει σε αυτόν. Το κόλπο είναι να βρούμε τον SHA-1 της τελευταίας υποβολης —δεν τον έχουμε απομνημονεύσει κιόλας, σωστά;

Συχνά, ο πιο γρήγορος τρόπος είναι να χρησιμοποιήσουμε ένα εργαλείο που ονομάζεται git reflog. Καθώς δουλεύουμε, το Git καταγράφει σιωπηλά ποιος είναι ο HEAD μας κάθε φορά που τον αλλάζουμε. Κάθε φορά που αλλάζουμε κλάδο ή υποβάλλουμε, το reflog ενημερώνεται. Το 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, η οποία θα μας δώσει μια κανονική έξοδο μητρώου για το 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

       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” (εκκρεμής υποβολή). Μπορούμε να την ανακτήσουμε με τον ίδιο τρόπο, προσθέτοντας έναν κλάδο που δείχνει σε αυτόν τον αριθμό SHA-1.

Διαγραφή αντικειμένων

Αν και το 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

Ωχ! Δεν θέλουμε να προσθέσουμε ένα τεράστιο tarball στο έργο μας. Καλύτερα να το ξεφορτωθούμε:

$ 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 είναι το μέγεθος των πακέτων σε KB, επομένως χρησιμοποιούμε σχεδόν 5 MB. Πριν από την τελευταία υποβολή, χρησιμοποιούσαμε περίπου 2 ΚB —σαφώς, η κατάργηση του αρχείου από την προηγούμενη υποβολή δεν το κατήργησε από το ιστορικό μας. Κάθε φορά που κάποιος κλωνοποιεί αυτό το αποθετήριο, θα πρέπει να κλωνοποιήσει όλα τα 5MB μόνο για να πάρει αυτό το μικροσκοπικό έργο, επειδή προσθέσαμε κατά λάθος ένα μεγάλο αρχείο. Κάθε φορά που κάποιος κλωνοποιεί αυτό το αποθετήριο, θα πρέπει να κλωνοποιήσει και αυτά τα 5 MB μόνο για να πάρει αυτό το μικροσκοπικό έργο, επειδή προσθέσαμε κατά λάθος ένα μεγάλο αρχείο. Ας απαλλαγούμε από αυτό.

Πρώτα πρέπει να το βρούμε. Σε αυτήν την περίπτωση, ξέρουμε ήδη τι αρχείο είναι. Αλλά ας υποθέσουμε ότι δεν ξέρουμε· πώς θα προσδιορίζαμε ποιο αρχείο ή αρχεία καταλάμβαναν τόσο μεγάλο χώρο; Αν εκτελέσουμε την git gc, θα δούμε ότι όλα τα αντικείμενα βρίσκονται σε ένα πακέτο· μπορούμε να προσδιορίσουμε τα μεγάλα αντικείμενα, αν τρέξουμε μια άλλη εντολή διοχέτευσης που ονομάζεται 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 MB. Για να μάθουμε ποιο αρχείο είναι, θα χρησιμοποιήσουμε την εντολή rev-list, την οποία χρησιμοποιήσαμε για λίγο στο Επιβολή συγκεκριμένης μορφής μηνύματος υποβολής. Εάν περάσουμε την επιλογή --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

Πρέπει να ξαναγράψουμε όλες τις υποβολές downstream από την 7b30847 για να καταργήσουμε πλήρως αυτό το αρχείο από το ιστορικό του Git. Για να το κάνουμε αυτό, χρησιμοποιούμε την 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 που χρησιμοποιείται και στην ενότητα Η ιστορία ξαναγράφεται με την εξαίρεση ότι αντί να περάσουμε μια εντολή που τροποποιεί τα αρχεία που έχουν ενημερωθεί (checkout) στον δίσκο, τροποποιούμε το στάδιο καταχώρισης ή ευρετήριο κάθε φορά.

Αντί να διαγράψουμε ένα συγκεκριμένο αρχείο χρησιμοποιώντας π.χ. την rm file, θα πρέπει να το διαγράψουμε με την git rm --cached —πρέπει να το αφαιρέσουμε από το ευρετήριο, όχι από τον δίσκο. Αυτό το κάνουμε για μεγαλύτερη ταχύτητα— επειδή το Git δεν χρειάζεται να κάνει ενημερώσει (checkout) κάθε αναθεώρηση στον δίσκο προτού εκτελέσει το φίλτρο μας, η διαδικασία μπορεί να είναι πολύ, πολύ γρηγορότερη. Μπορούμε να ολοκληρώσουμε την ίδια εργασία με --tree-filter, αν το θέλουμε. Η επιλογή --ignore-unmatch στην git rm της λέει να μην τερματίσει με σφάλμα αν το μοτίβο που προσπαθούμε να καταργήσουμε δεν υπάρχει. Τέλος, ζητάμε από τη filter-branch να ξαναγράψει το ιστορικό μας μόνο από την υποβολή 7b30847, επειδή ξέρουμε ότι από αυτήν ξεκίνησε το συγκεκριμένο πρόβλημα. Διαφορετικά, θα ξεκινήσει από την αρχή και η διαδικασία θα διαρκέσει περισσότερο χωρίς λόγο.

Το ιστορικό μας δεν περιέχει πλέον καμία αναφορά στο συγκεκριμένο αρχείο. Ωστόσο, το reflog μας και ένα νέο σύνολο αναφορών που πρόσθεσε το Git όταν τρέξαμε τη 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

Το μέγεθος του αποθηκευμένου αποθετηρίου είναι κάτω από 8K, το οποίο είναι πολύ καλύτερο από τα 5 MB. Μπορούμε να δούμε από την τιμή 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