Voici donc le 1er article de ma série git-tips.

Je vais ici vous présenter quelques idées qui me semblent importantes sur la manière d’utiliser Git. Je ne me veux pas exhaustif ici, je veux surtout revenir sur des petits points très spécifiques qui — d’après ce que j’ai pu voir — posent problème à beaucoup.

Ce n’est pas tout de connaitre les commandes, il faut aussi savoir quand utiliser quoi. Quand préférer un merge à un rebase, quand créer de nouvelles branches, etc.

Je place donc dans cette section les idées que j’ai dégoté (ou inventé avec mes petits m… ha bah non).

Merge fast-forward ou non ?

Pour commencer, petite présentation de ce que veut dire « fast-forward » (vous pouvez aussi aller voir ce lien) : l’idée est de modifier uniquement la référence de la branche, sans créer un commit de merge. Cela n’est donc possible que si la branche à merger est basée sur la branche principale (le dernier commit de la branche sur laquelle on merge doit être l’unique parent direct du premier commit de la branche à merger).

Il y a 3 modes pour la commande merge :

--ff (par défaut)
un merge fast-forward est effectué si possible. Sinon un merge normal est utilisé.
--ff-only
On demande ici à ce que seul un merge fast-forward soit effectué. Si les conditions ne le permettent pas, la commande ne fait rien.
--no-ff
On demande ici à ce qu’un un merge normal soit effectué, même si les conditions permettent de réaliser un merge fast-forward.

Dans les faits, vous devriez rarement utiliser --ff : - soit vous effectuez un merge fast-forward pour vous synchroniser (là où vous auriez tout aussi bien pu utiliser un rebase) ; - soit vous souhaitez réellement merger une branche (par exemple la branche sur laquelle vous développiez vous nouvelle fonctionnalité) et en ce cas vous vous tournez vers un merge non fast-forward.

La première option est donc à voir comme un raccourci : lorsque je sais que l’option --ff va faire ce que je veux, alors je ne suis pas forcé de préciser le comportement via les option.

Commiter puis réfléchir

Information logique mais importante, donc je la marque pour que ce soir clair : Git ne gère pas des fichiers modifiés mais des commits. Donc si vous voulez sauvegarder des modifications importantes, commitez-les.

Je me permets de me la jouer Captain Obvious car j’ai vu plusieurs fois des personnes qui laissaient des modifications non commités pendant des jours, voire des semaines, dans leur dépôt, car « c’est pas encore fini alors je vais pas le commiter ».

Que ce soit clair : ce raisonnement est complètement faux. La base même de Git est qu’il est distribué : chacun à son propre dépôt, que seul lui peut voir. Il est donc tout à faire possible — en fait c’est normal — d’avoir des commits dans son dépôt qui n’ont pas été pushés dans des dépôts commun.

Par exemple, lorsque vous travaillez sur votre branche perso, vous créez plein de petits commits, puis les mergez à la branche principale et ne les pushez qu’à partir de ce moment là. Ainsi vous avez pu avoir tous les avantages de Git sans pour autant ennuyer vos collègues.

De même, il est tout à faire normal de créer des commits temporaires (que j’appelle WIP1), puis de les supprimer plus tard tout en conservant les modifications.

Prenons un exemple : imaginons que je sois sur ma branche « feature/export », avec des modifications en cours. Un élément déclencheur (on me demande quelque chose, je viens d’y penser, le grand Ordonnanceur me parle, ou que sais-je) fait que je dois changer de branche (par exemple pour aller sur master afin de tester quelque chose).

Voici 2 solutions possibles pour réaliser cela sans risque :

Commit « WIP »

Git permet de commiter facilement (par exemple via la commande « git commit -am WIP ») mais aussi d’annuler un commit très simplement, via la commande reset.

# Sauvegarde de l'état courant
git add . && git commit -m WIP
# Changement de branche
git checkout master
# Je fais mes petites affaires
[…]
# Je reviens sur ma branche, donc sur le commit « WIP »
git checkout feature/export
# Je restaure l'état, en annulant le commit SANS CHANGER LES FICHIERS
git reset HEAD~1
# Je peux continuer ce que je faisais

Le commit WIP ne devrait pas être pushé sur un dépôt commun, mais peut être envoyé à un dépôt personnel si vous en avez un (un dépôt distant que seul vous utilisez, afin d’avoir une copie de sauvegarde).

Étant donné que l’état est sauvegarder dans un commit, il sera possible de ne retrouver (si le besoin s’en fait sentir) dans le reflog.

Stash

Git fourni un outil dédié à ce genre de cas : git stash.

Il permet de sauvegarder facilement toutes les modifications actuelles dans un stash, une sorte de commit capable de préserver l’index.

# Sauvegarde de l'état courant
git stash
# Changement de branche
git checkout master
# Je fais mes petites affaires
[…]
# Je reviens sur ma branche, donc sur le commit « WIP »
git checkout feature/export
# Je restaure l'état, en annulant le commit SANS CHANGER LES FICHIERS
git stash pop
# Je peux continuer ce que je faisais

