Git
Chapters ▾ 2nd Edition

A2.2 Anhang B: Git in deine Anwendungen einbetten - Libgit2

Libgit2

Eine weitere Möglichkeit, die dir zur Verfügung steht, ist die Verwendung von Libgit2. Mit Libgit2 ist eine von Abhängigkeiten freie Implementierung von Git, wobei der Schwerpunkt auf einer ansprechenden API zur Integration in andere Anwendungen liegt. Du findest das Programm unter https://libgit2.org.

Lass uns zunächst einen Blick auf die C-API werfen. Hier ist eine kurzer Überblick:

// 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);

Die ersten beiden Zeilen öffnen ein Git-Repository. Der git_repository Typ repräsentiert einen Handle auf ein Repository mit Cache im Arbeitsspeicher. Diese Methode ist die am einfachsten anzuwendende, wenn man den genauen Pfad zum Arbeitsverzeichnis oder zum .git Ordner eines Repositorys kennt. Es gibt auch git_repository_open_ext, das Optionen für die Suche enthält, git_clone und seine Verwandten für das Erstellen eines lokalen Klons eines Remote-Repositorys und git_repository_init für das Erstellen eines völlig neuen Repositorys.

Der zweite Teil des Codes verwendet die rev-parse Syntax (siehe auch Branch Referenzen), um den Commit zu erhalten, auf den HEAD letztlich zeigt. Der zurückgegebene Typ ist ein git_object Pointer, der ein Objekt repräsentiert, das in der Git-Objekt-Datenbank für ein Repository steht. Bei git_object handelt es sich eigentlich um einen „Parent“-Typ für mehrere verschiedene Arten von Objekten. Das Speicher-Layout für jeden der „Child“-Typen ist das Gleiche wie bei git_object, so dass du sicher auf den richtigen Typ verweisen kannst. In diesem Fall würde git_object_type(commit) ein GIT_OBJ_COMMIT zurückgeben, so dass es sicher auf einen git_commit Pointer zeigt.

Der nächste Abschnitt zeigt, wie auf die Eigenschaften des Commits zugegriffen werden kann. Die letzte Zeile, hier, verwendet einen git_oid Typ; das ist das Anzeige-Format von Libgit2 für einen SHA-1-Hash.

Aus diesem Beispiel, wurden einige Pattern erstellt:

  • Wenn du einen Pointer definieren und ihm eine Referenz in einem Libgit2-Aufruf übergibst, wird dieser Aufruf wahrscheinlich einen ganzzahligen Fehlercode zurückgeben. Ein Wert von 0 zeigt den Erfolg an; alles andere signalisiert einen Fehler.

  • Auch wenn Libgit2 einen Zeiger für dich erstellt, du bist dafür verantwortlich, ihn freizugeben.

  • Wenn Libgit2 einen const Pointer aus einem Aufruf zurückgegeben hat, musst du ihn nicht freigeben, aber er wird ungültig, wenn das Objekt, zu dem er gehört, freigegeben wird.

  • Das Schreiben in C kann ein bisschen mühselig sein.

Das letzte Argument kann bedeuten, dass du eventuell keinen C-Code schreiben wirst, wenn du Libgit2 verwendest. Glücklicherweise gibt es eine Reihe von sprachspezifischen Anbindungen, die es ziemlich einfach machen, mit Git-Repositorys in Ihrer spezifischen Programmier-Sprache und -Umgebung zu arbeiten. Schauen wir uns das obige Beispiel an, das mit den Ruby-Anbindungen für Libgit2 geschrieben wurde, die Rugged genannt werden und unter https://github.com/libgit2/rugged zu finden sind.

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

Wie du siehst, ist der Code viel weniger verwirrend. Erstens verwendet Rugged Ausnahmeregeln; es kann Dinge wie ConfigError oder ObjectError aktivieren, um so Fehlerzustände zu signalisieren. Zweitens gibt es keine explizite Freigabe von Ressourcen, da in Ruby eine automatische Speicherbereinigung aktiv ist. Schauen wir uns ein etwas komplexeres Beispiel an: einen Commit von Grund auf neu zu erstellen

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. Einen neuen Blob erstellen, der den Inhalt einer neuen Datei enthält.

  2. Ergänze den Index mit dem ersten Wert des Commit-Baums und füge die Datei im Pfad newfile.txt hinzu.

  3. Damit wird ein neuer Baum in der Objektdatenbank (ODB) erstellt und für den neuen Commit verwendet.

  4. Wir verwenden die gleiche Signatur für das Autoren- und das Committer-Feld.

  5. Die Commit-Beschreibung.

  6. Wenn du einen Commit erstellst, musst du die Elternteile des neuen Commits angeben. Dazu wird das Ende von HEAD für den einzelnen Elternteil verwendet.

  7. Rugged (und Libgit2) können optional eine Referenz aktualisieren, wenn sie einen Commit erstellen.

  8. Der Rückgabewert entspricht dem SHA-1-Hash eines neuen Commit-Objekts, mit dem du dann ein Commit Objekt erzeugen kannst.

Der Ruby-Code ist zwar ganz nett und klar und da Libgit2 die Ausführung übernimmt, wird dieser Code auch relativ schnell laufen. Falls du kein Ruby-Anhänger bist, dann zeigen wir Alternativen in Andere Anbindungen.

