Personnalisation de Git

Jusqu’ici, nous avons traité les bases du fonctionnement et de l’utilisation de Git et introduit un certain nombre d’outils fournis par Git pour travailler plus facilement et plus efficacement. Dans ce chapitre, nous aborderons quelques opérations permettant d’utiliser Git de manière plus personnalisée en vous présentant quelques paramètres de configuration importants et le système d’interceptions. Grâce à ces outils, il devient enfantin de faire fonctionner Git exactement comme vous, votre société ou votre communauté en avez besoin.

Configuration de Git

Comme vous avez pu l’entrevoir dans Démarrage rapide, vous pouvez spécifier les paramètres de configuration de Git avec la commande git config. Une des premières choses que vous avez faites a été de paramétrer votre nom et votre adresse de courriel :

$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com

À présent, vous allez apprendre quelques-unes des options similaires les plus intéressantes pour paramétrer votre usage de Git.

Vous avez vu des détails de configuration simple de Git au premier chapitre, mais nous allons les réviser. Git utilise une série de fichiers de configuration pour déterminer son comportement selon votre personnalisation. Le premier endroit que Git visite est le fichier /etc/gitconfig qui contient des valeurs pour tous les utilisateurs du système et tous leurs dépôts. Si vous passez l’option --system à git config, il lit et écrit ce fichier.

L’endroit suivant visité par Git est le fichier ~/.gitconfig qui est spécifique à chaque utilisateur. Vous pouvez faire lire et écrire Git dans ce fichier au moyen de l’option --global.

Enfin, Git recherche des valeurs de configuration dans le fichier de configuration du répertoire Git (.git/config) du dépôt en cours d’utilisation. Ces valeurs sont spécifiques à un unique dépôt.

Chaque niveau surcharge le niveau précédent, ce qui signifie que les valeurs dans .git/config écrasent celles dans /etc/gitconfig.

Ces fichiers de configuration Git sont des fichiers texte, donc vous pouvez positionner ces valeurs manuellement en éditant le fichier et en utilisant la syntaxe correcte, mais il reste généralement plus facile de lancer la commande git config.

Configuration de base d’un client

Les options de configuration reconnues par Git tombent dans deux catégories : côté client et côté serveur. La grande majorité se situe côté client pour coller à vos préférences personnelles de travail. Parmi les tonnes d’options disponibles, seules les plus communes ou affectant significativement la manière de travailler seront couvertes. De nombreuses options ne s’avèrent utiles qu’en de rares cas et ne seront pas traitées. Pour voir la liste de toutes les options que votre version de Git reconnaît, vous pouvez lancer :

$ man git-config

Cette commande affiche toutes les options disponibles avec quelques détails. Vous pouvez aussi trouver des informations de référence sur http://git-scm.com/docs/git-config.html.

core.editor

Par défaut, Git utilise votre éditeur par défaut ($VISUAL ou $EDITOR) ou se replie sur l’éditeur Vi pour la création et l’édition des messages de validation et d’étiquetage. Pour modifier ce programme par défaut pour un autre, vous pouvez utiliser le paramètre core.editor :

$ git config --global core.editor emacs

Maintenant, quel que soit votre éditeur par défaut, Git démarrera Emacs pour éditer les messages.

commit.template

Si vous réglez ceci sur le chemin d’un fichier sur votre système, Git utilisera ce fichier comme message par défaut quand vous validez. Par exemple, supposons que vous créiez un fichier modèle dans $HOME/.gitmessage.txt qui ressemble à ceci :

ligne de sujet

description

[ticket: X]

Pour indiquer à Git de l’utiliser pour le message par défaut qui apparaîtra dans votre éditeur quand vous lancerez git commit, réglez le paramètre de configuration commit.template :

$ git config --global commit.template ~/.gitmessage.txt
$ git commit

Ainsi, votre éditeur ouvrira quelque chose ressemblant à ceci comme modèle de message de validation :

ligne de sujet

description

[ticket: X]
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
# modified:   lib/test.rb
#
~
~
".git/COMMIT_EDITMSG" 14L, 297C

Si vous avez une règle de messages de validation, placez un modèle de cette règle sur votre système et configurez Git pour qu’il l’utilise par défaut, cela améliorera les chances que cette règle soit effectivement suivie.

core.pager

Le paramètre core.pager détermine quel pager est utilisé lorsque des pages de Git sont émises, par exemple lors d’un log ou d’un diff. Vous pouvez le fixer à more ou à votre pager favori (par défaut, il vaut less) ou vous pouvez le désactiver en fixant sa valeur à une chaîne vide :

$ git config --global core.pager ''

Si vous lancez cela, Git affichera la totalité du résultat de toutes les commandes d’une traite, quelle que soit sa longueur.

user.signingkey

Si vous faites des étiquettes annotées signées (comme décrit dans Signer votre travail), simplifiez-vous la vie en définissant votre clé GPG de signature en paramètre de configuration. Définissez votre ID de clé ainsi :

$ git config --global user.signingkey <gpg-key-id>

Maintenant, vous pouvez signer vos étiquettes sans devoir spécifier votre clé à chaque fois que vous utilisez la commande git tag :

$ git tag -s <nom-étiquette>

core.excludesfile

Comme décrit dans Ignorer des fichiers, vous pouvez ajouter des patrons dans le fichier .gitignore de votre projet pour indiquer à Git de ne pas considérer certains fichiers comme non suivis ou pour éviter de les indexer lorsque vous lancez git add sur eux.

Mais vous pouvez souhaiter dans quelques cas ignorer certains fichiers dans tous vos dépôts. Si votre ordinateur utilise Mac OS X, vous connaissez certainement les fichiers .DS_Store. Si votre éditeur préféré est Emacs ou Vim, vous connaissez sûrement aussi les fichiers qui se terminent par ~ ou .swp.

Cette option vous permet d’écrire un fichier .gitignore global. Si vous créez un fichier ~/.gitignore_global contenant ceci :

*~
.*.swp
.DS_Store

et que vous lancez git config --global core.excludesfile ~/.gitignore_global, Git ne vous importunera plus avec ces fichiers.

help.autocorrect

Si vous avez fait une faute de frappe en tapant une commande Git, il vous affiche quelque chose comme :

$ git chekcout master
git : 'chekcout' n'est pas une commande git. Voir 'git --help'.

Vouliez-vous dire cela ?
        checkout

Git essaie de deviner ce que vous avez voulu dire, mais continue de refuser de le faire. Si vous positionnez le paramètre help.autocorrect à 1, Git va réellement lancer cette commande à votre place :

$ git chekcout master
ATTENTION : vous avez invoqué une commande Git nommée 'chekcout' qui n'existe pas.
Continuons en supposant que vous avez voulu dire 'checkout'
dans 0.1 secondes automatiquement...

Notez l’histoire des « 0.1 secondes ». help.autocorrect est un fait un entier qui représente des dixièmes de seconde. Ainsi, si vous le réglez à 50, Git vous laissera 5 secondes pour changer d’avis avant de lancer la commande qu’il aura devinée.

Couleurs dans Git

Git sait coloriser ses affichages dans votre terminal, ce qui peut faciliter le parcours visuel des résultats. Un certain nombre d’options peuvent vous aider à régler la colorisation à votre goût.

color.ui

Git colorise automatiquement la plupart de ses affichages mais il existe une option globale pour désactiver ce comportement. Pour désactiver toute la colorisation par défaut, lancez ceci :

$ git config --global color.ui false

La valeur par défaut est auto, ce qui colorise la sortie lorsque celle-ci est destinée à un terminal, mais élimine les codes de contrôle de couleur quand la sortie est redirigée dans un fichier ou l’entrée d’une autre commande.

Vous pouvez aussi la régler à always (toujours) pour activer la colorisation en permanence. C’est une option rarement utile. Dans la plupart des cas, si vous tenez vraiment à coloriser vos sorties redirigées, vous pourrez passer le drapeau --color à la commande Git pour la forcer à utiliser les codes de couleur. Le réglage par défaut est donc le plus utilisé.

color.*

Si vous souhaitez être plus spécifique concernant les commandes colorisées, Git propose des paramètres de colorisation par action. Chacun peut être fixé à true, false ou always.

color.branch
color.diff
color.interactive
color.status

De plus, chacun d’entre eux dispose d’un sous-ensemble de paramètres qui permettent de surcharger les couleurs pour des parties des affichages. Par exemple, pour régler les couleurs de méta-informations du diff avec une écriture en bleu gras (bold en anglais) sur fond noir :

$ git config --global color.diff.meta "blue black bold"

