LYCÉE → PRÉPA · L06

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

Définition (dépôt, commit, branche). Un dépôt Git est un graphe orienté acyclique de commits ; chaque commit est un instantané complet des fichiers, identifié par le SHA-1 de son contenu et de ses parents (donc immuable). Une branche est un simple pointeur vers un commit ; HEAD pointe vers la branche courante. Un merge crée un commit à deux parents ; un rebase réécrit des commits sur une nouvelle base (nouveaux SHA).
Définition (test unitaire, test d’intégration, test par propriétés). Unitaire : une fonction isolée, entrées choisies, sortie attendue. Intégration : plusieurs composants ensemble. Par propriétés : des entrées générées et une propriété qui doit toujours tenir (hypothesis). La couverture mesure les lignes exécutées par les tests — nécessaire, pas suffisante.
Définition (shell, processus, flux). Un processus a trois flux standard : stdin (0), stdout (1), stderr (2), et un code de retour (0 = succès). Le tube 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.
Définition (environnement virtuel, dépendance, version sémantique). Un venv isole les paquets d’un projet. requirements.txt (ou pyproject.toml) fige les dépendances. Version MAJEUR.MINEUR.CORRECTIF : MAJEUR change = incompatibilité possible ; MINEUR = ajouts compatibles ; CORRECTIF = corrections.
Définition (débogueur, profileur). Débogueur : exécution pas à pas avec points d’arrêt (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

BesoinGitEffet sur le graphe
Voir l’étatgit status, git log --oneline --graph --all
Enregistrergit add -p puis git commit -m "…"Nouveau commit, la branche avance
Branchegit switch -c featNouveau pointeur sur HEAD
Intégrergit merge feat / git rebase mainCommit à 2 parents / commits recopiés
Annuler un commit publiégit revert <sha>Nouveau commit inverse (l’historique reste)
Annuler localementgit reset --soft HEAD~1 ; git restore fichierRecule la branche / restaure le fichier
Retrouver un commit « perdu »git reflogJournal des positions de HEAD (≈ 90 jours)
Chercher le commit fautifgit bisect start/bad/goodDichotomie sur l’historique : log₂ n essais
Un bug introduit parmi n commits se localise en ⌈log₂ n⌉ tests avec git bisect 1024 commits → 10 essais

Shell : 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

Proposition 1 (intégrité de l’historique Git). Modifier un commit ancien change nécessairement l’identifiant de tous ses descendants ; un historique ne peut donc pas être altéré silencieusement.
L’identifiant d’un commit est h(contenu, message, auteur, identifiants des parents), h étant une fonction de hachage (SHA-1, en transition vers SHA-256) supposée résistante aux collisions. Changer un commit C change h(C). Tout enfant C′ contient h(C) dans son entrée, donc h(C′) change ; par récurrence sur la profondeur, tout descendant change. Un dépôt distant qui connaît l’ancien identifiant détecte l’écart. C’est exactement le principe d’une chaîne de blocs.
Proposition 2 (git bisect est optimal). Pour localiser le premier « mauvais » commit sur une chaîne de n commits où la propriété « bon/mauvais » est monotone, ⌈log₂ n⌉ tests sont nécessaires et suffisants.
Suffisant : dichotomie (Théorème 1 de L01). Nécessaire : chaque test a deux issues, k tests distinguent au plus 2k positions ; il y a n positions possibles pour le premier mauvais commit, donc 2k ≥ n. Remarque : si la propriété n’est pas monotone (bug intermittent), la dichotomie ne s’applique pas — d’où l’importance de tests déterministes.
Proposition 3 (un test réussi ne prouve rien, un test échoué prouve quelque chose). Les tests peuvent montrer la présence d’un bug, jamais son absence (Dijkstra).
Un test vérifie la spécification sur une entrée. L’ensemble des entrées est en général infini (ou énorme) : un nombre fini de tests réussis ne couvre pas toutes les entrées, et il existe des programmes corrects sur ces tests et faux ailleurs (il suffit de prendre un programme correct et d’y ajouter 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

Méthode — un commit. Une intention par commit (« ajoute le filtre médian », pas « divers »), message à l’impératif, corps qui explique pourquoi. Avant : git diff --staged et les tests. Jamais de fichiers générés ni de secrets (.gitignore).
Méthode — déboguer. (1) Reproduire de façon déterministe (graine, entrée minimale). (2) Lire le message d’erreur et la dernière ligne de la trace qui est dans votre code. (3) Former une hypothèse, la tester (print ciblé ou débogueur), pas au hasard. (4) Réduire l’entrée jusqu’au plus petit cas qui échoue. (5) Une fois corrigé, ajouter le cas comme test de non-régression.
Méthode — optimiser. Mesurer (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.
Méthode — lire du code inconnu. README, structure des dossiers, point d’entrée (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

Exercice 1. Sur 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 ?
Correction. (a) main : A→B→C→F→M avec M commit de fusion ayant pour parents F et E ; feat pointe toujours sur E ; aucun identifiant ne change. (b) Le rebase recopie D et E sur F : D′, E′ (nouveaux SHA car leur parent change), feat = E′ ; le merge est alors un fast-forward : main avance simplement sur E′, sans commit de fusion. Historique linéaire A→B→C→F→D′→E′. D et E deviennent inaccessibles (récupérables via reflog). Règle : on ne rebase que des commits non publiés.
Exercice 2. Écrire les tests pytest d’une fonction mediane(L) qui doit lever ValueError sur liste vide, et un test par propriétés avec hypothesis.
Correction.
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).
Exercice 3. Un programme est lent. Le profil montre : 92 % du temps dans list.index appelé 10⁶ fois. Que faire, et quel gain attendre ?
Correction. 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 reprendre
Un 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 main

Un 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és

Le 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 reset

Avec 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

ZoneContenuCommandes qui la modifient
Répertoire de travailVos fichiers, tels qu’ils sont sur le disqueéditeur, git restore, git switch
Index (staging)L’instantané en préparation pour le prochain commitgit add, git rm, git restore --staged
Dépôt (.git)Le graphe des commits, les branches, les tags, les objetsgit 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.log

Quand 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.

BesoinOutil
Chercher du texte dans des fichiersgrep -rn, rg (ripgrep, plus rapide)
Trouver des fichiersfind . -name "*.py" -mtime -1 ; fd
Transformer du texte ligne à lignesed 's/ancien/nouveau/g', awk '{print $2}'
Comparerdiff -u a b ; git diff --no-index a b
Copier vers un robotscp fichier pi@robot.local:~/ ; rsync -av src/ pi@robot.local:robot/
Session distantessh pi@robot.local ; tmux pour garder une session ouverte

Cours

Cours 3 — Qualité industrielle : versions, journal des changements, revue

# .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 commit

TP guidé

TP — Un projet d’équipe de bout en bout (sur PC, 2 personnes, 2 h)

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

MotDéfinition
Shell / bashInterpréteur de commandes ; tubes |, redirections >.
venvEnvironnement Python isolé par projet.
CommitInstantané + parent + message, nommé par son SHA.
Index (staging)Zone où l’on prépare le prochain commit.
BranchePointeur mobile vers un commit.
Merge / rebaseFusionner deux historiques / rejouer des commits.
ConflitMêmes lignes modifiées des deux côtés ; se résout à la main.
Pull requestProposition de fusion soumise à revue et tests.
CIIntégration continue : tests automatiques à chaque push.
bisectRecherche 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

← L05SommaireL07 : C et OCaml →