Benötigt Ihre selbst geschriebene Anwendung eine Versionsverwaltung, dann bauen Sie doch einfach Git ein. Das gelingt auf drei verschiedenen Wegen, die allerdings allesamt ein paar Fußangeln aufweisen.
Log-Daten, Konfigurationsdateien, Medikationspläne – es gibt viele Fälle, in denen eine Anwendung von ihr erzeugte Dateien versionieren muss. Statt selbst umständlich eine Versionsverwaltung zu implementieren, greift man heute zu einem bereits etablierten vorgefertigten System. Bei Textinhalten drängt sich dazu Git geradezu auf: Zahlreiche Entwickler nutzen die Versionsverwaltung sowieso für ihren Quellcode und kennen sich dementsprechend gut damit aus. Git zeigt sich zudem als zuverlässig und erfüllt professionelle Ansprüche. Bei Bedarf tauschen Sie die Dateien sogar über Github zwischen mehreren Instanzen aus oder teilen sie mit der Öffentlichkeit. Abhängig von der verwendeten Programmiersprache lässt sich Git auf bis zu drei verschiedenen Wegen in eine Anwendung integrieren (Abbildung 1). Ausgerechnet das naheliegendste und besonders einfach zu implementierende Verfahren sollten Sie jedoch tunlichst nicht wählen.

