Git
Chapters ▾ 2nd Edition

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

JGit

Εάν θέλουμε να χρησιμοποιήσουμε το Git μέσα σε ένα πρόγραμμα Java, υπάρχει μια πλήρως εξοπλισμένη βιβλιοθήκη Git που ονομάζεται JGit. Το JGit είναι μια σχετικά πλήρης εφαρμογή του Git γραμμένη σε Java και χρησιμοποιείται ευρέως στην κοινότητα της Java. Το έργο JGit βρίσκεται κάτω από ομπρέλα τοου Eclipse και το σπίτι του βρίσκεται στο http://www.eclipse.org/jgit.

Getting Set Up

Υπάρχουν διάφοροι τρόποι για να συνδέσουμε το έργο μας με το JGit και να αρχίσουμε να γράφουμε κώδικα σε αυτό. Πιθανώς το πιο εύκολο είναι να χρησιμοποιήσουμε τη Maven —η ενσωμάτωση ολοκληρώνεται προσθέτοντας το ακόλουθο απόσπασμα στην ετικέτα <εξάρτηση> στο αρχείο pom.xml:

<dependency>
    <groupId>org.eclipse.jgit</groupId>
    <artifactId>org.eclipse.jgit</artifactId>
    <version>3.5.0.201409260305-r</version>
</dependency>

Η version σίγουρα έχει προχωρήσει από τότε που γραφόταν αυτό το βιβλίο· ελέγχουμε τη http://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit για ενημερωμένες πληροφορίες για το αποθετήριο. Μόλις ολοκληρωθεί αυτό το βήμα, η Maven θα αποκτήσει και θα χρησιμοποιήσει αυτόματα τις βιβλιοθήκες JGit που χρειαζόμαστε.

Αν προτιμάμε να διαχειρίιζόμαστε μόνοι μας τις δυαδικές εξαρτήσεις, τα προ-κατασκευασμένα δυαδικά αρχεία JGit διατίθενται από τη http://www.eclipse.org/jgit/download. Μπορούμε να τα χτίσουμε στο έργο μας, τρέχοντας μια εντολή όπως αυτή:

javac -cp .:org.eclipse.jgit-3.5.0.201409260305-r.jar App.java
java -cp .:org.eclipse.jgit-3.5.0.201409260305-r.jar App

Διοχέτευση

Το JGit έχει δύο βασικά επίπεδα API: διοχέτευσης και πορσελάνης. Η ορολογία για αυτά προέρχεται από το ίδιο το Git και το JGit χωρίζεται σε περίπου τα ίδια είδη: τα API πορσελάνης είναι ένα φιλικό πρόσθιο σύστημα για κοινές ενέργειες σε επίπεδο χρήστη (τα πράγματα που θα έκανε κανονικός χρήστης στη γραμμή εντολών Git), ενώ τα API διοχέτευσης είναι για την άμεση αλληλεπίδραση με αντικείμενα χαμηλού επιπέδου του αποθετηρίου.

Το σημείο εκκίνησης για τις περισσότερες συνεδρίες του JGit είναι η κλάση Repository και το πρώτο πράγμα που θα θελήσουμε να κάνουμε είναι να δημιουργήσουμε ένα στιγμιότυπό (instance) του. Για ένα αποθετήριο με βάση το σύστημα αρχείων (ναι, το JGit επιτρέπει και άλλα μοντέλα αποθήκευσης), αυτό επιτυγχάνεται χρησιμοποιώντας το FileRepositoryBuilder:

// Δημιουργία νέου αποθετηρίου
Repository newlyCreatedRepo = FileRepositoryBuilder.create(
    new File("/tmp/new_repo/.git"));
newlyCreatedRepo.create();

// Άνοιγμα υπάρχοντος αποθετηρίου
Repository existingRepo = new FileRepositoryBuilder()
    .setGitDir(new File("my_repo/.git"))
    .build();

Ο builder έχει ένα εύγλωττο API για να παρέχει ό,τι χρειάζεται για να βρει ένα αποθετήριο Git, ανεξάρτητα από το αν το πρόγραμμα μας γνωρίζει πού ακριβώς βρίσκεται. Μπορεί να χρησιμοποιήσει μεταβλητές περιβάλλοντος (.readEnvironment()), να ξεκινήσει από μια θέση στον κατάλογο εργασίας και να αναζητήσει (.setWorkTree(…).findGitDir()) ή απλά να ανοίξει έναν γνωστό κατάλογο .git όπως παραπάνω.

Μόλις έχουμε ένα στιγμιότυπο Repository, μπορούμε να κάνουμε διάφορα πράγματα με αυτό. Ακολουθεί μια γρήγορη επίδειξη:

