Go to Hackademy website

Les aliases Git de l'extrême

Tom Panier

Posté par Tom Panier dans les catégories outils

Si vous êtes un·e utilisateur·rice un tant soit peu aguerri·e de Git, il y a fort à parier que vous vous êtes mis à utiliser des aliases pour gagner quelques précieuses millisecondes à chaque commande, comme, par exemple, ceux fournis par OhMyZsh si vous en êtes adepte. En général, ces aliases sont conçus pour rester suffisamment génériques pour que chacun y trouve son compte, correspondent plus ou moins directement à des commandes habituellement plus longues, et sont idéalement intégrés à votre configuration locale de Git pour pouvoir être utilisés comme les commandes susmentionnées.

Rien de tout cela ici ! Nous allons retourner l’outil dans tous les sens et user de magie noire, pour emprunter de dangereux raccourcis et placer d’immenses pouvoirs au bout de nos petits doigts… Ces aliases ne plairont guère à tous, et ce n’est pas le but ; mais peut-être pourront-ils au moins vous inspirer les vôtres. Vous êtes prêt·e ? C’est parti !

git status, c’est pour les faibles

Personnellement, j’aime pouvoir visualiser en un coup d’œil la liste de mes branches locales, évidemment mises à jour par rapport à la remote ; cela me paraît important pour pouvoir donner du contexte à cette commande peu parlante qu’est git status. J’ai donc progressivement délaissé ce dernier au profit de state :

alias state='git fetch --prune ; git fetch --tags ; clear && git branch -vv && git status'
  • Mise à jour des branches, en indiquant celles qui n’existent plus en remote
  • Mise à jour des tags
  • Nettoyage de l’écran
  • Affichage de la liste des branches locales, avec un niveau de détail suffisant
  • git status (enfin)

git pull, c’est pour les poussins

La stratégie par défaut de git pull (le merge) me paraît inadéquate : c’est à mon dépôt local de s’adapter aux modifications distantes, et pas l’inverse. J’ai donc systématisé l’usage de la stratégie du rebase via update :

git-update() {
  branch=$1

  if [[ -z $branch ]]
  then
    branch=`git symbolic-ref HEAD | cut -d "/" -f 3-`
  else
    git checkout $branch
  fi

  git pull --rebase origin $branch
}

alias update='git-update'
$ update      # git pull --rebase sur la branche courante
$ update toto # git pull --rebase sur la branche toto, après s'être placé dessus

Il n’y a qu’un petit problème : si j’ai des modifications locales, Git exigera que j’en dispose avant de pouvoir lancer cette commande. J’ai donc mis en place une seconde version qui va les mettre au préalable en stash, puis les en ôter en fin de traitement :

git-updates() {
  git stash && git-update "$@" && git stash pop
}

alias updates='git-updates'

Dès lors, je n’ai qu’à ajouter un s à ma commande en cas de besoin. Vous constaterez que c’est un schéma qui se répète dans ma collection d’aliases.

git tag -l, c’est pour les murs de ta cité

Je n’ai pas grand-chose à reprocher à cette commande, si ce n’est l’ordre dans lequel les tags apparaissent : un tri bête et méchant qui ne va pas vraiment être logique en passant de v0.1 à v0.10. J’ai aussi un autre besoin : savoir quels sont les commits de ma branche courante depuis le dernier tag, afin de savoir ce qu’embarquera la « prochaine release » de mon projet.

alias tagz='git tag -l | sort -V'
alias nxtrlz='tagz | tail -n 1 && git log --oneline `tagz | tail -n 1`..HEAD'
$ tagz   # mes tags, triés comme il se doit
$ nxtrlz # le dernier tag, ainsi que la liste des commits ayant eu lieu depuis sur la branche courante

Alors, je change de branche, je mets à jour, je supprime l’autre… RAAAH !

Qui n’a jamais fait cette action 17 536 fois durant la même journée ? Votre MR vient d’être mergée, après avoir promis que vous ferez plus attention au coding style la prochaine fois, et vous devez donc, dans l’ordre :

  • vous placer sur la branche sur laquelle vous venez de merger (au hasard, master)
  • mettre cette dernière à jour
  • supprimer votre branche de développement locale, car vous êtes une personne consciencieuse

C’est vite rébarbatif, laissons merged faire le boulot :

