Créez votre Github-like : le lapin rouge dans le bosquet

GNU/Linux Magazine n° 121 | novembre 2009 | Thomas Riboulet
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 !
Ruby et Git sont probablement parmi les 10 hits des 2 ou 3 dernières années. Ruby parce que RubyOnRails et Git parce que Git... Si Ruby reste à parts égales avec Python, Perl et les autres, Git est devenu un des « hits » de l'année passée remplaçant souvent SVN, voire de vieux CVS qui traînaient encore.

1. Lapin rouge

On peut noter que l'apparition de Git a redonné de l'air à d'autres projets (Monotone, Mercurial, etc.), ouvert un peu l'esprit de certains projets (Trac,...), permit l'apparition de projets comme GitHub, et la popularisation de ces alternatives à SF.net et autres Gforge...

Pour mettre en place un outil comme GitHub, il a fallu développer un certain nombre de choses, et vu que GitHub repose sur du Ruby, il a fallu avoir une bibliothèque robuste et rapide pour accéder aux dépôts confiés à leurs soins. Le problème initial étant que ça prend beaucoup de temps de passer par le shell, la bibliothèque Grit a été développée afin d'implémenter les commandes du core de git directement en Ruby. Grit est donc un des maillons de base de GitHub :

Grit gives you object oriented read/write access to Git repositories via Ruby. The main goals are stability and performance. (...) This software was developed to power GitHub, and should be considered production ready. An extensive test suite is provided to verify its correctness.

Grit vous donne un accès objet en lecture/écriture à des dépôts Git via Ruby. Les buts principaux sont la stabilité et la performance. (...) Ce logiciel a été développé pour GitHub, et devrait être considéré comme utilisable en production. Un jeu de tests complet est livré afin de vérifier cela.

– dixit http://grit.rubyforge.org/

J'ai eu recours à Grit dans le cadre d'un de mes projets pour pouvoir publier et rendre accessible facilement les derniers commits, diff, bugs, etc... De fil en aiguille, je l'ai utilisé dans des hooks post, et pre commit pour générer différentes choses. Le côté objet et la syntaxe souple de Ruby facilite grandement la chose !

Au cas où vous seriez intéressé, voici donc une petite intro à Grit. Pour le but de l'article, nous allons générer des flux RSS à partir des différentes branches d'un dépôt. Ruby disposant d'une très bonne bibliothèque pour générer des flux RSS, on ne va pas s'en priver. Au programme donc : balade dans les dépôts, branches, commits et autres diffs ...

2. Préparatifs

Pour une fois, prenons donc un Lapin rouge aux yeux blancs comme cobaye. On appellera ce cobaye « Baïonnette ». Il s'agira d'un dépôt rempli de code et comme le code ne nous intéresse pas vraiment ici, on prendra un dépôt bidon (ou pas). Et donc, on pourra lâcher Baïonnette dans le jardin.

Ensuite, il vous faut un Ruby à jour, ou presque, RubyGems, et la gem Grit. Pour ceux qui suivent, il faut Ruby, Grit et le module RSS/Maker, donc, ci-dessous, une méthode généralement efficace en utilisant les RubyGems, mais si vous avez d'autres habitudes...

$> sudo gem install grit

...

Testez dans irb pour voir si tout s'est bien passé :

$> irb

irb(main):001:0> require 'rubygems'

=> true

irb(main):002:0> require 'grit'

=> true

irb(main):003:0> require 'rss/maker'

=> true

irb(main):004:0> exit

$>

Passons aux choses sérieuses. Clonez un dépôt ou repérez où se trouve un dépôt (local) à vous. Créez (ailleurs) un répertoire et un fichier .rb dedans :

$> cd ~/code/projet_bien

$> ls -a

.   ..    .git etc [...]

$> cd ~/code

$> mkdir griss

$> cd griss

$> touch griss.rb

3. Le script

Commençons donc notre script d'exemple :

#!/usr/bin/env ruby

require 'rubygems'

require 'grit'

include Grit

require 'rss/maker'

Rien de bien sorcier ici n'est-ce pas ? Pour faire les choses bien, on va récupérer les paramètres passés au script : en premier lieu, le chemin vers le dépôt (repos_path), et, en deuxième lieu, le répertoire de sortie (output_path là où les flux rss vont être générés). On vérifie juste avant si on a le bon nombre de paramètres.

