Kapitel ▾ 2. Auflage

A2.2 Anhang B: Einbetten von Git in Ihre Anwendungen - Libgit2

Libgit2

Eine weitere Option, die Ihnen zur Verfügung steht, ist die Verwendung von Libgit2. Libgit2 ist eine von Abhängigkeiten befreite Implementierung von Git, mit dem Fokus auf eine gute API für die Verwendung in anderen Programmen. Sie finden sie unter https://libgit2.org.

Betrachten wir zunächst, wie die C-API aussieht. Hier ist eine Schnelltour

// 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 paar Zeilen öffnen ein Git-Repository. Der Typ git_repository repräsentiert ein Handle zu einem Repository mit einem Cache im Speicher. Dies ist die einfachste Methode, wenn Sie den genauen Pfad zum Arbeitsverzeichnis oder zur .git-Datei eines Repositorys kennen. Es gibt auch git_repository_open_ext, das Optionen für die Suche enthält, git_clone und ähnliche Funktionen zum Klonen eines Remote-Repositorys und git_repository_init zum Erstellen eines komplett neuen Repositorys.

Der zweite Code-Schnipsel verwendet die Rev-Parse-Syntax (siehe Branch References für weitere Informationen), um den Commit zu erhalten, auf den HEAD schließlich zeigt. Der zurückgegebene Typ ist ein git_object-Zeiger, der etwas repräsentiert, das in der Git-Objektdatenbank eines Repositorys existiert. git_object ist eigentlich ein "Übertyp" für verschiedene Arten von Objekten; das Speicherlayout für jeden der "Untertypen" ist dasselbe wie für git_object, sodass Sie sicher in den richtigen Typ umwandeln können. In diesem Fall würde git_object_type(commit) GIT_OBJ_COMMIT zurückgeben, sodass es sicher ist, in einen git_commit-Zeiger umzuwandeln.

Der nächste Schnipsel zeigt, wie auf die Eigenschaften des Commits zugegriffen wird. Die letzte Zeile hier verwendet einen git_oid-Typ; dies ist die Darstellung von Libgit2 für einen SHA-1-Hash.

Aus diesem Beispiel haben sich bereits einige Muster herauskristallisiert

  • Wenn Sie einen Zeiger deklarieren und eine Referenz darauf an einen Libgit2-Aufruf übergeben, wird dieser Aufruf wahrscheinlich einen ganzzahligen Fehlercode zurückgeben. Ein Wert von 0 bedeutet Erfolg; alles darunter ist ein Fehler.

  • Wenn Libgit2 einen Zeiger für Sie befüllt, sind Sie für dessen Freigabe verantwortlich.

  • Wenn Libgit2 einen const-Zeiger von einem Aufruf zurückgibt, müssen Sie ihn nicht freigeben, aber er wird ungültig, wenn das Objekt, zu dem er gehört, freigegeben wird.

  • Das Schreiben von C ist etwas mühsam.

Das Letzte bedeutet, dass es unwahrscheinlich ist, dass Sie C schreiben werden, wenn Sie Libgit2 verwenden. Glücklicherweise gibt es eine Reihe sprachspezifischer Bindungen, die es relativ einfach machen, mit Git-Repositories aus Ihrer spezifischen Sprache und Umgebung zu arbeiten. Betrachten wir das obige Beispiel, geschrieben mit den Ruby-Bindungen für Libgit2, die Rugged heißen 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, ist der Code viel übersichtlicher. Erstens verwendet Rugged Ausnahmen; es kann Dinge wie ConfigError oder ObjectError auslösen, um Fehlerbedingungen zu signalisieren. Zweitens gibt es keine explizite Freigabe von Ressourcen, da Ruby garbage-collected ist. Betrachten wir ein etwas komplizierteres Beispiel: einen Commit von Grund auf neu 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. Erstellen Sie einen neuen Blob, der den Inhalt einer neuen Datei enthält.

  2. Befüllen Sie den Index mit dem Tree des HEAD-Commits und fügen Sie die neue Datei unter dem Pfad newfile.txt hinzu.

  3. Dies erstellt einen neuen Tree in der ODB und verwendet ihn für den neuen Commit.

  4. Wir verwenden die gleiche Signatur für die Felder Autor und Committer.

  5. Die Commit-Nachricht.

  6. Beim Erstellen eines Commits müssen Sie die Eltern des neuen Commits angeben. Dies verwendet die Spitze von HEAD als einzelnen Elternteil.

  7. Rugged (und Libgit2) kann optional eine Referenz beim Erstellen eines Commits aktualisieren.

  8. Der Rückgabewert ist der SHA-1-Hash eines neuen Commit-Objekts, den Sie dann verwenden können, um ein Commit-Objekt zu erhalten.

