Chapitre 7. La maîtrise de Git

À ce stade, vous devez être capable de parcourir les pages de git help et comprendre presque tout (en supposant que vous lisez l’anglais). En revanche, retrouver la commande exacte qui résoudra un problème précis peut être fastidieux. Je peux sans doute vous aider à gagner un peu de temps : vous trouverez ci-dessous quelques-unes des recettes dont j’ai déjà eu besoin.

Publication de sources

Dans mes projets, Git gère exactement tous les fichiers que je veux placer dans une archive afin de la publier. Pour créer une telle archive, j’utilise :

$ git archive --format=tar --prefix=proj-1.2.3/ HEAD

Gérer le changement

Indiquer à Git quels fichiers ont été ajoutés, supprimés ou renommés est parfois pénible pour certains projets. À la place, vous pouvez faire :

$ git add .
$ git add -u

Git cherchera les fichiers du dossier courant et gérera tous les détails tout seul. En remplacement de la deuxième commande add, vous pouvez utiliser git commit -a pour créer un nouveau commit directement. Lisez git help ignore pour savoir comment spécifier les fichiers qui doivent être ignorés.

Vous pouvez effectuer tout cela en une seule passe grâce à :

$ git ls-files -d -m -o -z | xargs -0 git update-index --add --remove

Les options -z et -0 empêchent les effets secondaires imprévus dûs au noms de fichiers contenant des caractères étranges. Comme cette commande ajoutent aussi les fichiers habituellement ignorés, vous voudrez sûrement utiliser les options -x ou -X.

Mon commit est trop gros !

Avez-vous négligé depuis longtemps de faire un commit ? Avez-vous codé furieusement et tout oublié de la gestion de versions jusqu'à présent ? Faites-vous plein de petits changements sans rapport entre eux parce que c’est votre manière de travailler ?

Pas de soucis. Faites :

$ git add -p

Pour chacune des modifications que vous avez faites, Git vous montrera le bout de code qui a changé et vous demandera si elle doit faire partie du prochain commit. Répondez par "y" (oui) ou par "n" (non). Vous avez aussi d’autres options comme celle vous permettant de reporter votre décision ; tapez "?" pour en savoir plus.

Une fois satisfait, tapez :

$ git commit

pour faire un commit incluant exactement les modifications qui vous avez sélectionnées (les modifications indexées). Soyez certain de ne pas utiliser l’option -a sinon Git fera un commit incluant toutes vos modifications.

Que faire si vous avez modifié de nombreux fichiers en de nombreux endroits ? Vérifier chaque modification individuellement devient alors rapidement frustrant et abrutissant. Dans ce cas, utilisez la commande git add -i dont l’interface est moins facile mais beaucoup plus souple. En quelques touches vous pouvez ajouter ou retirer de votre index (voir ci-dessous) plusieurs fichiers d’un seul coup mais aussi valider ou non chacune des modifications individuellement pour certains fichiers. Vous pouvez aussi utiliser en remplacement la commande git commit --interactive qui effectuera un commit automatiquement quand vous aurez terminé.

L’index : l’aire d’assemblage

Jusqu’ici nous avons réussi à éviter de parler du fameux index de Git mais nous devons maintenant le présenter pour mieux comprendre ce qui précède. L’index est une aire d’assemblage temporaire. Git ne transfert que très rarement de données depuis votre dossier de travail directement vers votre historique. En fait, Git copie d’abord ces données dans l’index puis il copie toutes ces données depuis l’index vers leur destination finale.

Un commit -a, par exemple, est en fait un processus en deux temps. La première étape consiste à construire dans l’index un instantané de l'état actuel de tous les fichiers suivis par Git. La seconde étape enregistre cet instantané de manière permanente dans l’historique. Effectuer un commit sans l’option -a réalise uniquement cette deuxième étape et cela n’a de sens qu’après avoir effectué des commandes qui change l’index, telle que git add.

Habituellement nous pouvons ignorer l’index et faire comme si nous échangions directement avec l’historique. Dans certaines occasions, nous voulons un contrôle fin et nous gérons donc l’index. Nous plaçons dans l’index un instantané de certaines modifications (mais pas toutes) et enregistrons de manière permanente cet instantané soigneusement construit.

