Utiliser les API

Développer des applications utilisant les API des bibliothèques Subversion est plutôt simple. Subversion est d'abord un ensemble de bibliothèques en langage C, avec des fichiers d'en-têtes (.h) situés dans le répertoire subversion/include de l'arborescence des sources. Ces en-têtes sont copiés dans votre arborescence système (par exemple /usr/local/include) quand vous compilez et installez Subversion à partir des sources. Ces en-têtes contiennent l'ensemble des fonctions et des types censés être accessibles aux utilisateurs des bibliothèques Subversion. La communauté des développeurs Subversion apporte beaucoup d'attention à la disponibilité et la qualité de la documentation des API publiques — reportez-vous directement aux fichiers d'en-têtes pour cette documentation.

Quand vous examinez les fichiers d'en-tête publics, la première chose que vous remarquez est que les types de données et les fonctions ont un espace de nommage réservé. Cela veut dire que tous les noms de symboles Subversion publics commencent par svn_, suivi d'un code indiquant la bibliothèque dans laquelle le symbole est défini (par exemple wc, client, fs, etc.), suivi d'un unique caractère souligné (_) puis du reste du nom du symbole. Les fonctions semi-publiques (utilisées par plusieurs fichiers au sein d'une bibliothèque mais pas par du code extérieur à cette bibliothèque, on peut les trouver au sein des répertoires de la bibliothèque) suivent une règle de nommage légèrement différente dans le sens où, au lieu d'un unique caractère souligné après le code indiquant la bibliothèque, elles utilisent deux caractères souligné consécutifs (_ _). Les fonctions qui sont propres à un fichier source (c'est-à-dire privées) n'ont pas de préfixe particulier et sont déclarées avec le mot-clé static. Bien sûr, un compilateur n'a que faire de ces conventions de nommage, mais elles sont une aide précieuse pour clarifier la portée d'une fonction ou d'un type de données particuliers.

Une autre bonne source d'informations sur la programmation avec les API Subversion est constituée par les bonnes pratiques de programmation au sein du projet lui-même, que vous pouvez trouver à l'adresse suivante http://subversion.apache.org/docs/community-guide/ (pages en anglais). Ce document contient des informations particulièrement utiles qui, bien que destinées aux développeurs (ou aux personnes désireuses de le devenir) de Subversion lui-même, peuvent également servir à tous ceux qui développent des applications utilisant Subversion comme bibliothèque tierce [55].

APR, la bibliothèque Apache de portabilité des exécutables

À côté des types de données propres à Subversion, vous trouverez de nombreuses références à des types de données qui commencent par apr_ : ce sont les symboles de la bibliothèque pour la portabilité d'Apache (Apache Portable Runtime en anglais, soit APR). APR est un jeu de bibliothèques Apache, originellement extraites du code source du serveur pour essayer de séparer ce qui dépendait du système d'exploitation de ce qui n'en dépendait pas. Au final, on obtient une bibliothèque qui fournit une API permettant d'effectuer des opérations qui changent un peu (ou beaucoup) en fonction du système d'exploitation. Alors que le serveur HTTP Apache était le premier utilisateur (et pour cause) de la bibliothèque APR, les développeurs Subversion ont immédiatement perçu les avantages qu'il y a à utiliser APR. Cela signifie qu'il n'y a pratiquement aucun code spécifique à un système d'exploitation dans Subversion en tant que tel. Cela veut aussi dire que le client Subversion peut être compilé et exécuté partout où un serveur Apache peut l'être. Actuellement, cette liste comprend toutes les variantes d'Unix, Win32, BeOS, OS/2 et Mac OS X.

