Git
Chapters ▾ 2nd Edition

A2.2 Annexe B: Embarquer Git dans vos applications - Libgit2

Libgit2

Une autre option à votre disposition consiste à utiliser Libgit2. Libgit2 est une mise en œuvre de Git sans dépendance externe, qui se focalise sur une interface de programmation agréable à utiliser depuis d’autres programmes. Vous pouvez la trouver sur https://libgit2.github.com.

Voyons d’abord à quoi ressemble l’API C. En voici un tour rapide :

// Ouvrir un depot
git_repository *repo;
int error = git_repository_open(&repo, "/path/to/repository");

// Dereferencer HEAD vers un commit
git_object *head_commit;
error = git_revparse_single(&head_commit, repo, "HEAD^{commit}");
git_commit *commit = (git_commit*)head_commit;

// afficher quelques proprietes du commit
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);

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

Les deux premières lignes ouvrent un dépôt Git. Le type git_repository représente un identificateur de dépôt avec un cache en mémoire. C’est la méthode la plus simple, quand vous connaissez le chemin exact vers le répertoire de travail ou le répertoire .git d’un dépôt. Il y a aussi git_repository_open_ext qui inclut des options pour chercher, git_clone et ses déclinaisons pour créer un clone local d’un dépôt distant et git_repository_init pour créer un dépôt entièrement nouveau.

Le second bloc de code utilise la syntaxe « rev-parse » (voir Références de branches pour plus de détails) pour obtenir le commit sur lequel HEAD peut pointer. Le type retourné est un pointeur sur git_object qui représente quelque chose qui existe dans la base de données des objets de Git pour un dépôt. git_object est en fait une type « parent » pour différentes sortes d’objets ; l’agencement en mémoire de chacun de ces types « enfants » est identique à celui de git_object, donc on peut forcer la conversion vers le type désiré en toute sécurité. Dans notre cas, git_object_type(commit) retournerait GIT_OBJ_COMMIT, il est donc permis de le convertir en un pointeur de git_commit.

Le bloc suivant montre comment accéder aux propriétés d’un commit. La dernière ligne utilise un type git_oid ; c’est la représentation d’une empreinte SHA-1 dans Libgit2.

De cet exemple, une structure générale commence à émerger :

  • Si vous déclarez un pointeur et que vous en passez une référence dans un appel à Libgit2, cet appel renverra vraisemblablement un code de retour entier. Une valeur 0 indique un succès ; toute valeur négative est une erreur.

  • Si Libgit2 peuple un pointeur pour vous, vous êtes responsable de sa libération.

  • Si Libgit2 retourne un pointeur const après un appel, vous n’avez pas besoin de le libérer mais il deviendra invalide quand l’objet qui le possède sera lui-même libéré.

  • Écrire en C est un exercice plutôt douloureux.

Cette dernière remarque signifie qu’il est fort peu probable que vous écrirez du C pour utiliser Libgit2. Heureusement, il existe un certain nombre de liaisons vers d’autres langages qui rendent plus facile l’interaction avec des dépôts Git depuis votre environnement et votre langage spécifiques. Voyons l’exemple ci-dessus réécrit en utilisant le portage Ruby de Libgit2, appelé Rugged et qui peut être trouvé sur 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

Tout de suite, le code est moins verbeux. Déjà, Rugged utilise des exceptions ; il peut lever ConfigError ou ObjectE pour signaler des conditions d’erreur. Ensuite, il n’y a pas de libération explicite des ressources, puisque Ruby utilise un ramasse-miettes. Voyons un exemple légèrement plus compliqué : créer un commit à partir de rien.

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. Créer un nouveau blob qui contient le contenu d’un nouveau fichier.

  2. Peupler l’index avec l’arbre du commit HEAD et ajouter le nouveau fichier sous le chemin newfile.txt.

  3. Ceci crée un nouvel arbre dans la base de données des objets et l’utilise pour le nouveau commit.

  4. Nous utilisons la même signature pour l’auteur et le validateur.

  5. Le message de validation.

  6. À la création d’un commit, il faut spécifier les parents du nouveau commit. on utilise le sommet de HEAD comme parent unique.

  7. Rugged (et Libgit2) peuvent en option mettre à jour la référence lors de la création du commit.

  8. La valeur retournée est une empreinte SHA-1 du nouvel objet commit que vous pouvez alors utiliser pour obtenir un objet Commit.

Le code Ruby est joli et propre, mais comme Libgit2 réalise le gros du travail, il tourne aussi plutôt rapidement. Si vous n’êtes pas rubyiste, nous aborderons d’autres portages dans Autres liaisons.

