Chapitre 2 : Les notions intermédiaires
Concepts clés abordés
- Attached & detached HEAD
- Stash
- Forcer l'application d'une modification (push --force, --force-with-lease)
- Diff
- Divergence
- Cherry-pick
- Merge (fast-forward, commit de merge, options)
- Rebase (interactif, squash)
- Merge vs rebase
- Workflow (Gitflow, Trunkbase)
- Corriger les erreurs (amend, rebase interactif, reset)
Attached & detached HEAD
HEAD est en état "attached" lorsqu'il pointe vers une branche. Dans ce cas, les nouveaux commits avancent la branche automatiquement. C'est l'état par défaut lorsque vous effectuez un switch ou un checkout sur une branche.
HEAD est en état "detached" lorsqu'il pointe directement vers un commit (via son hash ou un tag). Dans cet état, les nouveaux commits créent un "dangling commit" : un commit non référencé par une référence, qui peut être perdu si vous déplacez votre HEAD (switch ou checkout) de branche sans créer de référence. Pour créer une branche à partir d'un état detached et repassé en état attached, utilisez git checkout -b nouvelle-branche.
L'état detached est l'un des rares cas présentant un risque de perdre votre travail ! Utilisez-le seulement pour de la lecture. Pour modifier du code, il est impératif d'être dans l'état attached pour sécuriser votre travail.

Le stash
Le stash permet de sauvegarder temporairement des modifications non commitées pour pouvoir changer de branche ou récupérer des modifications distantes sans perdre votre travail en cours.
git stash sauvegarde les modifications du working copy et du stage dans une pile locale. Les modifications sont référencées par un identifiant (ex: stash@{0} pour le plus récent).
Le stash est purement local et n'est jamais envoyé sur le remote. Pour récupérer les modifications, utilisez git stash pop (récupère et supprime le stash) ou git stash apply (récupère sans supprimer). git stash list affiche tous les stashes disponibles.
Forcer l'application d'une modification
Certaines commandes Git réécrivent l'historique : elles modifient les commits existants en créant de nouveaux commits avec de nouveaux hash. C'est le cas notamment du rebase, du rebase interactif, du reset, ou de l'amendement d'un commit déjà poussé.
Lorsque l'historique a été réécrit localement, un simple git push sera refusé car la branche distante contient des commits que votre historique local n'a plus (les anciens commits remplacés), soit une divergence. Git protège ainsi contre l'écrasement accidentel de l'historique partagé.
Pour forcer la mise à jour du remote avec votre historique réécrit, vous devez utiliser git push --force. Cette commande remplace la branche distante par votre version locale, écrasant définitivement les commits qui n'existent plus dans votre historique local.
--force est dangereux car il peut écraser le travail d'autres développeurs si quelqu'un a poussé des commits entre-temps. Il est fortement recommandé d'utiliser --force-with-lease à la place.
--force-with-lease vérifie que la branche distante n'a pas été modifiée depuis votre dernière récupération (fetch ou pull). Si quelqu'un a poussé entre-temps, la commande échoue, vous protégeant ainsi contre l'écrasement accidentel du travail d'un collègue. C'est une sécurité supplémentaire qui devrait être privilégiée systématiquement lors des push forcés.
Mettre en commun des modifications
Diff
git diff affiche les différences entre différentes zones ou états de votre dépôt. Sans argument, il compare le working copy avec le stage, montrant les modifications non encore ajoutées.
git diff --staged (ou --cached) compare le stage avec le dernier commit, affichant les modifications qui seront incluses dans le prochain commit.
git diff <commit1> <commit2> compare deux commits spécifiques. git diff <branch1> <branch2> compare deux branches. git diff HEAD compare le working copy avec le dernier commit, incluant les modifications du stage.
Même si rarement utilisée, cette commande est essentielle pour comprendre les changements avant de les commiter ou de les fusionner, et pour identifier les différences entre branches ou commits.
Divergence
Une divergence survient lorsque deux branches ont évolué indépendamment : elles partagent un ancêtre commun mais ont chacune des commits uniques. Git doit alors fusionner ces histoires pour créer un état cohérent.