Erweiterte Funktionalität

Libgit2 verfügt über einige Funktionen, die nicht zum eigentlichen Umfang von Git gehören. Ein Beispiel ist die Erweiterbarkeit: Libgit2 erlaubt es dir, benutzerdefinierte „Backends“ für unterschiedliche Betriebsarten anzubieten, so dass du Dinge auf eine andere Art speichern kannst, als es mit Git möglich ist. Libgit2 erlaubt benutzerdefinierte Backends unter anderem zum Konfigurieren, die Ref-Speicherung und für die Objektdatenbank.

Schauen wir uns an, wie das genau funktioniert. Der untenstehende Code ist aus einer Reihe von Backend-Beispielen des Libgit2-Teams entlehnt (die unter https://github.com/libgit2/libgit2-backends zu finden sind). So wird ein benutzerdefiniertes Backend für die Objektdatenbank eingerichtet:

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)

Beachte, dass Fehler zwar erfasst, aber nicht bearbeitet werden. Wir hoffen, dass dein Code besser als unserer ist.

  1. Erzeugen einer leeren Objektdatenbank (ODB) „Frontend“, die als eine Art Container für die „Backends“ dient, mit denen die eigentliche Arbeit erledigt wird.

  2. Ein benutzerdefiniertes ODB-Backend einrichten.

  3. Das Backend dem Frontend hinzufügen.

  4. Ein Repository öffnen und so einrichten, dass es unsere ODB zum Suchen von Objekten verwendet.

Aber was ist das für ein git_odb_backend_mine? Nun, das ist der Konstruktor für Ihre eigene ODB-Implementation. Du kannst dort machen, was immer du willst, solange du die git_odb_backend Struktur richtig eingibst. Hier sieht man, wie es ausschauen könnte:

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

Die wichtigste Bedingung dabei ist, dass das erste Element von my_backend_struct eine git_odb_backend Struktur sein muss. Dadurch wird sichergestellt, dass das Speicherlayout dem entspricht, was der Libgit2-Code erwartet. Der restliche Teil ist beliebig. Diese Struktur kann so groß oder klein sein, wie du es brauchst.

Die Initialisierungsfunktion weist der Struktur Speicherplatz zu, richtet den benutzerdefinierten Kontext ein und fügt dann die Mitglieder der von ihr unterstützten parent Struktur ein. Schaue dir die Datei include/git2/sys/odb_backend.h im Libgit2-Quelltext an, um einen vollständigen Satz von Aufrufsignaturen zu erhalten. Dein spezieller Anwendungszweck wird dir helfen, zu bestimmen, welche dieser Signaturen du dann benötigst.

Andere Anbindungen

Libgit2 hat Anbindungen für viele Programmier-Sprachen. Hier zeigen wir eine kleine Auswahl der wichtigsten Pakete, die zum Zeitpunkt der Erstellung dieses Textes verfügbar waren. Es gibt Bibliotheken für viele andere Sprachen, darunter C++, Go, Node.js, Erlang und die JVM, die alle unterschiedlich weit entwickelt sind. Die offizielle Kollektion von Anbindungen kann in den Repositorys unter https://github.com/libgit2 gefunden werden. Der Code, den wir schreiben werden, wird die Commit-Nachricht des Commits zurückliefern, auf den HEAD letztlich zeigt (etwa so wie git log -1).

LibGit2Sharp

Wenn du eine .NET- oder Mono-Anwendung schreibst, ist LibGit2Sharp (https://github.com/libgit2/libgit2sharp) das Richtige für dich. Die Anbindungen sind in C# geschrieben und es wurde große Sorgfalt darauf verwendet die unverarbeiteten Libgit2-Aufrufe in CLR-APIs mit der ursprünglichen Funktionalität zu verpacken. So sieht unser Beispielprogramm aus:

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

Für Windows-Desktop-Anwendungen gibt es außerdem ein NuGet-Paket, das dir einen schnellen Einstieg ermöglicht.

objective-git

Wenn deine Anwendung auf einer Apple-Plattform läuft, verwendest du wahrscheinlich Objective-C als Programmiersprache. Objective-Git (https://github.com/libgit2/objective-git) ist der Name der Libgit2-Anbindungen für diese Umgebung. Das Beispielprogramm sieht wie folgt aus:

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

Objective-git ist vollständig kompatibel mit Swift. du brauchst also keine Angst zu haben, wenn du Objective-C aufgegeben hast.

pygit2

Die Anbindungen für Libgit2 in Python heißen Pygit2 und sind unter https://www.pygit2.org zu finden. Hier ist unser Beispielprogramm:

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

Weiterführende Informationen

Eine vollständige Beschreibung der Funktionen von Libgit2 liegt natürlich nicht im Rahmen dieses Buches. Wenn du weitere Informationen über Libgit2 selbst wünschst, findest du eine API-Dokumentation unter https://libgit2.github.com/libgit2 und eine Reihe von Anleitungen unter https://libgit2.github.com/docs. Für die anderen Anbindungen schaue dir das enthaltene README und die Tests an. Häufig gibt es auch kleine Tutorials und Hinweise zum Weiterlesen.

scroll-to-top