// -*- mode: doc; mode: flyspell; coding: utf-8; fill-column: 79; -*- == 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. // LocalWords: doc flyspell coding utf fill-column Git git help tar prefix add // LocalWords: HEAD ls-files xargs update-index remove Faites-vous staged tag // LocalWords: reset commits SHA ORIG venez-vous refs reflog log checkout // LocalWords: ago Specifying Revisions rev-parse config gc.pruneexpire days // LocalWords: gc gc.auto run d'UNIX web patch alias.co get-regexp co foo cut // LocalWords: symbolic-ref heads contrib Debian Ubuntu workdir repertoire cd // LocalWords: git-new-workdir merge push hard Branch branch target Clean hook // LocalWords: effacez-les clean qu'inoffensives hooks cp pre-commit.sample // LocalWords: pre-commit chmod grep txt then echo FAIL Untracked post-update // LocalWords: HTTP