Git dans la pratique (2/2)

Dans une première partie, nous avons abordé la notion d’index et la différence entre une branche locale et une branche distante. Une fois les notions d’index et de branches locales et distantes bien comprises, il est possible d’aborder des fonctionnalités plus avancées de Git.

Oui, Git est efficace et flexible

Mettre de côté des modifications

Régulièrement, on se retrouve avec des modifications en cours sur notre copie de travail quand vient une tâche plus prioritaire. Plutôt que d’abandonner les modifications déjà effectuées, de commiter des modifications qui cassent une fonctionnalité, ou encore de créer un patch dans un fichier que l’on met de côté, Git nous propose d’utiliser git stash.
Cette commande permet de mettre de côté toutes les modifications de la copie de travail et de l’index (il faut penser à ajouter les nouveaux fichiers qui ne sont pas encore suivis par Git).
Après avoir exécuté cette commande, l’index et la copie de travail seront dans le même état qu’après un git reset --hard HEAD (ie. plus aucune modification en cours et/ou à commiter) et un groupe de modifications est créé et visible avec git stash list.

Pour reprendre le travail commencé et rangé il est possible de le récupérer de 2 façons différentes :

  • soit nous avons la possibilité de créer une branche sur laquelle nous pourrons créer autant de commits que nécessaire, de basculer entre cette tâche que l’on peut faire avancer au rythme souhaité si elle n’est plus prioritaire. Dans ce cas, la création de la branche s’effectue à partir des modifications mises de côté avec :
    $ git stash branch myfeature

    À la suite de cette commande, nous nous retrouvons sur la nouvelle branche myfeature avec les modifications telles qu’elles étaient avant le git stash (ie. dans la copie de travail ou « stagées » sur l’index mais non commitées),

  • soit nous pouvons réappliquer les modifications sur la branche actuelle avec :
    $ git stash pop

Cherry-picking

Il est classique d’utiliser des branches pour gérer les différentes versions des différents environnements où est déployée l’application. Dans notre cas, nous utilisons la branche master pour le développement au quotidien, une branche recette et une branche de production associées à leur environnement respectif.
Régulièrement, lors de la découverte d’un bug, une correction de ce bug était déjà présente sur la branche master et nécessitait d’être appliquée soit sur la branche recette ou production. Seulement, il n’est pas envisageable de réaliser un merge de master vers l’une de ces 2 branches car nous ne voulons pas intégrer toutes les autres modifications de la branche master.

Git possède la commande cherry-pick qui permet de sélectionner un commit quelconque et de l’appliquer sur la branche actuelle.

$ git checkout production
$ git cherry-pick d42c389f

Merge vs Rebase

Un dernier point qui surprend lorsqu’on débute avec Git est l’historique qui apparait très peu lisible. En effet, avec 10 développeurs travaillant sur la branche master et synchronisant sur un dépôt centralisé, l’historique finit par ressembler à ça :

Lorsqu’on utilise git pull, la stratégie par défaut est de merger la branche distante dans la branche locale. Git considère donc qu’il s’agit de 2 branches différentes alors qu’en réalité, nous voulons considérer qu’il s’agit d’une seule et même branche comme nous le faisions avec Subversion. L’historique reflète donc autant de branches qu’il y a de développeurs et Git cré des commits de merge lors d’un git pull qui intègre des commits distants et locaux.

Git permet cependant de travailler avec une logique plus proche de celle de Subversion. Au lieu d’effectuer un merge, il est possible de réaliser un « rebase » lors d’un git pull. Le principe du rebase est de revenir en arrière dans l’historique en mettant de côté les commits qui n’ont pas encore été pushés, d’appliquer les commits de la branche distante sur la branche locale, puis d’appliquer les commits mis de côté à la suite. L’historique est alors linéaire et ne laisse plus de traces de branche ou de commits de merge.

Exemple

Si l’on prend l’exemple suivant qui contient des commits sur la branche master et d’autres provenant de la branche master du dépôt origin :
Git avant un merge

Cas d’un merge

En utilisant git pull (ou git merge origin/master), un commit de merge est créé (G) et l’historique ressemble alors à ça :
Git merge

Cas d’un rebase

En utilisant git pull --rebase (ou git rebase origin/master), les commits qui n’existaient que sur la branche master (E et F) sont supprimés et réappliqués à la suite des commits de la branche origin/master. Ce sont de nouveaux commits (E’ et F’), c’est pour cela qu’il ne faut pas faire un rebase sur des commits qui sont déjà présents sur un dépôt partagé.
Git rebase

Résolution de conflit

