Git - Quelques configurations

dim. 08 juil. 2018 by Marmotte

Git est un système de gestion de versions décentralisé. Le côté décentralisé implique que chaque client télécharge une copie complète du dépôt, la plupart des opérations étant donc réalisées directement sur la machine de l'utilisateur.

Cet article fait partie d'une série expliquant quelques fonctionnalités avancées que j'utilise régulièrement.

  1. Généralités et notations
  2. Quelques configurations
  3. Notes diverses
  4. Dépôts et répertoires de travail
  5. L'historique local
  6. Retravailler l'historique des commits
  7. Trouver le commit introduisant une régression

Les possibilités de configuration de git sont très vastes. Je n'explique ici qu'une partie des paramètres que j'utilise, sans chercher à être exhaustif, le but n'étant pas de recopier la page de man git-config.

Divers

Voici quelques paramètres que j'utilise, pour lesquels je n'ai pas jugé utile d'écrire un paragraphe spécifique.

Par défaut, lorsqu'un chemin contient des caractères au delà du 0x80, git affiche le chemin entier entre quotes, et remplace ces caractères spéciaux par leur forme échapée (\303\251 pour un é, par exemple). Le paramètre code.quotepath permet de désactiver ce comportement pour afficher ces caractères directement :

$ git config --global core.quotePath false

Afficher le numéro de la ligne de chaque correspondance lors de l'utilisation de la commande git grep :

$ git config --global grep.lineNumber true

Toujours afficher le patch lors de l'utilisation de la commande git stash show :

$ git config --global stash.showPatch true

Signer automatiquement tous les commits en utilisant le programme gpg2 :

$ git config --global commit.gpgSign true
$ git config --global gpg.program gpg2

Passer des paramètres à un alias

Pour définir un alias prenant des paramètres, il est nécessaire de le déclarer sous forme de fonction. La fonction est alors appelée lorsqu'on utilise cet alias, en lui passant directement les paramètres donnés. En effet, sans utiliser de fonction, les paramètres seraient remplacés dans la commande par git, mais ils seront en plus passés en paramètre lors de l'appel de la commande complétée, ce qui fournit rarement le résultat attendu.

alias_name = "!f() { commande $1 $2; }; f"

Attention : Il faut bien respecter le format : doubles guillemets autour de la commande, points virgules et espaces autour des accolades. En effet, modifier un seul de ces éléments provoquera des erreurs de syntaxe.

Si l'appel ne fournit pas le résultat attendu, utiliser la variable d'environnement GIT_TRACE=1 permettra d'afficher des détails de toutes les commandes exécutées lors de l'appel d'un alias.

Ignorer les fichiers inutiles

Lorsqu'on travaille sur un projet, il est quasi-inévitable que des fichiers soient générés automatiquement, ou d'avoir à créer des fichiers qui ne doivent pas être enregistrés dans le dépôt. Pour éviter d'avoir à se rappeler de quels fichiers sont utiles, et pour simplifier l'utilisation des commandes globales (comme git add .), il est possible de demander à git d'ignorer automatiquement certains chemins.

Pour cela, git prend les chemins à ignorer dans deux fichiers :

  • ~/.config/git/ignore : Configuration de l'utilisateur
  • .gitignore : Configuration du répertoire dans lequel on se trouve

Une pratique courante est donc de mettre un fichier .gitignore à la racine du dépôt de projet, dans lequel les fichiers à ignorer spécifiques au projet sont placés. Il est aussi possible de mettre des fichiers .gitignore dans des sous-répertoires, pour ignorer des chemins spécifiques à ces sous-répertoires, mais j'ai peu rencontré ce cas.

Par contre, je vois souvent les fichiers .gitignore des projets contenir des motifs correspondant à tous les outils que les développeurs de ce projet pourraient utiliser. C'est une mauvaise pratique : Le fichier .gitignore du projet ne doit contenir que les éléments spécifiques au projet, afin de conserver des .gitignore lisibles. On peut alors savoir exactement ce que le projet va générer comme fichiers, mais surtout, il est impossible de prévoir tous les cas. Lorsqu'un nouvel outil apparaît, ou que la modification de configuration des outils d'un développeur créent de nouveaux fichiers, il n'est alors pas nécessaire mettre à jour le fichier .gitignore de tous les projets de la terre. Il suffit au développeur d'ajouter ces chemins dans son propre ~/.config/git/ignore. Cela fait en effet partie de la configuration de son environnement de développement, pas des projets sur lesquels il travaille.