La couleur peut prendre les valeurs suivantes : normal, black, red, green, yellow, blue, magenta, cyan ou white. Si vous souhaitez ajouter un attribut de casse, les valeurs disponibles sont bold (gras), dim (léger), ul (underlined, souligné), blink (clignotant) et reverse (inversé).

Outils externes de fusion et de différence

Bien que Git ait une implémentation interne de diff que vous avez déjà utilisée, vous pouvez sélectionner à la place un outil externe. Vous pouvez aussi sélectionner un outil graphique pour la fusion et la résolution de conflit au lieu de devoir résoudre les conflits manuellement. Je démontrerai le paramétrage avec Perforce Merge Tool (P4Merge) pour visualiser vos différences et résoudre vos fusions parce que c’est un outil graphique agréable et gratuit.

Si vous voulez l’essayer, P4Merge fonctionne sur tous les principaux systèmes d’exploitation. Dans cet exemple, je vais utiliser la forme des chemins usitée sur Mac et Linux. Pour Windows, vous devrez changer /usr/local/bin en un chemin d’exécution d’un programme de votre environnement.

Pour commencer, téléchargez P4Merge depuis http://www.perforce.com/downloads/Perforce/. Ensuite, il faudra mettre en place un script d’enrobage pour lancer les commandes. Je vais utiliser le chemin Mac pour l’exécutable ; dans d’autres systèmes, il résidera où votre binaire p4merge a été installé. Créez un script enveloppe nommé extMerge qui appelle votre binaire avec tous les arguments fournis :

$ cat /usr/local/bin/extMerge
#!/bin/sh
/Applications/p4merge.app/Contents/MacOS/p4merge $*

L’enveloppe diff s’assure que sept arguments ont été fournis et en passe deux à votre script de fusion. Par défaut, Git passe au programme de diff les arguments suivants :

chemin ancien-fichier ancien-hex ancien-mode nouveau-fichier nouveau-hex nouveau-mode

Comme seuls les arguments ancien-fichier et nouveau-fichier sont nécessaires, vous utilisez le script d’enveloppe pour passer ceux dont vous avez besoin.

