Skywalker13

Diary of an ex-GeeXboX developer…

Mercurial, depuis fin 2006 ~ 🇫🇷

Posted at — Jan 10, 2010

Hello,

Cela fait plus de 3 ans qu’on utilise Mercurial dans les projets GeeXboX (avant on travaillait avec TLA Arch). Néanmoins on a de la peine à se conformer aux nouvelles fonctionnalités. Je souhaite en fait parler des branches. Avec Subversion (par exemple), les branches se présentent sous la forme de répertoires. Ça a le mérite d’être clair mais c’est un peu comme collectionner des copies du dépôt principal. Je n’ai jamais trop aimé cette approche, car personnellement je trouve ça un peu brouillon (ça n’engage que moi).

En parlant de brouillon, je vous invite à consulter cette page http://hg.geexbox.org qui liste tous les dépôts existants sous GeeXboX. Si vous regardez la colonne “Last change”, vous verrez des “3 years ago” par exemple. Autant dire que certains dépôts sont complètement obsolètes aujourd’hui. Et c’est même confus, prenez par exemple “geexbox-v2”. Son nom est devenu absurde avec le temps. Car la dedans vous trouverez nos tentatives foireuses de supporter Freevo ;-). Je vous épargne la description des autres.

Bref, là où je veux en venir c’est que pour faire des branches, on s’y prend à l’ancienne et on clone les dépôts. Je pense donc spécialement à “geexbox-1.2” qui est de loin pas obsolète, car c’est lui qui contient tous les backports (spécialement les corrections de bugs) qui ont permis les versions 1.2.1 à 1.2.4 de GeeXboX. Personnellement il ne me plait pas beaucoup. Toutes les versions 1.2.x devraient être dans le dépôt principal “geexbox”.

Ainsi depuis quelques temps je me suis un peu intéressé aux branches Mercurial. Et c’est sur ce point que je vais m’attarder dans ce billet. Il faut aussi savoir qu’en 2006-2007, une fonctionnalité que je vais présenter ici n’existait pas encore (en tout cas pas sous la même forme). Il n’y a donc pas vraiment de mal d’avoir trainé les bonnes vieilles copies de dépôts pour faire des branches…

Cet intérêt soudain pour les branches m’est venu spécialement depuis les releases de libnfo, libplayer et libvalhalla. Je pense par exemple à avoir des branches pour des éventuels corrections sur les versions 1.0.0. Et surtout, sans faire des copies à l’ancienne.

Les branches nommées (NamedBranches)

Mon but n’est pas d’écrire un tutoriel sur Mercurial. Je vais ainsi essayer d’aller à l’essentiel. Un dépôt peut contenir des “heads” et des branches. Plusieurs “heads” apparaissent quand on commit des changements par rapport à une révision antérieure au “tip”. Ce qui demande donc de faire un “merge” et de le “comitter”.

Par défaut il y a toujours une branche dans chaque dépôt et elle se nomme “default”. Passons directement à la pratique. J’aimerais gérer les corrections de bugs pour libplayer-1.0.0. Admettons que je sois à la racine du dépôt, je créer alors une branche v1.0 à partir du tag v1.0.0.

$ hg update v1.0.0
33 files updated, 0 files merged, 7 files removed, 0 files unresolved
$ hg branch v1.0
marked working directory as branch v1.0

A priori j’ai bien une nouvelle branche. Mais voyons ce que dit hg branch et hg branches.

$ hg branch
v1.0
$ hg branches
default                     1326:cac53e7d727f

Du fait que je n’ai pas “commité” la nouvelle branche, on ne la voit pas dans la liste des branches. Mais la première commande confirme que je suis bien dans la v1.0 fraichement créée. Je vais donc “commiter” ce changement localement.

$ hg commit -m "new branch v1.0 for bugfix"
created new head
$ hg branches
v1.0                        1327:1c0c025f8a73
default                     1326:cac53e7d727f (inactive)

Si on désire passer d’une branche à l’autre, c’est très simple.

