Git
Chapters ▾ 2nd Edition

A2.2 Appendix B: Ενσωμάτωση του Git στις εφαρμογές μας - Libgit2

Libgit2

© Μια άλλη επιλογή στη διάθεσή μας είναι να χρησιμοποιήσουμε το Libgit2. Το Libgit2 είναι μια υλοποίηση του Git χωρίς εξαρτήσεις (dependencies), με έμφαση σε ένα ωραίου API για χρήση σε άλλα προγράμματα. Μπορούμε να το βρούμε στην http://libgit2.github.com.

Αρχικά, ας ρίξουμε μια ματιά στο API για C. Ακολουθεί ένα παράδειγμα τυφώνας!

// Άνοιγμα αποθετηρίου
git_repository *repo;
int error = git_repository_open(&repo, "/path/to/repository");

// Αφαίρεση της αναφοράς του HEAD στην υποβολή
git_object *head_commit;
error = git_revparse_single(&head_commit, repo, "HEAD^{commit}");
git_commit *commit = (git_commit*)head_commit;

// Εκτύπωση μερικών από τις ιδιότητες της υποβολής
printf("%s", git_commit_message(commit));
const git_signature *author = git_commit_author(commit);
printf("%s <%s>\n", author->name, author->email);
const git_oid *tree_id = git_commit_tree_id(commit);

// Συμμάζεμα
git_commit_free(commit);
git_repository_free(repo);

Οι πρώτες δύο γραμμές ανοίγουν ένα αποθετήριο Git. Ο τύπος git_repository αντιπροσωπεύει έναν χειριστή ελέγχου (handle) σε ένα αποθετήριο με προσωρινή μνήμη στη μνήμη. Αυτή είναι η απλούστερη μέθοδος εφόσον γνωρίζουμε την ακριβή διαδρομή στον κατάλογο εργασίας του αποθετηρίου ή στον φάκελο .git. Υπάρχούν επίσης η git_repository_open_ext που περιλαμβάνει επιλογές αναζήτησης, η git_clone και οι σχετικές εντολές για τη δημιουργία τοπικού κλώνου ενός απομακρυσμένου αποθετηρίου και η git_repository_init για τη δημιουργία ενός εντελώς νέου αποθετηρίου.

Το δεύτερο κομμάτι του κώδικα χρησιμοποιεί σύνταξη rev-parse (βλ. ενότητα Αναφορές κλάδων σχετικά) για να πάρει την υποβολή στην οποία δείχνει τελικά ο HEAD. Ο τύπος που επιστρέφεται είναι ένας δείκτης git_object, που αντιπροσωπεύει κάτι που υπάρχει στη βάση δεδομένων αντικειμένων Git για ένα αποθετήριο. Το git_object είναι στην πραγματικότητα ένας τύπος “parent” για πολλά διαφορετικά είδη αντικειμένων· η διάταξη μνήμης για καθέναν από τους τύπους “παιδιών” είναι ίδια με εκείνη του git_object, ώστε να μπορούμε να κάνουμε casting με ασφάλεια στο σωστό. Σε αυτήν την περίπτωση, η git_object_type(commit) θα επέστρεφε GIT_OBJ_COMMIT, επομένως είναι ασφαλές να κάνουμε casting σε έναν δείκτη git_commit.

Το επόμενο κομμάτι δείχνει τον τρόπο πρόσβασης στις ιδιότητες της υποβολής. Η τελευταία γραμμή εδώ χρησιμοποιεί έναν τύπο git_oid· αυτή είναι η αναπαράσταση του Libgit2 για έναν αριθμό SHA-1.

Από αυτό το δείγμα, έχουν αρχίσει να αναδύονται δύο μοντέλα:

  • Αν δηλώσουμε έναν δείκτη και περάσουμε μια αναφορά σε αυτό σε μια κλήση Libgit2, αυτή η κλήση θα επιστρέψει πιθανώς έναν ακέραιο κωδικό σφάλματος.   Η τιμή 0 υποδηλώνει επιτυχία· ο,τιδήποτε άλλο υποδηλώνει σφάλμα.

  • Αν το Libgit2 συγκεντρώσει έναν δείκτη για εμάς, είμαστε υπεύθυνοι για την απελευθέρωσή του.

  • Αν το Libgit2 επιστρέψει έναν δείκτη const από μια κλήση, δεν χρειάζεται να τον απελευθερώσουμε, αλλά θα ακυρωθεί όταν το αντικείμενο στο οποίο ανήκει ελευθερωθεί.

  • Ο προγραμματισμός σε C είναι λίγο οδυνηρός.