$ cat /usr/local/bin/extDiff
#!/bin/sh
[ $# -eq 7 ] && /usr/local/bin/extMerge "$2" "$5"

Vous devez aussi vous assurer que ces fichiers sont exécutables :

$ sudo chmod +x /usr/local/bin/extMerge
$ sudo chmod +x /usr/local/bin/extDiff

À présent, vous pouvez régler votre fichier de configuration pour utiliser vos outils personnalisés de résolution de fusion et de différence. Pour cela, il faut un certain nombre de personnalisations : merge.tool pour indiquer à Git quelle stratégie utiliser, mergetool.<tool>.cmd pour spécifier comment lancer cette commande, mergetool.<tool>.trustExitCode pour indiquer à Git si le code de sortie du programme indique une résolution de fusion réussie ou non et diff.external pour indiquer à Git quelle commande lancer pour les différences. Ainsi, vous pouvez lancer les quatre commandes :

$ git config --global merge.tool extMerge
$ git config --global mergetool.extMerge.cmd \
  'extMerge "$BASE" "$LOCAL" "$REMOTE" "$MERGED"'
$ git config --global mergetool.trustExitCode false
$ git config --global diff.external extDiff

ou vous pouvez éditer votre fichier ~/.gitconfig pour y ajouter ces lignes :

[merge]
  tool = extMerge
[mergetool "extMerge"]
  cmd = extMerge "$BASE" "$LOCAL" "$REMOTE" "$MERGED"
  trustExitCode = false
[diff]
  external = extDiff

Après avoir réglé tout ceci, si vous lancez des commandes de diff telles que celle-ci :

$ git diff 32d1776b1^ 32d1776b1

Au lieu d’obtenir la sortie du diff dans le terminal, Git lance P4Merge, ce qui ressemble à ceci :

P4Merge.
Figure 170 : P4Merge.

Figure 143. P4Merge.

Si vous essayez de fusionner deux branches et créez des conflits de fusion, vous pouvez lancer la commande git mergetool qui démarrera P4Merge pour vous laisser résoudre les conflits au moyen d’un outil graphique.

Le point agréable avec cette méthode d’enveloppe est que vous pouvez changer facilement d’outils de diff et de fusion. Par exemple, pour changer vos outils extDiff et extMerge pour une utilisation de l’outil KDiff3, il vous suffit d’éditer le fichier extMerge :

$ cat /usr/local/bin/extMerge
#!/bin/sh
/Applications/kdiff3.app/Contents/MacOS/kdiff3 $*

À présent, Git va utiliser l’outil KDiff3 pour visualiser les différences et résoudre les conflits de fusion.

Git est livré préréglé avec un certain nombre d’autres outils de résolution de fusion pour vous éviter d’avoir à gérer la configuration cmd. Pour obtenir une liste des outils qu’il supporte, essayez ceci :

$ git mergetool --tool-help
'git mergetool --tool=<tool>' may be set to one of the following:
        emerge
        gvimdiff
        gvimdiff2
        opendiff
        p4merge
        vimdiff
        vimdiff2

The following tools are valid, but not currently available:
        araxis
        bc3
        codecompare
        deltawalker
        diffmerge
        diffuse
        ecmerge
        kdiff3
        meld
        tkdiff
        tortoisemerge
        xxdiff

Some of the tools listed above only work in a windowed
environment. If run in a terminal-only session, they will fail.

Si KDiff3 ne vous intéresse pas pour gérer les différences mais seulement pour la résolution de fusion et qu’il est présent dans votre chemin d’exécution, vous pouvez lancer :

$ git config --global merge.tool kdiff3

Si vous lancez ceci au lieu de modifier les fichiers extMerge ou extDiff, Git utilisera KDiff3 pour les résolutions de fusion et l’outil diff normal de Git pour les différences.

Formatage et espaces blancs

Les problèmes de formatage et de blancs sont parmi les plus subtils et frustrants que les développeurs rencontrent lorsqu’ils collaborent, spécifiquement d’une plate-forme à l’autre. Il est très facile d’introduire des modifications subtiles de blancs lors de soumission de patchs ou d’autres modes de collaboration, car les éditeurs de texte les insèrent silencieusement ou les programmeurs Windows ajoutent des retours chariot à la fin des lignes qu’ils modifient. Git dispose de quelques options de configuration pour traiter ces problèmes.

core.autocrlf

Si vous programmez vous-même sous Windows ou si vous utilisez un autre système d’exploitation mais devez travailler avec des personnes travaillant sous Windows, vous rencontrerez à un moment ou à un autre des problèmes de caractères de fin de ligne. Ceci est dû au fait que Windows utilise pour marquer les fins de ligne dans ses fichiers un caractère « retour chariot » (carriage return, CR) suivi d’un caractère « saut de ligne » (line feed, LF), tandis que Mac et Linux utilisent seulement le caractère « saut de ligne ». C’est un cas subtil mais incroyablement ennuyeux de problème généré par la collaboration inter plate-forme.

Git peut gérer ce cas en convertissant automatiquement les fins de ligne CRLF en LF lorsque vous validez, et inversement lorsqu’il extrait des fichiers sur votre système. Vous pouvez activer cette fonctionnalité au moyen du paramètre core.autocrlf. Si vous avez une machine Windows, positionnez-le à true. Git convertira les fins de ligne de LF en CRLF lorsque vous extrairez votre code :

$ git config --global core.autocrlf true

Si vous utilisez un système Linux ou Mac qui utilise les fins de ligne LF, vous ne souhaitez sûrement pas que Git les convertisse automatiquement lorsque vous extrayez des fichiers. Cependant, si un fichier contenant des CRLF est accidentellement introduit en version, vous souhaitez que Git le corrige. Vous pouvez indiquer à Git de convertir CRLF en LF lors de la validation mais pas dans l’autre sens en fixant core.autocrlf à input :

$ git config --global core.autocrlf input

Ce réglage devrait donner des fins de ligne en CRLF lors d’extraction sous Windows mais en LF sous Mac et Linux et dans le dépôt.

Si vous êtes un programmeur Windows gérant un projet spécifique à Windows, vous pouvez désactiver cette fonctionnalité et forcer l’enregistrement des « retour chariot » dans le dépôt en réglant la valeur du paramètre à false :

$ git config --global core.autocrlf false

core.whitespace

Git est paramétré par défaut pour détecter et corriger certains problèmes de blancs. Il peut rechercher six problèmes de blancs de base. La correction de trois problèmes est activée par défaut et peut être désactivée et celle des trois autres n’est pas activée par défaut mais peut être activée.

Les trois activées par défaut sont blank-at-eol qui détecte les espaces en fin de ligne, blank-at-eof qui détecte les espaces en fin de fichier et space-before-tab qui recherche les espaces avant les tabulations au début d’une ligne.

Les trois autres qui sont désactivées par défaut mais peuvent être activées sont indent-with-non-tab qui recherche des lignes qui commencent par des espaces au lieu de tabulations (contrôlé par l’option tabwidth), tab-in-indent qui recherche les tabulations dans la portion d’indentation d’une ligne et cr-at-eol qui indique à Git que les « retour chariot » en fin de ligne sont acceptés.

Vous pouvez indiquer à Git quelle correction vous voulez activer en fixant core.whitespace avec les valeurs que vous voulez ou non, séparées par des virgules. Vous pouvez désactiver des réglages en les éliminant de la chaîne de paramétrage ou en les préfixant avec un -. Par exemple, si vous souhaitez activer tout sauf cr-at-eol, vous pouvez lancer ceci :

$ git config --global core.whitespace \
    trailing-space,space-before-tab,indent-with-non-tab

Git va détecter ces problèmes quand vous lancez une commande git diff et essayer de les coloriser pour vous permettre de les régler avant de valider.

Il utilisera aussi ces paramètres pour vous aider quand vous appliquerez des patchs avec git apply.

Quand vous appliquez des patchs, vous pouvez paramétrer Git pour qu’il vous avertisse s’il doit appliquer des patchs qui présentent les défauts de blancs :

$ git apply --whitespace=warn <patch>

Ou vous pouvez indiquer à Git d’essayer de corriger automatiquement le problème avant d’appliquer le patch :

$ git apply --whitespace=fix <patch>

Ces options s’appliquent aussi à git rebase. Si vous avez validé avec des problèmes de blancs mais n’avez pas encore poussé en amont, vous pouvez lancer un rebase avec l’option --whitespace=fix pour faire corriger à Git les erreurs de blancs pendant qu’il réécrit les patchs.

Configuration du serveur

Il n’y a pas autant d’options de configuration de Git côté serveur, mais en voici quelques unes intéressantes dont il est utile de prendre note.

receive.fsckObjects

Git est capable de vérifier que tous les objets reçus pendant une poussée correspondent à leur somme de contrôle SHA-1 et qu’ils pointent sur des objets valides. Cependant, il ne le fait pas par défaut sur chaque poussée. C’est une opération relativement lourde qui peut énormément allonger les poussées selon la taille du dépôt ou de la poussée. Si vous voulez que Git vérifie la cohérence des objets à chaque poussée, vous pouvez le forcer en fixant le paramètre receive.fsckObjects à true :

$ git config --system receive.fsckObjects true

Maintenant, Git va vérifier l’intégrité de votre dépôt avant que chaque poussée ne soit acceptée pour s’assurer que des clients défectueux (ou malicieux) n’introduisent pas des données corrompues.

receive.denyNonFastForwards

Si vous rebasez des commits que vous avez déjà poussés, puis essayez de pousser à nouveau, ou inversement, si vous essayez de pousser un commit sur une branche distante qui ne contient pas le commit sur lequel la branche distante pointe, votre essai échouera. C’est généralement une bonne politique, mais dans le cas d’un rebasage, vous pouvez décider que vous savez ce que vous faites et forcer la mise à jour de la branche distante en ajoutant l’option -f à votre commande.

Pour désactiver la possibilité de forcer la mise à jour des branches distantes autres qu’en avance rapide, réglez receive.denyNonFastForwards :

$ git config --system receive.denyNonFastForwards true

Un autre moyen de faire consiste à utiliser des crochets côté-serveur, point qui sera abordé plus loin. Cette autre approche permet de réaliser des traitements plus complexes comme de refuser l’avance rapide seulement à un certain groupe d’utilisateurs.

receive.denyDeletes

Un des contournements possible à la politique denyNonFastForwards consiste à simplement effacer la branche distante et à la repousser avec les nouvelles références. Pour interdire ceci, réglez receive.denyDeletes à true :

$ git config --system receive.denyDeletes true

Ceci interdit la suppression de branches ou d’étiquettes. Aucun utilisateur n’en a le droit. Pour pouvoir effacer des branches distantes, vous devez effacer manuellement les fichiers de référence sur le serveur. Il existe aussi des moyens plus intéressants de gérer cette politique utilisateur par utilisateur au moyen des listes de contrôle d’accès, point qui sera abordé dans Exemple de politique gérée par Git.

Attributs Git

Certains de ces réglages peuvent aussi s’appliquer sur un chemin, de telle sorte que Git ne les applique que sur un sous-répertoire ou un sous-ensemble de fichiers. Ces réglages par chemin sont appelés attributs Git et sont définis soit dans un fichier .gitattributes dans un répertoire (normalement la racine du projet), soit dans un fichier .git/info/attributes si vous ne souhaitez pas que le fichier de description des attributs fasse partie du projet.

Les attributs permettent de spécifier des stratégies de fusion différentes pour certains fichiers ou répertoires dans votre projet, d’indiquer à Git la manière de calculer les différences pour certains fichiers non-texte, ou de faire filtrer à Git le contenu avant qu’il ne soit validé ou extrait. Dans ce chapitre, nous traiterons certains attributs applicables aux chemins et détaillerons quelques exemples de leur utilisation en pratique.

Fichiers binaires

Les attributs Git permettent des trucs cool comme d’indiquer à Git quels fichiers sont binaires (dans les cas où il ne pourrait pas le deviner par lui-même) et de lui donner les instructions spécifiques pour les traiter. Par exemple, certains fichiers peuvent être générés par machine et impossible à traiter par diff, tandis que pour certains autres fichiers binaires, les différences peuvent être calculées. Nous détaillerons comment indiquer à Git l’un et l’autre.

Identification des fichiers binaires

Certains fichiers ressemblent à des fichiers texte mais doivent en tout état de cause être traités comme des fichiers binaires. Par exemple, les projets Xcode sous Mac contiennent un fichier finissant en .pbxproj, qui est en fait un jeu de données JSON (format de données en texte JavaScript) enregistré par l’application EDI pour y sauver les réglages entre autres de compilation. Bien que ce soit techniquement un fichier texte en ASCII, il n’y a aucun intérêt à le gérer comme tel parce que c’est en fait une mini base de données. Il est impossible de fusionner les contenus si deux utilisateurs le modifient et les calculs de différence par défaut sont inutiles. Ce fichier n’est destiné qu’à être manipulé par un programme. En résumé, ce fichier doit être considéré comme un fichier binaire opaque.

Pour indiquer à Git de traiter tous les fichiers pbxproj comme binaires, ajoutez la ligne suivante à votre fichier .gitattributes :

*.pbxproj binary

À présent, Git n’essaiera pas de convertir ou de corriger les problèmes des CRLF, ni de calculer ou d’afficher les différences pour ces fichiers quand vous lancez git show ou git diff sur votre projet.

Comparaison de fichiers binaires

Dans Git, vous pouvez utiliser la fonctionnalité des attributs pour comparer efficacement les fichiers binaires. Pour ce faire, indiquez à Git comment convertir vos données binaires en format texte qui peut être comparé via un diff normal.

Comme c’est une fonctionnalité vraiment utile et peu connue, nous allons détailler certains exemples. Premièrement, nous utiliserons cette technique pour résoudre un des problèmes les plus ennuyeux de l’humanité : gérer en contrôle de version les documents Word. Tout le monde convient que Word est l’éditeur de texte le plus horrible qui existe, mais bizarrement, tout le monde persiste à l’utiliser. Si vous voulez gérer en version des documents Word, vous pouvez les coller dans un dépôt Git et les valider de temps à autre. Mais qu’est-ce que ça vous apporte ? Si vous lancez git diff normalement, vous verrez quelque chose comme :

$ git diff
diff --git a/chapter1.docx b/chapter1.docx
index 88839c4..4afcb7c 100644
Binary files a/chapter1.docx and b/chapter1.docx differ

Vous ne pouvez pas comparer directement les versions à moins de les extraire et de les parcourir manuellement. En fait, vous pouvez faire la même chose plutôt bien en utilisant les attributs Git. Ajoutez la ligne suivante dans votre fichier .gitattributes :

*.docx diff=word

Cette ligne indique à Git que tout fichier correspondant au patron (.docx) doit utiliser le filtre word pour visualiser le diff des modifications. Qu’est-ce que le filtre « word » ? Nous devons le définir. Vous allez indiquer à Git d’utiliser le programme docx2txt qui a été écrit spécifiquement pour extraire le texte d’un document MS Word, qu’il pourra comparer correctement.

Installez déjà docx2text. Vous pouvez le télécharger depuis http://docx2txt.sourceforge.net. Suivez les instruction dans le fichier INSTALL pour le placer à un endroit où votre shell peut le trouver. Ensuite, écrivons un script qui convertit la sortie dans le format que Git comprend. Créez un fichier dans votre chemin d’exécution appelé docx2txt et ajoutez ce contenu :

#!/bin/bash
docx2txt.pl $1 -

N’oubliez pas de faire un chmod a+x sur ce fichier. Finalement, vous pouvez configurer Git pour qu’il utilise ce script :

$ git config diff.word.textconv docx2txt

À présent, Git sait que s’il essaie de faire un diff entre deux instantanés et qu’un des fichiers finit en .docx, il devrait faire passer ces fichiers par le filtre word défini comme le programme docx2txt. Cette méthode fait effectivement des jolies versions texte de vos fichiers Word avant d’essayer de les comparer.

Voici un exemple. J’ai mis le chapitre 1 de ce livre dans Git, ajouté du texte à un paragraphe et sauvegardé le document. Puis, j’ai lancé git diff pour visualiser ce qui a changé :

$ git diff
diff --git a/chapter1.docx b/chapter1.docx
index 0b013ca..ba25db5 100644
--- a/chapter1.docx
+++ b/chapter1.docx
@@ -2,6 +2,7 @@
 This chapter will be about getting started with Git. We will begin at the beginning by explaining some background on version control tools, then move on to how to get Git running on your system and finally how to get it setup to start working with. At the end of this chapter you should understand why Git is around, why you should use it and you should be all setup to do so.
 1.1. About Version Control
 What is "version control", and why should you care? Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. For the examples in this book you will use software source code as the files being version controlled, though in reality you can do this with nearly any type of file on a computer.
+Testing: 1, 2, 3.
 If you are a graphic or web designer and want to keep every version of an image or layout (which you would most certainly want to), a Version Control System (VCS) is a very wise thing to use. It allows you to revert files back to a previous state, revert the entire project back to a previous state, compare changes over time, see who last modified something that might be causing a problem, who introduced an issue and when, and more. Using a VCS also generally means that if you screw things up or lose files, you can easily recover. In addition, you get all this for very little overhead.
 1.1.1. Local Version Control Systems
 Many people's version-control method of choice is to copy files into another directory (perhaps a time-stamped directory, if they're clever). This approach is very common because it is so simple, but it is also incredibly error prone. It is easy to forget which directory you're in and accidentally write to the wrong file or copy over files you don't mean to.

