Git Attitude

La gestion de sources qui fait du bien

Une configuration Git aux petits oignons

| Commentaires

Git propose 3 niveaux de configuration :

  1. Locale. Spécifique à chaque dépôt local, elle est stockée dans le .git/config. On y trouve notamment les trackings de branches, le remote, etc.
  2. Globale. Spécifique à l’utilisateur, elle est stockée à la racine de son compte, dans le fichier ~/.gitconfig. C’est celle qui nous intéresse ici.
  3. Système. Généralement stockée dans /etc/gitconfig, elle est partagée par tous les utilisateurs. Elle est rarement employée.

Les priorités vont dans l’ordre usuel : du local au système.

La majorité des utilisateurs se contente de coller dans son .gitconfig ses nom, e-mail et préférences d’éditeur. C’est là une sous-utilisation navrante. De très nombreux aspects quotidiens de Git sont paramétrables, et donc améliorables, au travers de la configuration.

Cet article explore ma configuration personnelle pour vous faire découvrir de nombreuses astuces utiles.

Avant toute chose, assurez-vous que vous utilisez un Git récent (1.8+). Plusieurs options de configuration évoquées ci-après n’ont fait leur apparition que récemment. Si une option semble ignorée chez vous, vérifiez soigneusement son nom et sa valeur, puis jetez un œil aux notes de version pour retrouver la version dans laquelle l’option est apparue.

Je maintiens une version publique générisée de ma configuration dans ce Gist. Par « générisée », je veux dire que j’en retire, ou que j’y commente, quelques traitements qui me semblent délicats à activer par défaut, car ils concernent des comportements subtils de Git qu’il vaut mieux bien connaître avant de les appliquer.

Un fichier de configuration Git est d’une syntaxe similaire à un bon vieux fichier INI : les sections sont délimitées par un en-tête [nom.section], et les lignes de propriété qui suivent appartiennent à la section. L’indentation est recommandée mais nullement obligatoire. La représentation linéarisée d’une propriété est section.nom, par exemple core.whitespace pour la propriété whitespace au sein de la section [core].

Voici déjà le gist global :

Examinons à présent les réglages un par un.

Identité par défaut pour les commits

C’est la base de la base : identifier vos commits à venir. On utilise pour cela user.name et user.email. Évitez toutefois de pourrir la config Git d’un utilisateur de déploiement sur un serveur de prod avec ce genre de trucs, si vous êtes plusieurs personnes à pouvoir déployer. Préférez qualifier un éventuel commit en prod (ouh là là…) avec git commit --author=… afin de ne pas vous mélanger les pinceaux.

Colorisation

De nombreuses commandes Git sont capables de coloriser leur affichage à l’aide de codes ANSI VT lorsque le terminal le permet. La valeur auto a le même sens que dans de nombreuses autres commandes Linux (par exemple ls) : Git détectera s’il est utilisé en direct par un terminal VT capable de gérer les codes de couleur, et si c’est le cas, exploitera cette possibilité.

La propriété color.ui est un paravent générique pour l’ensemble des propriétés plus spécifiques de gestion de couleur, par exemple color.branch, color.diff, color.grep, color.interactive, etc. En gros, on veut de la couleur !

Aliases

Git ne permet par défaut que les noms de commande complets (commit, checkout, etc.), ce qui fait parfois rager les Subversioniens débutant sur Git, qui cherchent en vain leur git ci. Outre qu’un prompt correctement configuré proposera une complétion avancée (ça fera l’objet d’un autre billet), la vraie raison est que Git permet de se créer autant d’aliases qu’on le désire, au moyen des réglages de la section [alias]. Chaque propriété a pour nom celui de l’alias et pour valeur la ligne de commande Git sans le git initial.

Ma configuration prévoit 3 aliases sans lesquels je ne saurais bosser :

  • ci pour commit
  • st pour status
  • lg pour un log textuel avancé qui, en ce qui me concerne, est tout-à-fait à la hauteur des logs graphiques (graphe de branches et fusions, références symboliques, tags et têtes de branches, SHAs, auteurs et horodatage relatif). Et le bonus, c’est qu’il marche dans un terminal, donc via SSH…

Pagination, whitespace et autres réglages « noyau »

