Leçon 10-2 — Branches Git : première vraie utilisation

1. Objectif

À la fin de cette leçon tu sauras : créer une branche (git switch -c), y développer une fonctionnalité complète, revenir sur main, fusionner (git merge), supprimer la branche mergée, résoudre un conflit de merge en lisant calmement les marqueurs <<<<<<</>>>>>>>, ouvrir une pull request sur ton propre repo, et t'en sortir quand ça part mal (git log, git merge --abort, rapatrier du travail commité au mauvais endroit).

2. Pourquoi c'est important

Depuis le niveau 00, tout ton travail vit sur main — c'était le bon choix (voir GIT_WORKFLOW.md : seul, risque de conflit nul, simplicité d'abord). Ça change maintenant, pour deux raisons. Un : le projet final (10-P) exige que main soit toujours en état de marche pendant que tu développes des fonctionnalités qui, elles, cassent des choses temporairement — la branche est l'outil exact de cette exigence. Deux : dans le monde professionnel, personne ne pousse sur main directement ; la branche + la pull request sont LE geste quotidien du métier. Cette leçon est ta répétition générale, sur ton propre repo, sans pression.

3. Explication simple

Une branche, c'est un chantier parallèle. main est la maison habitable ; quand tu veux ajouter une véranda, tu ne démolis pas le salon — tu montes la véranda à côté, tu vérifies qu'elle tient, PUIS tu la raccordes à la maison. Le raccordement, c'est le merge.

Le cycle complet, quatre commandes :

git switch -c feat/report-command   # 1. ouvrir le chantier (créer + y aller)
# ... on code, on committe normalement, autant de fois que nécessaire ...
git switch main                     # 2. revenir à la maison
git merge feat/report-command       # 3. raccorder le chantier
git branch -d feat/report-command   # 4. démonter les échafaudages

Pendant que tu es sur la branche, main ne bouge pas d'un octet : tu peux expérimenter, casser, recommencer — la maison reste habitable. Et si deux chantiers ont touché le même mur, Git te demande d'arbitrer : c'est le conflit. Il est bruyant mais bénin — Git ne perd jamais rien, il attend juste ta décision.

4. Explication approfondie

a) Une branche est un post-it sur un commit. Souviens-toi de l'image de la leçon 01-1 : une variable est un post-it sur une valeur. Une branche, c'est exactement pareil : un post-it sur un commit. Rien de plus — pas de copie de dossier, pas de duplication de fichiers. Quand tu committes sur une branche, son post-it avance sur le nouveau commit. HEAD, c'est le post-it qui dit « tu es ici ». Créer une branche est donc instantané et gratuit — c'est pour ça que les développeurs en créent des dizaines sans y penser.

b) Deux sortes de merge. Si main n'a pas avancé pendant ta branche, Git se contente de faire glisser le post-it main en avant : c'est le fast-forward (tu le reconnaîtras au mot Fast-forward dans la sortie). Historique parfaitement linéaire, aucun commit créé. Si main a avancé (toi-même, sur un autre sujet), Git crée un commit de merge à deux parents qui réunit les deux lignées (Merge made by the 'ort' strategy.ort est simplement le nom de l'algorithme de fusion). Les deux cas sont normaux ; tu n'as rien à choisir, Git décide seul.

c) Le conflit, démystifié. Un conflit survient quand les deux lignées ont modifié les mêmes lignes d'un même fichier. Git fusionne tout le reste automatiquement (Auto-merging ...) et, pour les lignes disputées, écrit les DEUX versions dans le fichier, encadrées de marqueurs, puis s'arrête et t'attend. Résoudre = ouvrir le fichier, choisir (l'une, l'autre, ou un mélange), supprimer les trois lignes de marqueurs, git add, git commit. C'est tout. Le premier conflit fait peur, le troisième est une formalité — le README du niveau te l'a promis, cette leçon te le prouve en te faisant en provoquer un exprès.