Αυτός ο τελευταίος σημαίνει ότι δεν είναι πολύ πιθανό ότι θα γράφουμε C όταν χρησιμοποιούμε το Libgit2. Ευτυχώς, υπάρχει ένας αριθμός γλωσσών script που είναι διαθέσιμες και καθιστούν αρκετά εύκολο να δουλέψουμε με αποθετήρια Git από τη συγκεκριμένη γλώσσα και το περιβάλλον μας. Ας ρίξουμε μια ματιά στο παραπάνω παράδειγμα που γράφτηκε χρησιμοποιώντας τις συνδέσεις (bindings) Ruby για το Libgit2, οι οποίες ονομάζονται Rugged και μπορούν να βρεθούν στην https://github.com/libgit2/rugged.

repo = Rugged::Repository.new('path/to/repository')
commit = repo.head.target
puts commit.message
puts "#{commit.author[:name]} <#{commit.author[:email]}>"
tree = commit.tree

Όπως βλέπουμε ο κώδικας είναι πιο συμμαζεμένος. Καταρχάς, η Rugged χρησιμοποιεί εξαιρέσεις (exceptions)· μπορεί να επικαλεστεί σφάλματα όπως ConfigError ή ObjectError για να σηματοδοτήσει συνθήκες σφαλμάτων. Κατά δεύτερο λόγο, δεν υπάρχει ρητή απελευθέρωση πόρων, διότι η Ruby έχει αυτόματο συλλέκτη σκουπιδιών. Ας δούμε ένα λίγο πιο σύνθετο παράδειγμα: υλοποίηση μίας υποβολής εκ του μηδενός.

blob_id = repo.write("Blob contents", :blob)        (1)

index = repo.index
index.read_tree(repo.head.target.tree)
index.add(:path => 'newfile.txt', :oid => blob_id)  (2)

sig = {
    :email => "bob@example.com",
    :name => "Bob User",
    :time => Time.now,
}

commit_id = Rugged::Commit.create(repo,
    :tree => index.write_tree(repo),                (3)
    :author => sig,
    :committer => sig,                              (4)
    :message => "Add newfile.txt",                  (5)
    :parents => repo.empty? ? [] : [ repo.head.target ].compact, (6)
    :update_ref => 'HEAD',                                       (7)
)
commit = repo.lookup(commit_id)                     (8)
  1. Δημιουργούμε ένα νέο blob, που περιέχει τα περιεχόμενα ενός νέου αρχείου.

  2. Γεμίζουμε το ευρετήριο με το δέντρο της υποβολής της κεφαλής και προσθέτουμε το νέο αρχείο στη διαδρομή newfile.txt.

  3. Αυτό δημιουργεί ένα νέο δέντρο στην ODB και τη χρησιμοποιεί για τη νέα υποβολή.

  4. Χρησιμοποιούμε την ίδια υπογραφή για τα πεδία τόσο του συγγραφέα όσο και υποβάλοντος.

  5. Το μήνυμα υποβολής.

  6. Κατά τη δημιουργία μιας υποβολής, πρέπει να καθορίσουμε τους γονείς της νέας υποβολής.     Αυτό χρησιμοποιεί την άκρη του HEAD για τον μοναδικό γονέα.

  7. Η Rugged (και το Libgit2) μπορούν προαιρετικά να ενημερώσουν μια αναφορά όταν πραγματοποιούν μια υποβολή.

  8. Η τιμή επιστροφής είναι ο αριθμός SHA-1 ενός νέου αντικειμένου υποβολής, τον οποίο μπορούμε στη συνέχεια να χρησιμοποιήσουμε για να αποκτήσουμε ένα αντικείμενο Commit.

Ο κώδικας Ruby είναι ωραίος και καθαρός και δεδομένου ότι το Libgit2 κάνει τη βαριά δουλειά, αυτός ο κώδικας θα τρέξει και αρκετά γρήγορα. Αν δεν είμαστε ρουμπίστες, μπορούμε να δούμε κάποιες άλλες συνδέσεις στην ενότητα Other Bindings.