Git m’indique succinctement que j’ai ajouté la chaîne « Testing: 1, 2,

  1. », ce qui est correct. Ce n’est pas parfait – les modifications de formatage n’apparaissent pas – mais c’est efficace.

Un autre problème intéressant concerne la comparaison de fichiers d’images. Une méthode consiste à faire passer les fichiers image à travers un filtre qui extrait les données EXIF, les méta-données enregistrées avec la plupart des formats d’image. Si vous téléchargez et installez le programme exiftool, vous pouvez l’utiliser pour convertir vos images en texte de méta-données de manière que le diff puisse au moins montrer une représentation textuelle des modifications pratiquées. Mettez la ligne suivante dans votre fichier .gitattributes :

*.png diff=exif

Configurez Git pour utiliser cet outil :

$ git config diff.exif.textconv exiftool

Si vous remplacez une image dans votre projet et lancez git diff, vous verrez ceci :

diff --git a/image.png b/image.png
index 88839c4..4afcb7c 100644
--- a/image.png
+++ b/image.png
@@ -1,12 +1,12 @@
 ExifTool Version Number         : 7.74
-File Size                       : 70 kB
-File Modification Date/Time     : 2009:04:21 07:02:45-07:00
+File Size                       : 94 kB
+File Modification Date/Time     : 2009:04:21 07:02:43-07:00
 File Type                       : PNG
 MIME Type                       : image/png
-Image Width                     : 1058
-Image Height                    : 889
+Image Width                     : 1056
+Image Height                    : 827
 Bit Depth                       : 8
 Color Type                      : RGB with Alpha

Vous pouvez réaliser rapidement que la taille du fichier et les dimensions des images ont changé.

Expansion des mots-clés

L’expansion de mots-clés dans le style de CVS ou de SVN est souvent une fonctionnalité demandée par les développeurs qui y sont habitués. Le problème principal de ce système avec Git est que vous ne pouvez pas modifier un fichier avec l’information concernant le commit après la validation parce que Git calcule justement la somme de contrôle sur son contenu. Cependant, vous pouvez injecter des informations textuelles dans un fichier au moment où il est extrait et les retirer avant qu’il ne soit ajouté à un commit. Les attributs Git vous fournissent deux manières de le faire.

Premièrement, vous pouvez injecter automatiquement la somme de contrôle SHA-1 d’un blob dans un champ $Id$ d’un fichier. Si vous positionnez cet attribut pour un fichier ou un ensemble de fichiers, la prochaine fois que vous extrairez cette branche, Git remplacera chaque champ avec le SHA-1 du blob. Il est à noter que ce n’est pas le SHA du commit mais celui du blob lui-même.

Mettez la ligne suivante dans votre fichier .gitattributes :

*.txt ident

Ajoutez une référence $Id$ à un fichier de test :

$ echo '$Id$' > test.txt

À la prochaine extraction de ce fichier, Git injecte le SHA du blob :

$ rm test.txt
$ git checkout -- test.txt
$ cat test.txt
$Id: 42812b7653c7b88933f8a9d6cad0ca16714b9bb3 $

Néanmoins, ce résultat n’a que peu d’intérêt. Si vous avez utilisé la substitution avec CVS ou Subversion, il est possible d’inclure la date. Le code SHA n’est pas des plus utiles car il ressemble à une valeur aléatoire et ne vous permet pas de distinguer si tel SHA est plus récent ou plus ancien que tel autre.

Il apparaît que vous pouvez écrire vos propres filtres pour réaliser des substitutions dans les fichiers lors des validations/extractions. Ces filtres s’appellent « clean » et « smudge ». Dans le fichier .gitattributes, vous pouvez indiquer un filtre pour des chemins particuliers puis créer des scripts qui traiteront ces fichiers avant qu’ils soient extraits (« smudge », voir Le filtre « smudge » est lancé lors d’une extraction.) et juste avant qu’ils soient validés (« clean », voir Le filtre « clean » est lancé lorsque les fichiers sont indexés.). Ces filtres peuvent servir à faire toutes sortes de choses attrayantes.

Le filtre « \_smudge\_ » est lancé lors d’une
extraction.
Figure 171 : Le filtre « \_smudge\_ » est lancé lors d’une extraction.

Figure 144. Le filtre « smudge » est lancé lors d’une extraction.

Le filtre « \_clean\_ » est lancé lorsque les fichiers sont
indexés.
Figure 172 : Le filtre « \_clean\_ » est lancé lorsque les fichiers sont indexés.

Figure 145. Le filtre « clean » est lancé lorsque les fichiers sont indexés.

Le message de validation d’origine pour cette fonctionnalité donne un exemple simple permettant de passer tout votre code C par le programme indent avant de valider. Vous pouvez le faire en réglant l’attribut filter dans votre fichier .gitattributes pour filtrer les fichiers *.c avec le filtre « indent » :

*.c filter=indent

Ensuite, indiquez à Git ce que le filtre « indent » fait sur smudge et clean :

$ git config --global filter.indent.clean indent
$ git config --global filter.indent.smudge cat

Dans ce cas, quand vous validez des fichiers qui correspondent à *.c, Git les fera passer par le programme indent avant de les valider et les fera passer par le programme cat avant de les extraire sur votre disque. Le programme cat ne fait rien : il se contente de régurgiter les données telles qu’il les a lues. Cette combinaison filtre effectivement tous les fichiers de code source C par indent avant leur validation.

Un autre exemple intéressant fournit l’expansion du mot-clé $Date$ dans le style RCS. Pour le réaliser correctement, vous avez besoin d’un petit script qui prend un nom de fichier, calcule la date de la dernière validation pour le projet et l’insère dans le fichier. Voici un petit script Ruby qui le fait :

#! /usr/bin/env ruby
data = STDIN.read
last_date = `git log --pretty=format:"%ad" -1`
puts data.gsub('$Date$', '$Date: ' + last_date.to_s + '$')

Tout ce que le script fait, c’est récupérer la date de la dernière validation à partir de la commande git log, la coller dans toutes les chaînes $Date$ qu’il trouve et réécrire le résultat. Ce devrait être simple dans n’importe quel langage avec lequel vous êtes à l’aise. Appelez ce fichier expand_date et placez-le dans votre chemin. À présent, il faut paramétrer un filtre dans Git (appelons le dater) et lui indiquer d’utiliser le filtre expand_date en tant que smudge sur les fichiers à extraire. Nous utiliserons une expression Perl pour nettoyer lors d’une validation :