Dans l'exemple, feature et master ne sont pas divergentes car master est "inclue" dans feature. C'est-à-dire que l'ensemble des commits de master sont inclus dans feature. En revanche, feature et feature-2 sont divergentes car au moins un commit d'une branche n'appartient pas à l'autre et réciproquement.
Cherry-pick
git cherry-pick <commit-hash> applique un commit spécifique d'une autre branche sur la branche courante. Par défaut, il crée un nouveau commit avec le même message et les mêmes modifications, mais avec un hash différent (c'est une copie, pas le commit original).
Si le commit cherry-pické n'est référencé par aucune branche, il devient un dangling commit. La commande complète permet de spécifier la branche source : git cherry-pick <commit-hash> depuis la branche courante.
Voir : Cherry-pick
Merge
git merge fusionne une branche dans la branche courante en conservant l'historique complet des deux branches.
En l'absence de divergence (une branche est simplement "en avance" par rapport à l'autre), Git effectue un "fast-forward" : la branche courante est simplement avancée jusqu'à la pointe de la branche fusionnée, sans créer de commit de merge.

Si un fast-forward n'est pas possible (les branches ont divergé), Git crée un commit de merge qui combine les deux histoires. Cela a pour conséquence de créer un commit de merge qui se trouve sur la branche checkout et qui a pour 2 parents : le dernier commit de la branche source et le dernier commit de la branche cible.
L'option --no-ff force la création d'un commit de merge même si un fast-forward est possible, préservant ainsi la structure de branche dans l'historique. L'option --ff-only refuse le merge si un fast-forward n'est pas possible.
Rebase
git rebase est une commande qui permet d'appliquer une liste de commit à partir d'un autre point de l'historique. En un sens, c'est un chery-pick sur plusieurs commit d'un coup.
git rebase main (depuis une autre branche) réapplique les commits de la branche courante sur la pointe de main, créant une histoire linéaire. Par défaut, Git rebase la branche courante sur la branche spécifiée.

Le rebase réécrit l'historique : les commits sont recréés avec de nouveaux hash. Cela rend l'historique plus propre mais modifie l'historique partagé, nécessitant un git push --force (ou --force-with-lease pour plus de sécurité) pour mettre à jour le remote.
Le rebase interactif (git rebase -i) permet de modifier, réordonner, supprimer ou combiner (squash) des commits avant de les réappliquer. Le squash combine plusieurs commits en un seul, utile pour nettoyer l'historique avant de fusionner.
La commande complète permet de spécifier la liste exact de commit à appliquer sur le nouveau commit de base : git rebase <base-commit> <start-commit> <end-commit>. Les commits allant du start-commit (exclu) jusqu'au end-commit (inclu) seront tour à tour appliqués (cherry-pick) à partir du base-commit.
Merge vs rebase
Le merge préserve l'historique complet avec tous les commits et les points de fusion, ce qui facilite la compréhension de l'évolution du projet mais peut rendre l'historique complexe avec de nombreuses branches.
Le rebase crée un historique linéaire plus facile à lire, mais réécrit l'historique et peut compliquer la collaboration si les branches sont toutes partagées. Il est recommandé de rebaser uniquement les branches locales avant de les fusionner, jamais sur des branches déjà poussées et partagées.
Le choix dépend de la stratégie de l'équipe : merge pour préserver l'historique exact, rebase pour un historique plus propre au prix d'une réécriture.
Voir : What's the difference between 'git merge' and 'git rebase'?
Conflits (merge vs rebase)
Un conflit survient quand Git ne peut pas déterminer automatiquement comment combiner deux modifications (souvent parce que les mêmes lignes ont été modifiées différemment). Le point clé est que le processus de résolution dépend de la stratégie choisie : merge ou rebase.
Avec un merge, vous fusionnez deux pointes de branche. Les conflits se résolvent une seule fois dans un commit de merge (ou dans l'état de merge en cours), puis l'historique conserve explicitement ce point de jonction. En cas d'erreur, vous pouvez revenir en arrière avec git merge --abort. Une fois les fichiers corrigés, on valide avec git add puis git commit (ou git merge --continue selon le flux).
Avec un rebase, Git rejoue une série de commits les uns après les autres sur une nouvelle base. Vous pouvez donc avoir à résoudre des conflits plusieurs fois, commit par commit, jusqu'à ce que toute la série soit rejouée. Les commandes de pilotage sont git rebase --continue (après résolution + git add), git rebase --skip (sauter le commit en conflit) et git rebase --abort (annuler et revenir à l'état initial).
Dans les deux cas, les réflexes sont les mêmes : git status pour voir quoi est en conflit, git diff pour comprendre, puis éditer les fichiers (marqueurs <<<<<<<, =======, >>>>>>>) avant de re-stager avec git add. Le choix merge vs rebase est surtout un choix d'historique et de collaboration : merge "encapsule" le conflit dans un point de fusion, rebase "répartit" le conflit au fil des commits rejoués.