Contrairement aux commit « normaux », le stash n’est pas conservé dans le reflog.

En soit le stash est un très bon outil, mais j’ai vu beaucoup de mauvaises utilisations donc je préfère vous mettre en garde dès maintenant : git stash ne remplace en aucun cas les branches.

Lorsque vous démarrez quelque chose de complexe, créez-vous une branche dédiée, et sauvegardez dedans (via un commit WIP ou un stash), mais ne vous contentez surtout pas d’avoir une dizaines de stash pointant sur votre branche master : vous vous y perdrez à coup sûr (et aurez raté toute la puissance de Git).

Si vous n’êtes pas sûr de votre manière de travailler, je vous conseille de ne pas utiliser git stash, jusqu’à ce que vous vous sentiez plus à l’aise avec les branches et le reste.

Qui contrôle le futur contrôle le passé

Avec des outils comme git rebase -i il vous est possible de réécrire (un pan de) l’historique de votre projet. Ce qu’il ne faut pas oublier, c’est que — contrairement aux merges — cette opération supprime l’ancienne version de l’historique, vous empêchant de vous y référer plus tard en cas de problème.

Gardez ceci en tête lorsque vous vous lancerez dans un gros rebase : plus vous devez corriger de conflits, plus vous vous éloignez de la véritable forme de votre historique… et donc potentiellement de votre code final ! Je vous conseille donc vivement de relancer vos tests à la fin d’un rebase, comme si vous veniez d’effectuer un commit.

Petite note : je parle ici de modification d’historique, mais c’est un réalité un abus de langage : l’historique a en réalité été dupliqué. Il est donc tout à fait possible de stocker le SHA1 du commit (en bout de la branche) qu’on va rebaser2. Cela permet, à la fin de l’opération, de comparer l’ancien historique et le nouveau. Sachez que cette astuce est — en quelque sorte — offerte par Git sous la forme de sa référence magique ORIG_HEAD.

Le reflog

Le reflog est un super outil, destiné à être oublié jusqu’à ce qu’il vous sauve la vie.

Le modèle de Git empêche toute modification des objets existants, donc toutes les opérations que vous effectuerez consisteront en la création de nouveaux objets, avec (ou non) modification des références existantes.

Les anciens objets sont donc toujours intacts, mais plus personne ne les références et ils seront donc supprimés par Git lors du prochain git gc (ne vous en souciez pas, Git l’appellera automatiquement de temps en temps). De plus, elle sont (par défaut) conservée au minimum 30 jours. C’est à dire que si vous faites « git commit --amend », l’ancienne version du commit existera toujours dans le système dans 30 jours.

Cependant à quoi bon avoir des objets dans le système si personne ne les référence ? Et bien c’est ici que le reflog entre en scène : il liste tous les changements effectués sur des commits. Vous commitez, c’est dans le reflog. Vous faite un rebase, c’est dans le reflog. Vous changez de branche, c’est dans le reflog. Vous faites un git reset <ref>, c’est dans le reflog…

Il y a 2 méthodes pour visualiser le reflog : « git reflog » et « git log -g ».

Les branches c’est pas sale

Rien de nouveau ici si ce n’est une petite note : n’ayez pas peur d’avoir des merges dans votre historique : c’est fait pour. Git n’est pas perturbé par les branches, de même qu’il vous permet de masquer les commits de merge si vous voulez. Donc ne vous forcez surtout pas à ne pas avoir de merges, ni à avoir un historique parfait : ce qui compte c’est le contenu.

git add

Je vous ai parlé de l’index, qui contient les modifications que Git inclura dans le prochain commit.

La commande pour cela est git add, mais ses options lui confèrent beaucoup plus de finesse :

  • --edit (ou -e)
  • Affiche le diff complet de tous les changements vus par Git. Vous pouvez ainsi y enlever les parties non désirées et/ou modifier manuellement certains bouts.

  • --patch (ou -p)

  • Affiche le diff des changements, mais bouts par bouts. Vous pouvez ainsi n’inclure que les (parties de) fichiers qui vous intéresse, voire lancer un éditeur pour les modifier (à la --edit)

  • --interactive (ou -i)

  • Affiche un menu permettant de voir l’état des fichiers, en inclure, en utilisant les 2 options précédentes ou non.

Bref, il y a de quoi faire les choses bien, alors c’est vraiment du gâchis que de se cantonner à « git commit -a »…

Conclusion

Voilà, j’espère avoir pu vous donner des pistes utiles dans votre utilisation quotidienne de Git. N’hésitez pas à vous faire les dents sur ce que vous avez vu, et à me faire des retours si l’article vous a plu et/ou si vous voyez des corrections à y apporter :-)

Rendez-vous maintenant à la deuxième partie : Oubliez SVN.


  1. Work In Progress
  2. En le notant sur une feuille ou en créant une branche par exemple.