$ git config filter.dater.smudge expand_date
$ git config filter.dater.clean 'perl -pe "s/\\\$Date[^\\\$]*\\\$/\\\$Date\\\$/"'

Cette commande Perl extrait tout ce qu’elle trouve dans une chaîne $Date$ et la réinitialise. Le filtre prêt, on peut le tester en écrivant le mot-clé $Date$ dans un fichier, puis en créant un attribut Git pour ce fichier qui fait référence au nouveau filtre et en créant un fichier avec votre mot-clé $Date$ :

date*.txt filter=dater
$ echo '# $Date$' > date_test.txt

Si vous validez ces modifications et extrayez le fichier à nouveau, vous remarquez le mot-clé correctement substitué :

$ git add date_test.txt .gitattributes
$ git commit -m "Testing date expansion in Git"
$ rm date_test.txt
$ git checkout date_test.txt
$ cat date_test.txt
# $Date: Tue Apr 21 07:26:52 2009 -0700$

Vous pouvez voir à quel point cette technique peut être puissante pour des applications personnalisées. Il faut rester néanmoins vigilant car le fichier .gitattributes est validé et inclus dans le projet tandis que le gestionnaire (ici, dater) ne l’est pas. Du coup, ça ne marchera pas partout. Lorsque vous créez ces filtres, ils devraient pouvoir avoir un mode dégradé qui n’empêche pas le projet de fonctionner.

Export d’un dépôt

Les données d’attribut Git permettent aussi de faire des choses intéressantes quand vous exportez une archive du projet.

export-ignore

Vous pouvez dire à Git de ne pas exporter certains fichiers ou répertoires lors de la génération d’archive. S’il y a un sous-répertoire ou un fichier que vous ne souhaitez pas inclure dans le fichier archive mais que vous souhaitez extraire dans votre projet, vous pouvez indiquer ce fichier via l’attribut export-ignore.

Par exemple, disons que vous avez des fichiers de test dans le sous-répertoire test/ et que ce n’est pas raisonnable de les inclure dans l’archive d’export de votre projet. Vous pouvez ajouter la ligne suivante dans votre fichier d’attribut Git :

test/ export-ignore

À présent, quand vous lancez git archive pour créer une archive tar de votre projet, ce répertoire ne sera plus inclus dans l’archive.

export-subst

Quand vous exportez des fichiers pour un déploiement, vous pouvez appliquer le formatage de git log et l’expansion de mot-clés à des portions choisies de fichiers marquées avec l’attribut export-subst.

Par exemple, si vous voulez inclure un fichier appelé LAST_COMMIT dans votre projet et y injecter automatiquement la date de dernière validation lorsque git archive est lancé, vous pouvez définir vos fichiers .gitattributes et LAST_COMMIT comme ceci :

LAST_COMMIT export-subs
$ echo 'Last commit date: $Format:%cd by %aN$' > LAST_COMMIT
$ git add LAST_COMMIT .gitattributes
$ git commit -am 'adding LAST_COMMIT file for archives'

Quand vous lancez git archive, le contenu de ce fichier inclus dans l’archive ressemblera à ceci :

$ git archive HEAD | tar xCf ../test-deploiement -
$ cat ../test-deploiement/LAST_COMMIT
Last commit date: Tue Apr 21 08:38:48 2009 -0700 by Scott Chacon

Les substitutions peuvent inclure par exemple le message de validation et n’importe quelle note git, et git log peut faire du simple retour à la ligne :

$ echo '$Format:Last commit: %h by %aN at %cd%n%+w(76,6,9)%B$' > LAST_COMMIT
$ git commit -am 'export-subst uses git log'\''s custom formatter

git archive uses git log'\''s `pretty=format:` processor
directly, and strips the surrounding `$Format:` and `$`
markup from the output.
'
$ git archive @ | tar xfO - LAST_COMMIT
Last commit: 312ccc8 by Jim Hill at Fri May 8 09:14:04 2015 -0700
       export-subst uses git log's custom formatter

         git archive uses git log's `pretty=format:` processor directly, and
         strips the surrounding `$Format:` and `$` markup from the output.

L’archive résultante est appropriée pour le travail de déploiement, mais comme n’importe quelle archive exportée, elle n’est pas appropriée pour continuer un travail de développement.

Stratégies de fusion

Vous pouvez aussi utiliser les attributs Git pour indiquer à Git d’utiliser des stratégies de fusion différenciées pour des fichiers spécifiques dans votre projet. Une option très utile est d’indiquer à Git de ne pas essayer de fusionner des fichiers spécifiques quand ils rencontrent des conflits mais plutôt d’utiliser prioritairement votre version du fichier.

C’est très utile si une branche de votre projet a divergé ou s’est spécialisée, mais que vous souhaitez pouvoir fusionner les modifications qu’elle porte et vous voulez ignorer certains fichiers. Supposons que vous avez un fichier de paramètres de base de données appelé database.xml différent sur deux branches et vous voulez les fusionner dans votre autre branche sans corrompre le fichier de base de données. Vous pouvez déclarer un attribut comme ceci :

database.xml merge=ours

Et définir une bête stratégie de fusion ours avec :

$ git config --global merge.ours.driver true

Si vous fusionnez dans une autre branche, plutôt que de rencontrer des conflits de fusion avec le fichier database.xml, vous verrez quelque chose comme :

$ git merge topic
Auto-merging database.xml
Merge made by recursive.

Dans ce cas, database.xml reste dans l’état d’origine, quoi qu’il arrive.

Crochets Git

Comme de nombreux autres systèmes de gestion de version, Git dispose d’un moyen de lancer des scripts personnalisés quand certaines actions importantes ont lieu. Il y a deux groupes de crochets : ceux côté client et ceux côté serveur. Les crochets côté client concernent les opérations de client telles que la validation et la fusion. Les crochets côté serveur concernent les opérations de serveur Git telles que la réception de commits. Vous pouvez utiliser ces crochets pour toutes sortes de fonctions.

Installation d’un crochet

Les crochets sont tous stockés dans le sous-répertoire hooks du répertoire Git. Dans la plupart des projets, c’est .git/hooks.

Par défaut, Git popule ce répertoire avec quelques scripts d’exemple déjà utiles par eux-mêmes ; mais ils servent aussi de documentation sur les paramètres de chaque script. Tous les exemples sont des scripts shell avec un peu de Perl mais n’importe quel script exécutable nommé correctement fonctionnera. Vous pouvez les écrire en Ruby ou Python ou ce que vous voudrez. Pour les versions de Git postérieures à 1.6, ces fichiers crochet d’exemple se terminent en .sample et il faudra les renommer. Pour les versions de Git antérieures à 1.6, les fichiers d’exemple sont nommés correctement mais ne sont pas exécutables.

Pour activer un script de crochet, placez un fichier dans le sous-répertoire hook de votre répertoire Git, nommé correctement et exécutable. À partir de ce moment, il devrait être appelé. Abordons donc les noms de fichiers de crochet les plus importants.

Crochets côté client

Il y a de nombreux crochets côté client. Ce chapitre les classe entre crochets de traitement de validation, scripts de traitement par courriel et le reste des scripts côté client.

Il est important de noter que les crochets côté client ne sont pas copiés quand le dépôt est cloné. Si vous avez l’intention d’utiliser ces scripts pour faire respecter une politique de validation, il vaut mieux utiliser des crochets côté serveur, comme Exemple de politique gérée par Git.

Crochets de traitement de validation

Les quatre premiers crochets ont trait au processus de validation.

Le crochet pre-commit est lancé en premier, avant même que vous ne saisissiez le message de validation. Il est utilisé pour inspecter l’instantané qui est sur le point d’être validé, pour vérifier si vous avez oublié quelque chose, pour s’assurer que les tests passent ou pour examiner ce que vous souhaitez inspecter dans le code. Un code de sortie non nul de ce crochet annule la validation, bien que vous puissiez le contourner avec git commit --no-verify. Vous pouvez réaliser des actions telles qu’une vérification de style (en utilisant lint ou un équivalent), d’absence de blancs en fin de ligne (le crochet par défaut fait exactement cela) ou de documentation des nouvelles méthodes.