Fonctionnalité avancée

Libgit2 a certaines capacités qui ne sont pas disponibles dans Git natif. Un exemple est la possibilité de greffons : Libgit2 vous permet de fournir des services « d’arrière-plan » pour différents types d’opérations, pour vous permettre de stocker les choses d’une manière différente de Git. Libgit2 autorise des services d’arrière-plan pour la configuration, le stockage des références et la base de données d’objets, entre autres.

Voyons comment cela fonctionne. Le code ci-dessous est emprunté à un ensemble d’exemples de services fourni par l’équipe Libgit2 (qui peut être trouvé sur https://github.com/libgit2/libgit2-backends). Voici comment un service d’arrière-plan pour une base de données d’objets peut être créée :

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)

(Notez que les erreurs sont capturées, mais ne sont pas gérées. Nous espérons que votre code est meilleur que le nôtre).

  1. Initialise une enveloppe d’interface d’une base de données d’objets vide (ODB) qui agit comme un conteneur pour les tâches de fond qui feront le vrai travail.

  2. Initialise une tâche de fond ODB.

  3. Ajoute la tâche de fond dans l’enveloppe.

  4. Ouvre le dépôt et le paramètre pour utiliser notre ODB pour rechercher les objets.

Mais qu’est-ce que ce git_odb_backend_mine ? Hé bien, c’est le constructeur de notre propre réalisation de l’ODB et nous pouvons la faire comme nous voulons tant que la structure git_odb_backend est correctement renseignée. Voici à quoi elle pourrait ressembler :

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

La contrainte la plus subtile ici est que le premier membre de my_backend_structure doit être une structure git_odb_backend ; cela assure que la disposition en mémoire correspond à ce qu’attend le code de Libgit2. Le reste est totalement arbitraire ; cette structure peut être aussi grande ou petite que nécessaire.

La fonction d’initialisation alloue de la mémoire pour la structure, initialise le contexte spécifique, puis remplit les membres de la structure parent qu’elle supporte. Référez-vous au fichier include/git2/sys/odb_backend.h dans les sources de Libgit2 pour l’intégralité des signatures d’appels ; votre cas d’utilisation particulier vous permettra de déterminer lesquelles vous souhaitez supporter.

Autres liaisons

Libgit2 dispose de liaisons vers de nombreux langages. Nous allons montrer ici un petit exemple en utilisant quelques-unes des liaisons les plus abouties au moment de la rédaction de ce livre ; des bibliothèques existent pour de nombreux autres langages qui incluent C++, Go, Node.js, Erlang et la JVM à différents stades de maturité. La collection officielle de liaisons peut être trouvée en parcourant les dépôts sur https://github.com/libgit2. Le code que nous allons écrire retournera le message de validation du commit finalement pointé par HEAD (git log -1 en quelque sorte).

LibGit2Sharp

Si vous écrivez une application .NET ou Mono, LigGit2Sharp (https://github.com/libgit2/libgit2sharp) est tout ce que vous cherchez. Les liaisons sont écrites en C# et une grande attention a été portée à envelopper les appels directs à Libgit2 avec une interface de programmation naturelle en C#. Voici à quoi notre programme d’exemple ressemble :

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

Pour les applications graphiques Windows, il existe même un paquet NuGet qui vous permettra de démarrer vos développements rapidement.

objective-git

Si votre application tourne sur une plateforme Apple, vous avez de grandes chances d’utiliser Objective-C comme langage de programmation. Objective-Git (https://github.com/libgit2/objective-git) est le nom de la liaison de Libgit2 pour cet environnement. Le programme d’exemple ressemble à ceci :

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

Objective-git est totalement interopérable avec Swift, donc n’ayez crainte si vous avez abandonné Objective-C.

pygit2

La liaison avec Libgit2 en Python s’appelle Pygit2 et elle peut être trouvée sur https://www.pygit2.org/. Notre programme d’exemple :

pygit2.Repository("/chemin/du/depot") # ouvre le depot
    .head                             # recupere la branche en cours
    .peel(pygit2.Commit)              # descend au commit
    .message                          # lit le message

Pour aller plus loin

Bien sûr, un traitement complet des capacités de Libgit2 est hors du cadre de ce livre. Si vous souhaitez plus d’information sur Libgit2 elle-même, la documentation de programmation se trouve sur https://libgit2.github.com/libgit2 et un ensemble de guides sur https://libgit2.github.com/docs. Pour les autres liaisons, cherchez dans le README et dans les tests ; il y a souvent des petits didacticiels et des pointeurs sur d’autres documents.

scroll-to-top