Προηγμένη λειτουργικότητα

Το Libgit2 έχει κάποιες δυνατότητες που δεν εμπίπτουν στο πεδίο εφαρμογής του πυρήνα του Git. Ένα παράδειγμα είναι η σύνδεσιμότητα (pluggability): Το Libgit2 μάς επιτρέπει να παρέχουμε προσαρμοσμένα οπίσθια συστήματα (“backends”) για διάφορους τύπους λειτουργιών, ώστε μπορούμε να αποθηκεύουμε πράγματα με διαφορετικό τρόπο από ό,τι κάνει το stock Git. Το Libgit2 επιτρέπει εξατομικευμένα οπίσθια συστήματα για τη διαμόρφωση, την αποθήκευση αναφορών και τη βάση δεδομένων αντικειμένων, μεταξύ άλλων.

Ας ρίξουμε μια ματιά στο πώς λειτουργεί αυτό. Ο παρακάτω κώδικας δανείζεται από ένα σύνολο παραδειγμάτων συστημάτων υποστήριξης που παρέχονται από την ομάδα Libgit2 (και βρίσκεται στη διεύθυνση https://github.com/libgit2/libgit2-backends). Ακολουθεί ένας τρόπος που ένα εξατομικευμένο σύστημα υποστήριξης βάσης δεδομένων αντικειμένων εγκαθιστάται:

git_odb *odb;
int error = git_odb_new(&odb);                    (1)

git_odb_backend *my_backend;
error = git_odb_backend_mine(&my_backend, /*…*/); (2)

error = git_odb_add_backend(odb, my_backend, 1);  (3)

git_repository *repo;
error = git_repository_open(&repo, "some-path");
error = git_repository_set_odb(odb);              (4)

(Ας σημειωθεί ότι τα σφάλματα καταγράφονται, αλλά δεν έχουν αντιμετωπίζονται.)

  1. Αρχικοποιούμε μια κενή βάση δεδομένων αντικειμένων (ODB) “frontend”, που θα λειτουργήσει ως δοχείο (container) για το οπίσθιο σύστημα που είναι αυτά που κάνουν την πραγματική δουλειά.

  2. Αρχικοποιούμε ένα οπίσθιο σύστημα εξατομικευμένης ODB.

  3. Προσθέτουμε το οπίσθιο σύστημα στο πρόσθιο σύστημα.

  4. Ανοίγουμε ένα αποθετήριο και το ρυθμίζουμε ώστε να χρησιμοποιεί την ODB μας για να αναζητά αντικείμενα.

Αλλά τι είναι αυτό το git_odb_backend_mine; Λοιπόν, αυτός είναι η κατασκευή (constructor) για τη δική μας υλοποίηση της ODB και μπορούμε να κάνουμε ό,τι θέλουμε εκεί, αρκεί να συμπληρώσουμε τη δομή git_odb_backend σωστά. Ας δούμε πώς θα μπορούσε να είναι:

typedef struct {
    git_odb_backend parent;

    // Κάποια άλλα πράγματα
    void *custom_context;
} my_backend_struct;

int git_odb_backend_mine(git_odb_backend **backend_out, /*…*/)
{
    my_backend_struct *backend;

    backend = calloc(1, sizeof (my_backend_struct));

    backend->custom_context = …;

    backend->parent.read = &my_backend__read;
    backend->parent.read_prefix = &my_backend__read_prefix;
    backend->parent.read_header = &my_backend__read_header;
    // …

    *backend_out = (git_odb_backend *) backend;

    return GIT_SUCCESS;
}

Ο πιο ανεπαίσθητος περιορισμός εδώ είναι ότι το πρώτο μέλος της my_backend_struct πρέπει να είναι μια δομή git_odb_backend· αυτό εξασφαλίζει ότι η διάταξη μνήμης είναι αυτό που ο κώδικας Libgit2 αναμένει να είναι. Το υπόλοιπο είναι αυθαίρετο· αυτή η δομή μπορεί να είναι όσο μεγάλη ή όσο μικρή θέλουμε να είναι.

Η συνάρτηση αρχικοποίησης εκχωρεί κάποια μνήμη για τη δομή, ρυθμίζει το εξατομικευμένο πλαίσιο και στη συνέχεια συμπληρώνει τα μέλη της δομής parent που υποστηρίζει. Στο αρχείο include/git2/sys/odb_backend.h στον πηγαίο κώδικα του Libgit2 μπορούμε να βρούμε ένα πλήρες σύνολο υπογραφών κλήσεων· η συγκεκριμένη περίπτωση χρήσης μας θα μας βοηθήσει να προσδιορίσουμε ποια από αυτές θα θέλουμε να υποστηρίξουμε.

Other Bindings

Άλλες συνδέσεις

Το Libgit2 έχει συνδέσεις για πολλές γλώσσες. Εδώ παρουσιάζουμε ένα μικρό παράδειγμα χρησιμοποιώντας μερικά από τα πιο ολοκληρωμένα πακέτα υποβολής την εποχή που γράφεται αυτό το βιβλίο· υπάρχουν βιβλιοθήκες για πολλές άλλες γλώσσες, συμπεριλαμβανομένων των C++, Go, Node.js, Erlang και JVM, όλες σε διάφορα στάδια ωριμότητας. Η επίσημη συλλογή συνδέσεων μπορεί να βρεθεί με την περιήγηση των αποθετηρίων στη διεύθυνση https://github.com/libgit2. Ο κώδικας που θα γράψουμε θα επιστρέφει το μήνυμα υποβολής από την υποβολή που στην οποία δείχνει τελικά τελικάο HEAD (κάτι σαν git log -1).

LibGit2Sharp

Αν γράφουμε μια εφαρμογή .NET ή Mono, το LibGit2Sharp (https://github.com/libgit2/libgit2sharp) είναι αυτό που ψάχνουμε. Οι συνδέσεις είναι γραμμένες σε C# και δοθεί ιδιαίτερη φροντίδα οι κλήσεις της Libgit2 να δίνοην ένα αίσθημα API CLR. Ακολουθεί το παράδειγμά μας:

new Repository(@"C:\path\to\repo").Head.Tip.Message;

Για εφαρμογές Windows σε επιτραπέζιους υπολογιστές, υπάρχει ακόμα ένα πακέτο NuGet που θα μας βοηθήσει να ξεκινήσουμε γρήγορα.

objective-git

Εάν η εφαρμογή μας εκτελείται σε πλατφόρμα της Apple, πιθανώς να χρησιμοποιούμε τη γλώσσα Objective C ως γλώσσα υλοποίησης. Objective-Git (https://github.com/libgit2/objective-git) είναι το όνομα των συνδέσεων Libgit2 για αυτό το περιβάλλον. Το πρόγραμμα παράδειγμα μοιάζει με αυτό:

GTRepository *repo =
    [[GTRepository alloc] initWithURL:[NSURL fileURLWithPath: @"/path/to/repo"] error:NULL];
NSString *msg = [[[repo headReferenceWithError:NULL] resolvedTarget] message];

Το Objective-git είναι πλήρως διαλειτουργικό με το Swift, άρα δεν υπάρχει πρόβλημα αν έχουμε αφήσει πίσω την Objective-C.

pygit2

Οι συνδέσεις για το Libgit2 στην Python ονομάζονται Pygit2 και μπορούν να βρεθούν στην http://www.pygit2.org/. Το παράδειγμα του προγράμματος μας:

pygit2.Repository("/path/to/repo") # άνοιξε αποθετήριο
    .head                          # πάρε τον τρέχοντα κλάδο
    .peel(pygit2.Commit)           # πήγαινε στην υποβολή
    .message                       # διάβασε το μήνυμα

Περαιτέρω ανάγνωση

Φυσικά, μία πλήρης αντιμετώπιση των δυνατοτήτων του Libgit2 είναι εκτός του σκοπού αυτού του βιβλίου. Αν θέλουμε περισσότερες πληροφορίες σχετικά με το ίδιο το Libgit2, υπάρχει τεκμηρίωση API στη διεύθυνση https://libgit2.github.com/libgit2 και ένα σύνολο οδηγών στη διεύθυνση https://libgit2.github.com/docs. Για τις άλλες συνδέσεις, θα πρέπει να ελέγξουμε το δεματιασμένο README και τις δοκιμές· υπάρχουν συχνά μικρά tutorial και δείκτες για την περαιτέρω ανάγνωση εκεί.