Le format du contenu de ce fichier est assez simple :

  • Un motif par ligne.
  • * : N'importe quelle suite de caractères autres que /.
  • ** : N'importe quelle suite de caractères, incluant /.
  • Préfixe ! : Inverse la condition.
  • Préfixe / : Restreint le motif aux correspondances depuis l'emplacement du fichier .gitignore.
  • Préfixe # : Commentaire.

Ajout d'informations dans le prompt

Le fichier /usr/lib/git-core/git-sh-prompt (sous Debian/Ubuntu, son chemin peut être différent sur d'autres distributions) définit une fonction __git_ps1, destinée à être appelée dans le prompt de bash ou zsh. Pour l'utiliser, il faut sourcer ce fichier, ce qui est fait automatiquement lors de l'utilisation du paquet bash-completion.

Le comportement de cette fonction est différent selon le nombre de paramètres :

  • Zéro ou un paramètre : Le paramètre optionnel est utilisé comme mise en forme de la sortie de __git_ps1.
  • Deux ou trois paramètres :
    • Le premier paramètre définit le début du prompt.
    • Le second paramètre définit la fin du prompt.
    • Le troisième paramètre (optionnel) est utilisé comme mise en forme de la sortie de __git_ps1.

Le paramètre correspondant à la mise en forme de la sortie de la fonctione __git_ps1, s'il est défini, doit contenir au moins %s, qui sera remplacé par le texte généré. S'il est omis, sa valeur est définie à (%s).

Cette fonction est prévue pour être ajoutée dans la variable PROMPT_COMMAND pour bash, ou dans la fonction precmd de zsh. Exemple avec bash :

# Informations basiques ajoutées devant le prompt
PROMPT_COMMAND="__git_ps1 '{%s} '"
# Redéfinition complète du prompt, qui permet de bénéficier de fonctionnalités avancées, comme la mise en couleurs
# Dans mon cas, je conserve le prompt déjà défini dans $PS1, en ajoutant juste les informations de __git_ps1 devant
PROMPT_COMMAND="__git_ps1 '' '$PS1' '{%s} '"

Par défaut, seul le nom de la branche courante est affiché. Cependant, il est possible d'afficher d'autres informations au moyen de quelques variables d'environnement :

  • GIT_PS1_SHOWDIRTYSTATE=1 : Affiche l'état du répertoire.
    • * : Des modifications existent entre le répertoire de travail et l'index.
    • + : Des modifications existent entre l'index et le HEAD.
  • GIT_PS1_SHOWSTASHSTATE=1 : Affiche un $ si au moins un stash existe.
  • GIT_PS1_SHOWUNTRACKEDFILES=1 : Affiche un % si des fichiers non versionnés et non ignorés sont présents.
  • GIT_PS1_SHOWUPSTREAM=1 : Affiche des informations par rapport à la branche distante liée.
    • = : La branche courante et la branche distante sont au même commit.
    • < : La branche courante est en retard sur la branche distante.
    • > : La branche courante est en avance sur la branche distante.
    • <> : La branche courante et la branche distante ont divergé.

      Note : Il est aussi possible de mettre une liste de paramètres séparés par des espaces pour afficher plus d'informations, mais je ne trouve pas cet affichage assez lisible.

  • GIT_PS1_DESCRIBE_STYLE="<style>" : Modifie la façon d'afficher le nom de la branche, utile en mode detached HEAD. J'utilise la valeur branch, qui affiche le nombre de commits de différence par rapport à la branche la plus récente.
  • GIT_PS1_SHOWCOLORHINTS=1 : Permet d'afficher les informations en couleurs. Ce paramètre ne fonctionne qu'en mode avancé.

Distribuer des hooks dans le dépôt

Il arrive parfois qu'on ait besoin de certains hooks pour effectuer des opérations de manière automatique. Par exemple, pour effectuer une vérification automatique avant de commiter, ou pour reconstruire un fichier après avoir mis le dépôt à jour.

Pour permettre le partage de hooks simplement dans le dépôt, il est possible d'utiliser le paramètre core.hooksPath.

$ # Définir le répertoire .hooks présent à la racine du dépôt comme contenant les hooks à exécuter pour ce projet
$ git config corehooksPath .hooks

Gestion des diffs et conflits

Par défaut, git affiche les diffs dans le format unifié. Cependant, les différences ne sont pas toujours simples à observer dans ce format. L'outil vimdiff permet d'afficher les diffs en mode côte-à-côte, en mettant en évidence les différences en couleur.

Il est possible de l'utiliser pour afficher les diffs depuis git en définissant une valeur au paramètre diff.tool. Il suffit ensuite de lancer la commande git difftool pout l'utiliser.

$ git config --global diff.tool vimdiff

De la même manière, il est possible d'utiliser vimdiff pour éditer les fichiers en cas de conflit lors d'un merge, en définissant le paramètre merge.tool. La commande git mergetool ouvrira alors l'utilitaire vimdiff avec quatre buffers. En haut seront présents l'état du fichier dans le commit courant (à gauche), lors du dernier point commun entre les deux commits (au milieu), et dans le commit distant (à droite). En bas, l'état du fichier dans le répertoire de travail, soit celui qui sera conservé lors du commit.

$ git config --global merge.tool vimdiff

Pour définir si git doit demander avant d'ouvrir l'outil externe pour chaque fichier, il est possible de définir les paramètres difftool.prompt et mergetool.prompt à une valeur booléenne. Personnellement, je préfère que git me demande lors de l'utilisation de difftool, pour éviter d'être bloqué dans une longue série d'ouvertures automatiques de vimdiff. Lorsque git pose la question, il est possible de répondre y pour ouvrir l'outil externe, n pour passer au fichier suivant, ou de le stopper avec un simple Ctrl+c, ce qui est utile en cas de lancement involontaire sur un grand nombre de fichiers. Au contraire, pour mergetool, je préfère que git ouvre automatiquement vimdiff, puisqu'il ne le fait que pour les fichiers en conflit, qui ont donc forcément besoin d'être ouverts pour corriger les conflits.

$ git config --global difftool.prompt true
$ git config --global mergetool.prompt false

Résolution automatique des conflits déjà rencontrés

Résoudre un conflit est toujours une source potentielle d'erreurs, et peut parfois être complexe. Avoir plusieurs fois le même conflit à résoudre peut se produire lorsqu'on refait un merge, ou lors d'un rebase, par exemple. Pour réduire les risques et le temps de résolution des conflits, git permet de conserver la résolution appliquée, pour la rejouer automatiquement si le même conflit apparaît ultérieurement.

Cette fonctionnalité est nommée rerere, pour Reuse recorded resolution :

$ git config --global rerere.enabled true

Cependant, si l'enregistrement des résolutions de conflits est très intéressant, il peut être dérangeant lorsque l'on se trompe pendant la résolution du conflit. En effet, annuler l'opération en cours puis la relancer rejouera automatiquement la résolution erronée. Dans ce cas, il faut demander à git d'oublier la résolution erronée, avec la commande git rerere forget -- <path>. L'erreur ayant déjà été rejouée, il est aussi nécessaire de remettre les conflits dans le fichier, afin de pouvoir les résoudre de nouveau, avec la commande git checkout --merge -- <path>.

Gestion des dépôts distants

Git permet de configurer plusieurs dépôts distants dans un unique dépôt local. Lors du clonage, le dépôt distant utilisé est nommé origin par défaut. Lorsque ce nom ne convient pas, il est possible d'en spécifier un autre avec le paramètre --origin <name>.

Utiliser plusieurs dépôts distants dans un même dépôt git local peut permettre, par exemple :

  • Utiliser un dépôt distant comme source en lecture seule, et un autre comme destination des push. C'est souvent le cas lorsque l'on veut contribuer sur une plateforme comme GitHub ou GitLab.
  • Pousser simultanément sur plusieurs dépôts. Cela peut être le cas si on travaille en utilisant un dépôt privé, tout en poussant les modifications sur un autre dépôt, accessible publiquement en lecture seule.

Ajouter un second dépôt distant

Dans le cas d'une contribution sur GitHub, je clone généralement le dépôt de référence, alors nommé origin, et j'ajoute ensuite mon fork comme second dépôt distant :

$ git clone https://github.com/<some-username>/<reference-repo-name>
$ git remote add -f <username> git@github.com:<username>/<repo-name>

Si au contraire, on veut ajouter une seconde adresse de push pour le dépôt distant, il faut utiliser la commande suivante :

$ git remote set-url --add --push <remote-name> <other-hostname>:<remote-path>

Pousser une branche sur un autre dépôt

Pour créer une branche en partant de la branche master du dépôt de référence, et la pousser sur un autre dépôt distant, il suffit de le nommer lors de l'utilisation de la commande push :

$ git checkout -b <branch-name> origin/master
$ git push -u <remote-name> <branch-name>

Remplacer un dépôt

Contrairement à ce que j'ai déjà vu faire plusieurs fois, il n'est pas nécessaire de supprimer le dépôt local, puis de le re-cloner, lorsqu'on veut changer l'adresse d'un dépôt distant. La commande git remote set-url permet de modifier l'adresse utilisée pour un dépôt :

$ git remote set-url <remote-name> <other-hostname>:<remote-path>

Cela évite de devoir retélécharger le dépôt, mais aussi de perdre le reflog du dépôt local, les branches non poussées, les stashs, les configurations spécifiques, etc.

Utiliser des références autres que les branches

Par défaut, git ne récupère que les branches (refs/heads/*) des dépôts distants. Il est cependant possible de récupérer d'autres références, en ajoutant des valeurs au paramètre remote.remote-name.fetch.

$ # Récupérer les informations des remotes du dépôt distant
$ git config --add remote.remote-name.fetch '+refs/remotes/*:refs/remotes/remote-name/remotes/*'
$ # Récupérer les branches des pull requests d'un dépôt GitHub
$ git config --add remote.remote-name.fetch '+refs/pull/*:refs/remotes/remote-name/pr/*'
$ # Récupérer les branches des merge requests d'un dépôt GitLab
$ git config --add remote.remote-name.fetch '+refs/merge-requests/*:refs/remotes/remote-name/mr/*'
$ # Réinitialiser la liste des références à récupérer
$ git config --replace-all remote.remote-name.fetch '+refs/heads/*:refs/remotes/remote-name/*'

Note : Le paramètre remote.remote-name.fetch utilise le format d'association des références décrit dans l'article précédent. Il est donc tout à fait possible de ne pas mettre le caractère + pour éviter que les références ne soient écrasées automatiquement lors de l'utilisation de la commande git fetch.

Affichage des diffs

Il est parfois appréciable de pouvoir compléter ou modifier l'affichage des différences entre deux états. J'en montre ici quelques exemples que j'utilise.

Détection de copie et renommage de fichiers

Git est capable de détecter et d'afficher lorsqu'un fichier a été renommé, ou dupliqué, en se basant sur la similarité entre les différents fichiers du dépôt. Par défaut, ce comportement est désactivé pour des raisons de performances, mais il est possible de l'activer pour une commande précise, au moyen de différents paramètres :

  • -MXX : Détecte les renommages, basé sur un seuil de similarité de XX%.
  • -CXX : Détecte les copies, basé sur un seuil de similarité de XX%. Pour détecter les copies de fichiers modifiés dans des commits différents, il est nécessaire d'ajouter le paramètre --find-copies-harder.

Activer la détection des copies et renommages de fichiers peut aussi se faire dans la configuration. Le seuil de détection sera alors de 50% (non configurable).

$ # Activation de la détection des renommages et des copies dans un même commit
$ git config --global diff.renames true
$ # Activation de la détection des copies, mêmes pour les fichiers modifiés dans différents commits
$ git config --global diff.renames copies

Ce comportement fonctionne avec différentes commandes comme diff, show, log ou encore status.

Nom de la fonction modifiée

Lors de l'affichage des différences entre deux commits, git ajoute le nom de la fonction contenant la modification dans l'en-tête de chaque partie du patch.

L'expression régulière par défaut cherche les lignes commençant par un caractère alphabétique, un underscore, ou le caractère dollar. Comme ce format ne correspond pas à tous les langages, et il est possible de définir des expressions régulières spécifiques.

Pour définir une expression régulière personnalisée, il faut tout d'abord indiquer à git quel driver de diff utiliser, dans les attributs. Cela peut se faire dans un fichier nommé .gitattributes, dans un dépôt git, ou dans le fichier ~/.config/git/attributes, pour une configuration globale sur la machine.

*.py    diff=python

L'expression régulière est ensuite définie dans le paramètre diff.<driver-name>.xfuncname. Il est donc possible d'avoir un paramétrage global, et de le redéfinir pour un dépôt spécifique.

Cependant, pour l'exemple donné, il n'est pas nécessaire de définir d'expression régulière, puisque git en propose une par défaut pour le langage python. La liste des langages supportés par défaut peut être trouvée au paragraphe Defining a custom hunk-header de la page de manuel gitattributes.

Formats binaires

Lorsque le diff implique un fichier binaire, git annonce juste qu'il a été modifié. Cependant, certains fichiers binaires sont convertibles en texte, ce qui permet d'afficher un diff plus utile.

Pour cela, il faut définir le driver de diff à utiliser, et paramétrer ensuite ce driver spécifique. Par exemple, pour afficher le diff réel entre des fichiers chiffrés par GPG, on peut ajouter cette ligne dans le fichier ~/.config/git/attributes :

*.gpg   diff=gpg

Il faut ensuite définir la commande à exécuter pour convertir les fichiers de ce format en texte. Dans le cas de fichiers GPG, la commande est :

$ git config --global diff.gpg.textconv 'gpg2 --no-tty --decrypt'