Chapters ▾ 2nd Edition

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

Libgit2

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

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

// Open a repository
git_repository *repo;
int error = git_repository_open(&repo, "/path/to/repository");

// Dereference HEAD to a commit
git_object *head_commit;
error = git_revparse_single(&head_commit, repo, "HEAD^{commit}");
git_commit *commit = (git_commit*)head_commit;

// Print some of the commit's properties
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);

// Cleanup
git_commit_free(commit);
git_repository_free(repo);

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

Το δεύτερο κομμάτι του κώδικα χρησιμοποιεί σύνταξη rev-parse (βλ. ενότητα Αναφορές κλάδων σχετικά) για να πάρει την υποβολή στην οποία δείχνει τελικά ο HEAD. Ο τύπος που επιστρέφεται είναι ένας δείκτης git_object, που αντιπροσωπεύει κάτι που υπάρχει στη βάση δεδομένων αντικειμένων Git για ένα αποθετήριο. Το git_object είναι στην πραγματικότητα ένας τύπος “parent” για πολλά διαφορετικά είδη αντικειμένων· η διάταξη μνήμης για καθέναν από τους τύπους “child” είναι ίδια με εκείνη του git_object, ώστε να μπορούμε να κάνουμε μετατροπή στο σωστό με ασφάλεια. Σε αυτήν την περίπτωση, η git_object_type(commit) θα επέστρεφε GIT_OBJ_COMMIT, επομένως είναι ασφαλές να κάνουμε μετατροπή σε έναν δείκτη 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. Γεμίζουμε το ευρετήριο με το δέντρο της υποβολής της κεφαλής και προσθέτουμε το νέο αρχείο στη διαδρομή (path) newfile.txt.

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

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

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

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

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

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

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

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

Το 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(repo, odb); // (4)

Ας σημειωθεί ότι τα σφάλματα καταγράφονται, αλλά δεν αντιμετωπίζονται. Ελπίζουμε ότι ο κώδικά σας είναι καλύτερος από τον δικό μας.

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

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

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

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

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

typedef struct {
    git_odb_backend parent;

    // Some other stuff
    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 μπορούμε να βρούμε ένα πλήρες σύνολο υπογραφών κλήσεων· η συγκεκριμένη περίπτωση χρήσης μας θα μας βοηθήσει να προσδιορίσουμε ποια από αυτές θα θέλουμε να υποστηρίξουμε.

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

Το 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 και μπορούν να βρεθούν στην https://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 και δείκτες για την περαιτέρω ανάγνωση εκεί.