Git avancé : rebase, cherry-pick, bisect et stash
Réécrire un historique propre, récupérer un commit perdu, trouver le commit qui a cassé le build : les commandes Git qui servent après les bases.
Vous savez faire add, commit, push, pull, créer une branche et ouvrir une pull request. Ça couvre 80 % du quotidien. Les 20 % restants arrivent toujours au mauvais moment : un correctif urgent à appliquer sur une autre branche, un historique de quinze commits « wip » à présenter en revue, un bug apparu quelque part dans les deux cents derniers commits.
Git avancé, ce n'est pas une collection de commandes obscures. C'est une poignée d'outils qui manipulent l'historique : le réordonner, le nettoyer, en extraire un morceau, ou le parcourir pour trouver une régression. Si les bases ne sont pas encore solides, Git et GitHub pour débutants les pose d'abord.
Merge ou rebase : deux façons d'intégrer une branche
Votre branche feature part de main. Pendant que vous travaillez, main avance. Il faut récupérer ces changements.
Avant :
main A──B──C──D
\
feature E──F
Après git merge main (depuis feature) :
main A──B──C──D
\ \
feature E──F──M ← commit de fusion
Après git rebase main (depuis feature) :
main A──B──C──D
\
feature E'──F' ← commits rejoués par-dessus DMerge crée un commit de fusion. L'historique est fidèle à ce qui s'est passé, mais il se remplit de croisements.
Rebase rejoue vos commits un par un au-dessus de la nouvelle base. L'historique devient une ligne droite, lisible avec git log. Mais E' et F' sont de nouveaux commits, avec de nouveaux identifiants.
git switch feature
git rebase main
# en cas de conflit : corriger, puis
git add fichier-corrige.ts
git rebase --continue
# ou tout annuler
git rebase --abortLa règle d'or
Ne jamais rebaser une branche que d'autres personnes ont déjà récupérée. Le rebase remplace des commits par d'autres. Si un collègue a basé son travail sur les anciens, son historique et le vôtre divergent, et la réconciliation est pénible.
En pratique : rebaser sa propre branche de fonctionnalité avant de l'intégrer, oui. Rebaser main, jamais.
Après un rebase d'une branche déjà poussée, le push normal est refusé. On force, mais prudemment :
git push --force-with-lease--force-with-lease refuse d'écraser la branche distante si quelqu'un d'autre y a poussé entre-temps. --force seul écrase sans vérifier. Je n'utilise jamais le second.
Rebase interactif : nettoyer avant la revue
Le vrai super-pouvoir est rebase -i. Il ouvre la liste des derniers commits et permet de les réorganiser avant de les partager.
git rebase -i HEAD~4pick a1b2c3d Ajoute le formulaire d'inscription
pick e4f5g6h wip
pick i7j8k9l corrige typo
pick m0n1o2p Ajoute la validation des e-mails
# Commandes :
# p, pick = garder le commit
# r, reword = garder mais modifier le message
# s, squash = fusionner avec le précédent, en combinant les messages
# f, fixup = fusionner avec le précédent, en gardant son message
# d, drop = supprimer le commitOn modifie les mots en début de ligne, on peut aussi changer l'ordre des lignes :
pick a1b2c3d Ajoute le formulaire d'inscription
fixup e4f5g6h wip
fixup i7j8k9l corrige typo
pick m0n1o2p Ajoute la validation des e-mailsRésultat : deux commits propres au lieu de quatre. La revue de code devient lisible, et chaque commit a un sens si on doit l'annuler plus tard.
Une astuce qui évite d'y penser après coup : créer directement des commits de correction ciblés.
git commit --fixup a1b2c3d # crée "fixup! Ajoute le formulaire d'inscription"
git rebase -i --autosquash main # les place et les marque automatiquementModifier le dernier commit
Le cas le plus courant ne demande même pas de rebase :
# Oublié un fichier dans le dernier commit
git add fichier-oublie.ts
git commit --amend --no-edit
# Corriger le message du dernier commit
git commit --amend -m "Message correct"Même règle que pour le rebase : --amend crée un nouveau commit. Sans risque avant le push, à éviter sur un commit déjà partagé.
Cherry-pick : récupérer un seul commit
Un correctif a été fait sur develop, mais la production tourne sur main et il faut ce correctif tout de suite, sans le reste de develop.
git switch main
git cherry-pick 4f3a9c2cherry-pick copie le commit indiqué et l'applique sur la branche courante. Plusieurs commits d'un coup :
git cherry-pick 4f3a9c2 8b1d7e0
git cherry-pick a1b2c3d^..f6e5d4c # une plage, a1b2c3d inclusL'option -x ajoute au message une ligne « cherry picked from commit … ». C'est utile pour retrouver l'origine d'un correctif appliqué sur plusieurs branches de maintenance.
Si on se retrouve à cherry-picker régulièrement, c'est souvent le signe d'une stratégie de branches trop complexe. Le cherry-pick reste un outil de dépannage.
Stash : mettre son travail de côté
Vous êtes au milieu d'une modification quand un bug urgent tombe. Le travail en cours n'est pas prêt à être commité.
git stash push -m "formulaire à moitié fait"
git switch main
# ... corriger le bug, commiter, pousser ...
git switch feature
git stash popLes commandes utiles :
| Commande | Effet |
|---|---|
git stash push -m "msg" | Met de côté les modifications suivies |
git stash push -u | Inclut aussi les fichiers non suivis |
git stash list | Liste les stashes |
git stash pop | Réapplique le dernier stash et le supprime |
git stash apply stash@{2} | Réapplique un stash précis sans le supprimer |
git stash drop stash@{0} | Supprime un stash |
Un stash n'est pas fait pour durer. Au-delà d'une journée, une branche temporaire avec un commit « wip » est plus sûre et plus lisible.
Bisect : trouver le commit qui a tout cassé
La fonctionnalité marchait il y a deux semaines. Aujourd'hui, elle est cassée. Entre les deux : 180 commits. git bisect fait une recherche dichotomique : à chaque étape, il coupe l'intervalle en deux. 180 commits demandent environ 8 tests.
git bisect start
git bisect bad # la version actuelle est cassée
git bisect good v2.3.0 # cette version fonctionnait
# Git place le dépôt sur un commit au milieu. On teste, puis :
git bisect good # ou
git bisect bad
# ... jusqu'à ce que Git annonce :
# 7c2e1f0 is the first bad commit
git bisect reset # revenir à la branche de départLe mode automatique est encore plus puissant. Si un script ou un test détecte le bug (code de sortie 0 = OK, autre = cassé), Git fait tout seul :
git bisect start HEAD v2.3.0
git bisect run npm test -- tests/panier.test.tsC'est l'argument le plus convaincant pour écrire un test qui reproduit un bug avant de le corriger. Avec lui, bisect trouve le coupable en quelques minutes, sans intervention.
Reflog : le filet de sécurité
Un reset --hard de trop, une branche supprimée, un rebase raté. Le travail semble perdu. Il ne l'est presque jamais.
git reflog enregistre chaque déplacement de HEAD sur votre machine, y compris ceux qui ne sont plus visibles dans git log.
git reflog3e1f2a9 HEAD@{0}: reset: moving to HEAD~3
b7c8d9e HEAD@{1}: commit: Ajoute l'export CSV
a4b5c6d HEAD@{2}: commit: Ajoute le filtre par dateLe commit « Ajoute l'export CSV » est toujours là. On le récupère :
git branch recuperation b7c8d9e # nouvelle branche sur ce commit
# ou revenir carrément à cet état
git reset --hard b7c8d9eLe reflog est local et conservé environ 90 jours par défaut. Il ne sauve pas un travail jamais commité, mais il sauve à peu près tout le reste.
Annuler proprement : reset, revert, restore
Trois commandes, trois usages :
| Besoin | Commande | Réécrit l'historique ? |
|---|---|---|
| Annuler un commit déjà poussé | git revert <commit> | Non, crée un commit inverse |
| Défaire les derniers commits locaux, garder les modifications | git reset --soft HEAD~2 | Oui |
| Défaire les derniers commits et les modifications | git reset --hard HEAD~2 | Oui, et efface le travail |
| Abandonner les modifications d'un fichier | git restore fichier.ts | Non |
| Retirer un fichier de l'index | git restore --staged fichier.ts | Non |
Sur une branche partagée, revert est la seule option sûre : il ajoute un commit qui annule le précédent, sans réécrire ce que les autres ont déjà récupéré.
Des alias pour aller plus vite
Ces commandes deviennent quotidiennes. Autant les raccourcir, comme on le fait avec les alias Linux et Windows pour le terminal :
git config --global alias.lg "log --oneline --graph --decorate --all"
git config --global alias.amend "commit --amend --no-edit"
git config --global alias.pushf "push --force-with-lease"
git config --global alias.undo "reset --soft HEAD~1"git lg affiche l'arbre complet des branches en une ligne par commit. C'est l'alias que je recommande d'installer en premier : visualiser l'historique rend le rebase beaucoup moins intimidant.
Et dans un pipeline CI ?
Un historique propre a un effet direct sur l'intégration continue. Chaque commit rebasé déclenche un nouveau pipeline, et un commit qui ne compile pas casse bisect pour toute l'équipe. Les règles de protection de branche et les vérifications automatiques décrites dans l'automatisation avec GitHub Actions complètent bien ces pratiques : exiger que les tests passent avant la fusion, et choisir « Squash and merge » ou « Rebase and merge » selon l'historique voulu.
Rebase, cherry-pick, bisect, stash : faut-il vraiment tout maîtriser ? Non. Le rebase interactif et le reflog suffisent à changer la façon de travailler : le premier rend l'historique présentable, le second retire la peur de casser quelque chose. Le reste s'apprend le jour où on en a besoin, et ce jour-là, savoir que bisect existe fait gagner une après-midi.