// -*- 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*&#160;:

  $ 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*&#160;: 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*&#160;: un reset échoue aussi en présence de modifications non
intégrées. Pour passer outre, faites :

  $ git reset --hard 1b6d

*Branch*&#160;: 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*&#160;: 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