Que ce soit lors d’un merge ou d’un rebase, il arrive que des conflits apparaissent. Voici comment les résoudre avec Git.

Lors d’un merge

Lorsqu’on effectue un git merge (ou git pull) et qu’un conflit apparait, Git ne commit pas automatiquement. Les fichiers sans conflit seront alors déjà ajoutés à l’index, alors que les fichiers en conflits apparaîtront comme tel lors d’un git status :

$ git status
# On branch master
# Changes to be committed:
#
#       modified:   test3
#
# Unmerged paths:
#   (use "git add/rm < file >..." as appropriate to mark resolution)
#
#       both modified:      test
#

Pour résoudre le conflit, 2 possibilités :

  • git mergetool exécutera un outil de merge sur tous les fichiers en conflit,
  • les fichiers en conflit contiennent les 2 versions des lignes en conflit comme c’est le cas avec Subversion. Il est donc possible de résoudre manuellement le conflit en éditant ces fichiers. Une fois les conflits résolus, il est nécessaire d’appliquer un git stage sur ces fichiers.

Enfin, un git commit terminera l’opération de merge.

Pour revenir à l’état du dépôt avant la tentative de merge (au lieu du git commit) :

$ git reset --hard HEAD

Lors d’un rebase

Une opération de rebase (git rebase ou git pull --rebase) n’échappe pas aux conflits. La résolution des conflits peut cependant être nécessaire plusieurs fois lors d’un seul rebase. En effet, les commits étant appliqués 1 par 1, un conflit peut apparaître à chaque fois qu’un commit est rejoué.
La procédure est la même que pour le merge (ie. git mergetool ou édition manuelle des fichiers en conflit suivit d’un git stage) sauf que le dernier git commit sera remplacé par un git rebase --continue pour que Git continue d’appliquer les commits suivants.

$ git pull --rebase
...
## conflit
$ git status
# Unmerged paths:
#   (use "git add/rm < file >..." as appropriate to mark resolution)
#
#       both modified:      test
#
$ git mergetool
...
$ git rebase --continue

Pour revenir à l’état du dépôt avant la tentative de rebase (au lieu du git rebase --continue) :

$ git rebase --abort

Cas d’un conflit de binaires

Que ce soit lors d’un merge ou d’un rebase, la résolution d’un conflit de binaires ne se résout pas avec un git mergetool. En effet, pour ce genre de conflit, la résolution se fait en choisissant l’une des 2 versions possibles :

  • $ git checkout --ours -- binary_file_path

    pour sélectionner la version que nous avions avant le merge, ou la version provenant de la branche rebasée lors d’un rebase. En effet, la branche mergée est appliquée sur la branche en cours alors que c’est sur la branche rebasée que l’on applique les commits de la branche en cours. Le --ours est donc inversé selon l’opération.

  • $ git checkout --theirs -- binary_file_path

    pour sélectionner la version de la branche mergée, ou la version que nous avions avant le rebase.

Une fois la version du binaire à conserver sélectionnée, la procédure reste la même :

$ git stage binary_file_path
$ git commit # lors d'un merge
ou
$ git rebase --continue # lors d'un rebase

Modifier un commit

Il arrive que l’on crée des commits en ayant oublié quelques détails (d’ajouter un fichier, de faire passer les tests, …). Habituellement, il aurait fallu se résigner à créer un nouveau commit qui corrige nos erreurs. Avec Git, il est possible de modifier un commit existant.

Avant d’aller plus loin, attention toutefois, il est très fortement déconseiller de modifier un commit déjà partagé sur un dépôt accessible par d’autres développeurs (ie. après un git push généralement).

La façon la plus simple de modifier le dernier commit que l’on a effectué est d’utiliser git commit --amend au lieu de git commit -m '...'. L’option --amend va ajouter les modifications de l’index au commit précédent et aucun nouveau commit ne sera alors créé.

L’autre façon, plus avancée, de modifier des commits dans l’historique de Git est git rebase --interactive <commit>. Celle-ci permet de ne plus être restreint au dernier commit.
Par exemple pour éditer l’historique parmi les 5 derniers commits, il faut exécuter :

$ git rebase --interactive HEAD~6

HEAD~6 désigne le sixième commit ancêtre de la HEAD. Il est nécessaire d’englober un commit de plus que le nombre de commits que l’on veut éditer. Votre éditeur de texte favori sera exécuté par Git (en utilisant la variable d’environnement EDITOR) avec un fichier semblable à celui-là :

pick 4997150 commit n-5
pick 7be917b commit n-4
pick 83270c0 commit n-3
pick c65ad3b commit n-2
pick fa9252b commit n-1
pick 2bf85f8 last commit

