Module L06 · Partie F · Coder comme un professionnel
Le terminal, Git et le projet qui tient dans le temps.
Un programme de 50 lignes tient dans un fichier. Un projet de robot, c’est 40 fichiers, trois personnes, six mois et des versions qu’on doit pouvoir retrouver. Ce module donne les outils que tout développeur utilise chaque jour : la ligne de commande, Git, l’organisation d’un dépôt, l’intégration continue. Il se pratique sur votre PC ; la page montre les commandes et fait tourner un « mini-Git » en Python pour comprendre ce qui se passe sous le capot.
Durée : 2 séances · Prérequis : L01. Objectifs : naviguer et scripter dans un terminal, versionner avec Git (commit, branche, fusion, conflit, dépôt distant), structurer un projet Python (paquets, dépendances, tests, README), automatiser les tests avec GitHub Actions.
Ce que vous saurez faire à la fin
- Créer un dépôt, faire des commits atomiques avec de bons messages, travailler sur des branches, résoudre un conflit.
- Publier un projet sur GitHub avec tests automatiques à chaque push.
- Expliquer ce qu’est un commit (un instantané adressé par son hachage) et pourquoi Git ne perd rien.
Références : Pro Git (gratuit en ligne), MIT « The Missing Semester of Your CS Education », freeCodeCamp « Git and GitHub for Beginners ».
Fiche de cours · Définitions
Les outils du développeur : définitions précises
hypothesis). La couverture mesure les lignes exécutées par les tests — nécessaire, pas suffisante.a | b connecte stdout de a à stdin de b ; > redirige stdout vers un fichier, 2>&1 fusionne stderr dans stdout. Variables d’environnement : héritées par les processus enfants.requirements.txt (ou pyproject.toml) fige les dépendances. Version MAJEUR.MINEUR.CORRECTIF : MAJEUR change = incompatibilité possible ; MINEUR = ajouts compatibles ; CORRECTIF = corrections.breakpoint(), pdb). Profileur : mesure où passe le temps (cProfile) ou la mémoire ; on optimise après avoir mesuré.Fiche de cours · Formules
Commandes et règles à connaître par cœur
| Besoin | Git | Effet sur le graphe |
|---|---|---|
| Voir l’état | git status, git log --oneline --graph --all | — |
| Enregistrer | git add -p puis git commit -m "…" | Nouveau commit, la branche avance |
| Branche | git switch -c feat | Nouveau pointeur sur HEAD |
| Intégrer | git merge feat / git rebase main | Commit à 2 parents / commits recopiés |
| Annuler un commit publié | git revert <sha> | Nouveau commit inverse (l’historique reste) |
| Annuler localement | git reset --soft HEAD~1 ; git restore fichier | Recule la branche / restaure le fichier |
| Retrouver un commit « perdu » | git reflog | Journal des positions de HEAD (≈ 90 jours) |
| Chercher le commit fautif | git bisect start/bad/good | Dichotomie sur l’historique : log₂ n essais |
git bisect 1024 commits → 10 essaisShell : grep -rn motif ., find . -name "*.py", sort | uniq -c | sort -rn, head/tail -f, xargs, time, ps/kill, ssh/scp, chmod +x. Tests : pytest -x -q, pytest --cov, pytest -k nom, fixtures et @pytest.mark.parametrize.
Fiche de cours · Théorèmes et démonstrations
Ce qu’on peut prouver sur ces outils
if entree == x: return faux pour un x non testé). Un test échoué exhibe en revanche une entrée où la spécification est violée : contre-exemple. Les tests par propriétés élargissent la couverture ; la preuve (invariants, L09) est la seule garantie sur toutes les entrées.Fiche de cours · Méthodes
Méthodes de travail
git diff --staged et les tests. Jamais de fichiers générés ni de secrets (.gitignore).cProfile, timeit) ; corriger d’abord l’algorithme (complexité), puis la structure de données, puis seulement les micro-optimisations ; re-mesurer ; garder les tests verts.main), tests (ils montrent l’usage), puis suivre un appel de bout en bout avec le débogueur.Pièges : git push --force sur une branche partagée ; commiter venv/ ou des clés ; tests qui dépendent de l’ordre ou de l’heure ; except: pass qui masque le bug qu’on cherche ; optimiser sans mesurer.
Fiche de cours · Exercices corrigés
Exercices corrigés
main : commits A→B→C. On crée feat depuis C, on y fait D et E ; pendant ce temps main reçoit F. Décrire le graphe après (a) git merge feat depuis main, (b) git rebase main depuis feat puis merge. Quels commits ont changé d’identifiant ?mediane(L) qui doit lever ValueError sur liste vide, et un test par propriétés avec hypothesis.import pytest
from hypothesis import given, strategies as st
from stats import mediane
def test_impair(): assert mediane([3, 1, 2]) == 2
def test_pair(): assert mediane([1, 2, 3, 4]) == 2.5
def test_un(): assert mediane([7]) == 7
def test_vide():
with pytest.raises(ValueError): mediane([])
@given(st.lists(st.floats(allow_nan=False, allow_infinity=False), min_size=1))
def test_entre_min_et_max(L): assert min(L) <= mediane(L) <= max(L)
@given(st.lists(st.integers(), min_size=1))
def test_invariante_par_permutation(L):
import random; M = L[:]; random.shuffle(M); assert mediane(L) == mediane(M)Les deux propriétés (encadrement, invariance par permutation) attrapent la plupart des implémentations fausses (oubli du tri, mauvais indice).list.index appelé 10⁶ fois. Que faire, et quel gain attendre ?list.index est O(n) : 10⁶ appels sur une liste de taille n coûtent O(10⁶·n). Construire une fois un dictionnaire pos = {x: i for i, x in enumerate(L)} (O(n)) et remplacer L.index(x) par pos[x] (O(1)) : coût total O(n + 10⁶). Pour n = 10⁴, gain d’un facteur ~10⁴ sur cette partie, soit un programme ~12 fois plus rapide au total (loi d’Amdahl : le reste, 8 %, devient dominant). Attention aux doublons : index renvoie la première occurrence, le dict doit la conserver (itérer à l’envers ou tester if x not in pos).01 / Terminal
Le terminal : parler à la machine sans intermédiaire
# Se repérer et se déplacer
pwd # où suis-je ?
ls -la # lister (avec fichiers cachés et détails)
cd projets/robot # entrer ; cd .. pour remonter ; cd ~ pour le dossier personnel
mkdir -p src/tests # créer (avec les parents)
cp -r src src_backup # copier ; mv pour déplacer/renommer ; rm -r pour supprimer (irréversible !)
# Lire et chercher
cat fichier.py # afficher ; less pour paginer ; head -n 20 ; tail -f journal.log (suivre en direct)
grep -rn "def lire" src/ # chercher récursivement, avec numéros de ligne
find . -name "*.py" | wc -l # compter les fichiers Python
# Enchaîner : tubes et redirections
python capteur.py > mesures.txt # sortie vers un fichier (>> pour ajouter)
cat mesures.txt | sort -n | uniq -c | head # trier numériquement, compter les doublons
python robot.py 2> erreurs.log # seulement les erreurs (flux 2)
ls *.csv && echo "il y a des CSV" || echo "aucun"Sous Windows : installez Git Bash (fourni avec Git) ou WSL (Ubuntu dans Windows) pour avoir exactement ces commandes. PowerShell a des équivalents (Get-ChildItem, Select-String) mais tous les tutoriels, serveurs et robots (Raspberry Pi, Jetson) parlent bash.
Trois réflexes : Tab complète les noms ; ↑ rappelle les commandes ; Ctrl+C interrompt. Et man commande ou commande --help pour la documentation.
Un script shell est un fichier de commandes : #!/bin/bash en première ligne, chmod +x script.sh, puis ./script.sh. Variables $NOM, boucles for f in *.py; do …; done. Dès que la logique dépasse 20 lignes, passez à Python.
01 / Terminal
Environnements virtuels et dépendances
# Un environnement par projet : les bibliothèques n'entrent pas en conflit entre projets
python -m venv .venv # créer (une fois)
source .venv/bin/activate # activer (Linux/macOS) ; .venv\Scripts\activate sous Windows
pip install numpy matplotlib pyserial
pip freeze > requirements.txt # figer les versions exactes
pip install -r requirements.txt # les réinstaller ailleurs (un autre PC, un serveur, un collègue)
# Version moderne, en un fichier pyproject.toml (outil : uv, poetry ou pip ≥ 23)
[project]
name = "robot-explorateur"
version = "0.1.0"
dependencies = ["numpy>=1.26", "pyserial>=3.5"]
[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]La règle : le dépôt contient requirements.txt ou pyproject.toml, jamais .venv/ (des centaines de Mo, propres à une machine). Un projet qu’on ne peut pas réinstaller en une commande n’est pas reproductible — et en recherche, non reproductible = inexistant.
02 / Git
Ce qu’est vraiment un commit : un mini-Git en Python
Les trois objets
Blob : le contenu d’un fichier. Tree : la liste (nom → blob) d’un dossier. Commit : un tree + le commit parent + un message + un auteur. Chaque objet est nommé par le SHA-1 de son contenu : deux fichiers identiques ne sont stockés qu’une fois, et changer un octet dans le passé change tous les hachages qui suivent (c’est une chaîne de hachages — la même idée que la blockchain). Une branche n’est qu’un fichier contenant un hachage de commit ; HEAD pointe vers la branche courante. Voilà pourquoi créer une branche est instantané et gratuit.
02 / Git
Le cycle quotidien
git init # créer un dépôt dans le dossier courant (une fois)
git status # que s'est-il passé depuis le dernier commit ?
git add robot.py tests/ # mettre dans l'index (la « zone de préparation ») ; git add -p pour choisir par morceaux
git diff --staged # relire ce qu'on va valider
git commit -m "Ajoute la détection d'obstacle par ultrason"
git log --oneline --graph --all # l'historique, en arbre
# Revenir en arrière sans rien perdre
git diff HEAD~1 # ce qui a changé depuis l'avant-dernier commit
git restore robot.py # annuler les modifications non validées d'un fichier
git revert a1b2c3 # créer un commit qui annule le commit a1b2c3 (l'historique reste intact)
git stash / git stash pop # mettre de côté un travail en cours, le reprendreUn bon commit
- Atomique : un changement logique (une fonctionnalité, une correction), pas « travail du mardi ».
- Message : une ligne à l’impératif de moins de 72 caractères qui dit quoi et pourquoi (« Corrige le débordement du tampon série quand la ligne dépasse 64 octets »), puis une ligne vide, puis des détails si nécessaire.
- Compilable/testable : chaque commit doit laisser le projet dans un état qui marche, pour que
git bisect(recherche dichotomique du commit qui a introduit un bug !) reste utilisable.
Fichier .gitignore : .venv/, __pycache__/, *.pyc, .env (secrets), données lourdes. Ce qui est généré ne se versionne pas.
02 / Git
Branches, fusion, conflits
git switch -c capteur-ir # créer une branche et s'y placer (git checkout -b, ancienne syntaxe)
# ... commits sur la branche ...
git switch main
git merge capteur-ir # fusionner : Git crée un commit avec deux parents
git branch -d capteur-ir # supprimer la branche fusionnée
# Conflit : deux branches ont modifié les mêmes lignes
git merge experimental
# CONFLICT (content): Merge conflict in robot.py
# Le fichier contient :
<<<<<<< HEAD
VITESSE_MAX = 0.8
=======
VITESSE_MAX = 1.0 # après recalibration
>>>>>>> experimental
# → éditer pour garder la bonne version, supprimer les marqueurs, puis :
git add robot.py && git commit
# Rebase : rejouer ses commits par-dessus main pour un historique linéaire (avant de partager seulement !)
git rebase mainUn conflit n’est pas une erreur : c’est Git qui refuse de deviner. Il arrive quand deux personnes touchent la même ligne. On le résout en lisant les deux versions et en décidant — souvent en parlant à l’autre. Les petits commits fréquents et les branches courtes réduisent les conflits.
02 / Git
Travailler à plusieurs : dépôts distants et pull requests
git clone https://github.com/equipe/robot.git # récupérer un dépôt existant
git remote -v # origin = le dépôt distant
git pull # récupérer et fusionner les commits des autres (fetch + merge)
git push -u origin capteur-ir # publier sa branche
# Le flux « pull request » (GitHub / GitLab « merge request ») :
# 1. branche → 2. commits → 3. push → 4. ouvrir une PR → 5. revue par un pair, tests automatiques
# → 6. corrections → 7. fusion dans main. Personne ne pousse directement sur main.La revue de code
Un autre humain lit votre diff avant fusion. Ce n’est pas un examen : c’est la façon la plus efficace connue de trouver des bugs et de diffuser les bonnes pratiques dans une équipe. Comme relecteur : commentez le code, pas la personne ; posez des questions (« que se passe-t-il si la liste est vide ? ») ; distinguez le bloquant (« ce chemin lève une exception ») du goût (« j’aurais nommé ça autrement »). Comme auteur : petites PR (moins de 300 lignes se relisent, 3000 non), description claire, tests inclus.
03 / Projet
Anatomie d’un projet Python sérieux
robot-explorateur/
├── README.md # quoi, pourquoi, comment installer, comment lancer, captures
├── LICENSE # MIT, GPL… sans licence, personne n'a le droit de réutiliser
├── pyproject.toml # métadonnées + dépendances
├── .gitignore
├── .github/workflows/tests.yml # intégration continue
├── src/robot/ # le code, en paquet importable : from robot.capteurs import Ultrason
│ ├── __init__.py
│ ├── capteurs.py
│ ├── moteurs.py
│ ├── navigation.py
│ └── __main__.py # python -m robot
├── tests/
│ ├── test_capteurs.py
│ └── test_navigation.py
├── docs/ # schémas, notes de conception
├── data/ # petits jeux de données de test (les gros vont ailleurs)
└── scripts/ # outils annexes : calibration, tracésLe README répond en 30 secondes à « qu’est-ce que c’est ? » et en 2 minutes à « comment je le fais tourner ? ». Un projet de concours, de stage ou de recherche sera jugé d’abord sur son README.
03 / Projet
Intégration continue : les tests tournent à chaque push
# .github/workflows/tests.yml
name: tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install -e ".[dev]"
- run: ruff check src tests
- run: mypy src
- run: pytest --cov=robot --cov-report=term-missingÀ chaque push, un serveur installe le projet dans un environnement vierge et lance le style, les types et les tests. Une PR avec une croix rouge ne se fusionne pas. Ce filet de sécurité change la façon de travailler : on ose refactoriser, parce que si on casse quelque chose, on le saura dans les cinq minutes. Sur un projet de robot, on y ajoute la compilation du firmware (module L18) et parfois des tests sur matériel réel (hardware-in-the-loop).
03 / Projet
Reproduire un bug avec git bisect : la dichotomie appliquée à l’historique
git bisect start
git bisect bad # la version actuelle a le bug
git bisect good v1.2 # cette ancienne version marchait
# Git extrait un commit du milieu ; on teste, puis :
git bisect good # ou git bisect bad
# ... 8 fois pour 200 commits ...
git bisect resetAvec git bisect run pytest tests/test_nav.py, Git fait tout seul les 8 étapes. Voilà pourquoi chaque commit doit rester testable.
Cours
Cours 1 — Le modèle mental de Git : trois zones, un graphe
| Zone | Contenu | Commandes qui la modifient |
|---|---|---|
| Répertoire de travail | Vos fichiers, tels qu’ils sont sur le disque | éditeur, git restore, git switch |
| Index (staging) | L’instantané en préparation pour le prochain commit | git add, git rm, git restore --staged |
| Dépôt (.git) | Le graphe des commits, les branches, les tags, les objets | git commit, git merge, git fetch |
Le graphe. Les commits forment un DAG (module L05) : chaque commit pointe vers son ou ses parents (deux pour une fusion). Une branche est un pointeur nommé vers un commit qui avance à chaque nouveau commit ; HEAD pointe vers la branche courante (ou directement un commit : « HEAD détaché »). Un tag est un pointeur fixe. git log --graph --oneline --all dessine ce graphe ; apprenez à le lire avant toute opération délicate.
Trois opérations sur le graphe. merge crée un commit à deux parents (l’historique reste vrai) ; rebase recopie des commits ailleurs (nouveaux hachages : ne jamais rebaser ce qui est déjà partagé) ; reset déplace une branche (avec --hard, perd le travail non validé — le seul moyen de vraiment perdre quelque chose avec Git, avec reflog comme filet de sécurité pendant 30 jours).
Cours
Cours 2 — Le shell comme langage : tubes, variables, boucles, codes de retour
# Chaque commande renvoie un code : 0 = succès, autre = échec ; $? le contient ; && et || l'exploitent
python tests.py && echo "tests OK" || echo "ÉCHEC"
# Variables et substitution de commande
N=$(ls *.csv | wc -l); echo "$N fichiers CSV"
# Boucle sur des fichiers, avec protection des espaces ("$f")
for f in data/*.csv; do python convertir.py "$f" "sorties/$(basename "$f" .csv).json"; done
# Filtrer un journal : les 10 erreurs les plus fréquentes
grep ERROR robot.log | cut -d' ' -f3- | sort | uniq -c | sort -rn | head
# Un script robuste commence par : arrêt à la première erreur, variables non définies interdites, tubes stricts
#!/usr/bin/env bash
set -euo pipefail
# Exécuter en tâche de fond, rediriger, et surveiller
python telemetrie.py > tele.log 2>&1 &
tail -f tele.logQuand passer à Python. Dès qu’il y a des structures de données, des conditions imbriquées ou plus de 30 lignes : subprocess.run([...], check=True, capture_output=True) et pathlib remplacent le shell proprement. Le shell reste imbattable pour enchaîner des outils existants en une ligne.
| Besoin | Outil |
|---|---|
| Chercher du texte dans des fichiers | grep -rn, rg (ripgrep, plus rapide) |
| Trouver des fichiers | find . -name "*.py" -mtime -1 ; fd |
| Transformer du texte ligne à ligne | sed 's/ancien/nouveau/g', awk '{print $2}' |
| Comparer | diff -u a b ; git diff --no-index a b |
| Copier vers un robot | scp fichier pi@robot.local:~/ ; rsync -av src/ pi@robot.local:robot/ |
| Session distante | ssh pi@robot.local ; tmux pour garder une session ouverte |
Cours
Cours 3 — Qualité industrielle : versions, journal des changements, revue
- Versionnage sémantique : MAJEUR.MINEUR.CORRECTIF (2.3.1). Majeur : incompatible ; mineur : ajout compatible ; correctif : bug. Un tag Git par version :
git tag -a v2.3.1 -m "…". - CHANGELOG.md : pour chaque version, « Ajouté / Modifié / Corrigé / Supprimé ». Écrit par les humains, pour les humains ; les messages de commit ne le remplacent pas.
- Messages de commit conventionnels :
feat(capteurs): ajoute le filtre médian,fix(serie): borne la ligne à 64 octets,refactor,test,docs. Permet de générer le changelog et de trier l’historique. - Branches protégées :
mainn’accepte que des fusions de PR relues avec CI verte. Sur GitHub : Settings → Branches → protection rules. - Hooks :
pre-commit(outil du même nom) lanceruff,black,mypyavant chaque commit ; ce qui ne passe pas n’est pas validé. - Revue : une PR = un objectif ; description avec « quoi, pourquoi, comment tester » ; captures ou sorties de tests ; relecteur nommé ; on approuve ou on demande des changements, jamais de « LGTM » sans avoir lu.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.0
hooks: [{id: ruff}, {id: ruff-format}]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.0
hooks: [{id: mypy}]
# pip install pre-commit && pre-commit install → actif à chaque git commitTP guidé
TP — Un projet d’équipe de bout en bout (sur PC, 2 personnes, 2 h)
- Créer et publier. Personne A : dépôt
robot-equipesur GitHub avec README, licence MIT,.gitignorePython,pyproject.toml; clone local ; premier commit ;git push -u origin main. Personne B :git clone. - Travailler en parallèle. A sur la branche
capteur-ultrason(classe + tests), B surjournal-csv(classe + tests). Chacun : 3 commits atomiques avec messages conventionnels ;git push -u origin <branche>; ouvrir une PR. - Relire. Chacun relit la PR de l’autre : au moins deux commentaires précis (une question sur un cas limite, une suggestion de nommage). L’auteur corrige (nouveau commit, la PR se met à jour) ; le relecteur approuve ; fusion via l’interface (« Squash and merge » ou « Merge »).
- Provoquer un conflit. Les deux modifient la même ligne du README sur deux branches ; le second à fusionner résout le conflit localement :
git switch ma-branche && git fetch origin && git merge origin/main # éditer, supprimer les marqueurs <<<< ==== >>>> git add README.md && git commit && git push - CI. Ajouter
.github/workflows/tests.yml(module L06 §03) ; pousser ; vérifier la coche verte sur la PR ; activer la protection demain(CI obligatoire). Cassez un test volontairement dans une PR : la fusion doit être bloquée. - Version et changelog. Fusionner tout, écrire
CHANGELOG.md, taguerv0.1.0, créer une « Release » GitHub.git log --graph --oneline --alldoit montrer les deux branches fusionnées. - Livrable. Le lien du dépôt : historique lisible, 2 PR fusionnées avec revue, CI verte, tag. C’est la preuve que vous savez travailler en équipe — mettez-la sur votre CV.
Exercices
Exercices auto-corrigés — raisonner sur Git et le shell
Exercice 1 — Simuler les commandes sur un graphe de commits
Complétez le mini-dépôt : commit(msg) ajoute un commit sur la branche courante, branche(nom) crée une branche au commit courant, switch(nom), merge(nom) crée un commit à deux parents (ou avance simplement la branche si c’est un fast-forward, c’est-à-dire si la branche courante est ancêtre de l’autre), log() renvoie les messages depuis HEAD en remontant les premiers parents.
Correction
def _ajouter(self, msg, parents):
self.n += 1; self.commits[self.n] = {"msg": msg, "parents": parents}; self.branches[self.courante] = self.n; return self.n
def commit(self, msg):
h = self.branches[self.courante]; return self._ajouter(msg, [h] if h else [])
def branche(self, nom): self.branches[nom] = self.branches[self.courante]
def switch(self, nom): self.courante = nom
def ancetres(self, i):
vus, pile = set(), [i]
while pile:
c = pile.pop()
if c and c not in vus: vus.add(c); pile.extend(self.commits[c]["parents"])
return vus
def merge(self, nom):
moi, lui = self.branches[self.courante], self.branches[nom]
if moi in self.ancetres(lui): self.branches[self.courante] = lui # fast-forward
elif lui not in self.ancetres(moi): self._ajouter(f"merge {nom}", [moi, lui])
def log(self):
out, c = [], self.branches[self.courante]
while c: out.append(self.commits[c]["msg"]); c = self.commits[c]["parents"][0] if self.commits[c]["parents"] else None
return out
def commits_by_msg(self, msg): return next(i for i, c in self.commits.items() if c["msg"] == msg)Exercice 2 — Un pipeline shell en Python
Reproduisez grep ERROR robot.log | cut -d' ' -f3- | sort | uniq -c | sort -rn | head -3 avec des fonctions Python composables (chaque étape prend et renvoie un itérable de lignes).
Correction
def grep(motif, lignes): return (l for l in lignes if motif in l)
def cut(lignes, k): return (" ".join(l.split(" ")[k - 1:]) for l in lignes)
def uniq_c(lignes):
from itertools import groupby
return ((len(list(g)), k) for k, g in groupby(lignes))
def top(n, paires): return sorted(paires, key=lambda p: (-p[0], p[1]))[:n]04 / Défis
Défi ★ — Premier dépôt
Consigne (sur votre PC)
1. Créez un dossier mission-mars, un venv, et copiez-y votre projet de la séance 10. 2. git init, .gitignore, premier commit. 3. Créez une branche score où vous ajoutez le meilleur score sauvegardé dans un fichier ; commits atomiques. 4. Fusionnez dans main. 5. Publiez sur GitHub (dépôt privé ou public) avec un README de 10 lignes. Envoyez le lien.
Vérification
git log --oneline --graph doit montrer au moins 4 commits et une fusion. git status doit être propre. Le README doit contenir : titre, une phrase, comment installer (pip install -r requirements.txt), comment lancer.
04 / Défis
Défi ★★ — Provoquer et résoudre un conflit, puis bissecter
Consigne (sur votre PC)
1. Sur deux branches partant du même commit, modifiez la même constante avec deux valeurs. Fusionnez : résolvez le conflit en gardant la bonne valeur et en expliquant dans le message de commit. 2. Écrivez un test qui vérifie une fonction, faites 15 commits de modifications variées dont un (au milieu) casse la fonction sans que vous notiez lequel. Retrouvez-le avec git bisect run pytest.
Piste
Pour créer les 15 commits vite : un petit script bash for i in $(seq 1 15); do echo "# $i" >> notes.md; git commit -am "note $i"; done, en glissant à la main une modification cassante dans l’un d’eux. git bisect log montre le raisonnement de Git.
04 / Défis
Défi ★★★ — Esprit prépa : diff par plus longue sous-suite commune
Consigne
git diff calcule la plus longue sous-suite commune (LCS) entre les lignes des deux versions (module L04), puis affiche les lignes absentes de la LCS comme supprimées (−) ou ajoutées (+). Implémentez diff(ancien, nouveau) qui produit ce format, testez sur deux versions d’un programme, puis : 1) montrez que l’algorithme naïf est O(n·m) en mémoire et ramenez-le à O(min(n, m)) pour la longueur seule ; 2) cherchez ce qu’est l’algorithme de Myers (celui de Git) et pourquoi il est en O((n + m)·D) où D est la taille du diff.
Correction
def diff(a, b):
n, m = len(a), len(b)
L = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(n - 1, -1, -1):
for j in range(m - 1, -1, -1):
L[i][j] = L[i + 1][j + 1] + 1 if a[i] == b[j] else max(L[i + 1][j], L[i][j + 1])
i = j = 0; sortie = []
while i < n and j < m:
if a[i] == b[j]: sortie.append(" " + a[i]); i += 1; j += 1
elif L[i + 1][j] >= L[i][j + 1]: sortie.append("- " + a[i]); i += 1
else: sortie.append("+ " + b[j]); j += 1
sortie += ["- " + l for l in a[i:]] + ["+ " + l for l in b[j:]]
return "\n".join(sortie)
print(diff(ancien, nouveau))Remplir la table depuis la fin permet de reconstruire le diff en avançant depuis le début. Pour la longueur seule, deux lignes de la table suffisent (O(min(n, m))). Myers (1986) explore le « graphe d’édition » en diagonale et ne coûte que proportionnellement à la taille du diff : sur deux fichiers de 10 000 lignes qui diffèrent de 5, il est 2000 fois plus rapide que la table complète. Bel exemple de complexité sensible à la sortie.
05 / Vérification
Qu’est-ce qu’un commit Git ?
Deux questions supplémentaires
1. Pourquoi ne pas versionner .venv/ ? C’est généré, énorme et propre à une machine ; requirements.txt suffit à le recréer.
2. Combien de tests git bisect demande-t-il pour 1000 commits ? ⌈log₂ 1000⌉ = 10.
Référence
Les mots à retenir
| Mot | Définition |
|---|---|
| Shell / bash | Interpréteur de commandes ; tubes |, redirections >. |
| venv | Environnement Python isolé par projet. |
| Commit | Instantané + parent + message, nommé par son SHA. |
| Index (staging) | Zone où l’on prépare le prochain commit. |
| Branche | Pointeur mobile vers un commit. |
| Merge / rebase | Fusionner deux historiques / rejouer des commits. |
| Conflit | Mêmes lignes modifiées des deux côtés ; se résout à la main. |
| Pull request | Proposition de fusion soumise à revue et tests. |
| CI | Intégration continue : tests automatiques à chaque push. |
| bisect | Recherche dichotomique du commit fautif. |
Pour continuer
Vous travaillez comme une équipe
Module suivant : le langage C et OCaml, les deux langages de la prépa MP2I/MPI — pointeurs, mémoire, compilation, et la programmation fonctionnelle typée.
À faire chez soi
- Faire les 20 premiers niveaux de Learn Git Branching (site interactif gratuit).
- Mettre tous vos projets du parcours dans un dépôt GitHub avec README et CI.
- Lire les chapitres 1 à 3 de Pro Git.