Aller au contenu principal
Bonnes pratiques Git · 06

Git
avancé

Réécrire, annuler, récupérer et reporter des commits sans jamais mettre en danger le travail des autres.

Objectif

Maîtriser les commandes qui modifient l’historique et savoir, dans chaque situation, laquelle est sûre.

Le modèle mental : un graphe de commits​

Git construit un graphe orienté de commits. Chaque commit contient un instantané des fichiers, une date, un message et un ou plusieurs parents. Il est identifié par un hash calculé sur l’ensemble de ces éléments.

  • Une branche est un simple pointeur vers un commit, qui avance à chaque nouveau commit.
  • HEAD désigne le commit sur lequel on travaille.
  • Un tag est un pointeur fixe, utilisé pour marquer une version.
  • origin/develop est la copie locale de l’état de la branche distante, mise à jour par git fetch.

Conséquence directe : rebase, commit --amend et reset ne modifient pas les commits existants. Ils en créent de nouveaux, avec de nouveaux hash. Toute personne qui avait récupéré les anciens se retrouve désynchronisée.

Visualiser le graphe est le meilleur réflexe avant toute opération délicate :

git log --all --decorate --oneline --graph

# alias conseillé
git config --global alias.lg "log --all --decorate --oneline --graph"
git lg

:::danger Règle d’or Ne jamais réécrire un historique déjà poussé sur une branche partagée : develop, recette, main, et les branches d’environnement preprod et prod du dépôt GitOps. Sur ces branches, on annule avec git revert, jamais avec reset, rebase ou push --force. :::

Merge ou rebase​

Merge. Git compare les deux branches à leur ancêtre commun (three-way merge). Une ligne modifiée d’un seul côté est reprise automatiquement, une ligne modifiée des deux côtés provoque un conflit. Si la branche cible n’a pas bougé, Git avance simplement le pointeur (fast-forward). --no-ff force un commit de merge.

Rebase. Les commits de la branche sont rejoués un par un sur une nouvelle base. L’historique devient linéaire, mais chaque commit rejoué peut provoquer un conflit.

MergeRebase
HistoriqueFidèle au développement réelLinéaire, plus lisible
Réécrit l’historiqueNonOui
AnnulationSimple (un seul commit de merge)Plus délicate
Usage chez AGITEXIntégration via merge requestMise à jour de sa branche de travail

Le bon compromis : rebaser sa branche sur la cible juste avant la merge request, puis fusionner avec un commit de merge. Côté GitLab, c’est le mode Merge commit with semi-linear history : la fusion n’est possible que si la branche est à jour, et chaque fonctionnalité reste regroupée sous son commit de merge.

# se mettre à jour sans commits de merge parasites
git config --global pull.rebase true
git pull

# remettre sa branche au-dessus de develop avant la MR
git fetch origin
git rebase origin/develop

Pour déplacer une branche construite sur une autre branche de fonctionnalité :

# rejoue sur develop uniquement les commits de feature-b absents de feature-a
git rebase --onto develop feature-a feature-b

Nettoyer sa branche avant la merge request​

Le rebase interactif réécrit en une fois tous les commits postérieurs à une base. Il sert à présenter un historique lisible au relecteur.

git rebase -i origin/develop

Git ouvre la liste des commits, exécutée de haut en bas. On peut réordonner les lignes et changer la commande de chacune :

CommandeEffet
pickGarder le commit tel quel
rewordGarder le commit et modifier son message
editS’arrêter sur le commit pour le modifier
squashFusionner avec le commit précédent en combinant les messages
fixupFusionner avec le commit précédent en gardant uniquement le message de ce dernier
dropSupprimer le commit
execLancer une commande, par exemple les tests, après le commit précédent
attention

Supprimer une ligne de la liste supprime le commit correspondant. En cas de doute, git rebase --abort annule tout le rebase.

Pour corriger un commit précis sans éditer la liste à la main :

git commit --fixup <sha-du-commit-à-corriger>
git rebase -i --autosquash origin/develop

Après un rebase d’une branche déjà poussée, sur sa propre branche uniquement :

git push --force-with-lease

