Git
Chapters ▾ 2nd Edition

A2.2 Appendix B: Git in je applicaties inbouwen - Libgit2

Libgit2

© Een andere optie die tot je beschikking staat is om Libgit2 te gebruiken. Libgit2 is een implementatie van Git die nergens van afhankelijk is, met een focus op het behouden van een goede API om vanuit andere programma’s te gebruiken. Je kunt het vinden op http://libgit2.github.com.

Laten we allereerst eens kijken hoe de C API eruit ziet. Hier is een razendsnel overzicht:

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

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

// Druk een aantal kenmerken van de commit af
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);

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

De eerste paar regels openen een Git repository. Het git_repository type vertegenwoordigt een handvat (handle) naar een repository met een buffer in het geheugen. Dit is de eenvoudigste manier, voor als je het exacte pad naar de werk directory of .git folder weet. Er is ook git_repository_open_ext die opties voor zoeken in zich heeft, git_clone en z’n vriendjes voor het maken van een lokale clone van een remote repository, en git_repository_init voor het maken van een hele nieuwe repository.

Het tweede stuk code gebruikt rev-parse syntax (zie Branch referenties voor meer details) om de commit te krijgen waar HEAD uiteindelijk naar wijst. Het type dat je terugkrijgt is een git_object pointer, wat staat voor iets dat bestaat in de Git object database voor een repository. git_object is eigenlijk en “parent” type voor een aantal verschillende soorten objecten; de geheugenindeling voor elk van de “child” typen is hetzelfde als voor git_object, dus je kunt het veilig naar de juiste omzetten (casten). In dit geval, zou git_object_type(commit) GIT_OBJ_COMMIT teruggeven, dus het is veilig te casten naar een git_commit pointer.

Het volgende stuk laat zien hoe de kenmerken van een commit kunnen worden benadert. De laatste regel hier gebruikt een git_oid type; dit is hoe Libgit2 een SHA-1 hash representeert.

Uit dit voorbeeld beginnen een aantal patronen naar boven te komen:

  • ALs je een pointer declareert en een referentie hieraan doorgeeft met een Libgit2 aanroep, zal die aanroep waarschijnlijk een integer foutcode teruggeven. Een 0 waarde geeft een succes aan; al het andere is een fout.

  • Als Libgit2 een pointer voor je vult, ben jij verantwoordelijk deze weer vrij te geven.

  • Als Libgit2 een const pointer teruggeeft van een aanroep, hoef je deze niet vrij te geven, maar het wordt ongeldig als het object waar het toe behoort vrij wordt gegeven.

  • In C schrijven is nogal uitdagend.

Dat laatste houdt in dat het niet erg waarschijnlijk is dat je in C gaat schrijven als je Libgit2 gebruikt. Gelukkig is er een aantal taal-specifieke "bindings" beschikbaar die het redelijk eenvoudig maken om vanuit jouw specifieke taal en omgeving met Git repositories te werken. Laten we naar het bovenstaande voorbeeld kijken die we in Ruby schrijven met gebruikmaking van de bindings voor deze taal die Rugged heet, en gevonden kan worden op 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