Le crochet prepare-commit-msg est appelé avant que l’éditeur de message de validation ne soit lancé mais après que le message par défaut a été créé. Il vous permet d’éditer le message par défaut avant que l’auteur ne le voit. Ce crochet accepte quelques options : le chemin du fichier qui contient le message de validation actuel, le type de validation et le SHA-1 du commit si c’est un commit amendé. Ce crochet ne sert généralement à rien pour les validations normales. Par contre, il est utile pour les validations où le message par défaut est généré, tel que les modèles de message de validation, les validations de fusion, les commits écrasés ou amendés. Vous pouvez l’utiliser en conjonction avec un modèle de messages pour insérer de l’information par programme.

Le crochet commit-msg accepte un paramètre qui est encore le chemin du fichier temporaire qui contient le message de validation actuel. Si ce script sort avec un code de sortie non nul, Git abandonne le processus de validation, ce qui vous permet de vérifier l’état de votre projet ou du message de validation avant de laisser passer un commit. Dans la dernière section de ce chapitre, l’utilisation de ce crochet permettra de vérifier que le message de validation est conforme à un format obligatoire.

Après l’exécution du processus complet de validation, le crochet post-commit est appelé. Il n’accepte aucun argument mais vous pouvez facilement accéder au dernier commit grâce à git log -1 HEAD. Généralement, ce script sert à réaliser des notifications ou des choses similaires.

Crochets de gestion courriel

Vous pouvez régler trois crochets côté client pour la gestion à base de courriel. Ils sont tous invoqués par la commande git am, donc si vous n’êtes pas habitué à utiliser cette commande dans votre mode de gestion, vous pouvez simplement passer la prochaine section. Si vous acceptez des patchs préparés par git format-patch par courriel, alors certains de ces crochets peuvent vous être très utiles.

Le premier crochet lancé est applypatch-msg. Il accepte un seul argument : le nom du fichier temporaire qui contient le message de validation proposé. Git abandonne le patch si ce script sort avec un code non nul. Vous pouvez l’utiliser pour vérifier que le message de validation est correctement formaté ou pour normaliser le message en l’éditant sur place par script.

Le crochet lancé ensuite lors de l’application de patchs via git am s’appelle pre-applypatch. Il n’accepte aucun argument et son nom est trompeur car il est lancé après que le patch a été appliqué, ce qui vous permet d’inspecter l’instantané avant de réaliser la validation. Vous pouvez lancer des tests ou inspecter l’arborescence active avec ce script. S’il manque quelque chose ou que les tests ne passent pas, un code de sortie non nul annule la commande git am sans valider le patch.

Le dernier crochet lancé pendant l’opération git am s’appelle post-applypatch. Vous pouvez l’utiliser pour notifier un groupe ou l’auteur du patch que vous venez de l’appliquer. Vous ne pouvez plus arrêter le processus de validation avec ce script.

Autres crochets côté client

Le crochet pre-rebase est invoqué avant que vous ne rebasiez et peut interrompre le processus s’il sort avec un code d’erreur non nul. Vous pouvez utiliser ce crochet pour empêcher de rebaser tout commit qui a déjà été poussé. C’est ce que fait le crochet d’exemple pre-rebase que Git installe, même s’il fait des hypothèses qui peuvent ne pas correspondre avec votre façon de faire.

Le crochet post-rewrite est lancé par les commandes qui remplacent les commits, comme git commit --amend et git rebase (mais pas par git filter-branch). Son seul argument est la commande qui a déclenché la réécriture, et il reçoit une liste de réécritures sur stdin. Ce crochet a beaucoup des mêmes utilisations que les crochets post-checkout et post-merge.

Après avoir effectué avec succès un git checkout, le crochet post-checkout est lancé. Vous pouvez l’utiliser pour paramétrer correctement votre environnement projet dans votre copie de travail. Cela peut signifier y déplacer des gros fichiers binaires que vous ne souhaitez pas voir en gestion de source, générer automatiquement la documentation ou quelque chose dans le genre.

Le crochet post-merge s’exécute à la suite d’une commande merge réussie. Vous pouvez l’utiliser pour restaurer certaines données non gérées par Git dans la copie de travail telles que les informations de permission. Ce crochet permet même de valider la présence de fichiers externes au contrôle de Git que vous pourriez souhaiter voir recopiés lorsque la copie de travail change.

Le crochet pre-push est lancé pendant un git push, après la mise à jour des références distantes mais avant le transfert des objets. Il reçoit le nom et l’emplacement du dépôt distant en paramètre et une liste des références qui seront mises à jour sur stdin. Il peut servir à valider un ensemble de mises à jour de références avant que la poussée n’ait réellement lieu (la poussée est abandonnée sur un code de sortie non nul).

Git lance de temps à autre le ramasse-miettes au cours de son fonctionnement en invoquant git gc --auto. Le crochet pre-auto-gc est invoqué juste avant le démarrage du ramasse-miettes et peut être utilisé pour vous en notifier ou pour annuler le ramasse-miettes si le moment ne s’y prête pas.

Crochets côté serveur

En complément des crochets côté client, vous pouvez utiliser comme administrateur système quelques crochets côté serveur pour appliquer quasiment toutes les règles de votre projet. Ces scripts s’exécutent avant et après chaque poussée sur le serveur. Les crochets pre peuvent rendre un code d’erreur non nul à tout moment pour rejeter la poussée et afficher un message d’erreur au client. Vous pouvez mettre en place des règles aussi complexes que vous le souhaitez.

pre-receive

Le premier script lancé lors de la gestion d’une poussée depuis un client est pre-receive. Il accepte une liste de références lues sur stdin. S’il sort avec un code d’erreur non nul, aucune n’est acceptée. Vous pouvez utiliser ce crochet pour réaliser des tests tels que s’assurer que toutes les références mises à jour le sont en avance rapide ou pour s’assurer que l’utilisateur dispose bien des droits de création, poussée, destruction ou de lecture des mises à jour pour tous les fichiers qu’il cherche à mettre à jour dans cette poussée.

update

Le script update est très similaire au script pre-receive, à la différence qu’il est lancé une fois par branche qui doit être modifiée lors de la poussée. Si la poussée s’applique à plusieurs branches, pre-receive n’est lancé qu’une fois, tandis qu'update est lancé une fois par branche impactée. Au lieu de lire à partir de stdin, ce script accepte trois arguments : le nom de la référence (branche), le SHA-1 du commit pointé par la référence avant la poussée et le SHA-1 que l’utilisateur est en train de pousser. Si le script update se termine avec un code d’erreur non nul, seule la référence est rejetée. Les autres références pourront être mises à jour.

post-receive

Le crochet post-receive est lancé après l’exécution complète du processus et peut être utilisé pour mettre à jour d’autres services ou pour notifier des utilisateurs. Il accepte les mêmes données sur stdin que pre-receive. Il peut par exemple envoyer un courriel à une liste de diffusion, notifier un serveur d’intégration continue ou mettre à jour un système de suivi de tickets. Il peut aussi analyser les messages de validation à la recherche d’ordres de mise à jour de l’état des tickets. Ce script ne peut pas arrêter le processus de poussée mais le client n’est pas déconnecté tant qu’il n’a pas terminé. Il faut donc être prudent à ne pas essayer de lui faire réaliser des actions qui peuvent durer longtemps.

Exemple de politique gérée par Git

Dans ce chapitre, nous allons utiliser ce que nous venons d’apprendre pour installer une gestion Git qui vérifie la présence d’un format personnalisé de message de validation, n’autorise que les poussées en avance rapide et autorise seulement certains utilisateurs à modifier certains sous-répertoires dans un projet. Nous construirons des scripts client pour informer les développeurs que leurs poussées vont être rejetées et des scripts sur le serveur pour mettre effectivement en place ces règles.

Nous utilisons Ruby pour les écrire, d’abord par inertie intellectuelle, ensuite parce que ce langage de script s’apparente le plus à du pseudo-code. Ainsi, il devrait être simple de suivre grossièrement le code même sans connaître le langage Ruby. Cependant, tout langage peut être utilisé. Tous les scripts d’exemple distribués avec Git sont soit en Perl soit en Bash, ce qui donne de nombreux autres exemples de crochets dans ces langages.

Crochet côté serveur

Toutes les actions côté serveur seront contenues dans le fichier update dans le répertoire hooks. Le fichier update s’exécute une fois par branche poussée et accepte trois paramètres :

  • la référence sur laquelle on pousse

  • l’ancienne révision de la branche

  • la nouvelle révision de la branche.

Vous pouvez aussi avoir accès à l’utilisateur qui pousse si la poussée est réalisée par SSH. Si vous avez permis à tout le monde de se connecter avec un utilisateur unique (comme « git ») avec une authentification à clé publique, il vous faudra fournir à cet utilisateur une enveloppe de shell qui déterminera l’identité de l’utilisateur à partir de sa clé publique et positionnera une variable d’environnement spécifiant cette identité. Ici, nous considérons que la variable d’environnement $USER indique l’utilisateur connecté, donc le script update commence par rassembler toutes les informations nécessaires :

