Intégration de Git dans vos applications - Git en ligne de commande
Si votre application est destinée aux développeurs, il y a de fortes chances qu'elle puisse bénéficier d'une intégration avec le contrôle de source. Même les applications non destinées aux développeurs, telles que les éditeurs de documents, pourraient potentiellement bénéficier des fonctionnalités de contrôle de version, et le modèle de Git fonctionne très bien dans de nombreux scénarios différents.
Si vous devez intégrer Git à votre application, vous avez essentiellement deux options : générer un interpréteur de commande et appeler le programme de ligne de commande git, ou intégrer une bibliothèque Git dans votre application. Nous aborderons ici l'intégration de la ligne de commande et plusieurs des bibliothèques Git intégrables les plus populaires.
Git en ligne de commande
Une option consiste à générer un processus d'interpréteur de commande et à utiliser l'outil de ligne de commande Git pour effectuer le travail. Cela a l'avantage d'être canonique et toutes les fonctionnalités de Git sont prises en charge. Cela s'avère également assez simple, car la plupart des environnements d'exécution disposent d'une fonction relativement simple pour invoquer un processus avec des paramètres de ligne de commande. Cependant, cette approche présente quelques inconvénients.
L'un d'eux est que toute la sortie est en texte brut. Cela signifie que vous devrez analyser le format de sortie de Git, changeant occasionnellement, pour lire les informations de progression et de résultat, ce qui peut être inefficace et sujet aux erreurs.
Un autre problème est l'absence de récupération d'erreur. Si un dépôt est corrompu d'une manière ou d'une autre, ou si l'utilisateur a une valeur de configuration mal formée, Git refusera tout simplement d'effectuer de nombreuses opérations.
Un autre problème est la gestion des processus. Git vous oblige à maintenir un environnement interpréteur de commande sur un processus distinct, ce qui peut ajouter une complexité indésirable. Essayer de coordonner plusieurs de ces processus (en particulier lorsque vous accédez potentiellement au même dépôt à partir de plusieurs processus) peut être un véritable défi.
Git intégré par bibliothèque
Il existe de nombreuses bibliothèques dans différents langages de programmation permettant l'intégration Git vos applications. Parmi les plus populaires, on retrouvera celle-ci :
Langage/Technologie | Bibliothèques |
---|---|
C/C++ | Libgit2 |
Delphi | GitForDelphi |
Erlang | Egit, Git2Er, Grit |
Free Pascal | LazGitGui |
Go | go-git |
Java | JGit |
JavaScript/Node.js | NodeGit, simple-git |
.NET (C#, VB.NET) | LibGit2Sharp, NGit, SharpGit, GitSharp |
Objective-C | ObjectiveGit |
Perl | Git::Repository, Git::Raw, Git::Wrapper, Git::Sub |
PHP | PHP Git |
Python | GitPython, pygit2 |
Ruby | Git, Rugged |
Swift | SwiftGit2 |
Libgit2
Une autre option à votre disposition est d'utiliser Libgit2. Libgit2 est une implémentation de Git sans dépendance, visant à créer une API agréable à utiliser dans d'autres programmes. Vous pouvez la trouver sur https://libgit2.org.
Commençons par jeter un oeil à ce à quoi ressemble l'API C. Voici une visite éclair :
- // Ouvrir un dépôt
- git_repository *repo;
- int error = git_repository_open(&repo, "/path/to/repository");
-
- // Déréférencer HEAD vers un commit
- git_object *head_commit;
- error = git_revparse_single(&head_commit, repo, "HEAD^{commit}");
- git_commit *commit = (git_commit*)head_commit;
-
- // Affiche certaines propriétés 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);
-
- // Nettoyage
- 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 descripteur vers un dépôt avec un cache en mémoire. C'est la méthode la plus simple, lorsque vous connaissez le chemin exact vers le répertoire de travail ou le dossier .git d'un dépôt. Il existe également git_repository_open_ext incluant des options de recherche, git_clone et ses amis pour créer un clone local d'un dépôt à distance, et git_repository_init pour créer un dépôt entièrement nouveau.
Le deuxième morceau de code utilise la syntaxe rev-parse pour obtenir le commit vers lequel HEAD pointe finalement. Le type renvoyé est un pointeur git_object, représentant quelque chose qui existe dans la base de données d'objets Git pour un dépôt. git_object est en fait un type «parent» pour plusieurs types d'objets différents ; la disposition de la mémoire pour chacun des types «child» est la même que pour git_object, vous pouvez donc effectuer un cast en toute sécurité vers le bon. Dans ce cas, git_object_type(commit) renverrait GIT_OBJ_COMMIT, il est donc sûr de convertir en pointeur git_commit.
Le segment suivant montre comment accéder aux propriétés du commit. La dernière ligne ici utilise un type git_oid ; il s'agit de la représentation de Libgit2 pour un hachage SHA-1.
À partir de cet exemple, quelques modèles ont commencé à émerger :
- Si vous déclarez un pointeur et passez une référence à celui-ci dans un appel Libgit2, cet appel renverra probablement un code d'erreur entier. Une valeur 0 indique le succès ; toute valeur inférieure est une erreur.
- Si Libgit2 remplit un pointeur pour vous, vous êtes responsable de sa libération.
- Si Libgit2 renvoie un pointeur const à partir d'un appel, vous n'avez pas à le libérer, mais il deviendra invalide lorsque l'objet auquel il appartient sera libéré.
- Écrire du C est un peu pénible.
Ce dernier point signifie qu'il est peu probable que vous écriviez du C lorsque vous utilisez Libgit2. Heureusement, il existe un certain nombre de liaisons spécifiques au langage disponibles facilitant le travail avec les dépôts Git de votre langage et de votre environnement spécifiques. Jetons un oeil à l'exemple ci-dessus écrit à l'aide des liaisons Ruby pour Libgit2, étant nommées Rugged et peuvent être trouvées 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
Comme vous pouvez le constater, le code est beaucoup moins encombré. Tout d'abord, Rugged utilise des exceptions ; il peut générer des erreurs telles que ConfigError ou ObjectError pour signaler des conditions d'erreur. Ensuite, il n'y a pas de libération explicite de ressources, puisque Ruby est récupéré par le ramasse-miettes. Prenons un exemple un peu plus compliqué, créer un commit à partir de zéro :
- blob_id = repo.write("Contenu de Blob", :blob) # (1)
-
- index = repo.index
- index.read_tree(repo.head.target.tree)
- index.add(:path => 'newfile.txt', :oid => blob_id) # (2)
-
- sig = {
- :email => "smaltais@gladir.com",
- :name => "Sylvain Maltais",
- :time => Time.now,
- }
-
- commit_id = Rugged::Commit.create(repo,
- :tree => index.write_tree(repo), # (3)
- :author => sig,
- :committer => sig, # (4)
- :message => "Ajout newfile.txt", # (5)
- :parents => repo.empty? ? [] : [ repo.head.target ].compact, # (6)
- :update_ref => 'HEAD', # (7)
- )
- commit = repo.lookup(commit_id) # (8)
- Créez un nouveau blob, contenant le contenu d'un nouveau fichier.
- Remplissez l'index avec l'arborescence du commit principal et ajoutez le nouveau fichier au chemin newfile.txt.
- Cela crée une nouvelle arborescence dans l'ODB et l'utilise pour le nouveau commit.
- Nous utilisons la même signature pour les champs author et committer.
- Le message de commit.
- Lors de la création d'un commit, vous devez spécifier les parents du nouveau commit. Cela utilise la pointe de HEAD pour le parent unique.
- Rugged (et Libgit2) peuvent éventuellement mettre à jour une référence lors de la réalisation d'un commit.
- La valeur de retour est le hachage SHA-1 d'un nouvel objet commit, que vous pouvez ensuite utiliser pour obtenir un objet Commit.
Le code Ruby est beau et propre, mais comme Libgit2 fait le gros du travail, ce code s'exécutera également assez rapidement.
Fonctionnalités avancées
Libgit2 possède quelques fonctionnalités étant en dehors du champ d'application de Git de base. Un exemple est la possibilité de connexion : Libgit2 vous permet de fournir des «backends» personnalisés pour plusieurs types d'opérations, ce qui vous permet d'entreposer des éléments d'une manière différente de celle de Git standard. Libgit2 permet des backends personnalisés pour la configuration, l'entreposage des références et la base de données d'objets, entre autres.
Voyons comment cela fonctionne. Le code ci-dessous est emprunté à l'ensemble d'exemples de backends fournis par l'équipe Libgit2 (que vous pouvez trouver sur https://github.com/libgit2/libgit2-backends). Voici comment un backend personnalisé pour la base de données d'objets est configuré avec du code en C :
- 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)
Notez que les erreurs sont capturées, mais pas traitées. Nous espérons que votre code est meilleur que le nôtre.
- Initialisez une base de données d'objets vide (ODB) «frontend», servant de conteneur pour les «backends» étant ceux faisant le vrai travail.
- Initialisez un backend ODB personnalisé.
- Ajoutez le backend au frontend.
- Ouvrez un dépôt et configurez-le pour utiliser notre ODB pour rechercher des objets.
Mais qu'est-ce que ce truc git_odb_backend_mine ? Eh bien, c'est le constructeur de votre propre implémentation ODB, et vous pouvez y faire ce que vous voulez, tant que vous remplissez correctement la structure git_odb_backend. Voici à quoi cela pourrait ressembler :
- typedef struct {
- git_odb_backend parent;
-
- // D'autres trucs
- 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_struct doit être une structure git_odb_backend ; cela garantit que la disposition de la mémoire est celle attendue par le code Libgit2. Le reste est arbitraire; cette structure peut être aussi grande ou petite que vous le souhaitez.
La fonction d'initialisation alloue de la mémoire à la structure, définit le contexte personnalisé, puis remplit les membres de la structure parent qu'elle prend en charge. Jetez un oeil au fichier include/git2/sys/odb_backend.h dans la source Libgit2 pour un ensemble complet de signatures d'appel; votre cas d'utilisation particulier aidera à déterminer lesquelles d'entre elles vous voudrez prendre en charge.
Autres liaisons
Libgit2 possède des liaisons pour de nombreux langages de programmation. Nous montrons ici un petit exemple utilisant quelques-uns des paquets de liaisons les plus complets à ce jour; des bibliothèques existent pour de nombreux autres langages, notamment C++, Go, Node.js, Erlang et la JVM, tous à 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 renverra le message de validation du commit éventuellement pointé par HEAD (un peu comme git log -1).
LibGit2Sharp
Si vous écrivez une application .NET ou Mono, LibGit2Sharp (https://github.com/libgit2/libgit2sharp) est ce que vous recherchez. Les liaisons sont écrites en C# et un grand soin a été apporté à l'encapsulation des appels bruts de Libgit2 avec des API CLR natives. Voici à quoi ressemble notre exemple de programme :
- new Repository(@"C:\path\to\repo").Head.Tip.Message;
Pour les applications Windows de bureau, il existe même un paquet NuGet vous aidant à démarrer rapidement.
objective-git
Si votre application s'exécute sur une plateforme Apple, vous utilisez peut-être Objective-C comme langage d'implémentation. Objective-Git (https://github.com/libgit2/objective-git) est le nom des liaisons 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 entièrement interopérable avec Swift, alors ne vous inquiétez pas si vous avez laissé Objective-C derrière vous.
pygit2
Les liaisons pour Libgit2 en Python sont appelées Pygit2 et peuvent être trouvées sur https://www.pygit2.org. Notre exemple de programme :
- pygit2.Repository("/path/to/repo") # Ouvre le dépôt
- .head # Obtenir la branche actuelle
- .peel(pygit2.Commit) # Marcher jusqu'au commit
- .message # Lire le message
JGit
Si vous souhaitez utiliser Git à partir d'un programme Java, il existe une bibliothèque Git complète appelée JGit. JGit est une implémentation relativement complète de Git écrite nativement en Java et est largement utilisée dans la communauté Java. Le projet JGit est sous l'égide d'Eclipse et son site se trouve à l'adresse https://www.eclipse.org/jgit/.
Il existe plusieurs façons de connecter votre projet à JGit et de commencer à écrire du code dessus. Le plus simple est probablement d'utiliser Maven - l'intégration s'effectue en ajoutant l'extrait suivant à la balise dependencies dans votre fichier pom.xml :
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>3.5.0.201409260305-r</version>
</dependency>
La version aura probablement évolué au moment où vous lirez ceci ; consultez https://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit pour obtenir des informations actualisées sur le dépôt. Une fois cette étape terminée, Maven acquerra et utilisera automatiquement les bibliothèques JGit dont vous aurez besoin.
Si vous préférez gérer vous-même les dépendances binaires, des binaires JGit pré-construits sont disponibles sur https://www.eclipse.org/jgit/download. Vous pouvez les intégrer à votre projet en exécutant une commande comme celle-ci :
javac -cp .:org.eclipse.jgit-3.5.0.201409260305-r.jar App.java java -cp .:org.eclipse.jgit-3.5.0.201409260305-r.jar App |
plumbing
JGit possède deux niveaux d'API de base : plumbing et porcelain. La terminologie utilisée pour ces deux types de tâches provient de Git lui-même, et JGit est divisé en deux types de domaines à peu près identiques : les API de porcelain sont une interface conviviale pour les actions courantes au niveau de l'utilisateur (le genre de choses pour lesquelles un utilisateur normal utiliserait l'outil de ligne de commande Git), tandis que les API de plumbing servent à interagir directement avec les objets de dépôt de bas niveau.
Le point de départ de la plupart des sessions JGit est la classe Repository, et la première chose que vous voudrez faire est de créer une instance de celle-ci. Pour un dépôt basé sur un système de fichiers (oui, JGit autorise d'autres modèles d'entreposage), cela s'effectue à l'aide de FileRepositoryBuilder :
Le constructeur dispose d'une API fluide pour fournir tout ce dont il a besoin pour trouver un dépôt Git, que votre programme sache ou non exactement où il se trouve. Il peut utiliser des variables d'environnement (.readEnvironment()), démarrer à partir d'un emplacement dans le répertoire de travail et effectuer une recherche (.setWorkTree(...).findGitDir()), ou simplement ouvrir un répertoire .git connu comme ci-dessus.
Une fois que vous avez une instance de dépôt, vous pouvez faire toutes sortes de choses avec elle. Voici un échantillon rapide :
- // Demande une référence
- Ref master = repo.getRef("master");
-
- // Demande l'objet vers lequel pointe la référence
- ObjectId masterTip = master.getObjectId();
-
- // Analyse de Rev
- ObjectId obj = repo.resolve("HEAD^{tree}");
-
- // Charger le contenu brut de l'objet
- ObjectLoader loader = repo.open(masterTip);
- loader.copyTo(System.out);
-
- // Créer une branche
- RefUpdate createBranch1 = repo.updateRef("refs/heads/branch1");
- createBranch1.setNewObjectId(masterTip);
- createBranch1.update();
-
- // Supprimer une branche
- RefUpdate deleteBranch1 = repo.updateRef("refs/heads/branch1");
- deleteBranch1.setForceUpdate(true);
- deleteBranch1.delete();
-
- // Configuration
- Config cfg = repo.getConfig();
- String name = cfg.getString("user", null, "name");
Il se passe beaucoup de choses ici, alors passons en revue une section à la fois.
La première ligne obtient un pointeur vers la référence principale. JGit récupère automatiquement la référence principale réelle, se trouvant dans refs/heads/master, et renvoie un objet vous permettant de récupérer des informations sur la référence. Vous pouvez obtenir le nom (.getName()), et soit l'objet cible d'une référence directe (.getObjectId()), soit la référence pointée par une référence symbolique (.getTarget()). Les objets de référence sont également utilisés pour représenter les références et les objets de balise, vous pouvez donc demander si la balise est «peeled», ce qui signifie qu'elle pointe vers la cible finale d'une chaîne de caractères (potentiellement longue) d'objets de balise.
La deuxième ligne obtient la cible de la référence principale, étant renvoyée sous la forme d'une instance ObjectId. ObjectId représente le hachage SHA-1 d'un objet, pouvant ou non exister dans la base de données d'objets de Git. La troisième ligne est similaire, mais montre comment JGit gère la syntaxe rev-parse; vous pouvez passer n'importe quel spécificateur d'objet que Git comprend, et JGit renverra soit un ObjectId valide pour cet objet, soit null.
Les deux lignes suivantes montrent comment charger le contenu brut d'un objet. Dans cet exemple, nous appelons ObjectLoader.copyTo() pour diffuser le contenu de l'objet directement sur stdout, mais ObjectLoader dispose également de méthodes pour lire le type et la taille d'un objet, ainsi que pour le renvoyer sous forme de tableau d'octets. Pour les objets volumineux (où .isLarge() renvoie true), vous pouvez appeler .openStream() pour obtenir un objet de type InputStream pouvant lire les données brutes de l'objet sans les extraire toutes en mémoire en une seule fois.
Les quelques lignes suivantes montrent ce qu'il faut pour créer une nouvelle branche. Nous créons une instance RefUpdate, configurons certains paramètres et appelons .update() pour déclencher la modification. Juste après, voici le code pour supprimer cette même branche. Notez que .setForceUpdate(true) est requis pour que cela fonctionne ; sinon, l'appel .delete() renverra REJECTED et rien ne se passera.
Le dernier exemple montre comment récupérer la valeur user.name à partir des fichiers de configuration Git. Cette instance Config utilise le dépôt que nous avons ouvert précédemment pour la configuration locale, mais détectera automatiquement les fichiers de configuration globale et système et lira également les valeurs à partir de ceux-ci.
Ce n'est qu'un petit échantillon de l'API de plumbing complète ; il existe de nombreuses autres méthodes et classes disponibles. La manière dont JGit gère les erreurs, se faisant par l'utilisation d'exceptions, n'est pas non plus illustrée ici. Les API de JGit génèrent parfois des exceptions Java standard (telles que IOException), mais il existe également une multitude de types d'exceptions spécifiques à JGit (tels que NoRemoteRepositoryException, CorruptObjectException et NoMergeBaseException).
porcelain
Les API de plumbing sont assez complètes, mais il peut être difficile de les enchaîner pour atteindre des objectifs communs, comme ajouter un fichier à l'index ou effectuer un nouveau commit. JGit fournit un ensemble d'API de niveau supérieur pour vous aider, et le point d'entrée de ces API est la classe Git :
- Repository repo;
- // construire un dépôt...
- Git git = new Git(repo);
La classe Git dispose d'un bel ensemble de méthodes de haut niveau de type builder pouvant être utilisées pour construire un comportement assez complexe. Prenons un exemple : exécuter quelque chose comme git ls-remote :
- CredentialsProvider cp = new UsernamePasswordCredentialsProvider("monutilisateur", "motdepasse");
- Collection<Ref> remoteRefs = git.lsRemote()
- .setCredentialsProvider(cp)
- .setRemote("origin")
- .setTags(true)
- .setHeads(false)
- .call();
- for (Ref ref : remoteRefs) {
- System.out.println(ref.getName() + " -> " + ref.getObjectId().name());
- }
Il s'agit d'un modèle courant avec la classe Git ; les méthodes renvoient un objet de commande vous permettant d'enchaîner les appels de méthode pour définir des paramètres, étant exécutés lorsque vous appelez .call(). Dans ce cas, nous demandons à la télécommande d'origine des balises, mais pas des têtes. Notez également l'utilisation d'un objet CredentialsProvider pour l'authentification.
De nombreuses autres commandes sont disponibles via la classe Git, notamment, mais sans s'y limiter, add, blame, commit, clean, push, rebase, revert et reset.
go-git
Si vous souhaitez intégrer Git dans un service écrit en Go, il existe également une implémentation pure de la bibliothèque Go. Cette implémentation n'a aucune dépendance native et n'est donc pas sujette aux erreurs de gestion manuelle de la mémoire. Elle est également transparente pour les outils d'analyse des performances Go standard tels que le processeur, les profileurs de mémoire, le détecteur d'exécution,...
go-git se concentre sur l'extensibilité, la compatibilité et prend en charge la plupart des API de plomberie, étant documentées sur https://github.com/go-git/go-git/blob/master/COMPATIBILITY.md.
Voici un exemple de base d'utilisation des API de Go :
- import "github.com/go-git/go-git/v5"
-
- r, err := git.PlainClone("/tmp/foo", false, &git.CloneOptions{
- URL: "https://github.com/go-git/go-git",
- Progress: os.Stdout,
- })
Dès que vous disposez d'une instance de Repository, vous pouvez accéder aux informations et effectuer des mutations dessus :
- // Récupère la branche pointée par HEAD
- ref, err := r.Head()
-
- // Récupérer l'objet commit, pointé par ref
- commit, err := r.CommitObject(ref.Hash())
-
- // Récupère l'historique des commits
- history, err := commit.History()
-
- // Itère sur les commits et affiche chacun
- for _, c := range history {
- fmt.Println(c)
- }
Fonctionnalités avancées
go-git possède quelques fonctionnalités avancées notables, dont un système d'entreposage enfichable, similaire aux backends Libgit2. L'implémentation par défaut est l'entreposage en mémoire, étant très rapide :
- r, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{
- URL: "https://github.com/go-git/go-git",
- })
L'entreposage enfichable offre de nombreuses options intéressantes. Par exemple, https://github.com/go-git/go-git/tree/master/_examples/storage vous permet d'entreposer des références, des objets et des configurations dans une base de données Aerospike.
Une autre fonctionnalité est une abstraction flexible du système de fichiers. En utilisant https://pkg.go.dev/github.com/go-git/go-billy/v5?tab=doc#Filesystem, il est facile d'entreposer tous les fichiers de différentes manières, c'est-à-dire en les regroupant tous dans une seule archive sur disque ou en les conservant tous en mémoire.
Un autre cas d'utilisation avancé comprend un client HTTP affiné, tel que celui que l'on trouve sur https://github.com/go-git/go-git/blob/master/_examples/custom_http/main.go :
- customClient := &http.Client{
- Transport: &http.Transport{ // accepter n'importe quel certificat (peut être utile pour les tests)
- TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
- },
- Timeout: 15 * time.Second, // Délai d'attente de 15 secondes
- CheckRedirect: func(req *http.Request, via []*http.Request) error {
- return http.ErrUseLastResponse // don't follow redirect
- },
- }
-
- // Remplacer le protocole http(s) par défaut pour utiliser notre client personnalisé
- client.InstallProtocol("https", githttp.NewClient(customClient))
-
- // Cloner le dépôt à l'aide du nouveau client si le protocole est https://
- r, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{URL: url})