$ hg update default
40 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ hg branch
default
$ hg update v1.0
33 files updated, 0 files merged, 7 files removed, 0 files unresolved
$ hg branch
v1.0

A noter que s’il y avait des changements non “commités” dans le dépôt, il ne serait pas possible de passer d’une branche à l’autre sans qu’ils soient tous annulés ou “commités”. Mais regardons un peu ce que ça donne avec hg view.

On voit deux colonnes bien distinctes qui sont rattachées depuis le “tag” v1.0.0. Un petit tour dans hg heads nous montre bien qu’il y en a deux. Et chacun est sur une branche différente.

$ hg heads
changeset:   1327:1c0c025f8a73
branch:      v1.0
tag:         tip
parent:      1230:6b3e2fed5f7a
user:        Mathieu Schroeter <mathieu.schroeter@mycable.ch>
date:        Sun Jan 10 10:26:50 2010 +0100
summary:     new branch v1.0 for bugfix

changeset:   1326:cac53e7d727f
user:        Mathieu Schroeter <mathieu.schroeter@mycable.ch>
date:        Sat Jan 09 20:34:51 2010 +0100
summary:     set winid to 0 by default (fix warning if USE_X11 is not defined)

La branche “default” n’est pas montrée explicitement.

Ce qui m’intéresse maintenant c’est de gérer les corrections de bugs. Cette branche v1.0 est à considérer pour exister aussi longtemps que tout le dépôt.

Le “cherry picking”

C’est ce qu’on appel le “cherry picking” car seulement quelques “changesets” spécifiques m’intéressent. Les corrections de bugs sont toutes dans la branche “default”. Pour importer ces changements dans le dépôt v1.0 il y a plusieurs manières de faire. Mais personnellement la majorité des solutions que j’ai trouvé ne me plaisent pas. Et il y a peu de temps, je suis tombé sur une extension officielle (mais non activée par défaut) qui permet de faire exactement ce genre de traitements (et même plus). Elle s’appelle “transplant” et c’est elle que je vais utiliser pour mon exemple.

Il faut commencer par l’activer dans le fichier ~/.hgrc:

[extensions]
transplant=

Je vais prendre une partie des corrections intéressantes et les transplanter dans la nouvelle branche.

$ hg transplant 1242
application de 95461fb8613f
patching file Makefile
Hunk #1 succeeded at 18 with fuzz 1 (offset -3 lines).

Je répète pour chaque “changeset”. On peut spécifier plusieurs “changesets” en les séparant par : pour une plage REV1:REV4 par exemple. Mais avec le Mercurial d’Ubuntu j’évite de le faire, simplement parce que l’extension me retourne une exception et un joli traceback Python. En prenant un après l’autre, aucun problème.

Le “screenshot” ci-dessus permet de voir les différents “changesets” appliqués sur la branche v1.0.

Pour continuer à travailler sur la devel (branche default), rien de plus simple.

$ hg update default
40 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ vi Makefile
$ hg commit -m "dummy commit"
$ hg heads
changeset:   1335:55589e41d0eb
tag:         tip
parent:      1326:cac53e7d727f
user:        Mathieu Schroeter <mathieu.schroeter@mycable.ch>
date:        Sun Jan 10 13:07:34 2010 +0100
summary:     dummy commit

changeset:   1334:81c124600c6b
branch:      v1.0
user:        Mathieu Schroeter <mathieu.schroeter@mycable.ch>
date:        Sat Jan 09 20:29:01 2010 +0100
summary:     fix 'make dist' in src

On voit clairement évoluer le tip de la branche default en parallèle à la branche v1.0.

Un “push” avec plusieurs branches

Tout ceci n’est qu’un exemple, ainsi pour montrer le “push” je vais utiliser un répertoire local. Admettons que je clone depuis http://hg.geexbox.org/libplayer.

$ cd ..
$ hg clone http://hg.geexbox.org/libplayer libplayer-local
requesting all changes
adding changesets
adding manifests
adding file changes
added 1327 changesets with 2268 changes to 111 files
updating working directory
104 files updated, 0 files merged, 0 files removed, 0 files unresolved