Abbildung 1: Zugriff auf ein Git-Repository erhalten Sie auf drei Arten: über Shell-Befehle(links), die Bibliothek Libgit2 (Mitte) oder eine zur Programmiersprache passende Git-Implementierung (rechts).
Die meisten Programmiersprachen bieten eine Schnittstelle, über die Sie externe Programme oder Shell-Skripte starten. Unter Go gelingt das beispielsweise mit der Funktion »exec.Command()«, in Python über das Pendant »os.system()«. Mit ihrer Hilfe können Sie direkt den allseits bekannten Git-Client aufrufen. Listing 1 zeigt ein Beispiel in Go, das Vorgehen in anderen Sprachen ist identisch: In Zeile 9 erstellt das Programm zunächst mit dem Kommando »git init /tmp/logs« ein neues Git-Repository im Verzeichnis »/tmp/logs/«. Die Funktion »exec.Command()« verlangt dabei die einzelnen Git-Optionen jeweils in einem eigenen separaten Parameter. In vielen anderen Sprachen müssen Sie hingegen den kompletten Git-Befehl erst in einem einzigen String zusammenbauen, zum Beispiel beim Verwenden von »os.system()« unter Python.
Ab Zeile 14 erstellt das Programm aus Listing 1 die Textdatei »log.txt«. Dabei kommen Funktionen aus der Go-Standardbibliothek zum Einsatz. Die Datei fügt der Staging Area die Zeile 18 hinzu, indem sie den Befehl »git -C /tmp/logs add log.txt« absetzt. Anschließend erfolgt ein Commit über den Befehl »git -C /tmp/logs commit -am “System started”«. Ein praxisnahes Programm würde freilich die Commit Message in der Variablen »message« passend zum entsprechenden Commit wählen.
Listing 1
Git-Client aus Go aufrufen
package main
import (
"os"
"os/exec"
"time"
)
func main() {
// Repository im Verzeichnis /tmp/logs anlegen:
cmd := exec.Command("git", "init", "/tmp/logs")
cmd.Run()
// Neue (Log-)Datei im Repository erstellen:
ctime := time.Now().UTC()
stime := ctime.String()
file, _ := os.OpenFile("/tmp/logs/log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
file.WriteString(stime+" Service started\n")
defer file.Close()
// Datei hinzufügen:
cmd = exec.Command("git", "-C", "/tmp/logs", "add", "log.txt")
cmd.Run()
// Autor festlegen:
cmd = exec.Command("git", "-C", "/tmp/logs", "config", "--local", "user.name", "Tux")
cmd.Run()
cmd = exec.Command("git", "-C", "/tmp/logs", "config", "--local", "user.email", "tux@example.com")
cmd.Run()
// Commit erstellen:
message := "Commit Message"
cmd = exec.Command("git", "-C", "/tmp/logs", "commit", "-am", message)
cmd.Run()
}
Listing 1 prüft nicht, ob die einzelnen Aufrufe erfolgreich waren. In einem produktiven Programm müssen Sie die Rückgaben der Befehle auffangen und analysieren. Go liefert netterweise ebenso den Rückgabewert und die über die Standardausgabe tickernden Texte. Die Ausgaben von »git status« lassen sich darum wie in Listing 2 abfangen.
Listing 2
git status
cmd = exec.Command("git", "-C", "/tmp/logs", "status")
out, _ := cmd.Output() fmt.Println(string(out))
Die in der Variablen »out« steckenden Meldungen von »git« schreibt das Beispiel lediglich auf den Bildschirm. In einer Anwendung müssten Sie hingegen jetzt die Ausgaben umständlich mit Textoperationen auseinandernehmen und auswerten. Nur so finden Sie heraus, welche ID der letzte Commit trägt oder ob ein Fehler aufgetreten ist. Diese Tests erhöhen jedoch den Wartungsaufwand enorm: Wann immer eine neue Git-Version die Ausgaben ändert, müssten Sie das eigene Programm entsprechend anpassen.
Was bin ich?
Das komplette Verfahren funktioniert außerdem nur, wenn das Programm »git« installiert und einsatzbereit ist. Insbesondere wer Server-Dienste für Dritte entwickelt, kann sich dessen kaum sicher sein. Folglich kommen Sie nicht umhin, die Existenz von »git« zu prüfen, indem Sie es beispielsweise probeweise aufrufen. Wie solch ein Test in Go aussieht, zeigt Listing 3 direkt am Anfang: Es setzt zunächst den Befehl »git –version« ab und testet dann, ob beim Aufruf ein Fehler auftrat. In diesem Fall ließe sich »git« für das Programm nicht nutzen. Dieser Test funktioniert analog auch in anderen Sprachen.
Alternativ können Sie gezielt auf dem System nach dem Programm »git« suchen. Einige Programmiersprachen verfügen dazu über ein Äquivalent zum Shell-Kommando »whereis«. Unter Go heißt die entsprechende Funktion »exec.LookPath()«, den Einsatz demonstriert Listing 3 ab Zeile 6. In der Variablen »path« landet dabei der komplette Pfad zum Programm »git«.
Listing 3
Git vorhanden?
// git direkt aufrufen:
cmd := exec.Command("git", "--version")
err := cmd.Run()
if err != nil { fmt.Println("Kann git nicht aufrufen", err) }
// git-Pfad über exec.LookPath() finden:
path, err := exec.LookPath("git")
if err != nil {
fmt.Println("git nicht gefunden", err)
} else {
fmt.Println("git ist vorhanden im Pfad", path)
}
Wie auch immer Sie »git« aufstöbern: Es gibt keine Garantie, dass Sie damit tatsächlich Git erwischen oder doch nur ein anderes Programm, das schlicht »git« heißt. Schafft es beispielsweise ein Angreifer, ein eigenes maliziöses »git« einzuschleusen, würde es Listing 1 ganz ungeniert aufrufen, im Extremfall sogar mit Root-Rechten. Um das zu verhindern, könnten Sie Ihrem Programm eine Git-Kopie beilegen oder die Authentizität über Prüfsummen sicherstellen. Das müssen Sie aber nicht, denn es gibt deutlich bessere Integrationsmöglichkeiten.
Libgit2
Mit der Libgit2 (Abbildung 2) steht eine kompakte Bibliothek bereit, deren Funktionen ein Git-Repository erstellen und umfassend manipulieren können [1]. Sie lässt sich sowohl als dynamische Bibliothek nutzen als auch statisch zum eigenen Programm hinzu linken. Darüber hinaus ist die Libgit2 in portablem C-Code geschrieben, der sich auf nahezu allen Systemen ohne viele Abhängigkeiten übersetzen lässt. Offiziell unterstützt die Bibliothek die Compiler GCC, Clang und MSVC sowie die Betriebssysteme Linux, MacOS, iOS und Windows.
Zahlreiche Distributionen offerieren die Libgit2 standardmäßig in ihren Repositories. Obwohl die Bibliothek unter der GPLv2 steht, erlaubt die explizit gewährte Linking Exception einen Einsatz in kommerziellen Programmen. Unter anderem werkelt die Libgit2 bei Github, Gitlab und Microsoft. Sie darf somit als praxiserprobt gelten, verfügt über eine sehr stabile API und arbeitet obendrein noch recht flott. Das klingt nach einer runden Lösung für alle Lebenslagen – bis Sie einen Blick auf ein Codebeispiel werfen.

Abbildung 2: Wer auf Git-Repositories in eigenen Programmen zugreifen möchte, kommt an der Libgit2 kaum vorbei. Die zahlreichen empfohlenen Bindings sind direkt auf der Website verlinkt.
Als Low-Level-Bibliothek arbeiten die Funktionen Libgit2 direkt auf den Datenstrukturen eines Git-Repositorys. Damit dürfen Sie sämtliche Bestandteile eines Repositorys auf der untersten Ebene manipulieren, wie die Referenzen, den Index und die Baumstruktur. Doch das setzt im Gegenzug fundiertes Wissen über den internen Aufbau eines Git-Repositorys voraus. Anderenfalls laufen Sie schnell Gefahr, sich Ihr Git-Repository irreparabel zu zerschießen.
Quellcodesalat
Um die Low-Level-Funktionen in der Libgit2 von den bekannten einfacheren Git-Kommandozeilenbefehlen zu unterscheiden, bezeichnen die Entwickler letztere häufig als Porcellain. Um dieses Porzellan nachzubilden, etwa ein »git commit«, benötigen Sie mit der Libgit2 stets mehrere Funktionsaufrufe. Wie sich der Code dadurch aufbläht, demonstriert Listing 4. Es erstellt in der Programmiersprache C ein Repository, fügt ihm eine Datei hinzu und führt einen Commit aus. Für dieselben Aktionen benötigt Listing 1 gerade einmal 35 Zeilen. Um den Rahmen nicht zu sprengen, prügelt zudem Listing 4 die Datei ohne Rücksicht auf Verluste in das Repository. In einem realen Programm bräuchte es zumindest noch Fehlertests. Wenn Sie sich für die genaue Arbeitsweise des Codes interessieren, finden Sie eine Erklärung im Kasten “Unter der Lupe: Listing 4”.
Listing 4
Commit mit der Libgit2
#include <stdio.h>
#include <stdbool.h>
#include <time.h>
#include "git2.h"
int main() {
git_libgit2_init();
// Repository im Verzeichnis /tmp/logs erstellen:
git_repository *repo = NULL;
git_repository_init(&repo, "/tmp/logs", false);
// Neue (Log-)Datei im Repository erstellen:
FILE *file;
file = fopen("/tmp/logs/log.txt", "a+");
time_t t = time(NULL);
struct tm tm = *localtime(&t);
fprintf(file, "%d-%02d-%02d %02d:%02d:%02d Service started\n", tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
fclose(file);
// Datei hinzufügen:
git_index *index = NULL;
git_repository_index(&index, repo);
git_index_add_bypath(index, "log.txt");
git_index_write(index);
git_index_free(index);
// Commit erstellen:
git_oid commit_id, tree_id;
git_tree *tree;
git_signature *sig = NULL;
git_signature_now(&sig, "Tux", "tux@example.com");
git_repository_index(&index, repo);
git_index_write_tree(&tree_id, index);
git_tree_lookup(&tree, repo, &tree_id);
git_commit_create(&commit_id, repo, "HEAD", sig, sig, NULL, "Commit Message", tree, 0, NULL);
// Aufräumen:
git_index_free(index);
git_tree_free(tree);
git_signature_free(sig);
git_repository_free(repo);
git_libgit2_shutdown();
}
Unter der Lupe: Listing 4
Die ersten Schritte mit der Libgit2 sind noch kompakt und leicht verständlich: Der Befehl »git_libgit2_init()« aus Zeile 6 bereitet den Einsatz der Bibliothek vor, anschließend legt »git_repository_init()« (Zeile 9) ein Repository im Verzeichnis »/tmp/logs« an. Darauf können später andere Funktionen über die Variable »repo« zugreifen.
Ab Zeile 12 erstellen die entsprechenden Funktionen aus der Standard-C-Bibliothek eine Textdatei im Verzeichnis »/tmp/logs/«. Erst in Zeile 19 übernimmt wieder Libgit2. Dort holt das Kommando »git_repository_index()« zunächst den Index des neuen Repositorys in den Hauptspeicher. Die nächsten drei Befehle fügen dem Index die Datei »log.txt« hinzu, aktualisieren den Index im Repository und werfen ihn anschließend wieder aus dem Hauptspeicher.
Damit fehlt nur noch ein Commit, der ab Zeile 24 startet. Zu einem Commit gehören immer der Name und die E-Mail-Adresse des Autors. Beide Angaben gießt »git_signature_now()« (Zeile 27) in eine passende Datenstruktur vom Typ »git_signature«. Die folgenden Funktionen holen wieder den Index (»git_repository_index()«) und dann den Tree (»git_index_write_tree()« sowie »git_tree_lookup()«). Abschließend erstellt »git_commit_create()« (Zeile 31) den eigentlichen Commit. Dazu benötigt die Funktion neben dem Repository unter anderem ebenso die Angaben zum Autor, eine Commit-Nachricht (die in Listing 4 schlicht »Commit Message« lautet) und den Tree. Das Ergebnis ist die zugehörige Commit-ID, die in der Variablen »commit_id« landet. Mit ihr ließen sich unter anderem Informationen zum Commit abrufen.
Zum Schluss gibt Listing 4 noch die genutzten Datenstrukturen frei. Im Einzelnen verschwinden der Index, der Tree, die Angaben zum Autor und das Repository aus dem Hauptspeicher. Danach beendet »git_libgit2_shutdown()« in der vorletzten Zeile den Einsatz der Libgit2.
Kurz angebunden
Die Libgit2 lässt sich direkt in sämtlichen Programmiersprachen verwenden, die eine C-Bibliothek integrieren können. Darüber hinaus existieren mittlerweile zahlreiche Anbindungen für alle möglichen und unmöglichen Sprachen. So nutzen unter anderem Python-Entwickler die Libgit2 in ihren Skripten via Pygit2 [2], während Java-Programmierer auf Git24j zurückgreifen [3]. Mit Chicken-git gibt es sogar eine Anbindung für Chicken Scheme [4]. Die Bindings erlauben, die Libgit2-Funktionen bequem und teilweise vereinfacht zu nutzen. So kapseln beispielsweise Pygit2 und Git24j die Funktionen logisch in Objekten. Zum Hinzufügen der Datei »log.txt« genügen damit in Python wenige Zeilen (Listing 5).
Listing 5
log.txt hinzufügen
repo = pygit2.Repository('/tmp/logs')
index = repo.index
index.add('log.txt') index.write()
Die Programmierer der Bindings müssen gezwungenermaßen stets mit der Entwicklung der Libgit2 mithalten. Abhängig vom Engagement der Teams gelingt das mal mehr, mal weniger gut. So fehlte etwa dem Python-Binding Pygit2 zum Redaktionsschluss noch die Funktion »git_commit_create_from_stage()«, die Libgit2 mit der Version 1.8 eingeführt hat. Darüber hinaus reichen die Bindings in erstaunlich vielen Fällen nicht alle Funktionen durch oder lassen Fähigkeiten vermissen.

Abbildung 3: Die benötigte Pygit2-Version hängt sowohl von der eingesetzten Python- als auch von der Libgit2-Version ab.
Die Bindings nageln Sie außerdem häufig auf ganz bestimmte Libgit2-Versionen fest. So unterstützte etwa Pygit 1.14 zum Redaktionsschluss ausschließlich die Version 1.7 der Libgit2 (Abbildung 3). Aktualisierungen der Bindings sind dabei nicht garantiert: Bei Git2Go liegt die Weiterentwicklung bereits seit zwei Jahren kommentarlos auf Eis (Abbildung 4), das letzte Update des Kollegen PHP-Git erschien vor sieben Jahren. Solche Bindings arbeiten ausschließlich mit veralteten Libgit2-Versionen zusammen, weshalb man von ihrem Einsatz nur abraten kann. Achten Sie schon bei der Wahl eines Bindings unbedingt darauf, ob die geforderten Versionen zu Ihren Anforderungen passen und in welchem Tempo die Verantwortlichen ihre Bindings weiterentwickeln.

Abbildung 4: Die letzte Version von Git2go stammt aus dem Jahr 2022 und ließ sich nicht mehr zur Zusammenarbeit mit der aktuellen Libgit2-Version überreden.
Maßgeschneidert
Sofern Sie nicht in C programmieren, holen Sie sich mit einem Binding für Libgit2 eine weitere Abhängigkeit ins Haus. Das Problem umgehen Sie mit einer explizit für Ihre Sprache entwickelten Git-Bibliothek. Go-Programmierer greifen beispielsweise auf Go-Git zurück [5]. Ihr Programm hängt dann nur noch von einer Bibliothek ab, die sich obendrein optimal in die Sprache einfügt.
Viele Git-Bibliotheken offerieren zudem Porcellain-Funktionen und erleichtern so den Zugriff auf ein Git-Repository deutlich. Listing 6 zeigt ein Beispiel mit Go-Git, das man auch ohne Go-Kenntnisse versteht. Wie die übrigen Listings erzeugt es ein neues Repository, legt darin eine Datei an, fügt diese hinzu und erstellt einen Commit. Sieht man von den Zeilen ab, die den Namen und die E-Mail-Adresse des Autors in eine passende Datenstruktur verpacken, fällt der Code ähnlich kompakt aus wie in Listing 1.
Listing 6
Commit mit Go-Git
package main
import (
"os"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
func main() {
// Repository erstellen:
repo, _ := git.PlainInit("/tmp/logs", false)
// Neue (Log-)Datei im Repository erstellen:
ctime := time.Now().UTC()
stime := ctime.String()
file, _ := os.OpenFile("/tmp/logs/log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
file.WriteString(stime+" Service started\n")
defer file.Close()
// Datei hinzufügen:
tree, _ := repo.Worktree()
tree.Add("log.txt");
// Commit erstellen:
sig := object.Signature {
Name: "Tux",
Email: "tux@example.com",
When: time.Now(),
}
options := git.CommitOptions{ Author: &sig, }
tree.Commit("Commit Message", &options)
}
Go-Bibliotheken wie Go-Git unterstützen allerdings nicht immer alle prinzipiell von Git angebotenen Funktionen. Go-Git bietet beispielsweise keine Äquivalente zu den Befehlen »git init –template« und »git init –shared«, eine Implementierung des Stash fehlte zum Redaktionsschluss ebenso. Im Gegenzug eröffnen die Bibliotheken mitunter neue Optionen. So kann Go-Git die Repositories komplett im Hauptspeicher führen (In-Memory-Betrieb).
Wer eine Bibliothek wie Go-Git verwendet, macht sich jedoch auch von ihr abhängig. Insbesondere wenn Sie Zusatzfunktionen wie den erwähnten In-Memory-Betrieb nutzen, fällt später ein Tausch gegen eine andere Bibliothek schwer.
Einstiegshürden
Die Dokumentation der meisten Bibliotheken besteht im Wesentlichen aus einer Referenz und einer Handvoll kommentierter Beispiele (Abbildung 5). Daraus müssen Sie sich das Zusammenspiel und die Arbeitsweise der Funktionen zeitaufwendig selbst herleiten. Das gilt sogar für die etablierte Libgit2, zu der sich im Internet kaum tiefergehende Tutorials finden. Wer zuvor ausschließlich mit den Git-Kommandos auf der Shell gearbeitet hat, muss hier folglich mit einer steilen Lernkurve rechnen.

Abbildung 5: Wie hier bei Pygit2 besteht die Dokumentation vieler Libgit2-Bindings nur aus einer Referenz sowie ein paar einfachen Beispielen.
In jedem Fall müssen Sie sich mit allen Aspekten der Bibliotheken vertraut machen, denn weder die Porcellain-Funktionen noch der direkte Aufruf von »git« bewahren Sie vor dem Programmieren einer Fehlererkennung. Ihr Programm muss möglichst frühzeitig defekte Repositories identifizieren und entsprechende Reparaturmaßnahmen einleiten. Das klappt nur, wenn Sie um die diesbezüglichen Möglichkeiten der Bibliotheken wissen. Sie sollten dementsprechend unbedingt genügend Einarbeitungszeit einplanen.
Fazit
Welche Lösung für Ihr Projekt die richtige ist, hängt von Ihren Kenntnissen und Anforderungen ab. Das Kommando »git« direkt aus einem Programm aufzurufen, kommt nur als Notlösung infrage. Ansonsten sehen Sie sich mit einer umständlichen Handhabung und gravierenden Sicherheitsproblemen konfrontiert. Deswegen empfiehlt es sich, eine Bibliothek einzusetzen, die sich bei Bedarf statisch in das eigene Programm linken lässt.
Libgit2 zeigt sich besonders stabil, erfreut sich großer Beliebtheit und geht flott zu Werke. Die Bibliothek gilt deswegen als erste Wahl, sofern Sie nicht die Low-Level-Funktionen scheuen und das eventuell benötigte Binding eine aktive Entwicklergemeinde hinter sich versammelt. Alternative Bibliotheken vereinfachen teilweise mit Porcellain-Funktionen die Entwicklung und sind auf die jeweilige Programmiersprache abgestimmt. Sie bieten aber nicht immer den vollen, von Git gewohnten Funktionsumfang. (csi)
Infos
- Libgit2: https://libgit2.org/
- Pygit2: https://www.pygit2.org/
- Git24j: https://github.com/git24j/git24j
- Chicken-git: https://wiki.call-cc.org/eggref/5/git
- Go-Git: https://github.com/go-git/go-git