// Πάρε μία αναφορά
Ref master = repo.getRef("master");

// Πάρε το αντικείμενο στο οποίο δείχνει η αναφορά
ObjectId masterTip = master.getObjectId();

// Rev-parse
ObjectId obj = repo.resolve("HEAD^{tree}");

// Φόρτωσε τα ανεπεξέργαστα περιεχόμενα του αντικειμένου
ObjectLoader loader = repo.open(masterTip);
loader.copyTo(System.out);

// Δημιούργησε έναν κλάδο
RefUpdate createBranch1 = repo.updateRef("refs/heads/branch1");
createBranch1.setNewObjectId(masterTip);
createBranch1.update();

// Διάγραψε έναν κλάδο
RefUpdate deleteBranch1 = repo.updateRef("refs/heads/branch1");
deleteBranch1.setForceUpdate(true);
deleteBranch1.delete();

// Διαμόρφωση
Config cfg = repo.getConfig();
String name = cfg.getString("user", null, "name");

Εδώ συμβαίνουν διάφορα, οπότε ας δούμε ένα-ένα τα διάφορα τμήματα.

Η πρώτη γραμμή παίρνει έναν δείκτη στην αναφορά master. Το JGit αρπάζει αυτόματα την πραγματική αναφορά στον master, που βρίσκεται στη refs/heads/master και επιστρέφει ένα αντικείμενο που μας επιτρέπει να ανακτήσουμε πληροφορίες σχετικά με την αναφορά. Μπορούμε να πάρουμε το όνομα (.getName()) και είτε το αντικείμενο μιας άμεσης αναφοράς (.getObjectId()) είτε την αναφορά στην οποία δείχνει μία συμβολική αναφορά (.getTarget()). Τα αντικείμενα ref χρησιμοποιούνται επίσης για την αναπαράσταση αναφορών ετικετών και αντικειμένων, ώστε μπορούμε να ρωτήσουμε αν η ετικέτα είναι “ξεκολλημένη”, πράγμα που σημαίνει ότι δείχνει τον τελικό στόχο μιας (δυνητικά μακράς) συμβολοσειράς αντικειμένων ετικέτας.

Η δεύτερη γραμμή παίρνει το στόχο της αναφοράς master, ο οποίος επιστρέφεται ως παράμετρος ObjectId. Το ObjectId αντιπροσωπεύει τον αριθμό SHA-1 ενός αντικειμένου, το οποίο μπορεί να υπάρχει ή να μην υπάρχει στη βάση δεδομένων αντικειμένων του Git. Η τρίτη γραμμή είναι παρόμοια, αλλά δείχνει πώς το JGit χειρίζεται τη σύνταξη rev-parse (περισσότερες πληροφορίες στην ενότητα Αναφορές κλάδων)· μπορούμε να περάσουμε οποιονδήποτε προσδιοριστή αντικειμένου κατανοεί το Git και το JGit θα επιστρέψει είτε ένα έγκυρο ObjectId για αυτό το αντικείμενο είτε null.

Οι επόμενες δύο γραμμές δείχνουν τον τρόπο φόρτωσης των ανεπξέργαστων περιεχομένων ενός αντικειμένου. Σε αυτό το παράδειγμα, καλούμε την ObjectLoader.copyTo() για να μεταφέρουμε τα περιεχόμενα του αντικειμένου απευθείας στην stdout, αλλά το η ObjectLoader έχει επίσης μεθόδους για να διαβάσει τον τύπο και το μέγεθος ενός αντικειμένου, καθώς και να το επιστρέψει σαν ένα byte array. Για τα μεγάλα αντικείμενα (δηλαδή αυτά στα οποία η .isLarge() επιστρέφει `true), μπορούμε να καλέσουμε την .openStream() για να αποκτήσουμε ένα αντικείμενο τύπου InputStream που μπορεί να διαβάσει τα ανεπεξέργαστα δεδομένα του αντικειμένου χωρίς να το τραβήξει όλα στη μνήμη με τη μία.

Οι επόμενες γραμμές δείχνουν τι χρειάζεται για τη δημιουργία ενός νέου κλάδου. Δημιουργούμε ένα στιγμιότυπο RefUpdate, διαμορφώνουμε κάποιες παραμέτρους και καλούμε .update() για να ενεργοποιήσουμε την αλλαγή. Αμέσως μετά από αυτό είναι ο κώδικας για τη διαγραφή του ίδιου κλάδου. Ας σημειωθεί ότι η .setForceUpdate(true) απαιτείται για να λειτουργήσει αυτό· διαφορετικά η κλήση .delete() θα επιστρέψει 'REJECTED (ΑΠΟΡΡΙΠΤΕΤΑΙ`) και δεν θα συμβεί τίποτα.