d) La discipline « une branche par fonctionnalité ». Convention de nom : un préfixe + un slug descriptif — feat/export-csv, fix/negative-amount. (Tu verras aussi feature/ ailleurs ; ce repo utilise feat/, comme la fiche du projet final.) Règles : la branche naît de main, vit au plus 2-3 jours, et meurt aussitôt mergée. Une branche = une issue de ton cadrage 10-1. Et on ne commence JAMAIS la branche suivante sans être d'abord revenu sur main — sinon la branche 2 embarque la branche 1.

e) La pull request sur ton propre repo. Sur GitHub, au lieu de merger localement, tu peux pousser la branche (git push -u origin feat/...) et ouvrir une pull request : une page qui montre le diff complet et où tu cliques « Merge ». Seul, ça semble cérémonieux — mais ça t'offre une relecture de ton propre diff avant merge (tu y attraperas des print() de débogage oubliés, promis) et c'est le geste exact du travail en équipe. Recommandé au moins deux ou trois fois pendant le projet final ; localement, le simple git merge reste parfaitement valable.

Difficulté honnête : les commandes de cette leçon s'apprennent en une heure. Ce qui prend des semaines, c'est le réflexe — ne pas retomber sur main « juste pour un petit truc ». Prévois de te tromper : la section 6 montre précisément comment réparer chaque écart. Et rappelle-toi que tout ce qui est commité est récupérable ; avec Git, on ne perd du travail que non-commité.

5. Exemples commentés