Zoals je kunt zien, is de code een stuk schoner. Ten eerste, Rugged gebruikt exceptions; het kan dingen gooien als ConfigError of `ObjectError om fout situaties aan te geven. Ten tweede, er is geen expliciete vrij geven van middelen, omdat Ruby een garbage-collector kent. Laten we een kijkje nemen naar een iets ingewikkelder voorbeeld: vanaf het begin een commit samenstellen

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. Maak een nieuwe blob, die de inhoud van een nieuw bestand bevat.

  2. Vul de index met tree van de commit van de head, en voeg het nieuwe bestand toe aan het pad nexfile.txt.

  3. Dit maakt een nieuwe tree aan in de ODB, en gebruikt deze voor de nieuwe commit.

  4. We gebruiken hetzelfde kenmerk voor de velden van zowel de auteur en de committer.

  5. Het commit bericht.

  6. Als je een commit maakt, moet je de ouders van de nieuwe commit opgeven. Dit gebruikt de punt van HEAD voor de enige ouder.

  7. Rugged (en Libgit2) kan optioneel een referentie updaten als er een commit wordt gemaakt.

  8. De retourwaarde is de SHA-1 has van een nieuw commit object, die je dan weer kan gebruiken om een Commit object te krijgen.

De code in Ruby is mooi en schoon, maar omdat Libgit2 het zware werk doet, zal deze code ook redelijk snel draaien. Als je niet zo’n Rubyist bent, we behandelen nog een aantal andere bindings in Andere bindings.

Functionaliteit voor gevorderden

Libgit2 heeft een aantal mogelijkheden die buiten het bereik liggen van de kern van Git. Een voorbeeld is de inhaakmogelijkheid (pluggability): Libgit2 geeft je de mogelijkheid om eigengemaakte “backends” voor een aantal type operaties te verzorgen, zodat je dingen anders kunt opslaan dan Git standaard doet. Libgit2 staat, onder andere, eigengemaakte backends toe voor configuratie, opslag van refs en de object database.

Laten we eens kijken hoe dit in elkaar zit. De onderstaande code is geleend van de set van backend voorbeelden die door het Libgit2 team wordt geleverd (deze kunnen worden gevonden op https://github.com/libgit2/libgit2-backends). Hier zie je hoe een eigengemaakte backend voor de objectdatabase wordt opgezet:

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)

(Note that errors are captured, but not handled. We hope your code is better than ours.)

  1. Initialiseer een lege object database (ODB) “frontend”, die als een bevatter dient voor de “backends” die het echte werk zullen uitvoeren.

  2. Initialiseer een eigengemaakte ODB backend.

  3. Voeg de backend toe aan de frontend.

  4. Open een repository, en stel het in om onze ODB te gebruiken om objecten op te zoeken.

Maar wat is die git_odb_backend_mine voor een ding? Nou, dat is de constructor voor je eigen ODB implementatie, en je kunt daarin doen wat je wilt, zolang als je de git_odb_backend structuur maar juist vult. Hier is hoe het eruit zou kunnen zien:

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;
}

Een heel subtiele beperking hier is dat de eerste member van my_backend_struct een git_odb_backend structure moet zijn; dit zorgt ervoor dat de geheugenindeling dat is wat de Libgit2 code ervan verwacht. De rest is vrij in te vullen; deze structuur kan zo groot of zo klein zijn als je het nodig vindt.

De initialisatie functie alloceert wat geheugen voor de structure, richt de eigen context in en geeft daarna de implementaties van de members van de parent structure die het ondersteunt. Neem een kijkje in het include/git2/sys/odb_backend.h bestand in de Libgit2 broncode voor een volledige set van aanroep-signatures; jou specifieke use-case helpt je bepalen welke van deze je wilt ondersteunen.

Andere bindings

Libgit2 heeft bindings voor vele talen. Hier laten we je een klein voorbeeld waarbij we een aantal van de meer complete binding pakketen op het moment van schrijven; er bestaan libraries voor vele andere talen, waaronder C++, Go, Node.js, Erlang, en de JVM, alle in verschillende stadia van volwassenheid. De officiele verzameling van bindings kan je vinden door in de repositories te zoeken op https://github.com/libgit2. De code die we zullen schrijven gaat het commit bericht teruggeven van de commit waar de HEAD uiteindelijk naar toe wijst (vergelijkbaar met git log -l).

LibGit2Sharp

Als je een applicatie in .NET of Mono schrijft, is LigGit2Sharp (https://github.com/libgit2/libgit2sharp) waar je naar op zoek bent. De bindings zijn geschreven in C#, and er is veel zorg gegeven aan het inpakken van de ruwe Libgit2 calls met CLR APIs die natuurlijk aandoen. Dit is hoe ons voorbeeld programma eruit ziet:

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

Voor Windows applicaties voor het werkblad is er zelfs een NuGet pakket dat je helpt snel op gang te komen.

objective-git

Als je applicatie op een Apple platform draait, gebruik je waarschijnlijk Objective-C als je implementatie taal. Objective-Git (https://github.com/libgit2/objective-git) is de naam van de Libgit2 bindings voor die omgeving. Ons voorbeeld programma ziet er zo uit:

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

Objective-git kan naadloos samenwerken met Swift, dus je hoeft er niet bang voor te zijn als je Objective-C hebt laten rondslingeren.

pygit2

De bindings voor Libgit2 in Python heten Pygit2, en kunnen worden gevonden op http://www.pygit2.org/. Ons voorbeeld programma:

pygit2.Repository("/path/to/repo") # open repository
    .head                          # get the current branch
    .peel(pygit2.Commit)           # walk down to the commit
    .message                       # read the message

Meer lezen

Een volledige behandeling van de mogelijkheden van Libgit2 ligt buiten het bestek van dit boek. Als je meer informatie wilt over Libgit2 zelf, kan je de API documentatie vinden op https://libgit2.github.com/libgit2, en een aantal handboeken op https://libgit2.github.com/docs. Voor de andere bindings, kan je de meegeleverde README en testen bekijken; je kunt er vaak kleine tutorials en hints naar meer informatie vinden.