if ($ARGV.size != 2)

  printf("Use : #{$0} repo_path output_path\n")

  exit(1)

end

repo_path = $ARGV[0]

output_path = $ARGV[1].gsub("/\s/", "_")

webpath = "http://www.unixgarden.com/"

La variable webpath vous sera utile si vous utilisez un outil web pour rendre vos dépôts accessibles. Vous verrez plus bas qu'un lien vers chaque commit dans ce type d'interface pourra être généré. Attention, il ne doit pas être vide, donc faites le pointer vers le lieu de publication des flux par exemple.

3.1 Grit, scène 1

Grit, c'est de la bombe, mais il ne fait pas tout tout seul. Il faut lui dire où aller chercher le code : un dépôt ou repository. Grit fournit une classe Repo pour cela. Créons donc une nouvelle instance de cette classe :

repo = Repo.new(repo_path)

Et ensuite, on peut se balader dans ses branches, et, comme c'est de l'objet, il suffit de taper dans son attribut branches :

repo.branches.each do |b|

  # ...

end

Ouai, easy.

Pour être propre, on vérifie si le répertoire de sortie existe ou pas (à la place des... précédents).

  if not File.exist?(output_path)

    Dir.mkdir(output_path)

  end

On définit un nom de fichier proprement aussi :

  destination = output_path + "/" + b.name + ".xml"

Time for some RSS !

3.2 RSS, scène 1

Il y a plusieurs bibliothèques RSS pour Ruby, mais j'ai trouvé que RSS/Maker était relativement simple et rapide. Par contre, la création n’est pas forcément intuitive :

  content = RSS::Maker.make("2.0") do |m|

    # ...

  end

Ici, 2.0 correspond à la version de RSS que l'on veut utiliser (la DTD changeant...). La création du flux à proprement parler se passe donc dans cette boucle :

    m.channel.title = " :: " + b.name

    m.channel.link = webpath + ""

    m.channel.description = "Commit logs"

3.3 RSS & Grit, scène 10

Maintenant, on va pouvoir récupérer les infos sur les commits de la branche. Pour obtenir les 10 derniers commits d'une branche, il suffit d'utiliser la méthode commits de l'objet Repo en lui passant le nom de la branche concernée et le nombre de commits voulus :

  repo.commits(branche, 10)

Une fois que l'on tient un commit, on peut accéder à nombre d'informations clefs : id (checksum SHA), message (le commentaire) et committed_date (la date de commit). Pour chaque commit, nous allons donc créer un nouvel item rss, assigner titre, description, lien et date :

    repo.commits(b.name,10).each do |c|

      i = m.items.new_item                # creation d'un nouvel item

      i.title = c.id

      i.description = c.message

      i.link = webpath + c.id

      i.date = c.committed_date

    end

Comme vous pouvez le voir à la ligne i.link, on peut générer un lien hypertexte. Cela peut être utile si, quelque part, via un Tracs, ou ce que vous voulez, vous pouvez afficher les détails du commit en question... Une fois les commits ajoutés, il suffit de trier les items du flux en fonction de leur date (et donc de la date de commit) :

    m.items.do_sort = true                # trier les items par date

Mais, évidemment, cela n'est pas fini. Il faut encore écrire dans le fichier :

  File.open(destination,"w") do |f|

    f.write(content)

  end

4. Montage

#!/usr/bin/env ruby

require 'rubygems'

require 'grit'

include Grit

require 'rss/maker'

if ($ARGV.size != 2)

  printf("Use : #{$0} repo_path output_path\n")

  exit(1)

end

repo_path = $ARGV[0]

output_path = $ARGV[1].gsub("/\s/", "_")

webpath = "http://www.unixgarden.com/"

repo = Repo.new(repo_path)

repo.branches.each do |b|

  if not File.exist?(output_path)

    Dir.mkdir(output_path)

  end

  destination = output_path + "/" + b.name + ".xml"

  content = RSS::Maker.make("2.0") do |m|

    m.channel.title = ":: " +b.name

    m.channel.link = webpath + ""

    m.channel.description = "Commit logs"

    repo.commits(b.name,10).each do |c|

      i = m.items.new_item                # creation d'un nouvel item

      i.title = c.id

      i.description = c.message

      i.link = webpath + c.id

      i.date = c.committed_date

    end

    m.items.do_sort = true                # trier les items par date

  end

  File.open(destination,"w") do |f|

    f.write(content)

  end

end