Ne perdez pas la tête

Le tag HEAD est comme un curseur qui pointe habituellement vers le tout dernier commit et qui avance à chaque commit. Certaines commandes Git vous permettent de le déplacer. Par exemple :

$ git reset HEAD~3

déplacera HEAD trois commits en arrière. À partir de là, toutes les commandes Git agiront comme si vous n’aviez jamais fait ces trois commits, même si vos fichier restent dans leur état présent. Voir les pages d’aide pour quelques usages intéressants.

Mais comment faire pour revenir vers le futur ? Les commits passés ne savent rien du futur.

Si vous connaissez l’empreinte SHA1 du HEAD original, faites alors :

$ git reset 1b6d

Mais que faire si vous ne l’avez pas regardé ? Pas de panique : pour des commandes comme celle-ci, Git enregistre la valeur originale de HEAD dans un tag nommé ORIG_HEAD et vous pouvez revenir sain et sauf via :

$ git reset ORIG_HEAD

Chasseur de tête

Peut-être que ORIG_HEAD ne vous suffit pas. Peut-être venez-vous de vous apercevoir que vous avez fait une monumentale erreur et que vous devez revenir à une ancienne version d’une branche oubliée depuis longtemps.

Par défaut, Git conserve un commit au moins deux semaine même si vous avez demandé à Git de détruire la branche qui le contient. La difficulté consiste à retrouver l’empreinte appropriée. Vous pouvez toujours explorer les différentes valeurs d’empreinte trouvées dans .git/objects et retrouver celle que vous cherchez par essais et erreurs. Mais il existe un moyen plus simple.

Git enregistre l’empreinte de chaque commit qu’il traite dans .git/logs. Le sous-dossier refs contient l’historique de toute l’activité de chaque branche alors que le fichier HEAD montre chaque valeur d’empreinte que HEAD a pu prendre. Ce dernier peut donc servir à retrouver les commits d’une branche qui a été accidentellement élaguée.

La commande reflog propose une interface sympa vers ces fichiers de log. Essayez:

$ git reflog

Au lieu de copier/coller une empreinte listée par reflog, essayez :

$ git checkout "@{10 minutes ago}"

Ou basculez vers le cinquième commit précédemment visité via :

$ git checkout "@{5}"

Voir la section «Specifying Revisions» de git help rev-parse pour en savoir plus.

Vous pouvez configurer une plus longue période de rétention pour les commits condamnés. Par exemple :

$ git config gc.pruneexpire "30 days"

signifie qu’un commit effacé ne le sera véritablement qu’après 30 jours et lorsque $git gc* tournera.

Vous pouvez aussi désactiver le déclenchement automatique de git gc :

$ git config gc.auto 0

auquel cas les commits ne seront véritablement effacés que lorsque vous lancerez git gc manuellement.

Construire au-dessus de Git

À la manière UNIX, la conception de Git permet son utilisation comme un composant de bas niveau d’autres programmes tels que des interfaces graphiques ou web, des interfaces en ligne de commandes alternatives, des outils de gestion de patch, des outils d’importation et de conversion, etc. En fait, certaines commandes Git sont de simples scripts s’appuyant sur les commandes de base, comme des nains sur des épaules de géants. Avec un peu de bricolage, vous pouvez adapter Git à vos préférences.

Une astuce facile consiste à créer des alias Git pour raccourcir les commandes que vous utilisez le plus fréquemment :

$ git config --global alias.co checkout
$ git config --global --get-regexp alias  # affiche les alias connus
alias.co checkout
$ git co foo                              # identique à 'git checkout foo'

Une autre astuce consiste à intégrer le nom de la branche courante dans votre prompt ou dans le titre de la fenêtre. L’invocation de :

$ git symbolic-ref HEAD

montre le nom complet de la branche courante. En pratique, vous souhaiterez probablement enlever "refs/heads/" et ignorer les erreurs :

$ git symbolic-ref HEAD 2> /dev/null | cut -b 12-