#!/usr/bin/env ruby

$nomref       = ARGV[0]
$anciennerev  = ARGV[1]
$nouvellerev  = ARGV[2]
$utilisateur  = ENV['USER']

puts "Vérification des règles..."
puts "(#{$nomref}) (#{$anciennerev[0,6]}) (#{$nouvellerev[0,6]})"

Oui, ce sont des variables globales. C’est seulement pour simplifier la démonstration.

Application d’une politique de format du message de validation

Notre première tâche consiste à forcer que chaque message de validation adhère à un format particulier. En guise d’objectif, obligeons chaque message à contenir une chaîne de caractère qui ressemble à « ref: 1234 » parce que nous souhaitons que chaque validation soit liée à une tâche de notre système de tickets. Nous devons donc inspecter chaque commit poussé, vérifier la présence de la chaîne et sortir avec un code non-nul en cas d’absence pour rejeter la poussée.

Vous pouvez obtenir une liste des valeurs SHA-1 de tous les commits en cours de poussée en passant les valeurs $nouvellerev et $anciennerev à une commande de plomberie Git appelée git rev-list. C’est comme la commande git log mais elle n’affiche par défaut que les valeurs SHA-1, sans autre information. Donc, pour obtenir une liste de tous les SHA-1 des commits introduits entre un SHA de commit et un autre, il suffit de lancer quelque chose comme :

$ git rev-list 538c33..d14fc7
d14fc7c847ab946ec39590d87783c69b031bdfb7
9f585da4401b0a3999e84113824d15245c13f0be
234071a1be950e2a8d078e6141f5cd20c1e61ad3
dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a
17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475

Vous pouvez récupérer la sortie, boucler sur chacun de ces SHA-1 de commit, en extraire le message et le tester par rapport à une expression régulière qui cherche un patron.

Vous devez trouver comment extraire le message de validation à partir de chacun des commits à tester. Pour accéder aux données brutes du commit, vous pouvez utiliser une autre commande de plomberie appelée git cat-file. Nous traiterons en détail toutes ces commandes de plomberie dans Les tripes de Git mais pour l’instant, voici ce que cette commande affiche :

$ git cat-file commit ca82a6
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
author Scott Chacon <schacon@gmail.com> 1205815931 -0700
committer Scott Chacon <schacon@gmail.com> 1240030591 -0700

changed the version number

Un moyen simple d’extraire le message de validation d’un commit à partir de son SHA-1 consiste à rechercher la première ligne vide et à sélectionner tout ce qui suit. Cela peut être facilement réalisé avec la commande sed sur les systèmes Unix :

$ git cat-file commit ca82a6 | sed '1,/^$/d'
changed the version number

Vous pouvez utiliser cette ligne pour récupérer le message de validation de chaque commit en cours de poussée et sortir si quelque chose ne correspond pas à ce qui est attendu. Pour sortir du script et rejeter la poussée, il faut sortir avec un code non nul. La fonction complète ressemble à ceci :

$regex = /\[ref: (\d+)\]/

# vérification du format des messages de validation
def verif_format_message
  revs_manquees = `git rev-list #{$anciennerev}..#{$nouvellerev}`.split("\n")
  revs_manquees.each do |rev|
    message = `git cat-file commit #{rev} | sed '1,/^$/d'`
    if !$regex.match(message)
      puts "[REGLE] Le message de validation n'est pas conforme"
      exit 1
    end
  end
end
verif_format_message

Placer ceci dans un script update rejettera les mises à jour contenant des commits dont les messages ne suivent pas la règle.

Mise en place d’un système d’ACL par utilisateur

Supposons que vous souhaitiez ajouter un mécanisme à base de liste de contrôle d’accès (access control list : ACL) qui permette de spécifier quel utilisateur a le droit de pousser des modifications vers quelle partie du projet. Certaines personnes ont un accès complet tandis que d’autres n’ont accès que pour mettre à jour certains sous-répertoires ou certains fichiers. Pour faire appliquer ceci, nous allons écrire ces règles dans un fichier appelé acl situé dans le dépôt brut Git sur le serveur. Le crochet update examinera ces règles, listera les fichiers impactés par la poussée et déterminera si l’utilisateur qui pousse a effectivement les droits nécessaires sur ces fichiers.

Écrivons en premier le fichier d’ACL. Nous allons utiliser un format très proche de celui des ACL de CVS. Le fichier est composé de lignes dont le premier champ est avail ou unavail, le second est une liste des utilisateurs concernés séparés par des virgules et le dernier champ indique le chemin pour lequel la règle s’applique (le champ vide indiquant une règle générale). Tous les champs sont délimités par un caractère pipe « | ».

Dans notre cas, il y a quelques administrateurs, des auteurs de documentation avec un accès au répertoire doc et un développeur qui n’a accès qu’aux répertoires lib et tests. Le fichier ACL ressemble donc à ceci :

avail|nickh,pjhyett,defunkt,tpw
avail|usinclair,cdickens,ebronte|doc
avail|schacon|lib
avail|schacon|tests

Le traitement consiste à lire le fichier dans une structure utilisable. Dans notre cas, pour simplifier, nous ne traiterons que les directives avail. Voici une fonction qui crée à partir du fichier un tableau associatif dont la clé est l’utilisateur et la valeur est une liste des chemins pour lesquels l’utilisateur a les droits en écriture :

def get_acl_access_data(nom_fichier_acl)
  # Lire le fichier ACL
  fichier_acl = File.read(nom_fichier_acl).split("\n").reject { |line| line == '' }
  acces = {}
  fichier_acl.each do |line|
    avail, utilisateurs, chemin = line.split('|')
    next unless avail == 'avail'
    utilisateurs.split(',').each do |utilisateur|
      access[utilisateur] ||= []
      access[utilisateur] << chemin
    end
  end
  acces
end

Pour le fichier d’ACL décrit plus haut, la fonction get_acl_access_data retourne une structure de données qui ressemble à ceci :

{"defunkt"=>[nil],
 "tpw"=>[nil],
 "nickh"=>[nil],
 "pjhyett"=>[nil],
 "schacon"=>["lib", "tests"],
 "cdickens"=>["doc"],
 "usinclair"=>["doc"],
 "ebronte"=>["doc"]}

En plus des permissions, il faut déterminer les chemins impactés par la poussée pour s’assurer que l’utilisateur a bien droit d’y toucher.

La liste des fichiers modifiés est assez simplement obtenue par la commande git log complétée par l’option --name-only mentionnée dans Options usuelles de git log :

$ git log -1 --name-only --pretty=format:'' 9f585d

README
lib/test.rb

Chaque fichier des commits doit être vérifié par rapport à la structure ACL retournée par la fonction get_acl_access_data pour déterminer si l’utilisateur a le droit de pousser tous ses commits :

# permission à certains utilisateurs de modifier certains sous-répertoires du projet
def verif_perms_repertoire
  acces = get_acl_access_data('acl')

  # verifier si quelqu'un cherche à pousser où il n'a pas le droit
  nouveaux_commits = `git rev-list #{$anciennerev}..#{$nouvellerev}`.split("\n")
  nouveaux_commits.each do |rev|
    fichiers_modifies = `git log -1 --name-only --pretty=format:'' #{rev}`.split("\n")
    fichiers_modifies.each do |chemin|
      next if chemin.size == 0
      acces_permis = false
      acces[$utilisateur].each do |chemin_acces|
        if !chemin_acces || # l'utilisateur a un accès complet
          (chemin.index(chemin_acces) == 0) # acces à ce chemin
          acces_permis = true
        end
      end
      if !acces_permis
        puts "[ACL] Vous n'avez pas le droit de pousser sur #{chemin}"
        exit 1
      end
    end
  end
end

verif_perms_repertoire

On récupère la liste des nouveau commits poussés au serveur avec git rev-list. Ensuite, pour chacun des ces commits, on trouve les fichiers modifiés et on s’assure que l’utilisateur qui pousse a effectivement droit à l’accès au chemin modifié.

À présent, les utilisateurs ne peuvent plus pousser de commits comprenant un message incorrectement formaté ou des modifications à des fichiers hors de leur zone réservée.

Test de la politique

Après avoir lancé un chmod u+x .git/hooks/update, avec .git/hooks/update comme fichier dans lequel réside tout ce code, si vous essayez de pousser un commit avec un message de validation non conforme, vous obtiendrez la sortie suivante :