git-merged() {
  sourceBranch=`git symbolic-ref HEAD | cut -d "/" -f 3-`
  targetBranch=$1

  if [[ -z $targetBranch ]]
  then
    targetBranch='master'
  fi

  git-update $targetBranch && git branch -d $sourceBranch
}

alias merged='git-merged'
$ merged         # se place sur master, la met à jour, et vire la branche sur laquelle vous étiez
$ merged develop # pareil, mais avec develop au lieu de master

Évidemment, la petite sœur mergeds existe également :

git-mergeds() {
  git stash && git-merged "$@" && git stash pop
}

alias mergeds='git-mergeds'

J’ai besoin du hash de mon dernier commit… je pose mon aprèm’

git-lstcmt() {
  git log --oneline $1 | grep -v fixup | head -n 1 | cut -d " " -f 1
}

alias lstcmt='git-lstcmt'
$ lstcmt      # affiche le hash du dernier commit de la branche courante
$ lstcmt toto # affiche le hash du dernier commit de la branche toto

git log, c’est trop long

git-logz() {
  nb=$2

  if [[ -z $nb ]]
  then
      nb=10
  fi

  git log --oneline $1 | head -n $nb
}

alias logz='git-logz'
$ logz         # les 10 derniers commits de la branche courante, un par ligne (hash + message)
$ logz toto    # pareil sur la branche toto
$ logz HEAD 20 # les 20 derniers commits de la branche courante
$ logz toto 20 # pareil sur la branche toto

« Ingrid, est-ce que tu rebases ? »

Aaah, le rebase… la seule évocation de ce mot effraie les développeurs débutants. Une erreur classique consiste à ne pas mettre à jour la branche sur laquelle on veut se baser pour effectuer cette opération : rebase règle ce problème !

git-rebase() {
  sourceBranch=`git symbolic-ref HEAD | cut -d "/" -f 3-`
  targetBranch=$1

  if [[ -z $targetBranch ]]
  then
    targetBranch='master'
  fi

  git-update $targetBranch && git checkout $sourceBranch && git rebase $targetBranch
}

alias rebase='git-rebase'
$ rebase         # met à jour master et rebase la branche courante dessus
$ rebase develop # pareil, mais avec develop au lieu de master

Le propre du rebase, c’est qu’on rencontre parfois des conflits ; et une fois ceux-ci résolus, qu’y a-t-il de plus rageant que de parfois devoir saisir deux, voire trois commandes pour passer à l’étape suivante ? On aurait parfois simplement envie de dire à Git… de lui dire… oui, c’est ça, goon (« go on ») !

alias goon='git add . && git rebase --continue || git rebase --skip'

Et si vous avez des modifications locales ? Bingo, rebases !

git-rebases() {
  git stash && git-rebase "$@" && git stash pop
}

alias rebases='git-rebases'

« Oui, mais seulement en interactif ! »

La version interactive du rebase, tout aussi utile en d’autres circonstances, n’a pas échappé à ma fourberie ; étant donné que je m’en sers moi-même principalement pour fusionner des commits ensemble, la commande idoine se nomme squash :

git-squash() {
  ref=$1

  if [[ -z $ref ]]
  then
    ref=`git log --oneline | grep -v fixup | head -n 2 | tail -n 1 | cut -d " " -f 1`
  fi

  git rebase -i --autosquash $ref
}

alias squash='git-squash'

Je vais prendre un moment pour décomposer cette commande :

  • elle peut prendre en paramètre une référence, tel un hash ou autre HEAD~2
  • si la référence n’est pas fournie, on va s’appuyer sur le plus ancien commit ne contenant pas « fixup »

Euh, pourquoi ?

Déjà, cesse de m’interrompre. Ensuite, c’est parce qu’on va tirer parti de cette fonctionnalité absolument magistrale du rebase interactif : l’autosquash. Cette dernière va automatiquement « préremplir » votre éditeur dans le contexte du rebase interactif, en mettant tout seul s pour les commits dont le message commence par squash!, et f pour ceux dont le message commence par fixup!, et en mettant tout ce petit monde dans le bon ordre. La cerise sur le gâteau, c’est qu’il est possible d’automatiquement créer de tels commits, plutôt que de le faire à la main… et la pastèque sur la cerise, c’est, vous l’aurez deviné, que j’ai non pas un, mais deux aliases pour ça !