Par défaut, Git pagine tout ce qu’il affiche : si ça dépasse la hauteur du terminal, ça se prend un less dans la figure. Personnellement, je ne supporte pas ça : si je veux paginer, je | less moi-même ! En spécifiant via core.pager de passer par cat plutôt que less, je désactive de facto la pagination.

Lorsqu’il ouvre un éditeur (notamment pour les messages de commit ou la mise en place d’un rebase interactif), Git choisit l’éditeur comme ceci :

  1. Variable d’environnement GIT_EDITOR
  2. Réglage core.editor (c’est ce que je préfère)
  3. Variable d’environnement VISUAL
  4. Variable d’environnement EDITOR
  5. Binaire par défaut retenu à la compilation de Git ; en général, vi.

Si le comportement par défaut n’est pas votre tasse de thé, il vous faut spécifier la ligne de commande à exécuter. Lorsque l’éditeur est graphique, vous devrez l’invoquer de façon à ce qu’il soit en wait mode, c’est-à-dire qu’il ne rende la main qu’une fois le fichier fourni refermé. Tous les éditeurs graphiques n’ont pas cette possibilité.

Voici une liste de lignes de commandes compilée au fil du temps ; vous allez voir, on distingue une tendance :-)… Testez par vous-mêmes pour vérifier ce que ça donne, et si ça a changé, faites-moi signe en commentaires !

  • TextMate : l’invocation en ligne de commande mate doit être installée (ça se fait depuis Help > Terminal Usage… dans la v1 et depuis l’onglet Terminal des Préférences dans la v2), après quoi c’est juste mate -w.
  • SublimeText : il faut avoir un appel en ligne de commande, là aussi. Sur OSX avec ST2, j’ai un lien symbolique /usr/local/bin/subl vers /Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl, qui est fourni pour ça. Après quoi subl -w.
  • GEdit : gedit -w -s
  • GVim : gvim --nofork
  • Coda : Il vous faut installer coda-cli (généralement via Homebrew, toujours aussi utile…) après quoi coda -w.
  • Chocolat : Installez l’appel en ligne de commande (Chocolat > Install Command Line Tool…) après quoi choc -w
  • Notepad++ : vous aurez peut-être besoin d’adapter le chemin en fonction de là où vous avez installé Notepad++, mais voici l’idée : 'C:/Program Files (x86)/Notepad++/notepad++.exe' -multiInst -notabbar -nosession -noPlugin.
  • Ultra-Edit : uedit32 /fni

Enfin, le réglage core.whitespace est là pour éviter que certains types de différences liées au whitespace ne soient détectés ou posent problème à des commandes comme git diff, git add -p, git apply ou git rebase. Il existe toute une série de valeurs possibles et combinables ; trailing-space est une combo pour blank-at-eol (whitespace en fin de ligne) et blank-at-eof (whitespace, notamment lignes vides, en fin de fichier). Comme j’indique ici cette valeur en négatif (notez le - préfixe), je précise que ce type d’écarts ne doit pas poser souci ni être signalé.

Diffs améliorés

Par défaut, toute forme de diff signale les deux versions comme a et b :

1
2
3
4
5
[15:54] tdd@CodeWeaver:git-attitude (master *+%) $ git diff 2013-04-03-configuration-git.markdown
diff --git a/source/_posts/2013-04-03-configuration-git.markdown b/source/_posts/2013-04-03-configuration-git.markdown
index e69de29..44bd5ae 100644
--- a/source/_posts/2013-04-03-configuration-git.markdown
+++ b/source/_posts/2013-04-03-configuration-git.markdown

Ce n’est pas toujours évident de savoir ce qu’est a et ce qu’est b… Le HEAD ? Le stage ? Le working directory ? Récemment, Git a fourni un réglage, diff.mnemonicPrefix, qui lorsqu’il est à true va remplacer, lorsque c’est possible, ces initiales anonymes par trois autres :

  • c pour Commit (généralement le HEAD)
  • i pour Index (le stage)
  • w pour Working directory

Ainsi, un diff classique explicite qu’il est effectué entre le stage (l’index) et le working directory :

1
2
3
4
5
[16:02] tdd@CodeWeaver:_posts (master *+) $ git diff 2013-04-03-configuration-git.markdown
diff --git i/source/_posts/2013-04-03-configuration-git.markdown w/source/_posts/2013-04-03-configuration-git.markdown
index 202f830..cde050e 100644
--- i/source/_posts/2013-04-03-configuration-git.markdown
+++ w/source/_posts/2013-04-03-configuration-git.markdown