Tous les exemples utilisent un mini-projet budget.py. Les sorties sont réelles (Git 2.53 ; les hashs seront différents chez toi, c'est normal).

Exemple 1 — créer une branche et voir où on est

git switch -c feat/report-command
Switched to a new branch 'feat/report-command'
git branch
* feat/report-command
  main

L'étoile marque la branche courante (HEAD). git switch -c = créer ET y aller ; git switch (sans -c) = aller sur une branche existante. (git checkout fait la même chose dans les vieux tutos — switch est la commande moderne, ne mélange pas les deux.)

Exemple 2 — développer sur la branche, merger en fast-forward

Sur la branche, on ajoute une fonction report() à budget.py et on committe normalement (le cycle de GIT_WORKFLOW.md ne change pas). Puis :

git switch main
git merge feat/report-command
Updating a2a275e..7e5ef6f
Fast-forward
 budget.py | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

main n'avait pas bougé : Git fait simplement avancer le post-it main (fast-forward), aucun commit de merge. On nettoie :

git branch -d feat/report-command
Deleted branch feat/report-command (was 7e5ef6f).

Supprimer la branche ne supprime AUCUN commit : ils sont dans main maintenant. On ne jette que le post-it.

Exemple 3 — un conflit, du début à la fin

Mise en scène : sur la branche feat/csv-export, on modifie la ligne du rapport pour afficher le nombre de dépenses ; pendant ce temps sur main, on modifie LA MÊME ligne pour mettre le label en majuscules. Puis :

git switch main
git merge feat/csv-export
Auto-merging budget.py
CONFLICT (content): Merge conflict in budget.py
Automatic merge failed; fix conflicts and then commit the result.

Pas de panique — lis : Git a fusionné ce qu'il pouvait (Auto-merging) et te demande d'arbitrer le reste. git status confirme et donne la marche à suivre :

On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   budget.py

Le fichier contient maintenant les deux versions, encadrées :

def report(expenses):
<<<<<<< HEAD
    return f"TOTAL: {total(expenses):.2f} EUR"
=======
    return f"Total: {total(expenses):.2f} EUR ({len(expenses)} expenses)"
>>>>>>> feat/csv-export

Lecture : entre <<<<<<< HEAD et =======, la version de la branche où tu es (main) ; entre ======= et >>>>>>> feat/csv-export, la version de la branche mergée. Ici on veut les DEUX changements (majuscules + compteur), on écrit donc la ligne fusionnée à la main et on supprime les trois marqueurs :

def report(expenses):
    return f"TOTAL: {total(expenses):.2f} EUR ({len(expenses)} expenses)"

Puis on scelle la résolution :

git add budget.py
git commit -m "merge: combine uppercase label and expense count"
[main 1dea606] merge: combine uppercase label and expense count

C'est fini. Relance ton programme et tes tests pour vérifier la fusion — Git garantit le texte, pas la logique.

Exemple 4 — voir l'histoire à deux lignées

git log --oneline --graph
*   1dea606 merge: combine uppercase label and expense count
|\
| * 8411529 project: show expense count in report
* | f3fed4c project: uppercase report label
|/
* 7e5ef6f project: add report command
* a2a275e project: add budget skeleton

Se lit de bas en haut, comme un traceback : histoire commune, puis deux lignées parallèles (la branche à droite, main à gauche), réunies par le commit de merge à deux parents. Cette commande est ta carte : à utiliser dès que tu es perdu, AVANT de taper quoi que ce soit d'autre.

Exemple 5 — la même chose via pull request (sur ton propre repo)

git switch -c feat/monthly-report
# ... commits ...
git push -u origin feat/monthly-report

GitHub affiche alors un bandeau « Compare & pull request » sur la page du repo. Clique, relis le diff en entier (c'est le vrai bénéfice), crée la PR, puis « Merge pull request ». Ensuite, localement :

git switch main
git pull                              # rapatrie le merge fait sur GitHub
git branch -d feat/monthly-report

6. Erreurs fréquentes

1) Coder sur main par vieux réflexe — l'erreur emblématique du niveau.

Tu as déjà modifié des fichiers (voire commité) en oubliant la branche. Réparation selon le cas :

# Cas A — modifs NON commitées : il suffit de créer la branche,
# les modifications en cours te suivent :
git switch -c feat/oops

# Cas B — déjà commité sur main (1 commit, pas encore poussé) :
git branch feat/rescue        # pose un post-it sur le commit fautif
git reset --soft HEAD~1       # main recule d'un commit (modifs conservées) — vu dans GIT_WORKFLOW
git stash                     # met les modifs de côté
git switch feat/rescue        # va sur la branche qui a le commit
git stash drop                # jette la copie de côté (le commit l'a déjà)

(Le cas B se comprend avec l'image du post-it : on colle un post-it de sauvetage AVANT de faire reculer main.) Si c'est déjà poussé : git revert, comme toujours (GIT_WORKFLOW.md).

2) Laisser les marqueurs de conflit dans le fichier.

python budget.py
  File "budget.py", line 6
    <<<<<<< HEAD
    ^^
SyntaxError: invalid syntax

Tu as commité (ou exécuté) sans avoir retiré <<<<<<</=======/>>>>>>>. Correction : rouvrir le fichier, finir la résolution, recommitter. Prévention : toujours relancer le programme ET chercher <<< dans le fichier avant le git add de résolution.

3) Fuir le conflit : supprimer la branche et tout retaper. Tu perds ton travail et tu n'apprends rien — le conflit revient au prochain merge. Correction : si le conflit te prend de court, git merge --abort remet tout comme avant le merge (regarde : git status te le proposait). Respire, relis la section 5 exemple 3, retente. Le conflit se résout, il ne se contourne pas.

4) Supprimer une branche non mergée.

git branch -d feat/unfinished
error: the branch 'feat/unfinished' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D feat/unfinished'

Git te protège : cette branche contient des commits que main n'a pas. -D (majuscule) force la suppression — ne l'utilise que si tu veux VRAIMENT jeter ce travail. Dans le doute : merger d'abord, ou garder la branche.

5) Changer de branche avec des modifications non commitées. Souvent ça passe (Git emporte tes modifs si elles ne gênent pas), mais quand le fichier modifié diffère entre les deux branches :

error: Your local changes to the following files would be overwritten by checkout:
	wip.py
