Gérer des branches avec Git

De Ensiwiki
Aller à : navigation, rechercher

Ce document présente les bases de la gestion des branches avec Git, appliqué au Projet GL.

Des branches : C'est quoi ? Pourquoi ?

Dans le développement d'un projet, il arrive souvent qu'on souhaite travailler sur une fonctionnalité en limitant les interactions avec le reste de l'équipe.

Un moyen de faire ceci est d'utiliser des branches. Une branche est une « ligne de développement », ou un ensemble de commits. Par exemple, en projet GL, on peut imaginer les scénarios suivants :

  • Les utilisateurs Alice et Bob souhaitent travailler sur une fonctionnalité expérimentale qui risque d'introduire des régressions temporaires. Ils ne peuvent pas travailler sans utiliser « git commit » ou « git push », car ils souhaitent partager leur code, mais ne veulent pas déranger les autres membres de l'équipe avec. Ils créent une branche qu'ils appellent « experimental » dans le dépôt et travaillent dessus. Quand ils font des « git push » et « git pull », leurs commits sont envoyés dans cette branche, mais ne touchent pas à la branche « master » qui est la branche par défaut sous Git. Plus tard, ils fusionneront les changements de la branche « experimental » avec la branche « master » si la fonctionnalité atteint un niveau de fiabilité suffisant.
  • Le rendu intermédiaire approche, et l'équipe ne souhaite plus développer de nouvelles fonctionnalités sur la branche « master ». Mais ils souhaitent continuer à utiliser Git pour travailler. Ils créent une branche « devel » sur laquelle ils travaillent. Au cours de ces nouveaux développements, l'utilisateur Charlie trouve un bug, et souhaite le corriger dans la version « master », il laisse donc de côté son développement sur la branche « devel » et fait un « git commit » pour corriger un bug sur la branche « master », avant de revenir sur la branche « devel ». Vu que la correction de bug l'intéresse pour continuer son travail, il fusionne donc les corrections de la branche master avec sa branche « devel ». Il continue à travailler sur cette branche sur les futures fonctionnalités. Après le rendu intermédiaire, l'équipe fusionne la branche « devel » dans la branche « master » et continue le développement sur la branche « master ».

Ces des cas d'utilisations sont différents pour les utilisateurs, mais correspondent à la même chose techniquement parlant.

Les branches et le Projet GL

En pratique, sur un projet de la taille du Projet GL, l'utilisation des branches n'est pas du tout indispensable. Si vous n'êtes encore pas à l'aise avec Git, il est peut-être préférable de finir de vous familiariser avec les commandes et concepts de base. Dans tous les cas, en fin de projet, c'est le contenu de la branche « master » qui sera récupéré, donc si vous vous lancez dans l'aventure des branches, assurez-vous d'avoir suffisamment bien compris ce que vous faites pour savoir quel code se trouve dans cette branche !

Pour bien comprendre : comment est fait un dépôt Git ?

Pour bien comprendre comment fonctionnent les branches avec Git, il y a quelques concepts à comprendre sur la manière donc Git stocke les données.

En résumé, un dépôt Git est un ensemble de commits, et un ensemble de références vers ces commits (« référence » est à comprendre dans le même sens qu'une « référence » Java, ou qu'un pointeur en C).

Un commit contient :

  • Des méta-données (auteur, date, message de log),
  • Une ou plusieurs référence vers les commits parents. Pour un commit normal, cette référence pointe sur le commit précédent, et pour un commit de fusion, il y a plusieurs parents, qui pointent sur les commits que l'on a fusionné (« gitk --all » permet d'observer de manière graphique cette relation de parentée),
  • L'état du projet tel qu'il était au moment où on a fait ce commit.

Partant d'un commit, disons A, on peut faire plusieurs commits ayant comme parent ce commit A. C'est le cas dès que plusieurs personnes font un commit en parallèle en partant de la même version.

Une branche est simplement une référence sur un commit particulier. La branche par défaut est la branche « master », et quand on n'utilise pas la notion de branche, « master » pointe sur le dernier commit. Pour le premier scénario ci-dessus, on peut avoir quelque chose comme cela :

o---o---o---o <-- master
     \
      o---o---o <-- experimental

et si la branche « experimental » se stabilise assez pour être fusionnée avec master, on peut faire une fusion pour obtenir ceci :

o---o---o---o---o <-- master
     \         /
      o---o---o

et continuer à travailler. Ces petits dessins représentent exactement ce que l'on voit avec l'outil gitk.

Pour plus de détails sur le modèle de données de Git, lire par exemple Git for computer scientists ou bien le chapitre approprié du livre Pro Git.

Configuration qui rend la vie plus pratique

Les explications qui suivent supposent que tous les coéquipiers ont configuré Git de la manière suivante :

git config --global push.default tracking

(Une variante de cette valeur deviendra la valeur par défaut dans Git 2.0, prévu en 2014)

Création d'une branche locale

Git étant un outil décentralisé, on va d'abord travailler localement, puis on enverra nos changements aux coéquipiers avec « git push ». Alice va commencer le travail sur la branche « experimental ». Elle crée la branche avec :

$ git checkout -b experimental                                                                                      
Switched to a new branch 'experimental'

Cette commande a créé la branche, et a placé l'arbre de travail sur cette branche. Alice peut vérifier avec « gitk --all » que « experimental » et « master » pointent au même endroit :

Git-branches-checkout-b.png

Elle commence à travailler et fait le premier commit comme d'habitude :

$ git commit -a                                                                                                     
[experimental 5887794] Début du travail sur la fonctionalité XXX
 1 files changed, 1 insertions(+), 1 deletions(-)

