Solutions — série 10-2 (branches Git : première vraie utilisation)
Rappel de la règle d'or : on n'ouvre ce fichier qu'après avoir terminé (ou sérieusement séché sur) la série. Particularité de cette série : compare tes sorties autant que tes commandes — c'est la sortie qui dit ce que Git a vraiment fait. Tes hashs seront différents (ils dépendent du contenu, de la date et de l'auteur) ; la structure des sorties et des graphes, elle, doit correspondre. Toutes les sorties ci-dessous sont réelles (Git 2.53, Windows).
Exercice facile a — le cycle complet, merge fast-forward
Raisonnement — On déroule le rituel des quatre commandes de la leçon (section 3). La question piège est à la fin : fast-forward ou commit de merge ? On y répond AVANT de merger, en se demandant : main a-t-il avancé depuis la création de la branche ? Non → fast-forward.
Solution — la séquence dans le dojo, avec les sorties :
git init -b main # dans ~/Claude/git-dojo, HORS du repo learnpython
# ... création de app.py, premier commit sur main ...
git switch -c feat/greeting
Switched to a new branch 'feat/greeting'
# ... on ajoute une fonction greet(), commit ...
git switch main
git merge feat/greeting
Updating a2a275e..7e5ef6f
Fast-forward
app.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
git branch -d feat/greeting
Deleted branch feat/greeting (was 7e5ef6f).
Réponse : fast-forward, et la sortie le dit littéralement. Pourquoi : main n'a reçu aucun commit pendant la vie de la branche ; Git n'a donc rien à fusionner — il fait glisser le post-it main jusqu'au dernier commit de la branche (Updating a2a275e..7e5ef6f : de l'ancien commit au nouveau). Aucun commit de merge n'est créé ; git log --oneline montre une ligne droite.
Pourquoi ça marche — Une branche n'est qu'un post-it sur un commit (leçon, section 4a). Quand l'histoire n'a pas divergé, « fusionner » se réduit à déplacer un post-it — c'est pour ça que c'est instantané et sans risque. Et branch -d ne détruit rien : les commits appartiennent désormais à l'histoire de main.
Erreur classique sur cet exercice — Créer le dojo DANS le repo learnpython (repo dans un repo : Git s'y perd, toi aussi) — l'énoncé le proscrivait. Autre classique : s'étonner que git log ne montre pas de « commit de merge » et croire que le merge a échoué — non : fast-forward = pas de commit de merge, c'est le comportement normal.
Variante plus difficile — Refais le cycle en forçant un commit de merge même sans divergence : git merge --no-ff feat/greeting. Compare les deux git log --oneline --graph. Certaines équipes travaillent en --no-ff systématique pour que chaque feature laisse une « bosse » visible — les deux conventions existent.
Exercice facile b — merge avec divergence, sans conflit
Raisonnement — Cette fois main avance PENDANT la vie de la branche (un commit dans notes.md). Au moment du merge, les deux lignées ont chacune du travail que l'autre n'a pas : le fast-forward est impossible. Conflit ? Non : les deux lignées ont touché des FICHIERS différents.
Solution
git switch -c feat/farewell
# ... commit sur la branche (app.py) ...
git switch main
# ... commit sur main (notes.md) ...
git merge feat/farewell --no-edit
Merge made by the 'ort' strategy.
app.py | 4 ++++
1 file changed, 4 insertions(+)
Réponse aux deux questions : commit de merge (pas fast-forward) parce que main a avancé pendant la branche — Git doit créer un commit à deux parents pour réunir les lignées. Pas de conflit parce qu'aucune ligne n'a été modifiée des deux côtés : branche → app.py, main → notes.md ; Git fusionne ça tout seul.
Pourquoi ça marche — Le conflit ne dépend PAS de la divergence des histoires, mais de la collision des MODIFICATIONS (mêmes lignes, même fichier). Divergence sans collision = commit de merge automatique ; divergence avec collision = conflit à arbitrer. C'est la distinction clé de toute la leçon. (ort est le nom de l'algorithme de fusion de Git moderne — l'information utile de cette ligne est « merge automatique réussi ».)
Erreur classique sur cet exercice — Oublier de faire le commit sur main AVANT le merge (le dérouler après coup) : on obtient alors un fast-forward et l'exercice ne montre plus rien. L'ordre des opérations est le cœur du scénario. Sans --no-edit, Git ouvre aussi un éditeur pour le message du commit de merge — surprise classique : c'est normal, enregistre et ferme.
Variante plus difficile — Prédis puis vérifie : que donne git log --oneline --graph si tu refais ce scénario mais avec DEUX branches mergées successivement dans un main qui avance entre chaque ? Dessine avant d'exécuter.
Exercice facile c — lire le graphe
Raisonnement — Le graphe se lit de bas en haut (comme un traceback). On cherche : le commit à DEUX parents (la jointure |\), et les deux lignées qu'il réunit.
Solution — le graphe du dojo après les exercices a et b, annoté :
* e9513ad Merge branch 'feat/farewell' ← commit de merge : 2 parents
|\
| * 752b084 project: add farewell command ← la BRANCHE (lignée de droite)
* | 463328f docs: add notes ← MAIN pendant ce temps (gauche)
|/ ← ici les lignées se séparent
* 7e5ef6f project: add greeting command ← histoire commune (le ff de l'ex. a
* a2a275e project: add app skeleton a laissé une ligne droite)
Les deux parents du commit de merge sont 463328f (dernier commit de main) et 752b084 (dernier commit de la branche). Remarque que le merge fast-forward de l'exercice a n'a laissé AUCUNE bosse : ligne droite.
Pourquoi ça marche — --graph dessine la topologie réelle des commits : chaque * est un commit, les traits sont les liens de parenté. La bosse |\ ... |/ est la signature visuelle d'une feature mergée avec divergence. Savoir lire ça, c'est savoir répondre à « qu'est-ce qui a été mergé, et quand ? » sans rien casser.
Erreur classique sur cet exercice — Lire de haut en bas et conclure que la branche « part » du commit de merge. C'est l'inverse : en haut, c'est le plus récent. Autre confusion : croire que la colonne de gauche est « toujours main » — c'est vrai ici, mais c'est une convention d'affichage, pas une garantie ; fie-toi aux noms dans les messages de merge.
Variante plus difficile — git log --oneline --graph --all montre aussi les branches NON mergées. Crée une branche avec un commit, ne la merge pas, et regarde le graphe : tu vois maintenant l'état complet du repo, y compris le travail en attente.
Exercice moyen a — le conflit, provoqué et résolu
Raisonnement — Recette d'un conflit : la MÊME ligne modifiée dans les deux lignées. On modifie la ligne de retour d'une fonction sur la branche, puis la même ligne sur main, puis on merge. La consigne impose un MÉLANGE des deux versions : on écrira une troisième ligne, à la main.
Solution — le scénario complet (fichier budget.py, fonction report()) :
git switch -c feat/version-a
# sur la branche : la ligne devient
# return f"Total: {total(expenses):.2f} EUR ({len(expenses)} expenses)"
git commit -am "project: show expense count in report"
git switch main
# sur main : LA MÊME ligne devient
# return f"TOTAL: {total(expenses):.2f} EUR"
git commit -am "project: uppercase report label"
git merge feat/version-a
Auto-merging budget.py
CONFLICT (content): Merge conflict in budget.py
Automatic merge failed; fix conflicts and then commit the result.
Le bloc tel que Git l'écrit dans le fichier :
def report(expenses):
<<<<<<< HEAD
return f"TOTAL: {total(expenses):.2f} EUR"
=======
return f"Total: {total(expenses):.2f} EUR ({len(expenses)} expenses)"
>>>>>>> feat/version-a
La ligne fusionnée (mélange : majuscules DE main + compteur DE la branche), marqueurs supprimés :
def report(expenses):
return f"TOTAL: {total(expenses):.2f} EUR ({len(expenses)} expenses)"
Vérification anti-piège (python budget.py tourne, aucun <<< dans le fichier), puis :
git add budget.py
git commit -m "merge: combine uppercase label and expense count"
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
Pourquoi ça marche — Git ne sait pas CHOISIR entre deux modifications de la même ligne (aucun algorithme ne peut deviner l'intention) ; il te présente donc les deux et attend. La résolution n'a rien de magique : tu édites le fichier pour qu'il contienne ce que TU veux, tu enlèves les marqueurs, et git add signifie « cette version est ma décision ». Le commit qui suit est un commit de merge normal.
Erreur classique sur cet exercice — Choisir bêtement un des deux blocs (le « picking » réflexe) alors que la consigne demandait un mélange — dans un vrai projet, la bonne résolution est souvent une troisième version qui honore les DEUX intentions. Et bien sûr : committer avec un marqueur oublié — d'où la vérification python budget.py avant le git add (un SyntaxError: invalid syntax sur <<<<<<< HEAD sinon).
Variante plus difficile — Refais le scénario et, au milieu du conflit, tape git merge --abort. Vérifie avec git status et git log que tout est revenu comme avant le merge. Puis retente et résous. Savoir qu'on peut TOUJOURS annuler proprement est ce qui rend les conflits non stressants.
Exercice moyen b — le mauvais réflexe, réparé (modifs non commitées)
Raisonnement — Deux fichiers modifiés sur main, rien de commité. Les modifications non commitées n'appartiennent à AUCUNE branche : elles flottent dans le répertoire de travail. Créer une branche maintenant ne déplace donc rien — la branche naît ici, et les modifs flottantes restent en place, prêtes à être commitées dessus.
Solution
# deux fichiers modifiés sur main, PAS commités
git status --short # M app.py / M notes.md
git switch -c feat/oops
Switched to a new branch 'feat/oops'
git status --short # M app.py / M notes.md ← toujours là, intactes
git add app.py notes.md
git commit -m "project: add stats command"
git switch main
git status # nothing to commit, working tree clean
git log --oneline main # le commit n'y est PAS
git log --oneline feat/oops # le commit y est
Explication en deux phrases : les modifications non commitées ne sont attachées à aucune branche — elles vivent dans le répertoire de travail, au-dessus des commits. git switch -c déplace seulement le post-it HEAD ; les modifs flottantes suivent, et le commit qui les fige naît sur la nouvelle branche.
Pourquoi ça marche — C'est la conséquence directe du modèle « une branche = un post-it sur un commit » : tant que rien n'est commité, il n'y a rien à déplacer. C'est aussi pourquoi ce sauvetage est le plus simple des deux (cas A de la leçon) : il suffit de poser le post-it au bon endroit AVANT de figer.
Erreur classique sur cet exercice — Croire qu'il faut « déplacer les modifications » (stash, copies de fichiers…) : ici, rien à déplacer. À l'inverse, une fois les modifs COMMITÉES sur main, ce raccourci ne marche plus — c'est le cas B (voir exercice difficile a). Confondre les deux cas est LA source de manipulations hasardeuses.
Variante plus difficile — Même situation, mais tu veux séparer : une des deux modifs sur la branche, l'autre doit rester sur main. Piste : git switch -c, committer SEULEMENT le premier fichier sur la branche, puis retourner sur main… et constater le problème (la 2e modif t'a suivi). Solution propre : deux commits sur la branche, puis merger seulement… non — le plus simple à ton stade : committer les deux sur la branche, puis reporter la modif « main » à la main. Retiens surtout : séparer APRÈS coup est pénible — d'où la discipline « une branche AVANT de commencer ».
Exercice moyen c — la pull request sur son propre repo
Raisonnement — Le merge se fait sur GitHub au lieu du terminal ; la valeur de l'exercice est la RELECTURE du diff avant de cliquer.
Solution — les étapes (pas de sortie unique ici, ton repo est à toi) :
# le dojo a une remote GitHub (repo privé créé sur github.com, puis :)
git remote add origin https://github.com/<toi>/git-dojo.git
git push -u origin main
git switch -c feat/stats-command
# ... commits ...
git push -u origin feat/stats-command
Sur github.com : bandeau « Compare & pull request » → relire le diff onglet « Files changed », EN ENTIER → « Create pull request » → « Merge pull request ». Puis localement :
git switch main
git pull # rapatrie le merge fait côté GitHub
git branch -d feat/stats-command
Pourquoi ça marche — La PR n'est qu'un merge fait par GitHub, emballé dans une page de relecture. git pull ensuite est indispensable : le commit de merge existe sur GitHub mais pas encore sur ta machine — oublier ce pull est la cause classique du « mais je l'ai mergée, pourquoi mon main local ne l'a pas ?! ».
Erreur classique sur cet exercice — Répondre « rien » à « une chose que la relecture t'a fait remarquer ». Il y a TOUJOURS quelque chose : un print() de débogage, un commentaire périmé, un fichier commité par accident, un nom de variable honteux. Si tu n'as vraiment rien vu, tu as relu trop vite — c'est le muscle que cet exercice entraîne.
Variante plus difficile — Sur la PR suivante, écris une vraie description (quoi, pourquoi, comment tester) et relis-toi 24 h plus tard avant de merger. C'est exactement le rythme d'une équipe — et l'auteur qui relit à froid trouve la moitié des problèmes qu'un collègue aurait trouvés.
Exercice difficile a — sauvetage : deux commits égarés sur main
Raisonnement — Deux commits sur main par erreur, rien de poussé. Plan en trois temps, dicté par la règle « tout ce qui est commité est récupérable tant qu'un post-it pointe dessus » : (1) poser un post-it de sauvetage sur le commit fautif le plus récent ; (2) faire reculer main de deux crans ; (3) aller sur la branche de sauvetage. La seule différence avec le cas B de la leçon : HEAD~2 au lieu de HEAD~1.
Solution — séquence réelle, avec l'état avant :
git log --oneline
0d53b63 project: add stats tests ← égaré n°2
7c761d6 project: add stats command ← égaré n°1
a408b2d project: add app skeleton ← le vrai main
git branch feat/rescue # 1. post-it de sauvetage sur 0d53b63
git reset --soft HEAD~2 # 2. main recule de 2 ; les modifs restent en staging
git stash # 3. on les met de côté (feat/rescue les a déjà en commits)
git switch feat/rescue
git stash drop # 4. la copie de côté est redondante : on la jette
L'état final, vérifié :
git log --oneline main
a408b2d project: add app skeleton
git log --oneline feat/rescue
0d53b63 project: add stats tests
7c761d6 project: add stats command
a408b2d project: add app skeleton
main est revenu deux commits en arrière, feat/rescue contient les deux commits égarés (mêmes hashs : ce sont LES commits, pas des copies), rien n'est perdu. Il ne reste qu'à continuer le travail sur la branche et la merger normalement.
Pourquoi ça marche — git reset --soft HEAD~2 ne détruit aucun commit : il déplace le post-it main deux crans en arrière. Les commits 7c761d6 et 0d53b63 existent toujours — et comme feat/rescue a été posé AVANT le reset, ils restent accrochés à une branche, donc visibles et récupérables. Le stash/stash drop ne sert qu'à nettoyer le répertoire de travail (le --soft y avait laissé les modifs en staging, redondantes avec les commits de la branche).
Erreur classique sur cet exercice — Faire le reset AVANT de poser la branche : les commits deviennent orphelins (récupérables par git reflog, mais c'est un filet de sécurité, pas un plan). L'ordre post-it d'abord / recul ensuite n'est pas négociable. Autre erreur : tenter ça sur des commits POUSSÉS — là, c'est git revert, comme le martèle GIT_WORKFLOW.md.
Variante plus difficile — Version rapide des pros, à comprendre maintenant que le mécanisme est clair : git branch feat/rescue && git reset --hard HEAD~2 && git switch feat/rescue. Le --hard (interdit d'habitude) est acceptable ICI uniquement parce que le post-it de sauvetage protège déjà tout ce que le --hard va balayer du répertoire de travail. Sauras-tu expliquer pourquoi le stash devient inutile dans cette version ?
Exercice difficile b — prédire le graphe
Raisonnement — On déroule la séquence en suivant deux règles : un merge crée une bosse SEULEMENT si main a avancé pendant la branche ; sinon fast-forward, ligne droite. Séquence : A sur main ; feat/x avec B ; C sur main ; merge feat/x → main a avancé (C) → commit de merge M. Puis feat/y avec D ; retour main, merge feat/y → main n'a PAS bougé depuis la création de feat/y (il est resté sur M) → fast-forward, pas de bosse.
Solution — le graphe prédit, confirmé par l'exécution réelle :
* 3716d02 D ← fast-forward : D posé en ligne droite
* 94420b5 Merge branch 'feat/x' ← M, le seul commit de merge
|\
| * 8d62e52 B ← feat/x
* | 9f1a6b3 C ← main pendant feat/x
|/
* 94e89d8 A
Et la sortie du dernier merge le confirme mot pour mot :
Updating 94420b5..3716d02
Fast-forward
Réponse à la question : oui, le dernier merge est un fast-forward — UNE seule bosse dans le graphe, pas deux.
Pourquoi ça marche — Tout se déduit d'une seule question posée au moment de chaque merge : « le post-it main a-t-il bougé depuis la naissance de la branche ? ». Pour feat/x : oui (commit C) → bosse. Pour feat/y : non (personne n'a commité sur main entre M et le merge) → glissade de post-it. Le graphe n'est que la trace de ces deux réponses.
Erreur classique sur cet exercice — Dessiner deux bosses « parce qu'il y a deux merges ». Le nombre de bosses n'est pas le nombre de merges : c'est le nombre de merges AVEC divergence. C'est exactement la confusion que cet exercice sert à dissoudre — si tu l'as faite, refais l'exercice facile a puis b à la suite et compare leurs sorties.
Variante plus difficile — Même séquence, mais avec un commit E sur main entre la création de feat/y et son merge. Prédis le graphe (deux bosses cette fois), puis vérifie. Ensuite, prédis l'effet de --no-ff sur la séquence ORIGINALE : combien de bosses ?
Mini-projet — « répétition générale Git » : points de contrôle
Pas de solution unique (c'est TON projet du niveau 07), mais trois preuves objectives que la répétition est réussie :
git log --oneline --graphmontre au moins un commit de merge (la bosse) et pas UN SEUL commit de feature directement surmain.- Tu peux montrer le commit de merge de ton conflit et réciter : quelles étaient les deux versions, laquelle/quel mélange tu as gardé, pourquoi.
- Le projet tourne encore (lance-le !) et ses tests passent — un merge réussi pour Git peut casser la logique ; la vérification finale est toujours humaine.
Si l'un des trois manque, refais une passe : une branche de plus coûte cinq minutes, et c'est la dernière répétition avant le projet final.