git-fixup() {
  ref=$1

  if [[ -z $ref ]]
  then
    ref=`lstcmt`
  fi

  git commit --fixup $ref
}

alias fixup='git-fixup'

En effet, git commit --fixup exige qu’on lui passe une référence explicite pour savoir quel commit ajuster, alors qu’on veut bien souvent cibler le dernier.

$ fixup          # crée un commit d'ajustement destiné au dernier commit
$ fixup abcd1234 # idem, en ciblant le hash fourni

Ce workflow est particulièrement efficace pour répartir des modifications locales sur plusieurs commits de votre historique : vous utilisez git add et fixup pour créer vos commits d’ajustements, puis squash pour lancer le rebase interactif, sachant que vous pourrez immédiatement sauvegarder et quitter l’éditeur, tout étant déjà prémâché dedans !

Dis donc, tu as parlé de deux aliases, non ?

Finement observé ! Voici le second :

alias fixupa='adal && fixup'

wat

Justement, j’y venais, excellente transition !

git add, git commit et git push, c’est pour les noobs

On va finir en beauté avec les aliases les plus simples et élégants, mais surtout les plus efficaces de ma collection, et qui sont sûrement ceux que j’utilise le plus au quotidien après state.

Commençons par un cas d’usage courant : ajouter toutes vos modifications locales à la zone de staging, y compris les nouveaux fichiers et ceux que vous avez supprimé. C’est le susmentionné adal qui joue ce rôle :

alias adal='git add --all .'

En grand maniaque de l’historique devant l’Éternel, j’apprécie aussi de pouvoir facilement ajouter ce qui se trouve dans cette zone de staging au dernier commit sans changer son message ; cet alias-ci se nomme, fort logiquement, amend, et est accompagné d’une variante - amenda - qui le précède d’adal.

alias amend='git commit --amend --no-edit'
alias amenda='adal && amend'

Enfin, puisque nous avons évoqué plus haut le rebase, comment ne pas parler de git push --force ? J’ai aussi un alias pour ça (toute ressemblance avec un slogan publicitaire célèbre serait purement fortuite), dont le nom reflète bien sa philosophie : yolo !

alias yolo='git push origin HEAD --force-with-lease'

Pour ceux d’entre vous qui ne la connaîtraient pas, l’option --force-with-lease va faire échouer le push si la remote contient des commits que vous n’avez pas récupérés en local : pratique pour éviter les erreurs d’inattention ! Terminons avec deux variantes fort sympathiques :

alias yoloc='amend && yolo'
alias yoloca='adal && yoloc'
$ yoloc  # amende le dernier commit avec le contenu de la zone de staging, et pousse le tout en force
$ yoloca # idem, mais ajoute d'abord vos modifications locales à la zone de staging ; à utiliser avec prudence !

En conclusion

Vous voici désormais en possession de mes plus sombres secrets - en ce qui concerne Git, en tout cas. Je n’ai qu’un conseil supplémentaire à vous donner, qui vaut pour tous les aliases, quels qu’ils soient : ce n’est pertinent de les utiliser que quand vous savez parfaitement ce qu’ils font, sans quoi un accident est vite arrivé. De plus, vous vous retrouverez fort dépourvu si vous devez intervenir sur la machine d’un·e collègue, par exemple. À bon entendeur, salut !


L’équipe Synbioz.
Libres d’être ensemble.

Articles connexes

Un plugin Vim à la mimine

03/01/2019

Dans l’article précédent, intitulé une assez bonne intimité, je vous présentais GPG et le chiffrement de courriels. Nous avons alors remarqué que le contenu d’un courriel était encodé de sorte que le...

Une assez bonne intimité

20/12/2018

Si vous êtes utilisateur de MacOS, il y a de fortes chances que vous utilisiez Apple Mail pour échanger des courriels. Et comme vous êtes sensible à la confidentialité des informations que vous...

Tests end to end avec Jest et Puppeteer

05/07/2018

Dans cet article, on va partir sur des tests end to end. Pour planter le décor, un test end to end (e2e) est un test logiciel qui a pour but de valider que le système testé répond correctement à un...

Chasser les requêtes N+1 avec Bullet

05/04/2018

Aujourd’hui nous allons parler des requêtes N+1 dans une application Rails : vous savez ces lignes quasiment identiques, qui s’ajoutent de manière exponentielle aux logs, dès lors que l’on appelle...