Git
Chapters ▾ 2nd Edition

A2.3 Appendix B: Git in je applicaties inbouwen - JGit

JGit

Als je Git wilt gebruiken vanuit een Java programma, is er een Git library die de volledige Git functionaliteit ondersteunt JGit geheten. JGit is een relatief functioneel volledige implementatie van Git geschreven in zuiver Java, en het wordt veel gebruikt in de Java gemeenschap. Het JGit project valt onder de Eclipse paraplu, en het adres waar het kan worden gevonden is http://www.eclipse.org/jgit.

De boel opzetten

Er zijn een aantal manieren om verbinding te maken met je project via JGit, om code ervoor te schrijven. Waarschijnlijk is de eenvoudigste manier om Maven te gebruiken - de integratie wordt bereikt door het volgende fragment aan de <dependencies> tag in je pom.xml bestand toe te voegen:

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

De versie zal hoogstwaarschijnlijk al vooruit zijn gegaan tegen de tijd dat je dit leest; kijk even op http://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit voor bijgewerkte repository informatie. Als deze stap eenmaal is genomen, zal Maven automatisch een versie ophalen en de JGit libraries gebruiken die je nodig zult hebben.

Als je je binaire libraries liever zelf onderhoudt, zijn kant en klaar gebouwde JGit binairies beschikbaar op http://www.eclipse.org/jgit/download. Je kunt ze in je project inbouwen door een commando als deze aan te roepen:

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

Binnenwerk

JGit heeft twee basale niveaus van API: binnenwerk en koetswerk (plumbing en porcelain) De terminologie hiervoor komt van Git zelf, en JGit is ingedeeld in grofweg dezelfde soorten van gebieden: porcelain API’s zijn een vriendelijke voorkant voor reguliere gebruikers acties (het soort van dingen die een gewone gebruiker een commando regel voor zou gebruiken), terwijl de plumbing API’s er zijn voor interacties met repository objecten die zich diep in het systeem bevinden.

Het vertrekpunt voor de meeste JGit sessies is de Repository klasse, en het eerste wat je zult willen doen is om er een instantie voor te maken. Voor een repository die gebaseerd is op een bestandssysteem (ja, JGit ondersteunt ook andere opslagmodellen), wordt dit gedaan door middel van FileRepositoryBuilder:

// Create a new repository
Repository newlyCreatedRepo = FileRepositoryBuilder.create(
    new File("/tmp/new_repo/.git"));
newlyCreatedRepo.create();

// Open an existing repository
Repository existingRepo = new FileRepositoryBuilder()
    .setGitDir(new File("my_repo/.git"))
    .build();

Deze builder heeft een vloeiende API waarmee alles kan worden voorzien die het nodig heeft om een Git repository te vinden, of je programma de preciese locatie weet of niet. Het kan de omgevingsvariabelen (.readEnvironment()) gebruiken, ergens van een plaats in de werk directory beginnen te zoeken (.setWorkTree(...).findGitDir()), of gewoon een bekende .git directory openen zoals hierboven.

Als je eenmaal een Repository instantie hebt, kan je er van allerlei dingen mee doen. Hier is een kleine bloemlezing:

// Get a reference
Ref master = repo.getRef("master");

// Get the object the reference points to
ObjectId masterTip = master.getObjectId();

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

// Load raw object contents
ObjectLoader loader = repo.open(masterTip);
loader.copyTo(System.out);

// Create a branch
RefUpdate createBranch1 = repo.updateRef("refs/heads/branch1");
createBranch1.setNewObjectId(masterTip);
createBranch1.update();

// Delete a branch
RefUpdate deleteBranch1 = repo.updateRef("refs/heads/branch1");
deleteBranch1.setForceUpdate(true);
deleteBranch1.delete();

// Config
Config cfg = repo.getConfig();
String name = cfg.getString("user", null, "name");

Er gebeurt hier eigenlijk best wel veel, dus laten we het stukje bij beetje doornemen.