5. Le retour de la vengeance

Bien, nous avons donc rapidement vu comment se balader dans les branches et les commits. Je vous recommande de vous perdre un peu dans la doc de Grit, assez inspirante. Mais, pour ne pas vous laisser sur une telle faim, nous allons maintenant voir comment accéder à des informations plus cruciales : le diff d'un commit.

5.1. Rajout

Chaque commit contient différents diffs, correspondant aux différents fichiers impliqués. Ils sont accessibles via l'attribut diffs sous la forme d'un array. Chaque membre de cet array sont des objets disposant notamment des attributs suivants : b_path, d.diff. Le premier est le path complet du fichier au sein du dépôt, le deuxième le diff ...

Donc, si on reprend la boucle précédente :

    repo.commits(b.name,10).each do |c|

      # ...

    end

On peut accéder aux diffs du commit de cette façon :

    c.diffs.each do |d|

      # ...

      i.description += "d.b_path :\nd.diff\n"

    end

Evidemment, cela risque de ne pas sortir très proprement. Il faut donc prévoir de convertir le diff de texte brut en quelque chose de plus présentable dans du HTML, en remplaçant les retours à la ligne par des balises, etc. En Ruby, il y a une bibliothèque qui permet de faire ce genre de choses simplement : CodeRay.

Une fois installé (voir lien en fin d'article), il suffit d'appeler la méthode scan en lui passant le texte à mettre en forme, le format à utiliser (c, ruby, rhtml,...) et la forme qu'il doit utiliser pour générer la sortie (div,...). Dans l'exemple de code suivant, la variable format est déterminée ailleurs, en reconnaissant l'extension du fichier dans le d.b_path, et la méthode div est appelée pour spécifier que le code HTML généré doit être inclus dans des balises div, avec des définitions de classes pour pouvoir facilement faire la CSS correspondante.

  CodeRay.scan(d.diff, format).div(:css => :class)

Ce qui donnerait comme boucle pour générer les items du flux :

    repo.commits(b.name,10).each do |c|

      i = m.items.new_item                # creation d'un nouvel item

      i.title = c.id

      i.description = c.message

      c.diffs.each do |d|

        i.description += "<br />d.b_path :<br />"

        i.description += CodeRay.scan(d.diff, format).div(:css => :class)

      end

      i.link = webpath + c.id

      i.date = c.committed_date

    end

Ce qui, je vous l'accorde, ne serait pas des plus classes. Mais, je vous laisse le soin de broder à partir de ce point-là.

5.2. Cas par cas ?

Si on voulait le faire au cas par cas à partir du SHA d'un commit particulier, il faut utiliser les talents de Ruby. Car, il est impossible de faire une recherche directe dans le dépôt. Il faut donc ruser.

Soit grit_repo notre dépôt (et grit_repo_path son chemin), foo_sha l'id du commit voulu et foo_branch la branche voulue :

  grit_repo = Repo.new(grit_repo_path)

  a_commit = grit_repo.commits(foo_branch, 1000).find { |c| c.id == foo_sha)

Donc, désormais, on a le commit dans nos mains, reste à l'ouvrir. Chaque commit contient différents diffs, correspondant aux différents fichiers impliqués. Ils sont accessibles via l'attribut .diffs sous la forme d'un array donc les champs sont : b_path, d.id, d.diff. Ce qui nous intéresse surtout ce sont b_path et d.diff. Le premier est le path complet du fichier au sein du dépôt, le deuxième le diff...

Il suffit donc de parcourir cet array :

  a_commit.diffs.each do |d|

    printf("d.b_path :\nd.diff\n")

  end

Par exemple...

Conclusion

Voilà donc ce petit aperçu de Grit fini. Evidemment, il faut en avoir l'utilité, mais qui sait, vous trouverez peut être une utilité à la possibilité de farfouiller dans des dépôts Git aussi facilement ?

Bibliographie

Note

Je décline toute responsabilité quant à tout dommage que votre matériel (ou vous-même) pourrait subir au cours de ces manipulations : chezmoiçamarche(tm).

Note

_why

Récemment, une des personnes qui ont tant donné à la communauté Ruby et donc à la communauté en général a décidé (semble-t-il) de disparaître d'Internet. Je tiens à en profiter pour le remercier, le saluer pour tout ce qu'il a apporté, et lui souhaiter tout le meilleur pour la suite de ses aventures. Thanks mate wish you all the best !