$ git push -f origin master
Décompte des objets : 5, fait.
Compression des objets: 100% (3/3), fait.
Écriture des objets : 100% (3/3), 323 bytes, fait.
Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), fait.
Vérification des règles...
(refs/heads/master) (8338c5) (c5b616)
[REGLE] Le message de validation n'est pas conforme
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/master
To git@gitserver:project.git
 ! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'

Il y a plusieurs points à relever ici. Premièrement, une ligne indique l’endroit où le crochet est appelé.

Vérification des règles..
(refs/heads/master) (fb8c72) (c56860)

Le script update affiche ces lignes sur stdout au tout début. Tout ce que le script écrit sur stdout sera transmis au client.

La ligne suivante à remarquer est le message d’erreur.

[REGLE] Le message de validation n'est pas conforme
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/master

La première ligne a été écrite par le script, les deux autres l’ont été par Git pour indiquer que le script update a rendu un code de sortie non nul, ce qui a causé l’échec de la poussée. Enfin, il y a ces lignes :

To git@gitserver:project.git
 ! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'

Il y a un message d’échec distant pour chaque référence que le crochet a rejetée et une indication que l’échec est dû spécifiquement à un échec de crochet.

Par ailleurs, si quelqu’un cherche à modifier un fichier auquel il n’a pas les droits d’accès lors d’une poussée, il verra quelque chose de similaire. Par exemple, si un auteur de documentation essaie de pousser un commit qui modifie quelque chose dans le répertoire lib, il verra :

[ACL] Vous n'avez pas le droit de pousser sur lib/test.rb

À partir de maintenant, tant que le script update est en place et exécutable, votre dépôt ne peut plus subir de poussées hors avancée rapide, n’accepte plus de messages sans format et vos utilisateurs sont cloîtrés.

Crochets côté client

Le problème de cette approche, ce sont les plaintes des utilisateurs qui résulteront inévitablement des échecs de leurs poussées. Leur frustration et leur confusion devant le rejet à la dernière minute d’un travail minutieux est tout à fait compréhensible. De plus, la correction nécessitera une modification de leur historique, ce qui n’est pas une partie de plaisir.

Pour éviter ce scénario, il faut pouvoir fournir aux utilisateurs des crochets côté client qui leur permettront de vérifier que leurs validations seront effectivement acceptées par le serveur. Ainsi, ils pourront corriger les problèmes avant de valider et avant que ces difficultés ne deviennent des casse-têtes. Ces scripts n’étant pas diffusés lors du clonage du projet, il vous faudra les distribuer d’une autre manière, puis indiquer aux utilisateurs de les copier dans leur répertoire .git/hooks et de les rendre exécutables. Vous pouvez distribuer ces crochets au sein du projet ou dans un projet annexe mais il n’y a aucun moyen de les mettre en place automatiquement.

Premièrement, pour éviter le rejet du serveur au motif d’un mauvais format du message de validation, il faut vérifier celui-ci avant que chaque commit ne soit enregistré. Pour ce faire, utilisons le crochet commit-msg. En lisant le message à partir du fichier passé en premier argument et en le comparant au format attendu, on peut forcer Git à abandonner la validation en cas d’absence de correspondance :

#!/usr/bin/env ruby
fichier_message = ARGV[0]
message = File.read(fichier_message)

$regex = /\[ref: (\d+)\]/

if !$regex.match(message)
  puts "[REGLE] Le message de validation ne suit pas le format"
  exit 1
end

Avec ce fichier exécutable et à sa place dans .git/hooks/commit-msg, si une validation avec un message incorrect est tentée, voici le résultat :

$ git commit -am 'test'
[REGLE] Le message de validation ne suit pas le format

La validation n’a pas abouti. Néanmoins, si le message contient la bonne forme, Git accepte la validation :

$ git commit -am 'test [ref: 132]'
[master e05c914] test [ref: 132]
 1 file changed, 1 insertions(+), 0 deletions(-)

Ensuite, il faut s’assurer des droits sur les fichiers modifiés. Si le répertoire .git du projet contient une copie du fichier d’ACL précédemment utilisé, alors le script pre-commit suivant appliquera ses règles :

#!/usr/bin/env ruby

$utilisateur    = ENV['USER']

# [ insérer la fonction acl_access_data method ci-dessus ]

# Ne permet qu'à certains utilisateurs de modifier certains sous-répertoires
def verif_perms_repertoire
  acces = get_acl_access_data('.git/acl')

  fichiers_modifies = `git diff-index --cached --name-only HEAD`.split("\n")
  fichiers_modifies.each do |chemin|
    next if chemin.size == 0
    acces_permis = false
    acces[$utilisateur].each do |chemin_acces|
    if !chemin_acces || (chemin.index(chemin_acces) == 0)
      acces_permis = true
    end
    if !acces_permis
      puts "[ACL] Vous n'avez pas le droit de pousser sur #{path}"
      exit 1
    end
  end
end

verif_perms_repertoire

C’est grossièrement le même script que celui côté serveur, mais avec deux différences majeures. Premièrement, le fichier ACL est à un endroit différent parce que le script s’exécute depuis la copie de travail et non depuis le répertoire Git. Il faut donc changer le chemin vers le fichier d’ACL de :

access = get_acl_access_data('acl')

en :

access = get_acl_access_data('.git/acl')

L’autre différence majeure réside dans la manière d’obtenir la liste des fichiers modifiés. La fonction sur le serveur la recherche dans le journal des commits mais comme dans le cas actuel, le commit n’a pas encore été enregistré, il faut chercher la liste dans la zone d’index. Donc au lieu de :

files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`

on utilise :

files_modified = `git diff-index --cached --name-only HEAD`

Mais à ces deux différences près, le script fonctionne de manière identique. Ce script a aussi une autre limitation : il s’attend à ce que l’utilisateur qui le lance localement soit identique à celui sur le serveur distant. S’ils sont différents, il faudra positionner manuellement la variable $utilisateur.

La dernière action à réaliser consiste à vérifier que les références poussées sont bien en avance rapide, mais l’inverse est plutôt rare. Pour obtenir une référence qui n’est pas en avance rapide, il faut soit rebaser après un commit qui a déjà été poussé, soit essayer de pousser une branche locale différente vers la même branche distante.

Par hypothèse, le serveur est déjà configuré avec receive.denyDeletes et receive.denyNonFastForwards, donc la seule action accidentelle qu’il faut intercepter reste le rebasage de commits qui ont déjà été poussés.

Voici un exemple de script pre-rebase qui fait cette vérification. Ce script récupère une liste de tous les commits qu’on est sur le point de réécrire et vérifie s’ils existent dans une référence distante. S’il en trouve un accessible depuis une des références distantes, il interrompt le rebasage :

#!/usr/bin/env ruby

branche_base = ARGV[0]
if ARGV[1]
  branche_thematique = ARGV[1]
else
  branche_thematique = "HEAD"
end

sha_cibles = `git rev-list #{branche_base}..#{branche_thematique}`.split("\n")
refs_distantes = `git branch -r`.split("\n").map { |r| r.strip }

shas_cibles.each do |sha|
  refs_distantes.each do |ref_distante|
    shas_pousses = `git rev-list ^#{sha}^@ refs/remotes/#{ref_distante}`
    if shas_pousses.split(“\n”).include?(sha)
      puts "[REGLE] Le commit #{sha} a déjà été poussé sur #{ref_distante}"
      exit 1
    end
  end
end

Ce script utilise une syntaxe qui n’a pas été abordée à la section Sélection des versions. La liste des commits déjà poussés est obtenue avec cette commande :

`git rev-list ^#{sha}^@ refs/remotes/#{ref_distante}`
.

La syntaxe SHA^@ fait référence à tous les parents du commit. Les commits recherchés sont accessibles depuis le dernier commit distant et inaccessibles depuis n’importe quel parent de n’importe quel SHA qu’on cherche à pousser. C’est la définition d’avance rapide.

La limitation de cette approche reste qu’elle peut s’avérer très lente et non nécessaire. Si vous n’essayez pas de forcer à pousser avec l’option -f, le serveur vous avertira et n’acceptera pas la poussée. Cependant, cela reste un exercice intéressant qui peut aider théoriquement à éviter un rebasage qui devra être annulé plus tard.

Résumé

Nous avons traité la plupart des moyens principaux de personnaliser le client et le serveur Git pour mieux l’adapter à toutes les méthodes et les projets. Nous avons couvert toutes sortes de réglages de configurations, d’attributs dans des fichiers et de crochets d’événement et nous avons construit un exemple de politique de gestion de serveur. Vous voilà prêt à adapter Git à quasiment toutes les gestions dont vous avez rêvé.

results matching ""

    No results matching ""