Workflow
Un workflow Git définit les règles et processus pour organiser le travail collaboratif : gestion des branches, intégration des modifications, cycles de release, etc.
Avoir un workflow précis permet de standardiser les pratiques, réduire les conflits, faciliter les reviews, maintenir un historique cohérent et simplifier la gestion des releases. Sans workflow défini, les équipes risquent la confusion, les conflits fréquents, un historique désordonné et des difficultés à identifier l'origine des problèmes.
Pour vous adapter au mieux au workflow choisi, gardez en tête que git pull combine fetch (récupération des modifications distantes) et merge (fusion locale) par défaut. Vous pouvez changer cette configuration pour que pull soit en rebase mode afin de faire des rebase plutot que des merge : git config pull.rebase true.
Au delà du workflow, il est recommandé d'être en rebase mode pour éviter les commit de merge dans le cas de pull avec des conflits, cela vous permettra de conserver des branches de travail propres et lisibles.
Gitflow
Gitflow est un workflow structuré utilisant plusieurs types de branches avec des rôles et des règles de fusion précis. Chaque type de branche a un objectif spécifique dans le cycle de vie du projet.
main (ou master) : branche de production, contenant uniquement le code déployé en production. Cette branche doit toujours être stable et ne doit recevoir des modifications que via des merges depuis develop (pour les releases) ou depuis hotfix/* (pour les corrections urgentes). Chaque commit sur main correspond généralement à une version taguée.
develop : branche de développement principal, intégrant toutes les fonctionnalités en cours de développement. C'est la branche de référence pour le travail quotidien. Les branches feature/* sont fusionnées dans develop une fois terminées. Les branches release/* sont créées à partir de develop et y sont fusionnées après la release.
feature/* : branches de fonctionnalités créées à partir de develop. Chaque développeur crée une branche feature/nom-fonctionnalite pour travailler sur une nouvelle fonctionnalité. Une fois terminée et testée, elle est fusionnée dans develop via une pull request. Ces branches sont généralement supprimées après fusion.
release/* : branches de préparation de release créées à partir de develop lorsqu'une version est prête à être livrée. Elles permettent de finaliser la release (correction de bugs mineurs, mise à jour de la documentation, ajustement des numéros de version) sans bloquer le développement continu sur develop. Une fois la release prête, la branche est fusionnée dans main (avec un tag de version) et dans develop (pour inclure les corrections). La branche est ensuite supprimée.
hotfix/* : branches de corrections urgentes créées à partir de main pour corriger des bugs critiques en production. Elles permettent de corriger rapidement sans attendre le cycle de release normal. Une fois la correction validée, la branche est fusionnée dans main (avec un tag de version) et dans develop (pour éviter la régression). La branche est ensuite supprimée.
Les éléments clés sont la séparation stricte entre développement et production, des branches dédiées pour chaque phase du cycle de vie, et des règles de fusion précises. Ce workflow est adapté aux projets avec des cycles de release planifiés et une gestion stricte des versions.
La lourdeur de Gitflow peut être excessive pour des projets plus simples ou des équipes pratiquant le déploiement continu, où un workflow plus léger (comme trunk-based) peut être plus adapté.

Voir : What is a Git workflow? | GitLab
Trunkbase
Le trunk-based workflow privilégie une branche principale unique (main ou trunk) où tous les développeurs intègrent fréquemment leurs modifications. Les commits sont réalisés directement sur la branche principale.
Le risque principal est la complexité de gestion lorsque plusieurs développeurs travaillent simultanément sur la même branche, nécessitant une discipline stricte et des tests automatisés robustes pour éviter de casser la branche principale.
Le scaled trunk-based adapte ce workflow aux grandes équipes en utilisant des techniques comme les feature flags, les branches courtes (quelques heures à quelques jours maximum), et une intégration continue rigoureuse pour maintenir la stabilité de la branche principale.

Voir : Trunk-based vs Feature Branches
Corriger les erreurs
Voir : Et merde, Git!?! (ohshitgit.com)
Modification d'un commit
Les commits Git sont immuables : leur contenu et leur hash ne peuvent pas être modifiés. Pour "modifier" un commit, Git crée en réalité un nouveau commit avec les modifications souhaitées.
git commit --amend modifie le dernier commit en créant un nouveau commit qui le remplace. Cela permet de corriger le message ou d'ajouter des fichiers oubliés.
Ne jamais amender un commit déjà poussé sur une branche partagée.
Le rebase interactif (git rebase -i) permet de modifier plusieurs commits en amont dans l'historique, offrant des options comme edit (modifier un commit), squash (combiner avec le précédent), ou drop (supprimer).
Reset
git reset déplace la branche courante vers un commit spécifique, avec trois modes différents selon ce qui doit être conservé :
--soft: déplace uniquement la référence de branche. Les modifications restent dans le stage, prêtes à être recommitées.--mixed(par défaut) : déplace la branche et vide le stage, mais conserve les modifications dans le working copy.--hard: déplace la branche, vide le stage et supprime toutes les modifications du working copy.
L'opération git reset --hard : est destructive et peut entraîner une perte de données irrécupérable si les modifications n'ont pas été sauvegardées ailleurs (stash, branche, etc.).
Utilisez --hard avec précaution et uniquement lorsque vous êtes certain de vouloir abandonner les modifications en cours.
Questions clés (validation des acquis du chapitre)
- Stash : quelle différence entre
git stash applyetgit stash pop? Dans quel casapplyest préférable ? - Divergence : comment reconnaissez-vous une divergence dans
git log --all --graph --oneline? - Diff : que montre
git diffvsgit diff --staged? - Cherry-pick : pourquoi un cherry-pick crée-t-il un nouveau commit (hash différent) ? Quel est le risque de multiplier les cherry-picks ?
- Merge : dans quel cas Git peut faire un fast-forward ? Que garantit
--ff-only? - Merge (conflits) : quelles sont les étapes minimales de résolution d'un conflit (commandes + actions) ?
- Rebase : pourquoi dit-on qu'un rebase "réécrit l'historique" ? Quelle conséquence sur un push déjà partagé ?
- Rebase (conflits) : pourquoi peut-on résoudre "plusieurs fois" un conflit pendant un rebase ?
- Forcer l'upload : pourquoi
git pushest-il refusé après une réécriture ? Que protège--force-with-lease? - Reset : différence opérationnelle entre
--soft,--mixedet--hard(sur HEAD, stage, working copy) ?
Exercices :
Exercice 4 — Stash : interrompre proprement un travail en cours
Objectif : sauvegarder temporairement, changer de contexte, restaurer.
Étapes :
- Modifier un fichier sans commit, vérifier
git status. - Faire
git stashpuis vérifier que la working copy est propre. - Changer de branche, faire une modification/commit, revenir, puis restaurer avec
git stash apply(oupop). - Lister et comprendre la pile avec
git stash list.
Exercice 5 — Divergence + résolution de conflits (merge)
Objectif : provoquer une divergence et résoudre un conflit de merge.
Étapes :
- Créer 2 branches depuis le même commit (ex :
feature/aetfeature/b) et modifier la même ligne dans un même fichier sur chacune des branches. - Fusionner
feature/adansmain, puis tenter de fusionnerfeature/bdansmain. - Résoudre le conflit puis commiter
- Relire l'historique
git log --all --graph --onelinemontrant le commit de merge.
Exercice 6 — Cherry-pick : appliquer un correctif ciblé
Objectif : déplacer une modification sans fusionner toute une branche.
Étapes :
- Créer un commit "hotfix" sur une branche dédiée (ex :
hotfix/typo) avec un commit. - Depuis une autre branche (ex :
feature/a), appliquer le correctif avecgit cherry-pick <hash>. - Vérifier (via
git log/git show) que c'est bien un nouveau commit (hash différent) portant les mêmes changements.
Exercice 7 — Réécriture d'historique et push forcé (sécurisé)
Objectif : comprendre le rejet d'un push et utiliser --force-with-lease.
Étapes :
- Créer 2 branches depuis le même commit (ex :
feature/aetfeature/b) et modifier la même ligne dans un même fichier sur chacune des branches. - Rebase
feature/adansmain, puis tenter de rebasefeature/bdans main. - Résoudre le conflit puis commiter
- Relire l'historique
git log --all --graph --onelinemontrant la linéarité de l'historique.