Please commit your changes or stash them before you switch branches.
Aborting

Correction : committer (même un commit d'étape) avant de changer de branche. git stash existe aussi (mise de côté temporaire), mais un petit commit sur ta branche est plus simple et plus sûr à ton stade.

6) Empiler une branche sur une branche. Tu finis feat/a, et tu tapes git switch -c feat/b sans repasser par main : feat/b contient tout feat/a, et les merges deviennent confus. Correction : le rituel est toujours git switch main → merge → git switch -c feat/suivant. Vérifie avec git branch (l'étoile) avant chaque switch -c.

7. Lire les messages d'erreur

Les « tracebacks » de Git sont ses messages error:/CONFLICT, et ils se lisent aussi méthodiquement qu'un traceback Python — Git dit presque toujours QUOI faire dans le message lui-même.

Le message de conflit, ligne par ligne :

Auto-merging budget.py                                    ← Git a fusionné ce qu'il pouvait
CONFLICT (content): Merge conflict in budget.py           ← type (content) + fichier concerné
Automatic merge failed; fix conflicts and then commit the result.
                                                          ← la marche à suivre est ÉCRITE LÀ

Puis git status est ton traceback complet : section Unmerged paths = la liste exacte des fichiers à arbitrer, et entre parenthèses les deux issues possibles (git add pour valider ta résolution, git merge --abort pour tout annuler). Pendant un conflit, prends l'habitude : git status après chaque geste.

Les marqueurs dans le fichier, de haut en bas :

<<<<<<< HEAD              ← début de TA version (branche courante)
=======                   ← frontière
>>>>>>> feat/csv-export   ← fin de LEUR version (branche mergée), avec son nom

Enfin, quand tu es perdu (« je suis sur quelle branche ? qu'est-ce qui a été mergé ? ») : la réponse n'est jamais « taper des commandes au hasard » — c'est git status + git log --oneline --graph, puis seulement agir. En dernier recours, le prompt « SOS Git » de PROMPTS.md, avec la sortie de ces deux commandes en contexte.

8. Exercices — faciles

Règles : ces exercices se font dans un repo d'entraînement JETABLE, hors du repo learnpython (un repo dans un repo = ennuis garantis). Crée-le par exemple dans ~/Claude/git-dojo/ avec git init. À la fin, copie les sorties demandées dans lessons/level-10-autonomie/exercices/mes-reponses/ex-10-2-a.md, etc. Prédis chaque sortie AVANT de taper la commande.

a) Dans le dojo : crée un fichier app.py avec une fonction, committe sur main. Puis déroule le cycle complet de l'exemple 2 : branche feat/greeting, un commit dessus, retour main, merge, suppression de la branche. Colle la sortie du merge et réponds : fast-forward ou commit de merge ? Pourquoi ?

b) Recommence avec une branche feat/farewell, mais AVANT de merger, fais un commit sur main dans un AUTRE fichier (notes.md). Merge. Colle la sortie : pourquoi cette fois Merge made by the 'ort' strategy. et pas Fast-forward ? Y a-t-il eu conflit ? Pourquoi non ?

c) Sur le dojo, lance git log --oneline --graph et recopie le dessin. Annote à la main : où est le commit de merge, où sont ses deux parents, laquelle des deux lignées est ta branche.

9. Exercices — moyens

a) Provoque un vrai conflit, exprès : branche feat/version-a qui modifie la ligne de retour d'une fonction ; sur main, modifie LA MÊME ligne différemment ; merge. Résous en GARDANT UN MÉLANGE des deux versions (pas juste l'une des deux). Colle dans ta réponse : le message CONFLICT, le bloc de marqueurs tel que Git l'a écrit, ta ligne fusionnée, et le git log --oneline --graph final. Piège de la section 6 à éviter : vérifie qu'aucun marqueur ne survit (python app.py doit tourner) avant le commit.