Der Ruby-Code ist nett und sauber, aber da Libgit2 die Hauptarbeit leistet, wird dieser Code auch ziemlich schnell ausgeführt. Wenn Sie kein Ruby-Entwickler sind, behandeln wir einige andere Bindungen in Andere Bindungen.

Erweiterte Funktionalität

Libgit2 verfügt über einige Funktionen, die außerhalb des Kernbereichs von Git liegen. Ein Beispiel ist die Plugin-Fähigkeit: Libgit2 ermöglicht es Ihnen, benutzerdefinierte "Backends" für verschiedene Arten von Operationen bereitzustellen, sodass Sie Dinge anders speichern können als das Standard-Git. Libgit2 erlaubt benutzerdefinierte Backends für Konfiguration, Ref-Speicherung und die Objektdatenbank, unter anderem.

Betrachten wir, wie das funktioniert. Der folgende Code ist aus der Sammlung 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 erfasst, aber nicht behandelt werden. Wir hoffen, Ihr Code ist besser als unserer.

  1. Initialisieren Sie ein leeres Objekt-Datenbank (ODB) "Frontend", das als Container für die "Backends" dient, die die eigentliche Arbeit leisten.

  2. Initialisieren Sie ein benutzerdefiniertes ODB-Backend.

  3. Fügen Sie das Backend zum Frontend hinzu.

  4. Öffnen Sie ein Repository und legen Sie fest, dass unsere ODB zum Nachschlagen von Objekten verwendet wird.

Aber was ist dieses git_odb_backend_mine-Ding? Nun, das ist der Konstruktor für Ihre eigene ODB-Implementierung, und Sie können darin tun, was immer Sie wollen, solange Sie die Struktur git_odb_backend korrekt ausfüllen. So *könnte* es aussehen

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 subtilste Einschränkung hier ist, dass das erste Mitglied von my_backend_struct eine Struktur vom Typ git_odb_backend sein muss; dies stellt sicher, dass das Speicherlayout dem entspricht, was der Libgit2-Code erwartet. Der Rest ist beliebig; diese Struktur kann so groß oder klein sein, wie Sie sie benötigen.

Die Initialisierungsfunktion weist Speicher für die Struktur zu, richtet den benutzerdefinierten Kontext ein und füllt dann die von ihr unterstützten Mitglieder der parent-Struktur aus. Werfen Sie einen Blick auf die Datei include/git2/sys/odb_backend.h im Libgit2-Quellcode für eine vollständige Sammlung von Aufruf-Signaturen; Ihr spezifischer Anwendungsfall wird Ihnen helfen zu bestimmen, welche davon Sie unterstützen möchten.

Andere Bindungen

Libgit2 verfügt über Bindungen für viele Sprachen. Hier zeigen wir ein kleines Beispiel, das einige der zum Zeitpunkt des Schreibens umfassendsten Bindungspakete verwendet; es existieren Bibliotheken für viele andere Sprachen, darunter C++, Go, Node.js, Erlang und die JVM, alle in verschiedenen Reifestadien. Die offizielle Sammlung von Bindungen finden Sie, indem Sie die Repositories unter https://github.com/libgit2 durchsuchen. Der Code, den wir schreiben werden, gibt die Commit-Nachricht des Commits zurück, auf den HEAD letztendlich zeigt (ähnlich 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 Bindungen sind in C# geschrieben, und es wurde große Sorgfalt darauf verwendet, die rohen Libgit2-Aufrufe mit nativ wirkenden CLR-APIs zu umwickeln. So sieht unser Beispielprogramm aus

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

Für Desktop-Windows-Anwendungen gibt es sogar ein NuGet-Paket, das Ihnen den schnellen Einstieg erleichtert.

objective-git

Wenn Ihre Anwendung auf einer Apple-Plattform läuft, verwenden Sie wahrscheinlich Objective-C als Ihre Implementierungssprache. Objective-Git (https://github.com/libgit2/objective-git) ist der Name der Libgit2-Bindungen für diese Umgebung. Das Beispielprogramm sieht so 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 interoperabel mit Swift, also fürchten Sie sich nicht, wenn Sie Objective-C hinter sich gelassen haben.

pygit2

Die Bindungen für Libgit2 in Python heißen Pygit2 und sind unter https://www.pygit2.org zu finden. 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 Literatur

Natürlich sind die vollständigen Fähigkeiten von Libgit2 außerhalb des Rahmens dieses Buches. Wenn Sie weitere Informationen zu Libgit2 selbst wünschen, finden Sie API-Dokumentation unter https://libgit2.github.com/libgit2 und eine Reihe von Anleitungen unter https://libgit2.github.com/docs. Für die anderen Bindungen schauen Sie in die mitgelieferten READMEs und Tests; dort gibt es oft kleine Tutorials und Hinweise auf weiterführende Lektüre.