Git, trucs & astuces

De La Mouche VII


Insérer un commit avant un autre sans problème de merge

Note : je vous conseille de faire une archive de sauvegarde de votre dépôt avant ce genre de manipulation délicate.

Cas pratique : réindentation du code

J'utilise un programme d'intentation automatique, nommé astyle, pour réindenter mon code avant chaque commit. C'est pratique notamment si vous travaillez avec des gens qui utilisent un obscur éditeur sur un obscur système d'exploitation privateur. Bref, j'utilisais astyle avec bonheur, quand un jour, la nouvelle version de ce logiciel fut corrigée pour mieux coller au style GNU (que j'utilise, option --style=gnu).

Naturellement, tous mes fichiers se trouvant réindentés, la beauté des diff entre les commits s'en trouva affectée. Je résolus temporairement le problème en faisant un commit dédié « Reindent with astyle 1.24 », mais ce n'était pas très élégant. Je résolus alors de réindenter tous les commits, à partir d'un commit correspondant au début d'une Grande Réécriture de mon projet.

Mais, pour ne pas mélanger dans ce commit le début de la réécriture de mon code et l'indentation des vieux fichiers (certains devaient être conservés), il fallait néanmoins avoir un commit le précédant, dédié à l'indentation. Ainsi la beauté du diff reviendrait !

Réindentation de tous les commits

Pour effectuer la réindentation sur tous les commits, filter-branch est la solution. Depuis la racine du dépôt, taper :

git filter-branch -d /tmp/filter-branch --tree-filter \
 'cd répertoire/à/réindenter ; make style purge ; cd -' --prune-empty -- 9af224f^..HEAD

Explication :

  • -d /tmp/filter-branch est facultatif, il sert à spécifier un répertoire où seront effectuées les opérations de réécriture. Ma partition /tmp étant en tmpfs (en mémoire vive quoi), les accès sont bien plus rapide que sur un disque dur.
  • J'utilise un Makefile avec la cible style pour effectuer la réindentation (j'aurais pu l'appeler indent), et la cible purge pour nettoyer (j'aurais dû l'appeler dist-clean).
  • répertoire/à/indenter, comme son nom l'indique, est le répertoire où se situe le code à réindenter. Je fais un cd dessus car le filter-branch est obligatoirement lancé depuis la racine du dépôt ; ne pas oublier le cd - à la fin, car le reste des opérations doit se faire à la racine [1]. À noter qu'il aurait été possible d'utiliser l'option -C de make, mais cela ne marchait pas pour tous mes commits, je n'ai pas trop cherché à comprendre pourquoi.
  • --prune-empty sert à supprimer les commits vides. Dans mon cas mon commit de réindentation temporaire (cf. supra) va disparaître.
  • 9af224f^..HEAD signifie que l'on va travailler à partir du commit 9af224f (le commit où mon travail de réécriture a commencé), jusqu'au dernier commit de la branche courante. N'oubliez pas l'accent circonflèxe (9af224f^ et pas simplement 9af224f), car filter-branch ne va travailler que sur le commit suivant celui qu'on spécifie (on passe donc le commit parent du premier commit sur lequel on veut appliquer le filtre).

Création du commit de réindentation avant tous les autres

C'est la partie la plus compliquée. Il va falloir insérer un commit avant 971fb3d (qui est le nouveau SHA-1 de 9af224f). La difficulté est de ne pas avoir à merger successivement chaque commit : c'est inutile car on les a déjà tous réindentés.

Exemple de ce qui ne marche pas :

git rebase -i 971fb3d^
cd répertoire/à/réindenter
make style purge
git commit -a -m "Reindent with astyle 1.24"
git rebase --continue

En effet, git va alors vous signaler qu'il ne peut pas continuer le rebase car il y a conflit. Après avoir mergé à la main, le rebase va à nouveau s'arrêter au commit suivant, et ainsi de suite.

La bonne solution est la suivante :

git rebase -i 971fb3d^^
cd répertoire/à/réindenter
make style purge
git commit -a -m "Reindent with astyle 1.24"
git checkout 971fb3d -- .
git rm fichiers à supprimer # Facultatif
git commit -C 971fb3d
git rebase --continue

Si des fichiers étaient supprimés dans 971fb3d, il faut les supprimer manuellement, le checkout ne le fera pas pour vous.

Vérification

Le nouveau SHA-1 de 971fb3d est 7aae9cd. Pour vérifier que tout est en ordre, on peut extraire les logs de la nouvelle et de l'ancienne version de la branche (vous pouvez trouver les SHA-1 de l'ancienne version dans gitk --all) :

git log --stat 7aae9cd^..HEAD >/tmp/nouvelle.log
git log --stat 9af224f^..a2e6b1d >/tmp/ancienne.log
diff -u /tmp/ancienne.log /tmp/nouvelle.log

Personnellement j'utilise gvimdiff à la place de diff -u, utilisez l'outil de diff que vous préférez. Si tout s'est bien passé, il ne doit y avoir aucune différence, excepté en ce qui concerne :

  • les SHA-1 des commits, qui sont tous différents ;
  • les éventuels commits vides qui ont disparu de la nouvelle branche, comme expliqué plus haut ;
  • des modifications dues au changement de comportement de l'outil de réindentation, dans mon exemple, auquel cas il vaut mieux jeter un œil aux diffs pour s'assurer que tout va bien (et en cas de flemme, gardez une copie de votre dépôt avant modification, au cas où).

Afficher une liste de commits à partir de leurs hachages

Après un git fsck --lost-found, les objets récupérés se trouvent en vrac dans .git/lost-found/commit et .git/lost-found/other. Pour afficher une liste de commits classés par date à la façon de git log --pretty=oneline, on peut procéder comme suit :

cd .git/lost-found/commit
for COMMIT in *
  do git show --encoding=utf8 --quiet --format='%at - %H - %ad - %s' $COMMIT
done | sort | less

Dans le format d'affichage du commit, on utilise %at (date du commit au format « timestamp UNIX ») comme premier champ, pour que le tri fonctionne correctement.



Notes

  1. En fait à la racine du répertoire temporaire, /tmp/filter-branch dans mon cas.