Elle peut maintenant vérifier (encore « gitk --all ») que la branche « experimental » a avancé avec elle, mais que sa branche « master » n'a pas bougé :

Git-branches-commit-a.png

Envoi de la branche au dépôt partagé

Bob souhaite maintenant travailler sur la même branche. Alice envoie sa branche avec :

$ git push --set-upstream origin experimental
Counting objects: 7, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 577 bytes, done.
Total 6 (delta 2), reused 0 (delta 0)
To ssh://gl42@depots.ensimag.fr/~/git
 * [new branch]      experimental -> experimental
Branch experimental set up to track remote branch experimental from origin.

Cette commande envoie la branche « experimental » vers le dépôt « origin » (i.e. celui dont on a fait un clone au départ), et l'option --set-upstream permet de dire à Git de se souvenir qu'un « git push » de notre branche « experimental » envoie les changements à la branche « experimental » du dépôt distant. Le prochain push pourra donc se faire simplement avec :

git push

Alice peut visualiser ce qu'il s'est passé avec gitk :

Git-branches-first-push.png

Une nouvelle référence, « remotes/origin/experimental » est apparue. Cette référence pointe là où la branche « experimental » du dépôt partagé pointe, c'est à dire au même endroit que notre branche « experimental », puisque nous venons de nous synchroniser avec.

Récupération d'une branche par un coéquipier

De son côté, Bob fait maintenant :

$ git pull
From ssh://depots.ensimag.fr/~/git
 * [new branch]      experimental -> origin/experimental

Git informe Bob qu'une nouvelle branche est apparue. Cette branche s'appelle « experimental » dans le dépôt distant, mais pour l'instant, la copie locale de cette branche s'appelle « origin/experimental » :

Git-branches-first-pull.png

Bob se place maintenant sur la branche « experimental » :

$ git checkout experimental
Branch experimental set up to track remote branch experimental from origin.
Switched to a new branch 'experimental'

Git vient de créer une branche locale « experimental » sur laquelle Bob peut travailler. Il fait un commit :

$ git commit -a
Waiting for Emacs...
[experimental 25e2b47] Contribution à la fonctionalité XXX
 1 files changed, 1 insertions(+), 1 deletions(-)

Bob peut maintenant lancer gitk et vérifier que sa branche « experimental » a avancé d'un commit, alors que la copie locale de la branche distante (remote-tracking branch dans le jargon) « origin/experimental » n'a pas bougé :

Git-branches-bob-commit.png

Bob peut envoyer ses changements au dépôt partagé. S'il a bien positionné push.default à tracking comme expliqué ci-dessus, il lui suffit simplement de faire :

$ git push
Counting objects: 5, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 308 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
To ssh://gl42@depots.ensimag.fr/~/git
   5887794..25e2b47  experimental -> experimental

Alice et Bob peuvent maintenant travailler comme avant (commit, push, pull, ...). Leurs changements se feront sur la branche « experimental » et ne perturberont pas les coéquipiers qui travaillent sur la branche « master ».

Fusion de la branche master dans la branche experimental

Charlie, qui travaille toujours sur la branche « master », vient de faire des corrections importantes qui intéressent Alice et Bob. Alice veut donc fusionner la branche « master » dans la branche « experimental ». Cette fusion ne modifiera pas la branche « master », mais la branche « experimental » contiendra à la fois les nouveautés de cette branche et celles de la branche « master ».

Alice commence par récupérer les modifications du dépôt partagé :

$ git fetch
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From ssh://depots.ensimag.fr/~/git
   758b93f..4ea2e8f  master     -> origin/master

Ce qui donne ceci :

Git-branches-fetch-master.png

puis elle fusionne la branche « origin/master », qui vient d'être mise à jour :

$ git merge origin/master
Merge made by the 'recursive' strategy.
 Verif/Src/test_symb.adb |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)

(Si besoin, Alice résout les conflits et lance « git commit » pour conclure la fusion)

Cette fusion vient de créer un commit de fusion, qui réunit les deux branches :

Git-branches-merge-master.png

On peut aussi faire ces deux étapes en une avec :

git pull origin master

Passer d'une branche à l'autre

En travaillant sur la branche « experimental », Bob trouve un bug qu'il souhaite corriger. Pour en faire bénéficier tout le monde, il va faire la correction de bug sur la branche « master ». Après avoir commité son travail en cours, il repasse donc sur la branche « master » :

$ git checkout master      
Switched to branch 'master'

Il se met à jour pour faire sa correction sur la dernière version, comme d'habitude :

git pull

fait la correction de bug et en fait un commit, toujours comme d'habitude :

$ git commit -a
[master e2d8bde] Correction du bug XYZ
 1 files changed, 1 insertions(+), 1 deletions(-)
$ git push
[...]
   4ea2e8f..e2d8bde  master -> master

Il revient maintenant sur la branche « experimental » :

$ git checkout experimental
Switched to branch 'experimental'

et il récupère la correction de bug qu'il vient de faire sur « master » :

$ git merge master

Si Bob ne se souvient plus de la branche sur laquelle il se trouve, il peut à tout moment lancer la commande « git branch » sans argument pour le savoir :

$ git branch
* experimental
  master

Fusion de la branche dans la branche master

La branche « experimental » est maintenant prête à être fusionnée dans « master ». Après s'être assuré que tous les commits d'Alice et Bob ont été envoyés au dépôt (git push et git pull doivent confirmer que tout est à jour), Alice, ou Bob, se place sur la branche « master » pour faire la fusion :

$ git checkout master

Puis fait la fusion :

$ git merge experimental

(et envoie le résultat au dépôt partagé avec « git push », bien sûr)

Plus de documentation