C’est donc le dépôt original où j’ai qu’une seule branche. Je retourne dans le dépôt qui contient la branche v1.0 et je vais faire un “push” sur libplayer-local.

$ cd libplayer
$ hg push ../libplayer-local
pushing to ../libplayer-local
searching for changes
abort: push creates new remote branch 'v1.0'!
(did you forget to merge? use push -f to force)

Le “push” est automatiquement annulé. Ce qui est très bien, ainsi Mercurial nous préviens qu’on a créé une nouvelle branche. Il est alors nécessaire de le forcer (la première fois). Mercurial nous demande aussi si on a pas oublié de faire un “merge”. C’est légitime dans le cas où on ne veut pas avoir deux branches séparées sur le dépôt distant.

$ hg push -f ../libplayer-local
pushing to ../libplayer-local
searching for changes
adding changesets
adding manifests
adding file changes
added 9 changesets with 6 changes to 2 files (+1 heads)

Mercurial nous confirme qu’on a bien ajouté un “head”. A partir de ce point je me suis posé la question de ce qui se passe si quelqu’un clone ce dépôt avec ces deux heads. Est-ce qu’il va avoir un avertissement s’il tente de faire un “push”, du genre qu’il devrait faire un “merge” ou forcer le “push”?! Ce serait ennuyeux si c’était le cas, car les branches ne devraient pas intervenir. Le head n’étant pas sur la même branche. J’ai donc simplement testé pour m’en assurer :

$ cd ..
$ hg clone libplayer-local libplayer-foobar
updating working directory
104 files updated, 0 files merged, 0 files removed, 0 files unresolved

Cette manipulation revient au même que de cloner le dépôt sur hg.geexbox.org si j’avais “pushé” les modifications. Sans me soucier des branches, je vais faire une modification dans libplayer-foobar, la “commiter”, puis faire un “push” dans libplayer-local comme si c’était hg.geexbox.org/libplayer.

$ cd libplayer-foobar
$ vi Makefile
$ hg commit -m "still a dummy commit"
$ hg push ../libplayer-local
pushing to ../libplayer-local
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files

Il n’y a aucune remarque de Mercurial par rapport aux branches. Cela prouve que tout fonctionne correctement. Honnêtement j’avais toujours eu des doutes sur ces fonctionnalités. Au moins maintenant c’est clair. Je vais alors retourner dans le dépôt libplayer-local pour vérifier le résultat avec hg view.

Tout a fonctionné parfaitement…

Conclusion

Maintenant que j’en sais suffisamment sur les branches, je pense passer par ce moyen à l’avenir. Il pourrait être même judicieux (peut être) de repasser tous les backports de geexbox-1.2 dans le dépôt geexbox. L’idée serait de ne jamais écrire de nouveaux patchs dans les dépôts faisant références à des versions antérieures. Mais de toujours faire les modifications dans le default et uniquement des “transplant” dans les autres branches. Je pense par exemple au dossier debian/ dans les dépôts. Afin de garder l’historique, une nouvelle release tel que libplayer-1.0.1 par exemple, serait enregistrée dans les changelogs du default, puis un “transplant” mettrait à jour la branche v1.0.

Il reste une utilisation des branches qui me pose encore un problème. Les cas où l’écriture d’une fonctionnalité ne devrait pas se faire directement dans le default car elle introduirait des régressions. Une branche supplémentaire tel que experimental pourrait faire l’affaire. On devrait ensuite faire un “merge” de cette branche dans le default. Ce qui me dérange c’est qu’on finit par collectionner les branches comme par exemple ici: http://hg.geexbox.org/enna/. Il y a new_vfs et new_vfs_system. Ces branches n’ont plus vraiment de raison d’être car elles ont été “mergées”. Dans le cas de l’exemple présenté au-dessus avec la branche v1.0, c’est parfaitement normal de la garder pour toujours. Je n’ai donc pas encore trouvé de solution propre (c’est une question de point de vue) pour travailler avec des branches de courtes durées de vie.

Si quelqu’un à une idée…

A bientôt,
Mathieu SCHROETER