Configuration Git : 1ère partie
Par Christophe Porteneuve • Publié le 3 avril 2013
• 14 min
Git propose 3 niveaux de configuration :
- 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. - 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. - 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 leur .gitconfig
leur 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 (2.22+). 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 :
# Put this in your ~/.gitconfig or ~/.config/git/config
# Windows users: "~" is your profile's home directory, e.g. C:\Users\<YourName>
[user]
name = Your Full Name
email = your@email.tld
[color]
# Enable colors in color-supporting terminals
ui = auto
[alias]
# List available aliases
aliases = !git config --get-regexp alias | sed -re 's/alias\\.(\\S*)\\s(.*)$/\\1 = \\2/g'
# Command shortcuts
ci = commit
co = checkout
st = status
# Display tree-like log, because default log is a pain…
lg = log --graph --date=relative --pretty=tformat:'%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%an %ad)%Creset'
# Useful when you have to update your last commit
# with staged files without editing the commit message.
oops = commit --amend --no-edit
# Ensure that force-pushing won't lose someone else's work (only mine).
push-with-lease = push --force-with-lease
# Rebase won’t trigger hooks on each "replayed" commit.
# This is an ugly hack that will replay each commit during rebase with the
# standard `commit` command which will trigger hooks.
rebase-with-hooks = rebase -x 'git reset --soft HEAD~1 && git commit -C HEAD@{1}'
# List local commits that were not pushed to remote repository
review-local = "!git lg @{push}.."
# Edit last commit message
reword = commit --amend
# Undo last commit but keep changed files in stage
uncommit = reset --soft HEAD~1
# Remove file(s) from Git but not from disk
untrack = rm --cache --
[core]
# Don't paginate output by default
pager = cat
# Global ignore file (not shared)
excludesfile = ~/.gitignore_global
# Global shared hooks directory
# hooksPath = ~/.githooks
#
# Out of luck: on Windows w/o msysGit? You may have Notepad++…
# editor = 'C:/Program Files (x86)/Notepad++/notepad++.exe' -multiInst -notabbar -nosession -noPlugin
#
# If you want to use Sublime Text 2's subl wrapper:
# editor = subl -w
#
# Sublime Text 3 on Windows:
# editor = 'c:/Program Files/Sublime Text 3/subl.exe' -w
#
# VSCode
editor = code --wait
# Don't consider trailing space change as a cause for merge conflicts
whitespace = -trailing-space
[diff]
# Use better, descriptive initials (c, i, w) instead of a/b.
mnemonicPrefix = true
# Show renames/moves as such
renames = true
# When using --word-diff, assume --word-diff-regex=.
wordRegex = .
# Display submodule-related information (commit listings)
submodule = log
# Use VSCode as default diff tool when running `git diff-tool`
tool = vscode
[difftool "vscode"]
cmd = code --wait --diff $LOCAL $REMOTE
[fetch]
# Auto-fetch submodule changes (sadly, won't auto-update)
recurseSubmodules = on-demand
[grep]
break = true
heading = true
lineNumber = true
# Consider most regexes to be ERE
extendedRegexp = true
[log]
# Use abbrev SHAs whenever possible/relevant instead of full 40 chars
abbrevCommit = true
# Automatically --follow when given a single path
follow = true
# Disable decorate for reflog
# (because there is no dedicated `reflog` section available)
decorate = false
[merge]
# Display common-ancestor blocks in conflict hunks
# conflictStyle = diff3
# Disable fast-forward merges as default merge strategy.
# Force explicit call of `git merge --ff …`.
ff = false
# Custom/dedicated merge drive for npm lock files (`package-lock.json`).
# Also works with Yarn.
# Use Git attributes to set targeted files (located at `~/.config/git/attributes``
# when installed with npm).
# See https://www.npmjs.com/package/npm-merge-driver
# [merge "npm-merge-driver"]
# name = automatically merge npm lockfiles
# driver = npx npm-merge-driver merge %A %O %B %P
[mergetool]
# Clean up backup files created by merge tools on tool exit
keepBackup = false
# Clean up temp files created by merge tools on tool exit
keepTemporaries = false
# Put the temp files in a dedicated dir anyway
writeToTemp = true
# Auto-accept file prompts when launching merge tools
prompt = false
# [page]
# Use custom pager to get an better log on terminal.
# As this is an external tool it has to be installed.
# See https://github.com/so-fancy/diff-so-fancy
# show = diff-so-fancy | less --tabs=4 -RFX
[pull]
# This is GREAT… when you know what you're doing and are careful
# not to pull --no-rebase over a local line containing a true merge.
# rebase = true
# This option, which does away with the one gotcha of
# auto-rebasing on pulls, is only available from 1.8.5 onwards.
# rebase = preserve
# WARNING! This option, which is the latest variation, is only
# available from 2.18 onwards.
rebase = merges
[push]
# Default push should only push the current branch to its push target, regardless of its remote name
default = upstream
# When pushing, also push tags whose commit-ishs are now reachable upstream
followTags = true
# [rebase]
# Rebase advanced usage.
# Automagically reorder and prefix your commands while doing an interactive
# rebase. This has to be used with `--fixup` and/or `--squash` options for
# `git commit`.
# autoSquash = true
# Automatically stash current WD and stage when running rebase
# then you won't have to manually `git stash push` and `git stash pop`
# after and before your rebase.
# autoStash = true
# [rerere]
# If, like me, you like rerere, uncomment these
# autoupdate = true
# enabled = true
[status]
# Display submodule rev change summaries in status
submoduleSummary = true
# Recursively traverse untracked directories to display all contents
showUntrackedFiles = all
[color "branch"]
# Blue on black is hard to read in git branch -vv: use cyan instead
upstream = cyan
[tag]
# Sort tags as version numbers whenever applicable, so 1.10.2 is AFTER 1.2.0.
sort = version:refname
[versionsort]
prereleaseSuffix = -pre
prereleaseSuffix = .pre
prereleaseSuffix = -beta
prereleaseSuffix = .beta
prereleaseSuffix = -rc
prereleaseSuffix = .rc
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 commitst
pour statuslg
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 :
- Variable d’environnement
GIT_EDITOR
- Réglage
core.editor
(c’est ce que je préfère) - Variable d’environnement
VISUAL
- Variable d’environnement
EDITOR
- 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 quelques lignes de commandes !
☝️ Si vous êtes sous Windows : si votre terminal ne reconnaît pas la commande d’ouverture de l’éditeur, vous devrez envisager de passer le chemin complet vers l’exécutable (ça dépend généralement du terminal utilisé).
- VSCode :
code -w
- SublimeText :
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
. - 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
.
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
:
[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 :
[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) :
[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 :
[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
Pour celles et 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’unfetch
(et donc d’unpull
) 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 lesubmodule update
pour autant.status.submoduleSummary
prend soin de signaler dansgit 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 SHA 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 :
<body>
<<<<<<< HEAD
<h1>Super site de gros bills</h1>
=======
<h1>Super site de furieux</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, ou merge base). C’est le mode diff3, que j’active automatiquement avec le réglage merge.conflictStyle
:
<body>
<<<<<<< HEAD
<h1>Super site de gros bills</h1>
||||||| merged common ancestors
<h1>Super site de déglingos</h1>
=======
<h1>Super site de furieux</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 nous avons également un article détaillé, 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’ai écrit 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 :
git fetch
git merge origin/master
Quand vous bossez à plusieurs sur la branche, il est le plus souvent préférable de faire l’équivalent de :
git fetch
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 ça ne l’était longtemps 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.
Heureusement, Git propose désormais une option étendue git pull --rebase=merges
qu’on peut renseigner une fois pour toutes en configuration (pull.rebase = merges
) et qui permettra de préserver les fusions locales lors du rebase (elles seront rejouées intégralement).
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 :
- Je dois collaborer quelques temps à la branche
feature
: jegit switch feature
, ce qui cale une branche locale trackée sur la distante. - Je bosse, je committe, je pushe, tout roule…
- Au bout d’un moment, soit ça fait un bail que je ne bosse que sur
feature
et monmaster
prend du retard sur le distant, soit j’ai fini avecfeature
jusqu’à nouvel ordre, je repasse surmaster
et au bout d’un moment mafeature
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 était (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 !
Pour aller plus loin…
Cet article est toujours d’actualité après plus de 10 ans d’existence. Quelques nouveautés ont malgré tout fait leur apparition. On t’explique tout ça dans cet autre article.