Un diff --staged indique bien qu’il est effectué entre le HEAD (un commit) et le stage (l’index) :

1
2
3
4
5
[16:02] tdd@CodeWeaver:_posts (master *+) $ git diff --staged 2013-04-03-configuration-git.markdown
diff --git c/source/_posts/2013-04-03-configuration-git.markdown i/source/_posts/2013-04-03-configuration-git.markdown
index cde050e..202f830 100644
--- c/source/_posts/2013-04-03-configuration-git.markdown
+++ i/source/_posts/2013-04-03-configuration-git.markdown

Quant à un diff HEAD, qui explicite son commit de référence, il confirme qu’il compare un commit et le working directory :

1
2
3
4
5
[16:02] tdd@CodeWeaver:_posts (master *+) $ git diff HEAD 2013-04-03-configuration-git.markdown
diff --git c/source/_posts/2013-04-03-configuration-git.markdown w/source/_posts/2013-04-03-configuration-git.markdown
index cde050e..3f85496 100644
--- c/source/_posts/2013-04-03-configuration-git.markdown
+++ w/source/_posts/2013-04-03-configuration-git.markdown

Un autre aspect un peu dommage de git diff est le comportement par défaut de git diff --word-diff. S’il est bien agréable d’avoir par défaut un mode word diff, qui permet de cibler, au sein de deux lignes sans doute très similaires, la zone de différence (un peu comme le changement de contraste dans des outils de diff graphiques), Git considère par défaut comme « mot » toute séquence de caractères non-whitespace. Dans un bloc de code, ça fait vite long, vu que les délimiteurs et opérateurs seront aussi comptabilisés. Par exemple, @twitterClient.directMessage(contact, est considéré comme un mot. Snif.

C’est pourquoi git diff fournit une autre option, --word-diff-regex=…, qui permet de préciser une regex représentant un « mot ». Naturellement, en réduisant cette regex à ., on va tenter la correspondance minimale (jusqu’à un seul caractère). Dans la pratique, quand je fais un diff de niveau mot, je fais toujours comme ça.

C’est là tout l’intérêt du réglage diff.wordRegex : il permet de traiter un simple --word-diff comme un --word-diff-regex=…. Voyez plutôt :

Submodules

Les submodules sont une mauvaise idée. J’en reparlerai en détail dans un prochain article. Toutefois, pour ceux qui sont empêtrés dans les emmerdes quotidiennes des submodules (il y a des pièges à foison dans leur maintenance au quotidien), divers réglages permettent, au fil des versions, d’ajouter des garde-fous. J’en préconise quelques-uns :

  • fetch.recurseSubmodules détecte automatiquement lors d’un fetch (et donc d’un pull) si les références des submodules ont bougé, et propose de les récupérer automatiquement si c’est le cas. Attention, ça ne fait pas le submodule update pour autant.
  • status.submoduleSummary prend soin de signaler dans git status les submodules dont la référence a bougé, en listant les deltas de commits concernés.

Regex étendues par défaut

Git propose à de nombreux endroits de recourir aux expressions rationnelles (regex). Notamment git log --grep et git log -G, pour ne citer qu’eux. Par défaut cependant, seules les BRE (Basic Regular Expressions) sont traitées, c’est-à-dire que la majorité des caractères spéciaux tels que {}, ‘()’, + et ? sont ignorés. Dans la pratique, quand j’utilise une regex, je veux toujours disposer de la syntaxe étendue (ERE, Extended Regular Expressions), qui active toutes les syntaxes possibles.

Plutôt que d’avoir alors à ajouter -E à ma ligne de commande, j’active le réglage grep.extendedRegexp dans la configuration globale.

Attention cependant : vos regex ne seront pas insensibles à la casse pour autant. Si vous voulez garantir cela, vous devrez généralement ajouter -i à vos lignes de commande.

Les SHA1 complets, ça ne sert à rien

C’est un peu comme le H de Hawai.

La majorité des commandes de Git et de la documentation utilise des abbreviated SHA1s, ou abbrevs, soit les 7 premiers caractères seulement. La probabilité de collision de valeurs effectives à 7 caractères de SHA1 est infinitésimale (je ne suis pas expert, mais ça doit plafonner à 2−132, ce qui revient à dire que tu as à peine moins de chances de tomber sur un atome précis parmi tous ceux qui composent la Terre, nous inclus ; moi je dis, risque acceptable).

Histoire que même un bête git log sans options particulières se limite à ça, j’active log.abbrevCommit.

Des fusions plus pratiques

Si Git nous fournit effectivement pléthore de mécanismes afin de résoudre au mieux un conflit de fusion, il reste possible d’améliorer les comportements par défaut.

Ainsi, Git injecte dans les fichiers textuels des marqueurs de conflit classiques. Par exemple :

1
2
3
4
5
6
7
<body>
<<<<<<< HEAD
  <h1>Super site de gros bills</h1>
=======
  <h1>Super site de malades</h1>
>>>>>>> feature
</body>

Il est déjà possible d’obtenir davantage d’informations à ce stade, en affichant entre les deux (la branche récipiendaire de la fusion et la branche en cours de fusion) la version du texte dans l’ancêtre commun (le point de divergence). C’est le mode diff3, que j’active automatiquement avec le réglage merge.conflictStyle :

1
2
3
4
5
6
7
8
9
<body>
<<<<<<< HEAD
  <h1>Super site de gros bills</h1>
||||||| merged common ancestors
  <h1>Super site de oufs</h1>
=======
  <h1>Super site de malades</h1>
>>>>>>> feature
</body>

Par ailleurs, lorsqu’on recourt à un outil de résolution de conflits (un mergetool), celui-ci peut générer des fichiers temporaires ou de sauvegarde (souvent avec des extensions genre .rej, .orig, .local, .tmp, .bak…), qui viennent joyeusement pourrir votre working directory. Même si ces extensions sont protégées contre un ajout par inadvertance à l’aide d’un .gitignore ou équivalent, les fichiers restent là.

Vous pouvez vous assurer que votre outil va nettoyer derrière lui en désactivant les réglages mergetool.keepBackup et mergetool.keepTemporaries.

Pour finir sur les mergetools, lorsqu’on lance git mergetool, il demande par défaut confirmation quant au fichier à manipuler. Afin de vous éviter cette question le plus souvent superflue, désactivez mergetool.prompt.

Un autre sujet important, sur lequel j’écrirai un article un de ces jours, est Rerere. Cette fonctionnalité avancée de Git permet d’enregistrer (localement) des empreintes de conflits et les résolutions manuelles que vous leur avez apportées, pour ré-utiliser automatiquement ces résolutions par la suite. C’est extrêmement pratique et permet des workflows très puissants. Je vous invite à aller explorer la doc à ce sujet, et si vous souhaitez vous en servir, vous n’aurez qu’à activer rerere.enabled et, si vous souhaitez mettre automatiquement dans le stage ces résolutions automatisées, rerere.autoupdate.

Des pulls en rebase par défaut

J’écrirai prochainement un article détaillé sur les raisons pour lesquelles j’estime que l’immense majorité des pulls devraient utiliser un rebase plutôt qu’un merge. En gros, il s’agit d’éviter de pourrir l’historique quand on est à plusieurs à committer en même temps sur la même branche partagée.

La commande git pull est en fait une séquence de deux commandes plus spécifiques : git fetch suivie de git merge. Par exemple, si vous êtes sur master avec un remote par défaut origin, git pull revient à faire :

  1. git fetch
  2. git merge origin/master

Quand vous bossez à plusieurs sur la branche, il est le plus souvent préférable de faire l’équivalent de :

  1. git fetch
  2. git rebase origin/master

Git anticipe ce scénario et vous le permet avec git pull --rebase. Toutefois, il devrait s’agir à mon sens du cas général, et non du cas particulier. Il est en effet devenu possible de bosser ainsi en activant l’option pull.rebase.

Le seul danger potentiel, c’est qu’un rebase, par défaut, « inline » les fusions. C’est ajustable quand on git rebase manuellement, mais pas dans le cadre d’un pull. Prenons un exemple.

Disons que vous venez de procéder à une fusion légitime sur votre branche locale (par exemple, la branche feature était terminée et vous avez procédé à son rapatriement sur master à l’aide d’un git merge feature).

Le push vous est refusé car il s’avère que le master distant (origin/master) a bougé. Pas le choix, vous faites un pull avant de continuer. Si celui-ci est en mode rebase, vous allez automatiquement rebaser l’historique local récent (les commits depuis votre ancienne référence origin/master, dont votre commit de fusion) sur le nouveau origin/master. En temps normal c’est top, mais sur ce coup ça va inliner la fusion en remplaçant le true merge par un rebase. La branche, jusqu’ici bien visible dans le graphe du log, va disparaître.

Ma recommandation est la suivante :

  1. Si vous vous apprêtez à fusionner une branche et que vous avez l’excellente habitude de nettoyer votre historique local avant de pusher, faites-le dès maintenant (vous ne pourrez plus le faire après sans inliner la fusion).
  2. Pullez ensuite immédiatement, en mode rebase. Ce sera toujours ça comme risque en moins pour après.
  3. Effectuez la fusion.
  4. Pushez immédiatement ; si un petit malin a remis à jour le remote entre-temps, au pire, faites un git pull --no-rebase juste pour cette fois. L’historique ne sera pas bien joli, mais bon.

À noter que le recours à Rerere (voir plus haut) permet de préparer la fusion à l’avance (en résolvant d’éventuels conflits), puis l’annuler, et dans le cadre de la séquence ci-dessus, d’être certain que l’étape 3 sera très courte, laissant d’autant moins de temps aux copains pour refaire bouger le remote.

Des pushes plus ciblés

Par défaut, git push tente de publier toutes vos branches homonymes au remote. S’il m’arrive très rarement d’avoir de nouveaux travaux sur plusieurs branches en local qui soient trackées sur le remote, et donc d’avoir besoin de ça, il m’arrive en revanche régulièrement de tomber dans le scénario suivant :

  1. Je dois collaborer quelques temps à la branche feature : je git checkout feature, ce qui cale une branche locale trackée sur la distante.
  2. Je bosse, je committe, je pushe, tout roule…
  3. Au bout d’un moment, soit ça fait un bail que je ne bosse que sur feature et mon master prend du retard sur le distant, soit j’ai fini avec feature jusqu’à nouvel ordre, je repasse sur master et au bout d’un moment ma feature locale est en retard sur la version distante.

À ce moment-là, chaque push va me taper dessus en rejetant la ou les branches en retard. C’est assez pénible, parce que je ne cherche pas en fait à pusher ces branches, je les ai juste en local et je rebosserai sans doute dessus plus tard. C’est le mode matching, qui comme le dit la doc n’est pas approprié à un environnement multi-développeurs, mais est (jusqu’à Git 2.0 en tout cas) le mode par défaut.

En réglant push.default à upstream, je change le comportement par défaut de push afin qu’il ne se préoccupe que de la branche active. C’est une variante plus flexible du mode simple, qui exigerait que les branches locale et distante soient non seulement connectées (tracking) mais homonymes. À noter que ce mode simple sera le mode par défaut à partir de Git 2.0.

En conclusion…

Bon, voilà une bonne chose de faite ! Pensez à jeter un œil aux notes de version quand vous mettez à jour votre Git, et surtout à regarder de temps en temps ce qu’on trouve dans git help config, qui liste tous les réglages proposés, ou simplement si le git help de votre commande en cours ne fournit pas une section CONFIGURATION sur la fin, qui cible les réglages associés. On y apprend toujours plein de choses…

Bonne config à tous !

Envie d’en savoir plus ?

Nos formations Git au Quotidien explorent en profondeur les questions de configuration, de prompt et d’options avancées ou méconnues pour les commandes même les plus simples. Une excellente façon d’aller bien au-delà de ces quelques réglages !

    Commentaires

    Ils nous font confiance : Kelkoo, MisterGoodDeal, PriceMinister, Blablacar / Comuto, Sarenza, Voyages-SNCF, LeMonde.fr, Fnac DIRECT, 20minutes, Orange, l’OCDE, Cisco, Alcatel-Lucent, Dassault Systèmes, EADS, Atos, Lagardère Interactive, Lesieur, L’Occitane en Provence, Météo France, 4D, Securitas, Digitas, Vivaki, Fullsix, Ekino, TBWA \ Paris, Valtech, Is Cool Entertainment, Open Wide…