De eerste regel haalt een pointer naar de master referentie op. JGit pakt automatisch de echte master ref op, die gedefinieerd is op refs/heads/master, en geeft een object terug waarmee je de informatie over de referentie kunt ophalen. Je kunt de naam te pakken krijgen (.getName()), en het doel object van een directe referentie (.getObjectId()) of de referentie waar een symbolische ref naar wijst (.getTarget()). Ref objecten worden ook gebruikt om tag refs en objecten te vertegenwoordigen, dus je kunt vragen of de tag is “geschild” (“peeled”), wat inhoud dat het wijst naar het uiteindelijke doel van een (potentieel lange) tekenreeks van tag objecten.

De tweede regel haalt het doel van de master referentie op, die wordt teruggegeven als een ObjectId instantie. ObjectId vertegenwoordigt de SHA-1 hash van een object, die al dan niet kan bestaan in de object database van Git. De derde regel is vergelijkbaar, maar laat zien hoe JGit omgaat met de rev-parse syntax (meer hierover in Branch referenties); je kunt er elke object specificatie aan doorgeven die Git begrijpt, en JGit geeft een geldige ObjectId terug voor dat object, of null.

De volgende twee regels laten zien hoe de rauwe inhoud van een object wordt ingeladen. In dit voorbeeld, roepen we ObjectLoader.copyTo() aan om de inhoud van het object direct naar stdout doorgeven, maar ObjectLoader heeft ook methoden om het type, de groote van het object te lezen, zowel als om de inhoud als een byte array terug te geven. Voor grote objeten (waar .isLarge() de waarde true teruggeeft), kan je .openStream() aanroepen om een InputStream-achtig object terug te krijgen die de data van de rauwe kan lezen zonder het eerst in z’n geheel in geheugen te lezen.

De volgende paar regels laten zien wat er voor nodig is om een nieuwe branch te maken. We maken een RefUpdate instantie, configureren een paar parameters, en roepen .update() aan om de wijziging af te trappen. Direct hierna is de code om dezelfde branch te verwijderen. Merk op dat .setForceUpdate(true) nodig is om dit te laten werken; anders zal de aanroep naar .delete() de waarde REJECTED teruggeven, en er gebeurt helemaal niets.

Het laatste voorbeeld laat zien hoe je de waarde user.name uit de Git configuratie bestanden kan ophalen. Deze instantie van Config gebruikt de repository die we eerder geopende hebben voor de lokale configuratie, maar pakt automatisch de wijzigingen op in de globale en systeem configuratie bestanden en leest daar ook waarden uit.

Dit is maar een kleine voorproevertje van de volledige binnenwerk API; er zijn nog veel meer methoden en klassen beschikbaar. Wat we hier ook niet hebben laten zien is de manier waarop JGit fouten behandelt, namelijk door middel van exceptions. De API’s van JGit gooien soms standaard Java excepties (zoals IOException), maar er is een heel scala aan JGit-specifieke exceptie typen die ook beschikbaar zijn (zoals NoRemoteRepositoryException, CorruptObjectException, en NoMergeBaseException).

Porcelein

De binnenwerk API’s zijn nogal compleet, maar het kan nogal bewerkelijk zijn om ze aan elkaar te knopen om een regulier doel te bereiken, zoals het toevoegen van een bestand aan de index, of een nieuwe commit maken. JGit levert een aantal API’s op een hoger niveau om je hiermee te helpen, en het beginpunt van deze API’s is de Git klasse:

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

De Git klasse heeft een leuke verzameling hoog-niveau methoden in de builder-stijl die kunnen worden gebruikt om een redelijk ingewikkeld gedrag samen te stellen. Laten we een kijkje nemen naar een voorbeeld - iets doen als 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());
}

Dit is een normaal patroon met de Git klasse; de methode geeft een commando object terug waarmee je methode aanroepen aaneen kunt rijgen om parameters te zetten, die worden uitgevoerd als je .call() aanroept. In dit geval, vragen we de origin remote om tags, maar niet om heads. Merk oko het gebruik van een CredentialsProvider object op, voor autenticatie.

Vele andere commando’s zijn beschikbaar via de Git klasse, inclusief (maar niet beperkt tot) add, blame, commit, clean, push, rebase, revert en reset.

Meer lezen

Dit is maar een kleine selectie uit de volledige mogelijkheden van JGit. Als je geïnteresseerd bent en meer wilt lezen, kan je hier kijken voor meer informatie en inspiratie: