← Toutes les fiches

Fiche 4 Parcours : les fondamentaux Lecture : 15 minutes

Git au quotidien

Un historique qui raconte quelque chose, et le calme de savoir qu'on peut presque toujours revenir en arrière.

Beaucoup de développeurs utilisent Git avec cinq commandes apprises par cœur, et une petite angoisse dès que quelque chose sort de l'ordinaire. C'est normal : Git est puissant, et ses messages ne sont pas toujours rassurants.

Cette fiche vise deux choses. D'abord, un historique utile : qui permet de comprendre, d'annuler et de retrouver un bug. Ensuite, la confiance : une fois qu'un travail est commité, il est très difficile de le perdre vraiment.

À propos des exemples de cette fiche

Les commandes Git sont des illustrations : elles n'ont pas été exécutées pour produire cette page, et les identifiants de commit (8b14f72…) sont inventés. Essaie-les d'abord dans un dépôt d'entraînement, comme celui de l'exercice.

1.Des commits à sujet unique

Voici deux historiques du même travail :

exemples/04/history.sh · illustration, non exécuté
# Illustration : un historique tel que l'affiche « git log --oneline »

# ✗ Avant : impossible de savoir ce qui a changé, ni de revenir sur une seule modification
e41b2c9 wip
9a07f13 fix
51cc8d0 encore un fix
2f6e4ab modifs panier + typo + maj symfony

# ✓ Après : un commit = un changement, lisible et annulable séparément
c3d91e0 Offre les frais de port à partir de 50 €
8b14f72 Corrige le total du panier quand une remise s'applique
71ad5e9 Met à jour Symfony en 7.3.4
04f2b1c Corrige une faute dans le libellé du panier

Dans le premier, le commit 2f6e4ab mélange une correction, une faute de frappe et une mise à jour de Symfony. Si la mise à jour casse la production, impossible de l'annuler sans annuler la correction avec. Un commit à sujet unique (on dit aussi atomique) contient un seul changement logique, complet, et le projet fonctionne après lui.

  1. Il s'annule seul : git revert 71ad5e9 défait la mise à jour de Symfony, et rien d'autre.
  2. Il se relit vite en revue de code : un sujet à la fois.
  3. Il rend git bisect efficace (section 6) : le commit fautif désigne un seul changement.

Pour y arriver quand on a modifié plusieurs choses dans le même fichier, git add -p (« par morceaux ») propose chaque modification une par une :

exemples/04/single_topic_commit.sh · illustration, non exécuté
# Illustration : séparer deux changements faits dans le même fichier

git status                 # 1. Qu'est-ce qui a changé ?
git diff                   # 2. Relire chaque modification
git add -p src/Cart.php    # 3. Choisir morceau par morceau (y = oui, n = non, s = découper)
git diff --staged          # 4. Relire ce qui partira dans le commit
git commit                 # 5. L'éditeur s'ouvre pour écrire le message
git add src/Cart.php       # 6. Le reste du fichier, pour le commit suivant
git commit
Sécurité : ce qui part dans un commit y reste

git add . ajoute tout, y compris un .env.local avec un mot de passe de base de données. Un secret commité puis supprimé dans le commit suivant reste dans l'historique, et quiconque clone le dépôt peut le lire.

  1. Relis toujours git diff --staged avant de commiter.
  2. Garde les fichiers sensibles dans le .gitignore. Symfony en fournit un qui exclut déjà les bons fichiers :
exemples/04/gitignore.txt · illustration, non exécuté
# Extrait du .gitignore d'un projet Symfony
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/var/
/vendor/

Si un secret est parti malgré tout, considère-le comme compromis : change-le tout de suite (nouveau mot de passe, nouvelle clé), avant même de nettoyer l'historique. Pour les secrets de production, Symfony propose un coffre chiffré : bin/console secrets:set.

Quand ne pas s'en soucier

Sur une branche personnelle de travail, des commits « wip » réguliers sont une bonne sauvegarde. Ce qui compte, c'est l'historique que tu partages : range-le avant d'ouvrir la pull request (section 5), ou laisse l'outil fusionner la branche en un seul commit si l'équipe travaille ainsi (squash merge).

2.Des messages qui expliquent

Le diff montre ce qui a changé. Le message doit dire pourquoi, comme un bon commentaire (fiche 2).

exemples/04/commit_message.txt · illustration, non exécuté
Offre les frais de port à partir de 50 €

Le service marketing lance l'offre le 1er décembre (ticket SHOP-412).
Le seuil est comparé au total APRÈS remise, comme le demande
la règle validée avec la comptabilité.

Le montant du seuil est une constante pour pouvoir le changer
sans toucher au calcul.
  1. Une première ligne courte (une cinquantaine de caractères), qui résume le changement. C'est elle qu'affichent git log --oneline et les outils de revue.
  2. Une ligne vide, puis un corps qui explique la raison, le contexte, le ticket, les choix faits.
  3. Une convention d'équipe : langue, temps du verbe, préfixes. Certaines équipes utilisent les Conventional Commits (feat:, fix:…). Le plus important est que tout le monde fasse pareil.

3.Une branche par tâche

Une branche est une ligne de travail séparée. Chaque tâche a la sienne, ce qui permet de travailler sur plusieurs choses, de les faire relire séparément, et de les livrer dans n'importe quel ordre.

exemples/04/branch.sh · illustration, non exécuté
# Illustration : une branche par tâche

git switch main                        # partir de la branche principale…
git pull                               # …à jour
git switch -c feature/free-shipping    # créer la branche et s'y placer
# … travail, commits …
git push -u origin feature/free-shipping   # publier la branche, puis ouvrir la pull request

# Une urgence arrive alors que le travail n'est pas prêt à être commité
git stash push -m "frais de port en cours"   # mettre de côté
git switch main
# … correction urgente sur sa propre branche …
git switch feature/free-shipping
git stash pop                                # reprendre là où on en était
  1. Un nom qui dit la tâche : feature/free-shipping, fix/cart-total, ou le numéro du ticket.
  2. Une vie courte : quelques jours plutôt que quelques semaines. Plus une branche vit longtemps, plus elle s'éloigne de main et plus les conflits sont douloureux.
  3. git stash met de côté un travail pas encore prêt à être commité. C'est pratique pour une interruption courte. Pour plus long, un commit « wip » sur la branche est plus sûr : une remise oubliée finit souvent par être perdue.

4.Se mettre à jour : merge ou rebase

Pendant que tu travailles sur ta branche, main avance. Pour intégrer ces nouveautés, deux façons existent :

Merge ou rebase : deux façons d'intégrer main Au départ, main contient A, B, C, et ta branche est partie de B avec les commits D et E. Avec git merge main, un commit de fusion M est ajouté au bout de ta branche, avec deux parents, E et C. Avec git rebase main, D et E sont rejoués après C sous la forme de nouveaux commits D' et E' ; les anciens D et E ne sont plus désignés par aucune branche, mais restent un temps accessibles par le reflog. Situation de départ main ta branche main a avancé (commit C) pendant que tu travaillais sur ta branche (D, E). A B C D E Avec git merge main main ta branche M, le commit de fusion, a deux parents : E et C. L'historique garde les deux lignes parallèles. A B C D E M Avec git rebase main main ta branche D' et E' sont de nouveaux commits : même contenu, identifiants différents. L'historique devient une seule ligne. D et E restent un temps dans le reflog. A B C D E D' E' En bleu : les commits que la commande crée. En pointillés : les anciens commits, que plus aucune branche ne désigne.
Schéma 4.1 · Merge ou rebase : deux façons d'intégrer main
  1. merge ajoute un commit de fusion. L'historique est vrai (on voit que les deux lignes ont existé en parallèle), mais il se remplit de croisements.
  2. rebase rejoue tes commits par-dessus la version à jour. L'historique est linéaire, plus facile à lire et à bisect. Mais tes commits sont recréés avec de nouveaux identifiants (D', E').
exemples/04/rebase.sh · illustration, non exécuté
# Illustration : remettre sa branche à jour par-dessus main

git switch feature/free-shipping
git fetch origin               # récupérer les nouveautés, sans rien modifier chez soi
git rebase origin/main         # rejouer mes commits par-dessus la version à jour

# En cas de conflit, Git s'arrête sur le commit concerné :
git status                     # 1. voir les fichiers en conflit
# 2. ouvrir chaque fichier, garder la bonne version entre <<<<<<< et >>>>>>>
git add src/Cart.php           # 3. marquer le conflit comme résolu
git rebase --continue          # 4. passer au commit suivant
# ou, si ça tourne mal :
git rebase --abort             # tout annuler, retour à l'état d'avant le rebase

# La branche avait déjà été poussée : son historique a changé, il faut forcer
git push --force-with-lease    # refuse si la branche distante a reçu des commits non récupérés
La règle d'or du rebase

Ne rebase jamais des commits que d'autres personnes ont déjà récupérés, en particulier sur main. Leurs copies ne correspondraient plus aux tiennes, et forcer la publication peut écraser leur travail. Sur ta propre branche, c'est sans danger. Pour la republier, utilise --force-with-lease plutôt que --force : il refuse de publier si la branche distante contient des commits que tu n'as pas encore récupérés avec git fetch.

Lequel choisir

C'est une convention d'équipe, pas une question de bien ou de mal. Une pratique courante : rebase pour mettre à jour sa propre branche, et l'outil de pull request pour fusionner dans main. Si tu débutes avec le rebase et qu'une branche est partagée avec un collègue, préfère merge.

5.Ranger avant de partager

Tu as découvert un oubli qui appartient à un commit précédent de ta branche. Plutôt qu'un commit « fix » de plus, Git sait fusionner une correction dans le commit d'origine :

exemples/04/fixup.sh · illustration, non exécuté
# Illustration : corriger un commit précédent avant de partager sa branche

git log --oneline origin/main..HEAD
#   c3d91e0 Offre les frais de port à partir de 50 €
#   8b14f72 Corrige le total du panier quand une remise s'applique

# On découvre un oubli qui appartient au commit 8b14f72
git add src/Cart.php
git commit --fixup 8b14f72      # crée un commit « fixup! Corrige le total… »

# Avant d'ouvrir la pull request, on range tout
git rebase -i --autosquash origin/main
# L'éditeur montre la liste déjà triée : le fixup est placé sous 8b14f72.
# On enregistre et on ferme : les deux commits n'en font plus qu'un.

Ces deux commandes réécrivent l'historique : la règle d'or de la section 4 s'applique. Avant de partager, fais-en autant que tu veux. Une fois sur main, ne touche plus à l'historique : pour annuler un commit publié, git revert crée un nouveau commit qui fait l'inverse.

6.Trouver le commit fautif avec bisect

Le calcul des frais de port marchait il y a trois semaines. Depuis, 200 commits. Lequel l'a cassé ? git bisect applique la recherche dichotomique de la fiche 1 à l'historique : il se place au milieu, tu lui dis si c'est bon ou cassé, et il élimine la moitié restante. Pour 200 commits, il faut environ 8 étapes.

exemples/04/bisect.sh · illustration, non exécuté
# Illustration : trouver le commit qui a cassé le calcul des frais de port

git bisect start
git bisect bad                  # la version actuelle est cassée
git bisect good v2.3.0          # cette version-là fonctionnait

# Git se place au milieu. On teste, puis on répond :
git bisect good                 # « ici, ça marche »
git bisect bad                  # « ici, c'est cassé »
# … Git coupe en deux à chaque réponse, et finit par afficher :
#   8b14f72 is the first bad commit

git bisect reset                # revenir là où on était

# Mieux : laisser un test répondre à ta place (code de sortie 0 = bon, sinon mauvais)
git bisect start HEAD v2.3.0
git bisect run php bin/phpunit tests/Shipping/ShippingCalculatorTest.php
git bisect reset
  1. Donne-lui un commit mauvais (souvent l'actuel) et un commit bon (une version ou un tag où ça marchait).
  2. À chaque étape, teste et réponds good ou bad. Si un commit ne peut pas être testé (il ne compile pas), réponds git bisect skip.
  3. Avec git bisect run, c'est un script qui répond : un code de sortie 0 veut dire « bon », 125 « impossible à tester », et les autres valeurs de 1 à 127 veulent dire « mauvais ». Un test PHPUnit qui reproduit le bug (fiche 3) fait parfaitement l'affaire. S'il n'existait pas encore dans les anciens commits, copie-le hors du dépôt avant de lancer la recherche.
  4. Termine toujours par git bisect reset, pour revenir sur ta branche.

C'est là que les commits à sujet unique (section 1) paient : bisect désigne un commit de dix lignes, pas un commit « modifs panier + typo + maj symfony ».

7.Récupérer une erreur avec reflog

Un reset de trop, une branche supprimée, un rebase raté : les commits semblent avoir disparu. En réalité, Git note dans le reflog (journal des références) chaque position de HEAD, c'est-à-dire chaque endroit où tu t'es trouvé. Un commit « perdu » est encore là, il suffit de retrouver son identifiant.

exemples/04/reflog.sh · illustration, non exécuté
# Illustration : récupérer des commits « perdus » après une fausse manipulation

git reflog
#   4c2e8a1 HEAD@{0}: reset: moving to HEAD~3
#   c3d91e0 HEAD@{1}: commit: Offre les frais de port à partir de 50 €
#   8b14f72 HEAD@{2}: commit: Corrige le total du panier quand une remise s'applique

# 1. Mettre d'abord le bon état à l'abri, sur une nouvelle branche : rien n'est écrasé
git switch -c rescue c3d91e0

# 2. Vérifier que tout est là
git log --oneline -3

# 3. Seulement ensuite, décider quoi en faire (fusionner, renommer la branche…)

Le réflexe est de créer une nouvelle branche sur le bon commit, plutôt que de lancer un reset --hard : une branche ne détruit rien, alors qu'un reset --hard efface aussi les modifications non commitées.

J'ai…Je récupère avec…
fait un reset de tropgit reflog, puis git switch -c rescue <id>
supprimé une branchegit reflog pour trouver son dernier commit, puis une nouvelle branche dessus
raté un rebase en coursgit rebase --abort
terminé un rebase, et le résultat est fauxgit reflog pour retrouver l'état d'avant le rebase
abîmé un fichier, sans commitgit restore <fichier> (attention : ses modifications sont perdues)
publié un commit à annulergit revert <id>
Les limites du reflog

Pièges fréquents

PiègeCe qui arriveLa parade
git add . sans relireUn secret ou un fichier de debug part dans l'historiquegit diff --staged, et un bon .gitignore
Supprimer un secret dans le commit suivantIl reste lisible dans l'historiqueLe changer immédiatement, il est compromis
Un commit « modifs diverses »Impossible à annuler ou à relire séparémentUn commit par sujet, avec git add -p
Rebase de main ou d'une branche partagéeLes copies des collègues ne correspondent plusRebase seulement sur ta propre branche
git push --forceÉcrase le travail poussé par quelqu'un d'autre--force-with-lease
Une branche qui vit trois semainesDes conflits énormes au moment de fusionnerDes branches courtes, mises à jour souvent
reset --hard pour « réparer »Les modifications non commitées sont effacéesUne nouvelle branche sur le bon commit
Un stash oubliéUn travail perdu de vue pendant des semainesUn commit « wip » sur la branche

Checklist avant de pousser

Exercice : casser, chercher, réparer

Fais cet exercice dans un dossier d'entraînement, en dehors de tes vrais projets, par exemple ~/git-entrainement. Tu n'y risques rien.

  1. Crée le dossier, initialise un dépôt avec git init, et ajoute un fichier price.php avec une fonction de frais de port (reprends celle de la fiche 2).
  2. Fais une dizaine de petits commits à sujet unique : un commentaire, un renommage, une constante… Dans l'un d'eux, glisse discrètement un bug (> au lieu de >=).
  3. Pose un tag sur le premier commit, git tag v1, puis retrouve le commit fautif avec git bisect. Combien d'étapes a-t-il fallu ? Est-ce cohérent avec la fiche 1 ?
  4. Recommence avec git bisect run et un petit script de vérification inspiré de check.php (fiche 3), qui se termine par exit(1) en cas d'échec.
  5. Lance git reset --hard HEAD~3, constate que trois commits ont « disparu », puis récupère-les avec git reflog sur une branche rescue.
  6. Sur une nouvelle branche, fais un commit avec un oubli, corrige-le avec git commit --fixup, puis range avec git rebase -i --autosquash.

Note tes surprises et tes blocages : ce sont les meilleures questions pour la revue.

Conseils

  1. Lis ce que Git te dit. Ses messages, surtout pendant un conflit ou un rebase, indiquent presque toujours la commande suivante à taper.
  2. Regarde l'historique en graphe avec git log --oneline --graph --all, ou dans l'outil Git de ton éditeur. Voir les branches aide à comprendre ce que fait chaque commande.
  3. Avant une opération risquée, crée une branche de sauvegarde : git branch backup. Elle ne coûte rien et tu la supprimes après.
  4. Commite souvent, range ensuite. Un commit « wip » protège ton travail. Le rangement vient avant le partage.
  5. N'aie pas peur de demander. Tout le monde a déjà « perdu » un travail dans Git. Dans la grande majorité des cas, le reflog le retrouve.

Documentation officielle