Το τελευταίο παράδειγμα δείχνει τον τρόπο λήψης της τιμής user.name από τα αρχεία ρυθμίσεων του Git. Αυτό το στιγμιότυπο Config χρησιμοποιεί το αποθετήριο που ανοίξαμε νωρίτερα για την τοπική διαμόρφωση, αλλά θα ανιχνεύσει αυτόματα τα καθολικά αρχεία διαμόρφωσης και αρχεία διαμόρφωσης του συστήματος και θα διαβάσει τιμές από αυτά.

Αυτό είναι μόνο ένα μικρό δείγμα πλήρους API διοχέτευσης· υπάρχουν πολλές άλλες διαθέσιμες μέθοδοι και τάξεις. Επίσης, δεν εμφανίζεται εδώ ο τρόπος με τον οποίο η JGit χειρίζεται λάθη, που είναι με με τη χρήση εξαιρέσεων. Τα API του JGit μερικές φορές πετούν τις συνήθεις εξαιρέσεις της Java (όπως την IOException), αλλά υπάρχουν και αρκετοί ειδικοί τύποι εξαιρέσεων JGit που παρέχονται επίσης (όπως NoRemoteRepositoryException, CorruptObjectException και NoMergeBaseException).

Πορσελάνες

Τα API διοχετεύσεων είναι μάλλον πλήρη, αλλά η σύζευξή τους για την επίτευξη κοινών στόχων, όπως η προσθήκη ενός αρχείου στο ευρετήριο ή η πραγματοποίηση μιας νέας υποβολής, μπορεί να είναι δυσχερής. Το JGit παρέχει ένα σύνολο API υψηλότερου επιπέδου για να βοηθήσει με αυτό και το σημείο εισόδου σε αυτά τα API είναι η κλάση Git:

Repository repo;
// construct repo...
Git git = new Git(repo);

Η κλάση Git έχει ένα ωραίο σύνολο μεθόδων υψηλού επιπέδου σε στυλ builder, που μπορούν να χρησιμοποιηθούν για την κατασκευή κάποιων πολύ περίπλοκων συμπεριφορών. Ας ρίξουμε μια ματιά σε ένα παράδειγμα —κάνοντας κάτι σαν την git ls-remote:

CredentialsProvider cp = new UsernamePasswordCredentialsProvider("username", "p4ssw0rd");
Collection<Ref> remoteRefs = git.lsRemote()
    .setCredentialsProvider(cp)
    .setRemote("origin")
    .setTags(true)
    .setHeads(false)
    .call();
for (Ref ref : remoteRefs) {
    System.out.println(ref.getName() + " -> " + ref.getObjectId().name());
}

Αυτό είναι ένα κοινό μοτίβο με την κλάση Git· οι μέθοδοι επιστρέφουν ένα αντικείμενο εντολής που μας επιτρέπει να κάνουμε αλυσιδωτές κλήσεις μεθόδων για να ορίσουμε παραμέτρους, οι οποίες εκτελούνται όταν καλούμε την .call(). Σε αυτήν την περίπτωση, ζητάμε ετικέτες αλλά όχι αναφορές από τον origin του απομακρυσμένου αποθετηρίου. Ας σημειωθεί επίσης η χρήση ενός αντικειμένου CredentialsProvider για την ταυτοποίηση.

Πολλές άλλες εντολές είναι διαθέσιμες μέσω της κλάσης Git, συμπεριλαμβανομένων, μεταξύ άλλων, των add,blame, commit, clean, push, rebase, revert και reset.

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

Αυτό είναι μόνο ένα μικρό δείγμα των πλήρων δυνατοτήτων του JGit. Για όποιον ενδιαφέρεται και θέλιε να μάθει περισσότερα, θα βρούμε πληροφορίες και έμπνευση στα παρακάτω:

  • Η επίσημη τεκμηρίωση του API του JGit διατίθεται ηλεκτρονικά στην http://download.eclipse.org/jgit/docs/latest/apidocs.   Αυτά είναι τα πρότυπα Javadoc, έτσι ώστε το αγαπημένο μας IDE JVM να μπορεί να τα εγκαταστήσει και σε τοπικό επίπεδο.

  • Το JGit Cookbook στην https://github.com/centic9/jgit-cookbook έχει πολλά παραδείγματα για το πώς να κάνουμε συγκεκριμένες εργασίες με το JGit.

  • Υπάρχουν αρκετοί πόροι που έχουν επισημανθεί στην http://stackoverflow.com/questions/6861881.