b) Scénario « mauvais réflexe » : fais exprès deux modifications sur main SANS les committer, puis répare avec le cas A de la section 6 (git switch -c feat/oops). Vérifie avec git status et git log que main est propre et que tes modifs sont bien sur la branche. Explique en deux phrases pourquoi ça marche (où étaient les modifs ?).

c) Pousse ton dojo sur GitHub (repo privé), refais une petite branche, et merge-la via une pull request (exemple 5). Dans ta réponse : le lien de la PR, et une chose que la relecture du diff t'a fait remarquer (il y a toujours quelque chose).

10. Exercices — difficiles

a) Scénario « ça part mal », à réaliser puis documenter : tu as commité DEUX commits sur main par erreur (fais-le exprès dans le dojo), rien n'est poussé. Objectif : main revient deux commits en arrière, une branche feat/rescue contient les deux commits, rien n'est perdu. Documente chaque commande et la sortie de git log --oneline sur les deux branches à la fin.

b) Sans exécuter d'abord : dessine sur papier le git log --oneline --graph qu'on obtient après cette séquence — commit A sur main ; branche feat/x avec commit B ; retour main, commit C ; merge de feat/x (commit de merge M) ; branche feat/y avec commit D ; retour main, merge de feat/y. Puis exécute la séquence dans le dojo et compare. (Le dernier merge est-il un fast-forward ? Réfléchis avant de vérifier.)

11. Mini-projet lié — « répétition générale Git »

C'est le mini-projet prévu par le README du niveau : reprends un de TES projets du niveau 07 (le gestionnaire de tâches, par exemple) et ajoute-lui une vraie mini-fonctionnalité (au choix : une commande stats, un tri, un export) entièrement en branche : git switch -c feat/<slug>, commits propres, tests si le projet en a, merge dans main, suppression de la branche. Puis provoque et résous UN conflit exprès : une deuxième branche qui modifie une ligne, la même ligne modifiée sur main, merge, résolution calme. Durée : 30–45 min. À la fin, ton git log --oneline --graph doit montrer au moins un commit de merge — c'est ta preuve de répétition générale avant le projet final.

12. Correction / méthode de correction

Les solutions détaillées sont dans solutions/serie-10-2-solutions.md — avec, pour chaque scénario, la séquence complète de commandes ET les sorties réelles, y compris le conflit résolu de bout en bout et le sauvetage de commits égarés.

Méthode : compare tes SORTIES, pas seulement tes commandes — c'est la sortie qui dit ce que Git a vraiment fait (fast-forward ou ort ? conflit ou pas ?). Les hashs diffèrent forcément ; la structure du graphe, non. Pour chaque écart, note dans ton journal : « je pensais que Git ferait X, il a fait Y, parce que Z ».

13. À retenir

14. Questions de révision

  1. Que contient physiquement une branche Git, et pourquoi sa création est-elle instantanée ?
  2. Dans quel cas un merge est-il fast-forward ? Dans quel cas produit-il un commit de merge ?
  3. Quelles sont les trois lignes de marqueurs d'un conflit, et que trouve-t-on entre chacune ?
  4. Tu es au milieu d'un conflit et tu veux tout annuler : quelle commande, et dans quel état reviens-tu ?
  5. Tu as commité sur main au lieu d'une branche (non poussé) : quelles étapes pour réparer sans rien perdre, et pourquoi commence-t-on par git branch ?
  6. Pourquoi git branch -d refuse-t-il parfois, et que signifie ce refus ?
  7. Quel est l'intérêt d'une pull request quand on travaille seul ?
  8. Pourquoi faut-il repasser par main avant de créer la branche suivante ?

15. Checklist de compréhension

Si une case reste vide : refais les exercices de la section correspondante demain, AVANT d'ouvrir la leçon suivante.

16. Commit conseillé

git add lessons/level-10-autonomie/exercices/mes-reponses/
git commit -m "exercises: complete lesson 10-2 git branches and merges"

Puis, si c'est ta fin de session : entrée de journal + git push.