Faites migrer des rapports de bug de GitHub à GitLab

GNU/Linux Magazine HS n° 090 | mai 2017 | Cyril Roelandt
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
Apprenez à manier les API de GitHub et GitLab afin d'effectuer des opérations courantes sur vos dépôts.

Il est de nos jours très courant pour des services web de fournir à leurs utilisateurs une API REST permettant d'effectuer diverses opérations depuis la ligne de commandes (notamment avec curl), ou d'écrire des applications utilisables en dehors du navigateur. Python étant un langage très populaire, de nombreuses API bénéficient d'une bibliothèque Python permettant de les manipuler. Voyons deux d'entre elles, qui permettent d'interagir avec des services d'hébergement de dépôts Git au travers de leurs API.

1. Découverte de l'API GitHub

GitHub fournit une API dont la documentation est disponible sur le Web (voir https://developer.github.com/v3/). Elle utilise le format bien connu JSON.

1.1 Premiers pas

Nous pouvons commencer par lancer une requête GET sur la racine de l'API :

# Nous tronquons ici la sortie par souci de lisibilité, et afin de ne garder que

# les parties qui nous intéressent.

$ curl -s https://api.github.com/

{

  "current_user_url": "https://api.github.com/user",

  ...

  "repository_url": "https://api.github.com/repos/{owner}/{repo}",

  ...

}

On peut ensuite continuer notre exploration en utilisant les URL qui nous ont été retournées :

# Récupérons des informations sur le dépôt "GLMF175" de l'utilisateur "GLMF".

$ curl -s https://api.github.com/repos/GLMF/GLMF175

{

  "id": 23825643,

  "name": "GLMF175",

  "full_name": "GLMF/GLMF175",

  ...

  "description": "Codes sources du n°175 de GNU/Linux Magazine (Octobre 2014)",

  ...

  "created_at": "2014-09-09T08:32:32Z",

  "updated_at": "2014-11-03T10:12:27Z",

  "pushed_at": "2014-10-02T07:30:06Z",

  "git_url": "git://github.com/GLMF/GLMF175.git",

  "ssh_url": "git@github.com:GLMF/GLMF175.git",

  "clone_url": "https://github.com/GLMF/GLMF175.git",

  "svn_url": "https://github.com/GLMF/GLMF175",

  ...

}

Certaines requêtes nécessitent une authentification :

$ curl -s https://api.github.com/user

{

  "message": "Requires authentication",

  "documentation_url": "https://developer.github.com/v3"

}

$ curl -s https://api.github.com/user -u Steap

Enter host password for user 'Steap':

{

  "login": "Steap",

  "id": 416834,

  ...

  "html_url": "https://github.com/Steap",

  ...

  "repos_url": "https://api.github.com/users/Steap/repos",

  ...

  "name": "Cyril Roelandt",

  ...

}

Il existe plusieurs façons de s'authentifier, que nous détaillerons dans la partie suivante. Avant cela, force est de constater qu'effectuer toutes les requêtes avec curl va vite devenir relativement fastidieux ; il convient donc d'utiliser une bibliothèque Python rendant l'utilisation de l'API plus facile. Il en existe de nombreuses : nous avons choisi d'utiliser ici PyGithub (https://pypi.python.org/pypi/PyGithub), en version 1.32.

1.2 Authentification

Intéressons-nous à deux méthodes d'authentification : par mot de passe, et à l'aide d'un jeton privé. Notons qu'il est également possible d'enregistrer une application et de s'authentifier grâce au protocole OAuth, mais nous ne détaillerons pas ici cette méthode.

1.2.1 Par pseudo/mot de passe

Comme nous venons de le voir, il est possible de s'identifier en donnant tout simplement son pseudonyme (ou son adresse de courriel) et son mot de passe :

>>> import github

>>> gh = github.Github('Steap', 'motdepasse')

# On peut vérifier que nous sommes bien connectés :

>>> print(gh.get_user().login)

Steap

1.2.2 Par jeton privé

Il est également possible d'utiliser un jeton privé, ce qui nous permet de gérer plus finement les permissions. Pour ce faire, il suffit de se rendre sur la page https://github.com/settings/tokens/new et d'y sélectionner les permissions requises (cf. Figure 1).

Fig. 1 : Liste des permissions disponibles lors de la création d'un jeton.

Créons un jeton en ne lui donnant pas la permission gist, puis vérifions qu'il est effectivement impossible de créer un gist :

>>> import github

>>> gh = github.Github('tokensecret')

>>> gh.get_user().create_gist(True,

... {'hello.txt': github.InputFileContent('Hello GLMF')},

... 'A test gist')

Traceback (most recent call last):

...

github.GithubException.UnknownObjectException: 404...

Créons maintenant un autre jeton, en lui donnant la permission de créer des gists. Nous pouvons le faire directement depuis notre console Python :

>>> t = gh.get_user().create_authorization(["gist"], "Token-with-gist")

>>> t.token

'nouveautoken'

Tentons à nouveau de créer un gist :

>>> gh = github.Github('nouveautoken')

>>> gh.get_user().create_gist(True,

... {'hello.txt': github.InputFileContent('Hello GLMF')},

... 'A test gist')

Gist(id="a9050b4027ee16111458a6b34f959a94")

Cette fois-ci, la commande a fonctionné : nous pouvons le vérifier en nous rendant sur https://gist.github.com/Steap/a9050b4027ee16111458a6b34f959a94.

1.3 Aller plus loin

Nous n'avons pas détaillé, dans la partie précédente, les paramètres des méthodes utilisées (create_authorization, create_gist). Leur fonctionnement est relativement intuitif, et nous n'avons de toute façon pas la place d'écrire ici la documentation complète de PyGithub. Nous pouvons toutefois donner deux conseils pour appréhender ce type de bibliothèques :

- explorez les possibilités offertes par chaque objet Python dans l'interpréteur interactif : dir() et help() vous permettront de rapidement comprendre les subtilités de la bibliothèque utilisée ;

- lisez la documentation de l'API, même si vous ne comptez pas l'utiliser directement : les bibliothèques sont en général très proches de cette API, et apportent principalement une plus grande facilité d'utilisation.

Nous verrons dans la troisième partie de cet article d'autres cas d'utilisation de l'API GitHub.

2. Découverte de l'API GitLab

La documentation de l'API GitLab est bien évidemment disponible sur le Web (https://docs.gitlab.com/ee/api/README.html#gitlab-api). Montrons ici quelques cas d'utilisation, en utilisant l'instance GitLab hébergée par Framasoft (https://framagit.org/).

2.1 Authentification

Comme pour l'API GitHub, il est nécessaire de savoir s'authentifier.

2.1.1 Par pseudo/mot de passe

Sur GitLab, l'authentification par pseudo/mot de passe permet uniquement de récupérer un jeton privé (également disponible dans l'interface web) :

$ curl -X POST -s "https://framagit.org/api/v3/session?login=Steap&password=xxx"

{

   "web_url" : "https://framagit.org/Steap",

   ...

   "private_token" : "secret-token",

   ...

   "username" : "Steap",

   ...

}

Voyons maintenant comment utiliser ce jeton privé.

2.1.2 Jeton privé

Le jeton privé que nous venons de récupérer peut maintenant être utilisé pour interroger l'API, par exemple afin de lister nos projets :

$ curl -s https://framagit.org/api/v3/projects -H 'PRIVATE-TOKEN: secret-token'

[

  {

    ...

    "web_url" : "https://framagit.org/Steap/quiparrainequi",

    "id" : 13198,

    "name" : "quiparrainequi",

    ...

  }

  ...

]

2.1.3 OAuth2

Il est également possible d'utiliser OAuth2 afin de bénéficier d'une gestion plus fine des permissions. Une documentation complète est disponible sur le Web (https://docs.gitlab.com/ee/api/README.html#oauth-2-tokens), mais voyons tout de même rapidement les étapes nécessaires à l'obtention d'un jeton.

Il faut tout d'abord créer une application en passant par l'interface web de GitLab (voir figure 2).

Fig. 2 : Création d'une application dans GitLab.

GitLab affichera alors votre Application ID, votre secret ainsi que l'URL de redirection que vous avez indiquée. Utilisez ces informations pour construire l'URL suivante :

>>> url = "https://framagit.org/oauth/authorize"

>>> url+= "?client_id=%s" % APP_ID

>>> url+= "&redirect_uri=%s" % REDIRECT_URI

>>> url+= "&response_type=code"

Dégainez votre butineur et rendez-vous sur cette page, donnez à l'application les permissions qu'elle vous demande, et vous serez redirigé vers une page dont l'URL se terminera par code=$CODE. Une requête vous permettra d'obtenir (enfin !) votre jeton OAuth2 :

$ URL=https://framagit.org/oauth/token

$ URL="$URL?client_id=$APP_ID"

$ URL="$URL&client_secret=$SECRET"

$ URL="$URL&code=$CODE"

$ URL="$URL&grant_type=authorization_code"

$ URL="$URL&redirect_uri=$REDIRECT_URI"

$ curl -s -X POST $URL | json_pp

{

   "refresh_token" : "...",

   "scope" : "api",

   "created_at" : 1490500721,

   "token_type" : "bearer",

   "access_token" : "secret-access-token"

}

Vous pouvez ensuite utiliser ce jeton dans vos requêtes :

$ curl -H "Authorization: Bearer secret-access-token" \

    https://framagit.org/api/v3/projects/

[

  ...

]

2.2 Une bibliothèque : python-gitlab

Rendons-nous la vie plus simple en utilisant une bibliothèque Python : python-gitlab en version 0.20. S'authentifier en utilisant un jeton privé se fait très simplement :

>>> import gitlab

>>> gl = gitlab.Gitlab('https://framagit.org', 'secret-token')

Il est ensuite possible d'explorer la bibliothèque comme nous l'avons fait avec PyGithub :

>>> for project in gl.projects.list():

...     print(project.name)

...

quiparrainequi

framadate

sf2cf

Voyons dans la prochaine partie comment utiliser python-gitlab pour manipuler nos projets.

3. Exemple : migrations des rapports de bugs

Lorsqu'un utilisateur de GitLab veut créer un nouveau projet, il a la possibilité d'importer un projet existant depuis un autre site, notamment GitHub (voir figure 3). Le dépôt Git sera automatiquement importé, et les rapports de bugs seront copiés. Comment pourrions-nous utiliser les API de GitHub et de GitLab afin d'implémenter une fonctionnalité similaire ?

Fig. 3 : GitLab permet d'importer un projet depuis un autre service.

3.1 Idée générale

L'idée de base est très simple : il suffit en effet de créer un nouveau projet, puis de migrer chacun des services associés (bugs, wiki, etc.) de GitHub vers GitLab. Nous nous contenterons ici de migrer les bugs. Regardons le point d'entrée de notre code :

import github

import gitlab

GITHUB_TOKEN = 'secret-github-token'

gh = github.Github(GITHUB_TOKEN)

GITLAB_URL = 'https://framagit.org'

GITLAB_TOKEN = 'secret-gitlab-token'

gl = gitlab.Gitlab(GITLAB_URL, GITLAB_TOKEN)

def migrate_gh_project(project_name):

    # Create a new project on Gitlab

    gl_project = gitlab_create_project(project_name)

 

    gh_repo = gh.get_user().get_repo(project_name)

    # Migrate all issues

    for gh_issue in gh_repo.get_issues():

        gl_issue = gitlab_clone_github_issue(gl_project, gh_issue)

    # TODO: Migrate everything else!

migrate_gh_project('quiparrainequi')

Pour l'instant, tout est facilement compréhensible. On notera bien évidemment que gl fait référence à GitLab, et gh à GitHub. Il ne nous reste plus qu'à regarder dans le détail comment définir les fonctions que nous venons d'introduire.

3.2 Créer un nouveau projet

Cette étape est sans la plus facile : elle consiste en un seul appel à l'API GitLab, et ne nécessite qu'un seul paramètre, le nom du projet :

def gitlab_create_project(project_name):

    return gl.projects.create({'name': project_name})

Après avoir exécuté cette fonction, un nouveau projet apparaît dans notre tableau de bord GitLab.

3.3 Créer les rapports de bugs

Dans la fonction migrate_gh_project, nous itérons sur les rapports de bugs afin de les cloner un par un. Idéalement, nous aimerions conserver cinq informations :

- le titre du rapport ;

- la description du bug ;

- la date de création du bug ;

- l'auteur du bug ;

- les commentaires.

Comme nous pouvons le voir dans le code de la fonction gitlab_clone_github_issue, les trois premières informations peuvent être passées à gl_project.issues.create. Spécifier l'auteur du bug est plus difficile :

- il a peut-être un compte sur GitHub, mais pas sur votre instance GitLab ;

- un problème de permissions se pose : un utilisateur ne peut pas créer un bug en usurpant l'identité d'un autre.

Nous choisissons ici d'inclure le nom de l'auteur dans la description (c'est l'approche implémentée par l'outil officiel de migration fourni par GitLab).

def gitlab_clone_github_issue(gl_project, gh_issue):

    body = '[Created by %s] %s' % (gh_issue.user.login, gh_issue.body)

    gl_issue = gl_project.issues.create({

            'title': gh_issue.title,

            'description': body,

            'created_at': gh_issue.created_at.isoformat()

        })

    for gh_comment in gh_issue.get_comments():

        gitlab_clone_github_comment(gl_issue, gh_comment)

    return gl_issue

Il ne nous reste plus qu'à cloner les commentaires.

3.4 Créer les commentaires

Créer un commentaire est une opération relativement similaire à la création d'un rapport de bug. Nous rencontrons le même problème concernant les auteurs des commentaires, que nous contournons de la même façon que précédemment :

def gitlab_clone_github_comment(gl_issue, gh_comment):

    body = '[Created by %s] %s' % (gh_comment.user.login, gh_comment.body)

    gl.project_issue_notes.create({

            'body': body,

            'created_at': gh_comment.created_at.isoformat()

        }, project_id=gl_issue.project_id, issue_id=gl_issue.id)

Nous avons maintenant toutes les pièces du puzzle ! Si l'on exécute la fonction migrate_gh_project avec un nom de projet existant sur notre compte, un projet similaire apparaîtra dans GitLab, avec les bugs reproduits (presque) à l'identique.

3.5 Améliorations possibles

Bien entendu, ce code n'est qu'un simple exemple de l'utilisation combinée des API de GitHub et GitLab. Nous pourrions y apporter de nombreuses améliorations :

- seuls les bugs sont migré (même le code du dépôt est absent de notre nouveau projet), il conviendrait de tout copier ;

- seuls les bugs ouverts sont copiés (get_issues ne retourne par défaut que ceux-ci), il faudrait également copier ceux qui ont été résolus ;

- la visibilité du projet est « privée » par défaut, mais nous pourrions corriger cela en modifiant gitlab_create_project ;  

- afin de pouvoir migrer nos projets de n'importe quel service en ligne vers n'importe quel autre, il faudrait donner à notre code une meilleure architecture.

Tout ceci est possible grâce aux deux bibliothèques que nous avons utilisées jusqu'ici.

Conclusion

Nous avons montré à quel point il était facile d'utiliser les API de GitHub et GitLab grâce à des bibliothèques Python, et avons réussi à construire une application certes rudimentaire, mais tout à fait fonctionnelle, qui nous a permis de nous authentifier, de lire des données et d'en créer d'autres.

De nombreuses API fonctionnent de façon similaire, en proposant diverses méthodes d'authentification (par mot de passe, avec des jetons, avec OAuth2, etc.) et en permettant à l'utilisateur de récupérer et de modifier l'état du système. La plupart d'entre elles sont également utilisables au travers de bibliothèques Python tout aussi simples d'accès que celles dont il était question dans cet article. La méthodologie employée ici (exploration de l'API avec curl, authentification, découverte de la bibliothèque dans l'interpréteur Python...) aura donc sans doute l'occasion d'être réutilisée.