En plus de fournir des implémentations fiables des appels systèmes qui diffèrent d'un système d'exploitation à l'autre [56], APR fournit à Subversion un accès direct à de nombreux types de données personnalisés tels que les tableaux dynamiques et les tables de hachage. Subversion utilise abondamment ces types de données et le type de données APR le plus utilisé, que l'on retrouve dans presque tous les prototypes de l'API Subversion, est apr_pool_t — le réservoir de mémoire (memory pool en anglais) APR. Subversion utilise les réservoirs de mémoire en interne pour tous ses besoins d'allocation mémoire (à moins qu'une bibliothèque externe ne requière un autre mécanisme de gestion de la mémoire pour les données transmises via son API) [57] et, bien qu'une personne qui utilise l'API Subversion ne soit pas obligée d'en faire autant, elle doit fournir des réservoirs aux fonctions de l'API qui en ont besoin. Cela implique que les utilisateurs de l'API Subversion doivent également inclure l'APR lors de l'édition de liens, doivent appeler apr_initialize() pour initialiser le sous-système APR et doivent ensuite créer et gérer des réservoirs de mémoire pour les appels à l'API Subversion, généralement en utilisant svn_pool_create(), svn_pool_clear() et svn_pool_destroy().

Prérequis pour les URL et les chemins

Subversion a été conçu pour effectuer à distance des opérations de gestion de versions. À ce titre, les possibilités d'internationalisation (i18n) ont fait l'objet d'une attention toute particulière. Après tout, « à distance » peut vouloir dire depuis un ordinateur situé « dans le même bureau », mais aussi « à l'autre bout de la planète ». Pour faciliter cette prise en compte, toutes les interfaces publiques de Subversion qui acceptent des chemins comme argument s'attendent à ce que ces chemins soient rendus canoniques — la façon la plus facile de le faire étant de les passer en argument à la fonction svn_path_canonicalize() — et codés dans le format UTF-8. Cela signifie, par exemple, que tout nouveau programme client qui pilote l'interface libsvn_client doit d'abord convertir les chemins depuis le codage local vers UTF-8 avant de fournir ces chemins à la bibliothèque Subversion, puis doit reconvertir tout chemin renvoyé par Subversion vers le codage local avant d'utiliser ce chemin à des fins externes à Subversion. Heureusement, Subversion fournit un ensemble de fonctions (voir subversion/include/svn_utf.h) que tout programme peut utiliser pour réaliser ces conversions.

De plus, les API Subversion demandent que toutes les URL passées en paramètres respectent le format URI. Ainsi, au lieu de désigner par file:///home/utilisateur/Mon fichier.txt l'URL d'un fichier nommé Mon fichier.txt situé dans le répertoire home/utilisateur, vous devez utiliser file:///home/utilisateur/Mon%20fichier.txt. Là encore, Subversion fournit des fonctions utiles à votre application — svn_path_uri_encode() et svn_path_uri_decode() pour coder et décoder, respectivement, des URI.

Utiliser d'autres langages que C et C++

Si vous désirez utiliser les bibliothèques Subversion à partir d'un autre langage que le C (par exemple un programme Python ou Perl), Subversion offre cette possibilité via le générateur simplifié d'interface et d'encapsulation (Simplified Wrapper and Interface Generator ou SWIG en anglais). Les interfaces SWIG de Subversion sont situées dans le répertoire subversion/bindings/swig. Elles sont toujours en cours d'évolution mais sont utilisables. Elles vous permettent d'appeler les fonctions de l'API Subversion indirectement, en utilisant des interfaces qui traduisent les types de données natifs de votre langage de programmation vers les types de données utilisés par les bibliothèques C de Subversion.

Des efforts significatifs ont été fournis pour produire des interfaces SWIG pleinement fonctionnelles pour Python, Perl et Ruby. D'une certaine manière, le travail effectué pour réaliser les interfaces vers ces langages est réutilisable pour produire des interfaces vers d'autres langages supportés par SWIG (ce qui inclut, entre autres, des versions de C#, Guile, Java, MzScheme, OCaml, PHP et Tcl). Cependant, vous aurez besoin d'un peu de programmation supplémentaire pour aider SWIG à faire les traductions entre les langages pour les API complexes. Pour plus d'informations sur SWIG lui-même, visitez le site Web du projet à l'adresse suivante : http://www.swig.org/ (site en anglais).

Subversion fournit également une interface vers le langage Java. L'interface javahl (située dans subversion/bindings/java dans l'arborescence des sources Subversion) n'est pas basée sur SWIG mais est un mélange de Java et de JNI codé à la main. Javahl couvre le plus gros des API du client Subversion et se destine principalement aux développeurs d'environnements de développement intégrés (IDE) et de clients Subversion en Java.

Les interfaces Subversion vers les langages de programmation ne sont pas suivies avec le même niveau d'exigence que les modules du cœur de Subversion, mais peuvent généralement être utilisées en production. De nombreuses applications, de nombreux scripts, des clients graphiques alternatifs et des outils tiers utilisent aujourd'hui sans problème les interfaces vers les langages de programmation afin d'intégrer les fonctionnalités de Subversion.

Veuillez tout de même noter qu'il existe d'autres options pour s'interfacer avec Subversion dans d'autres langages : les interfaces pour Subversion qui ne sont pas fournies par la communauté de développement Subversion. Vous pouvez trouver des liens vers ces interfaces alternatives sur la page de liens externes du projet Subversion (à l'adresse http://subversion.tigris.org/links.html) et, en particulier, nous accordons une mention spéciale à deux d'entre elles. D'abord, l'interface PySVN de Barry Scott (http://pysvn.tigris.org/) est une interface reconnue vers Python. PySVN se targue d'une interface plus « pythonique » que les API « orientées C » fournies par l'interface standard de Subversion vers Python. Et si vous recherchez une implémentation 100 % Java de Subversion, jetez un œil à SVNKit (http://svnkit.com/), qui est une ré-écriture complète de Subversion en Java.

Exemples de code

L'Exemple 8.1, « Utilisation de la couche dépôt » contient un bout de code (écrit en C) qui illustre plusieurs concepts que nous venons d'aborder. Il utilise à la fois l'interface du dépôt et celle du système de fichiers (comme dénoté par les préfixes svn_repos_ et svn_fs_ des noms de fonctions) pour créer une nouvelle révision dans laquelle un répertoire est ajouté. Vous pouvez y observer l'utilisation du réservoir de mémoire APR qui est utilisé pour les besoins d'allocation mémoire. En outre, le code révèle le côté obscur de la gestion des erreurs de Subversion : toutes les erreurs Subversion doivent être explicitement prises en compte pour éviter des fuites de mémoire (et dans certains cas, le plantage de l'application).

Exemple 8.1. Utilisation de la couche dépôt

/* Convertit une erreur Subversion en un simple code d'erreur booléen
 *
 * NOTE:  Les erreurs Subversion doivent être effacées (en utilisant 
 *        svn_error_clear()) parce qu'elles sont allouées depuis le 
 *        réservoir global, sinon cela produit une fuite de mémoire.
 */
#define INT_ERR(expr)                           \
  do {                                          \
    svn_error_t *__temperr = (expr);            \
    if (__temperr)                              \
      {                                         \
        svn_error_clear(__temperr);             \
        return 1;                               \
      }                                         \
    return 0;                                   \
  } while (0)

/* Crée un nouveau répertoire NOUVEAU_REP dans le dépôt Subversion
 * situé à CHEMIN_DEPOT. Effectue toutes les allocations mémoire dans
 * RESERVOIR. Cette fonction créera une nouvelle révision pour l'ajout
 * de NOUVEAU_REP. Elle retourne zéro si l'opération se termine 
 * correctement, une valeur différente de zéro sinon.
 */
static int
cree_nouveau_rep(const char *chemin_depot,
                 const char *nouveau_rep,
                 apr_pool_t *reservoir)
{
  svn_error_t *err;
  svn_repos_t *depot;
  svn_fs_t *fs;
  svn_revnum_t derniere_rev;
  svn_fs_txn_t *transaction;
  svn_fs_root_t *racine_transaction;
  const char *chaine_conflit;

  /* Ouvre le dépôt situé à chemin_depot. 
   */
  INT_ERR(svn_repos_open(&depot, chemin_depot, reservoir));

  /* Obtient un pointeur sur l'objet du système de fichiers qui est 
   * stocké dans CHEMIN_DEPOT. 
   */
  fs = svn_repos_fs(depot);

  /* Demande au système de fichiers de nous fournir le numéro de la 
   * révision la plus récente.
   */
  INT_ERR(svn_fs_youngest_rev(&derniere_rev, fs, reservoir));

  /* Commence une nouvelle transaction qui est basée sur DERNIERE_REV.
   * Nous aurons moins de chance de voir notre propagation rejetée pour
   * cause de conflit si nous effectuons toujours nos changements à partir du 
   * dernier instantané de l'arborescence du système de fichiers.
   */
  INT_ERR(svn_repos_fs_begin_txn_for_commit2(&transaction, depot, 
                                             derniere_rev,
                                             apr_hash_make(reservoir), 
                                             reservoir));

  /* Maintenant qu'une nouvelle transaction Subversion est commencée, 
   * obtient l'objet racine qui représente cette transaction.
   */
  INT_ERR(svn_fs_txn_root(&racine_transaction, transaction, reservoir));
  
  /* Crée un nouveau répertoire sous la racine de la transaction, au 
   * chemin NOUVEAU_REP. 
   */
  INT_ERR(svn_fs_make_dir(racine_transaction, nouveau_rep, reservoir));

  /* Propage la transaction, créant une nouvelle révision du système de 
   * fichiers incluant le nouveau répertoire.
   */
  err = svn_repos_fs_commit_txn(&chaine_conflit, depot, 
                                &derniere_rev, transaction, reservoir);
  if (! err)
    {
      /* Pas d'erreur ? Excellent ! Indique brièvement la réussite 
       * de l'opération.
       */
      printf("Le répertoire '%s' a été ajouté en tant que nouvelle "
             "révision '%ld'.\n", nouveau_rep, derniere_rev);
    }
  else if (err->apr_err == SVN_ERR_FS_CONFLICT)
    {
      /* Oh-oh. La propagation a échoué pour cause de conflit (il semble
       * que quelqu'un d'autre a effectué des changements dans la même
       * zone du système de fichiers que celle que nous avons essayé de
       * modifier). Affiche un message d'erreur.
       */
      printf("Un conflit s'est produit pour le chemin '%s' lors de"
             " l'ajout du répertoire '%s' au dépôt '%s'.\n", 
             chaine_conflit, nouveau_rep, chemin_depot);
    }
  else
    {
      /* Une autre erreur s'est produite. Affiche un message d'erreur.
       */
      printf("Une erreur s'est produite lors de l'ajout du "
             "répertoire '%s' au dépôt '%s'.\n", 
             nouveau_rep, chemin_depot);
    }

  INT_ERR(err);
} 

Notez que dans l'Exemple 8.1, « Utilisation de la couche dépôt », le code aurait tout aussi bien pu propager la transaction en utilisant svn_fs_commit_txn(). Mais l'API du système de fichiers ignore tout des mécanismes de procédures automatiques de la bibliothèque du dépôt. Si vous voulez que votre dépôt Subversion effectue automatiquement certaines tâches externes à Subversion chaque fois qu'une transaction est propagée (par exemple envoyer un mail qui décrit les changements effectués dans la transaction à la liste de diffusion des développeurs), vous devez utiliser la version de la fonction encapsulée dans libsvn_repos qui ajoute la fonctionnalité d'activation des procédures automatiques : svn_repos_fs_commit_txn() (pour davantage d'informations sur les procédures automatiques des dépôts Subversion, consultez la section intitulée « Mise en place des procédures automatiques »).

Maintenant, changeons de langage. L'Exemple 8.2, « Utilisation de la couche dépôt en Python » est un programme de démonstration qui utilise l'interface SWIG vers Python pour parcourir récursivement la dernière révision du dépôt et afficher les différents chemins trouvés lors de ce parcours.

Exemple 8.2. Utilisation de la couche dépôt en Python

#!/usr/bin/python

"""Parcourir un dépôt en affichant les chemins des objets suivis en
versions."""

import sys
import os.path
import svn.fs, svn.core, svn.repos

def parcourir_rep_systemedefichiers(racine, repertoire):
    """Parcourt récursivement le REPERTOIRE situé sous RACINE dans le 
       système de fichiers. Renvoie la liste de tous les chemins sous et 
       de REPERTOIRE."""

    # Affiche le nom de ce chemin.
    print repertoire + "/"
    
    # Obtient les entrées du répertoire REPERTOIRE.
    entrees = svn.fs.svn_fs_dir_entries(racine, repertoire)

    # Pour chaque entrée
    noms = entrees.keys()
    for nom in noms:
        # Calcule le chemin complet de l'entrée.
        chemin_complet = repertoire + '/' + nom

        # Si l'entrée est un répertoire, effectue une récursion. La
        # récursion retournera une liste comprenant l'entrée et tous ses
        # enfants, que l'on ajoutera à notre liste.
        if svn.fs.svn_fs_is_dir(racine, chemin_complet):
            parcourir_rep_systemedefichiers(racine, chemin_complet)
        else:
            # Sinon, c'est un fichier donc l'afficher maintenant.
            print chemin_complet

def parcourir_la_plus_recente_revision(chemin_depot):
    """Ouvre le dépôt situé à CHEMIN_DEPOT et effectue un parcours 
       récursif de la révision la plus récente."""
    
    # Ouvre le dépôt situé à CHEMIN_DEPOT et obtient une référence de
    # son système de fichiers suivi en versions.
    objet_depot = svn.repos.svn_repos_open(chemin_depot)
    objet_fs = svn.repos.svn_repos_fs(objet_depot)

    # Obtient la révision la plus récente (HEAD).
    rev_la_plus_recente = svn.fs.svn_fs_youngest_rev(objet_fs)
    
    # Ouvre un objet racine représentant la révision la plus récente.
    objet_racine = svn.fs.svn_fs_revision_root(objet_fs, 
                                               rev_la_plus_recente)

    # Effectue le parcours récursif.
    parcourir_rep_systemedefichiers(objet_racine, "")
    
if __name__ == "__main__":
    # Vérifie que l'on est appelé correctement.
    if len(sys.argv) != 2:
        sys.stderr.write("Usage: %s CHEMIN_DEPOT\n"
                         % (os.path.basename(sys.argv[0])))
        sys.exit(1)

    # Transforme la chaîne en chemin canonique.
    chemin_depot = svn.core.svn_path_canonicalize(sys.argv[1])

    # Et c'est parti !
    parcourir_la_plus_recente_revision(chemin_depot)

Le même programme en C aurait besoin de faire appel aux réservoirs de mémoire d'APR. Mais Python gère l'utilisation de la mémoire automatiquement et l'interface Subversion vers Python se plie à cette convention. En C, vous auriez utilisé des types de données personnalisés (tels que ceux fournis par la bibliothèque APR) pour représenter la table de hachage des entrées et la liste des chemins, mais Python sait gérer nativement les tables de hachage (appelés « dictionnaires ») ainsi que les listes et possède une riche collection de fonctions pour travailler sur ces types de données. C'est pourquoi SWIG (avec l'aide de la couche d'interface vers les langages de programmation de Subversion, un peu modifiée) prend soin de faire correspondre ces types de données personnalisés aux types de données natifs du langage cible. On obtient ainsi une interface plus intuitive pour les utilisateurs de ce langage.

L'interface de Subversion vers Python peut également être utilisée pour effectuer des opérations dans la copie de travail. Dans la section précédente de ce chapitre, nous avons mentionné l'interface libsvn_client et le fait qu'elle a été conçue dans le seul but de faciliter l'écriture d'un client Subversion. L'Exemple 8.3, « Une version de status en Python » est un court exemple d'utilisation de cette bibliothèque via l'interface Python SWIG pour re-créer une version à petite échelle de la commande svn status.

Exemple 8.3. Une version de status en Python

#!/usr/bin/env python

"""Parcourir un répertoire d'une copie de travail en affichant les 
informations d'état."""

import sys
import os.path
import getopt
import svn.core, svn.client, svn.wc

def generer_code_etat(etat):
    """Traduit la valeur d'état vers un code à un caractère en 
    utilisant la même logique que le client Subversion en ligne de 
    commande."""
    association_etat = { svn.wc.svn_wc_status_none        : ' ',
                         svn.wc.svn_wc_status_normal      : ' ',
                         svn.wc.svn_wc_status_added       : 'A',
                         svn.wc.svn_wc_status_missing     : '!',
                         svn.wc.svn_wc_status_incomplete  : '!',
                         svn.wc.svn_wc_status_deleted     : 'D',
                         svn.wc.svn_wc_status_replaced    : 'R',
                         svn.wc.svn_wc_status_modified    : 'M',
                         svn.wc.svn_wc_status_merged      : 'G',
                         svn.wc.svn_wc_status_conflicted  : 'C',
                         svn.wc.svn_wc_status_obstructed  : '~',
                         svn.wc.svn_wc_status_ignored     : 'I',
                         svn.wc.svn_wc_status_external    : 'X',
                         svn.wc.svn_wc_status_unversioned : '?',
               }
    return association_etat.get(etat, '?')

def trouver_etat(chemin_copie_travail, verbeux):
    # Construit le "bâton" de contexte client.
    ctx = svn.client.svn_client_ctx_t()

    def _status_callback(path, etat):
        """Une fonction de renvoi ("callback") pour svn_client_status."""

        # Affiche le chemin, moins la partie déjà présente
        # dans la racine du parcours.
        text_status = generer_code_etat(etat.text_status)
        prop_status = generer_code_etat(etat.prop_status)
        print '%s%s  %s' % (text_status, prop_status, path)
        
    # Effectue le parcours des états, en utilisant _status_callback() 
    # comme fonction de renvoi ("callback").
    revision = svn.core.svn_opt_revision_t()
    revision.type = svn.core.svn_opt_revision_head
    svn.client.svn_client_status2(chemin_copie_travail, revision, 
                                  _status_callback, 
                                  svn.core.svn_depth_infinity, verbeux,
                                  0, 0, 1, ctx)

def utilisation_et_sortie(code_erreur):
    """Affiche le message d'utilisation et sort avec CODE_ERREUR."""
    stream = code_erreur and sys.stderr or sys.stdout
    stream.write("""Usage: %s OPTIONS CHEMIN_COPIE_TRAVAIL
Options:
  --help, -h    : Affiche ce message d'aide.
  --verbose, -v : Affiche l'état de tous les objets, sans exception.
""" % (os.path.basename(sys.argv[0])))
    sys.exit(code_erreur)
    
if __name__ == '__main__':
    # Analyse les options de la ligne de commande.
    try:
        opts, args = getopt.getopt(sys.argv[1:], "hv", ["help", "verbose"])
    except getopt.GetoptError:
        utilisation_et_sortie(1)
    verbeux = 0
    for opt, arg in opts:
        if opt in ("-h", "--help"):
            utilisation_et_sortie(0)
        if opt in ("-v", "--verbeux"):
            verbeux = 1
    if len(args) != 1:
        utilisation_et_sortie(2)
            
    # Transforme le chemin en chemin canonique.
    chemin_copie_travail = svn.core.svn_path_canonicalize(args[0])

    # Et c'est parti !
    try:
        trouver_etat(chemin_copie_travail, verbeux)
    except svn.core.SubversionException, e:
        sys.stderr.write("Erreur (%d): %s\n" % (e.apr_err, e.message))
        sys.exit(1)

Comme dans le cas de l'Exemple 8.2, « Utilisation de la couche dépôt en Python », ce programme voit sa mémoire gérée automatiquement et utilise en grande partie les types de données classiques de Python. L'appel de svn_client_ctx_t() est un peu trompeur parce que l'API publique de Subversion ne possède pas de telle fonction — la génération automatique de fonctions de SWIG ( une sorte d'usine à fonctions pour transformer des structures C complexes vers un équivalent en Python) est à la peine. Notez également que le chemin passé au programme (tout comme dans le programme précédent) est mouliné par svn_path_canonicalize() car, dans le cas contraire, on s'expose à un arrêt rapide et brutal du programme par la bibliothèque C Subversion sous-jacente qui effectue des tests de conformité.



[55] Après tout, Subversion utilise aussi les API Subversion.

[56] Subversion utilise les appels système et les types de données ANSI autant que possible.

[57] Neon et Berkeley DB par exemple.