Le sous-dossier contrib de Git est une mine d’outils construits au-dessus de Git. Un jour, certains d’entre eux pourraient être promus au rang de commandes officielles. Dans Debian et Ubuntu, ce dossier est /usr/share/doc/git-core/contrib.

L’un des plus populaires de ces scripts est workdir/git-new-workdir. Grâce à des liens symboliques intelligents, ce script crée un nouveau dépôt dont l’historique est partagé avec le dépôt original.

$ git-new-workdir un/existant/depot nouveau/repertoire

Le nouveau dossier et ses fichiers peuvent être vus comme un clone, sauf que l’historique est partagé et que les deux arbres des versions restent automatiquement synchrones. Nul besoin de merge, push ou pull.

Audacieuses acrobaties

À ce jour, Git fait tout son possible pour que l’utilisateur ne puisse pas effacer accidentellement des données. Mais si vous savez ce que vous faites, vous pouvez passer outre les garde-fous des principales commandes.

Checkout : des modifications non intégrées (via commit) peuvent causer l'échec d’un checkout. Pour détruire vos modifications et réussir quoi qu’il arrive un checkout d’un commit donné, utilisez l’option d’obligation :

$ git checkout -f HEAD^

Inversement, si vous spécifiez des chemins particuliers pour un checkout alors il n’y a pas de garde-fous. Le contenu des chemins est silencieusement réécrit. Faites attention lorsque vous utilisez un checkout de cette manière.

Reset : un reset échoue aussi en présence de modifications non intégrées. Pour passer outre, faites :

$ git reset --hard 1b6d

Branch : la suppression de branches échoue si cela implique la perte de certains commits. Pour forcer la suppression, tapez :

$ git branch -D branche_morte  # à la place de -d

De manière similaire, une tentative visant à renommer une branche existante vers le nom d’une autre branche échoue si cela amène la perte de commits. Pour forcer le changement de nom, tapez :

$ git branch -M source target  # à la place de -m

Contrairement à checkout et reset, ces deux dernières commandes n’effectuent pas la suppression des informations immédiatement. Les commits destinés à disparaître sont encore disponibles dans le sous-dossier .git et peuvent encore être retrouvés grâce aux empreintes appropriées tel que retrouvées dans .git/logs (voir "Chasseur de tête" ci-dessus). Par défaut, ils sont conservés au moins deux semaines.

Clean : certaines commandes Git refusent de s’exécuter pour ne pas écraser des fichiers non suivis. Si vous êtes certain que tous ces fichiers et dossiers peuvent être sacrifiés alors effacez-les sans pitié via :

$ git clean -f -d

Ensuite, la commande trop prudente fonctionnera !

Se prémunir des commits erronés

Des erreurs stupides encombrent mes dépôts. Les plus effrayantes sont dues à des fichiers manquants car oubliés lors des git add. D’autres erreurs moins graves concernent les espaces blancs inutiles ou les conflits de fusion non résolus : bien qu’inoffensives, j’aimerais qu’elles n’apparaissent pas dans les versions publiques.

Si seulement je m’en étais prémuni en utilisant un hook (un crochet) pour m’alerter de ces problèmes :

$ cd .git/hooks
$ cp pre-commit.sample pre-commit  # Vieilles versions de Git : chmod +x pre-commit

Maintenant Git empêchera un commit s’il détecte des espace inutiles ou s’il reste des conflits de fusion non résolus.

Pour gérer ce guide, j’ai aussi ajouté les lignes ci-dessous au début de mon hook pre-commit pour me prémunir de mes inattentions :

if git ls-files -o | grep '\.txt$'; then
  echo FAIL! Untracked .txt files.
  exit 1
fi

Plusieurs opération de Git acceptent les hooks ; voir git help hooks. Nous avons déjà utilisé le hook post-update lorsque nous avons parlé de Git au-dessus de HTTP. Celui-ci se déclenche à chaque mouvement de HEAD. Le script d’exemple post-update met à jour les fichiers Git nécessaires à une communication au-dessus de transports agnostiques tels que HTTP.