--force-with-lease refuse d’écraser la branche distante si quelqu’un d’autre y a poussé entre-temps, contrairement à --force.

Corriger une erreur​

SituationCommandeRéécrit l’historique
Dernier commit pas encore poussé (oubli, message)git commit --amendOui
Défaire des commits locaux en gardant les changements indexésgit reset --soft HEAD~2Oui
Défaire des commits locaux en gardant les fichiers modifiésgit reset HEAD~2Oui
Tout jeter et revenir à un commitgit reset --hard <sha>Oui, et écrase les fichiers
Commit déjà poussé sur sa branche de fonctionnalitérebase -i puis push --force-with-leaseOui, toléré sur sa branche
Commit déjà poussé sur une branche partagéegit revert <sha>Non : seul choix autorisé
Retirer un fichier de l’indexgit restore --staged <fichier>Non

Sur le dépôt GitOps, un retour arrière d’environnement se fait de la même façon : git revert du commit fautif sur la branche de l’environnement, puis push. ArgoCD réapplique l’état décrit dans Git ; un kubectl rollout undo manuel serait annulé au cycle suivant.

Récupérer un travail « perdu »​

Un commit retiré par reset ou rebase n’est pas effacé immédiatement : il reste accessible par son hash. git reflog liste tous les déplacements de HEAD et permet de le retrouver.

git reflog
# a1b2c3d HEAD@{0}: reset: moving to HEAD~3
# 9f8e7d6 HEAD@{1}: commit: feat(stock): ajouter le seuil d'alerte

git branch sauvetage 9f8e7d6 # recréer une branche sur le commit perdu
# ou
git reset --hard HEAD@{1} # revenir à l'état d'avant le reset

Reporter un correctif : cherry-pick​

git cherry-pick applique un commit précis sur la branche courante. C’est l’outil pour reporter un correctif d’une branche à une autre sans embarquer le reste.

git switch develop
git cherry-pick -x <sha-du-correctif>

L’option -x ajoute au message la référence du commit d’origine, ce qui garde la traçabilité. Des conflits sont possibles, à résoudre comme pour un merge.

Hotfix et versions​

Un hotfix ne contient que la correction d’un défaut critique en production, jamais de nouvelle fonctionnalité. Dans un dépôt applicatif (feature → develop → recette → main) :

  1. Créer la branche depuis le tag de la version en production, ou depuis origin/main s’il n’a pas bougé depuis : git switch -c hotfix/CMA-231-calcul-tva v1.4.0.
  2. Corriger avec un commit minimal et le tester.
  3. Ouvrir une merge request vers main, relue et validée par la CI.
  4. Reporter la correction dans recette et develop, par merge request de reprise ou par cherry-pick -x.

Les versions livrées sont marquées par des tags annotés, au format Semantic Versioning vMAJEUR.MINEUR.CORRECTIF :

git tag -a v1.4.1 -m "Correctif calcul TVA"
git push origin v1.4.1

git tag --list "v1.4.*" # lister les versions
git show v1.4.1 # voir le commit et le message du tag

Boîte à outils du quotidien​

BesoinCommandes
Mettre de côté un travail en coursgit stash push -m "wip formulaire", git stash list, git stash pop
Travailler sur un hotfix sans toucher à sa branchegit worktree add ../cma-hotfix origin/main
Trouver le commit qui a introduit une régressiongit bisect start, git bisect bad, git bisect good v1.3.0, puis git bisect reset
Automatiser la recherche avec les testsgit bisect run ./mvnw -q test
Savoir qui a modifié une ligne et pourquoigit blame -w <fichier>
Trouver quand un texte est apparu ou a disparugit log -S "seuilAlerte" --oneline
Suivre l’historique d’une fonctiongit log -L :calculerTotal:src/.../Facture.java
Suivre un fichier renommégit log --follow <fichier>
Mémoriser la résolution des conflits répétitifsgit config --global rerere.enabled true
astuce

git worktree évite le traditionnel « je stash tout, je change de branche, j’oublie mon stash » : chaque branche a son propre dossier de travail, dans le même dépôt.