Il est alors possible d’effectuer plusieurs actions sur chacune des lignes :

  • supprimer la ligne pour supprimer purement et simplement le commit de l’historique,
  • changer l’ordre des lignes pour changer l’ordre d’application des commits,
  • remplacer « pick » par « edit » pour pouvoir modifier le commit (cf. la résolution de conflit lors du rebase plus haut),
  • remplacer « pick » par « squash » pour merger le commit avec le précédent pour n’en créer qu’un seul,
  • remplacer « pick » par « reword » pour juste changer le message de commit ».

Après avoir enregistré et quitté l’éditeur de texte, Git tentera de rejouer les commits avec les modifications définies.

Il se peut qu’en rejouant ces commits, des conflits apparaissent. Dans ce cas là, Git s’arrêtera au commit qui a échoué ou au commit marqué « edit ». Une fois les conflits résolus et/ou le commit edité comme souhaité et les modifications ajoutées à l’index (ie. git stage), il reste à exécuter git rebase --continue pour poursuivre la réécriture de l’historique.

Conclusion

Comme nous avons pu le voir, git permet de pousser très loin la gestion des versions de ses sources. Jusqu’à présent, je ne me suis jamais retrouvé à ne pas pouvoir faire les manipulations que je pouvais imaginer, seulement souvent, il a fallu lire les pages de manuels et diverses documentations que l’on peut trouver sur internet (qui sont d’ailleurs très complètes) afin d’aller plus loin dans la compréhension de l’outil. Un conseil bien pratique lorsqu’on tente de nouvelles actions sur son dépôt Git est d’en faire une copie en cas de fausse manipulation. Donc, comme souvent, un outil qui offre une grande flexibilité requiert une bonne connaissance de son fonctionnement.

6 commentaires sur “Git dans la pratique (2/2)”

  • Merci pour l'article. Je me permet d'ajouter quelques points (surement évidents): - Si vous ne l'utilisez pas tout de suite, pensez a nommer votre stash' avec un message assez clair, ex : git stash save "ma super modif" au lieu de juste "git stash" - Finissez le merge des conflits avant de faire toute modification Maintenant, une question : je me sert de git en ligne de commande (comme vos exemple), c'est pas si compliqué que ca et ca me permet de bien comprendre ce que je fais, mais j'avoue qu'a la longue, c'est un peu pénible. Je suis sur mac et j'utilise GitX pour avoir une vision graphique de mes commits, mais cet outil est limité a la visualisation d'historique et de d'editions de commits (pas de push/pull par ex). Quels sont les outils de GUI pour Git ? (sous mac ou windows) As-tu essayé tortoisegit ?
  • Merci pour ces quelques précisions sur le git stash et le merge. Pour ce qui est des GUI, j'avoue ne pas vraiment m'en servir. Je préfère investir dans un alias ou une nouvelle commande pour simplifier mes lignes de commandes. Même pour chercher dans l'historique, l'alias 'lola' (cf. le premier article) me satisfait totalement. Pour créer de nouvelles commandes, il suffit de placer un script (dans son langage favori) qui se nomme git-toto pour que la commande toto soit ajoutée à Git. Peut-être que d'autres lecteurs pourraient nous donner leur ressenti sur ce type d'outils.
  • @David : un grand merci pour cet article qui eclaicit de nombreux points obscurs. @Benoit : j'utilise bcp la ligne de commande, mais j'ai toujours sous la main Smargit qui est très bien (et fait en java)
  • Perso j'utilise le plugin git de RubyMine ou IntellijIDEA (c'est le même) qui est vraiment très bien fait. D'ailleurs de base il propose de faire tes git pull avec l'option --rebase ou encore de mettre tes modifs de côté avec stash lorsque l'on veut puller qqchose sans rien avoir commité
  • Voilà une paire d'articles particulièrement bienvenue. Pour répondre à Benoît Lafontaine, GitBox est un peu le complice de GitX sur tous les aspects push/pull. J'utilise GitX et GitBox selon les besoins. Toutefois, je rejoins l'auteur : la ligne de commande fait des retours très explicites une fois que l'on comprend le mécanisme. Et quelques alias permettent d'oublier les commandes parfois obscures de Git (et ses mille et uns interrupteurs). Pour compléter, voici un joli schéma pour se faire une Quick Ref Card. http://panela.blog-city.com/git_supervisual_cheatsheet.htm
  • Super article. Merci! Juste une typo : "il est très fortement déconseiller de modifier " -> déconseillé ;)
    1. Laisser un commentaire

      Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *


      Ce formulaire est protégé par Google Recaptcha