Git
Chapters ▾ 2nd Edition

A2.2 Appendix B: Git in Ihre Anwendungen einbetten - Libgit2

Libgit2

Eine weitere Möglichkeit, die Ihnen 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. Sie finden das Programm unter https://libgit2.org.

Lassen Sie uns zunächst einen Blick auf die C-API werfen. Hier ist eine schnelle Tour:

// 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 Sie sicher auf den richtigen Typ verweisen können. In diesem Fall würde git_object_type(commit) ein GIT_OBJ_COMMIT zurückgeben, so dass es sicher auf einen git_commit Pointer zu 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.

From this sample, a couple of patterns have started to emerge:

  • Wenn Sie einen Pointer definieren und ihm eine Referenz in einem Libgit2-Aufruf übergeben, 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 Sie erstellt, Sie sind dafür verantwortlich, ihn freizugeben.

  • Wenn Libgit2 einen const Pointer aus einem Aufruf zurückgegeben hat, müssen Sie 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 bedeutet, dass es nicht sehr wahrscheinlich ist, dass Sie einen C-Code schreiben werden, wenn Sie Libgit2 verwenden. 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 Sie sehen können, 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änzen Sie den Index mit dem ersten Wert des Commit-Baums und fügen Sie 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 Sie einen Commit erstellen, müssen Sie die Elternteile des neuen Commits angeben. Dazu wird die Spitze 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 Sie dann ein Commit Objekt erzeugen können.

Der Ruby-Code ist zwar ganz nett und klar und da Libgit2 die Ausführung übernimmt, wird dieser Code auch relativ schnell laufen. Falls Sie kein Ruby-Anhänger sind, 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 Ihnen, benutzerdefinierte „Backends“ für unterschiedliche Betriebsarten anzubieten, so dass Sie Dinge auf eine andere Art speichern können, 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)

Beachten Sie, dass Fehler zwar erfasst, aber nicht bearbeitet werden. Wir hoffen, dass Ihr 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. Sie können dort machen, was immer Sie wollen, solange Sie die git_odb_backend Struktur richtig eingeben. 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 Sie es brauchen.

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. Schauen Sie sich die Datei include/git2/sys/odb_backend.h im Libgit2-Quelltext an, um einen vollständigen Satz von Aufrufsignaturen zu erhalten. Ihr spezieller Anwendungszweck wird Ihnen helfen, zu bestimmen, welche dieser Signaturen Sie dann benötigen.

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 in 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 Sie eine .NET- oder Mono-Anwendung schreiben, ist LibGit2Sharp (https://github.com/libgit2/libgit2sharp) das Richtige für Sie. 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 Ihnen einen schnellen Einstieg ermöglicht.

objective-git

Wenn Ihre Anwendung auf einer Apple-Plattform läuft, verwenden Sie 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. Sie brauchen also keine Angst zu haben, wenn Sie Objective-C aufgegeben haben.

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 Sie weitere Informationen über Libgit2 selbst wünschen, finden Sie 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 schauen Sie sich das enthaltene README und die Tests an. Häufig gibt es auch kleine Tutorials und Hinweise zum Weiterlesen.