DevOps, CI/CD et conteneurs
Les bases de Git : commits, working tree, dépôts distants et annulation
Débutant10 min de lectureMis à jour
Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise
Git s'utilise bien mieux une fois qu'on a compris le modèle qui se cache sous les commandes : un graphe de snapshots immuables, trois endroits où un changement peut se trouver, et des branches qui ne sont que des pointeurs mobiles. Ce guide construit ce modèle, puis déroule le workflow quotidien — clone, add, commit, fetch, pull, push — et les différentes façons d'annuler, pour que vous puissiez rattraper une erreur sans crainte.
Ce que vous allez apprendre
- Expliquer comment Git stocke l'historique sous forme de graphe de snapshots adressés par leur contenu
- Faire passer délibérément un changement du working tree à l'index puis au dépôt
- Se synchroniser avec un dépôt distant via fetch, pull et push, et comprendre ce qu'ils affichent
- Réparer une erreur avec restore, reset, revert et le reflog, en choisissant chaque fois l'option sûre
- Configurer Git, ignorer correctement les fichiers et écrire des messages de commit utiles aux futurs lecteurs
La plupart des tutoriels Git enseignent une suite de commandes à apprendre par cœur. Cela fonctionne jusqu'au jour où quelque chose tourne mal — et avec Git, quelque chose finit toujours par mal tourner : un commit sur la mauvaise branche, un pull qui produit un merge inattendu, un reset qui semble avoir englouti un après-midi de travail. Pour s'en sortir, il faut comprendre le petit nombre d'idées sur lesquelles reposent les commandes. Le modèle de Git est plus simple que ne le laisse croire sa ligne de commande, et dès que vous savez vous représenter l'effet d'une commande sur le graphe des commits, le manuel de référence devient lisible et la panique disparaît. Ce guide vise Git 2.4x et utilise les commandes modernes switch et restore, là où les anciens supports utilisaient checkout pour tout.
Ce que Git stocke
Un dépôt Git est une base de données de quatre types d'objets, chacun identifié par le hash SHA-1 (ou SHA-256 dans les dépôts plus récents) de son contenu :
- Un blob est le contenu d'un fichier, sans nom ni métadonnées.
- Un tree décrit le contenu d'un répertoire : noms, permissions et hashes des blobs et sous-trees qu'il contient.
- Un commit pointe vers un tree (le snapshot complet du projet à cet instant) et vers zéro, un ou plusieurs commits parents, et porte l'auteur, le committer, les horodatages et le message.
- Un objet tag associe à un commit un nom, un message et éventuellement une signature.
Deux conséquences en découlent. D'abord, un commit est un snapshot, pas un diff : Git stocke l'arborescence complète à chaque fois, dédupliquée par hash, si bien que les fichiers inchangés ne coûtent rien et que n'importe quel commit peut être extrait sans rejouer l'historique. Les diffs sont calculés à la demande. Ensuite, tout est immuable et adressé par le contenu : modifiez un seul octet n'importe où dans l'historique et tous les hashes situés au-dessus changent. C'est pourquoi « réécrire l'historique » signifie créer de nouveaux commits, et pourquoi un hash identifie de manière fiable un état unique du code.
Reliés par leurs parents, les commits forment un graphe orienté acyclique. Une branche n'est rien de plus qu'un fichier contenant le hash d'un commit — un pointeur mobile qui avance quand vous committez dessus. HEAD est un pointeur vers la branche actuellement extraite. Créer une branche revient à écrire 41 octets dans un fichier : c'est pour cela que l'opération est instantanée, et que les utilisateurs de Git créent des branches bien plus volontiers que ceux des systèmes plus anciens.
Les trois zones
Un changement transite par trois endroits, et l'essentiel de la confusion autour de Git vient de ce que l'on ne sait pas sur lequel d'entre eux une commande agit.
| Zone | Ce que c'est | Commandes qui y écrivent |
|---|---|---|
| Working tree | Les fichiers sur disque que vous modifiez | éditeur, git restore, git switch, git merge |
| Index (staging area) | Le prochain commit en préparation, stocké dans .git/index | git add, git rm, git restore --staged, git reset |
| Dépôt | La base d'objets et les refs sous .git | git commit, git fetch, git merge, git reset |
L'index est la pièce que les débutants trouvent étrange. Il sert à construire un commit délibérément : vous modifiez cinq fichiers, indexez les trois qui vont ensemble, committez, puis indexez les deux autres dans un commit séparé. git add -p va plus loin en indexant des hunks individuels à l'intérieur d'un fichier. git status indique toujours à la fois ce qui est indexé et ce qui est modifié mais pas encore indexé ; git diff compare le working tree à l'index, et git diff --staged compare l'index au dernier commit.
git status # what is staged, modified, untracked
git add src/Order.java # stage one file
git add -p # stage hunks interactively
git diff --staged # review exactly what the commit will contain
git commit -m "Reject orders with a negative quantity"
Le workflow quotidien
En partant d'un projet existant :
git clone [email protected]:acme/shop.git
cd shop
git switch -c fix-negative-quantity # create and switch to a branch
# ... edit, test ...
git add -A
git commit
git push -u origin fix-negative-quantity # -u sets the upstream so later pushes need no arguments
clone copie l'intégralité du dépôt — tout l'historique, toutes les branches — et configure un dépôt distant nommé origin qui pointe vers sa provenance. Pour chaque branche du dépôt distant, vous obtenez une branche de suivi distante : origin/main, origin/release-2.4, etc. Ce sont des marque-pages en lecture seule de l'état du dépôt distant lors de votre dernier échange avec lui ; vous ne committez jamais directement dessus.
git fetch met à jour les branches de suivi distantes et télécharge les nouveaux objets, sans rien changer à vos propres branches. git pull, c'est un fetch suivi de l'intégration de origin/<branch> dans votre branche courante, par merge ou par rebase selon la configuration. Comme un pull peut produire un commit de merge non voulu, beaucoup de développeurs règlent git config --global pull.rebase true : les commits locaux sont alors rejoués par-dessus ceux qui ont été récupérés et l'historique reste linéaire. Le guide sur les workflows de branches discute de ce compromis.
git push envoie vos commits au dépôt distant et y fait avancer la branche. Il est refusé si la branche distante contient des commits que vous n'avez pas — un non-fast-forward —, ce qui vous empêche d'écraser le travail de quelqu'un d'autre. La bonne réaction consiste à faire un fetch, intégrer, puis pusher à nouveau ; réservez --force aux branches sur lesquelles vous êtes seul, et même dans ce cas préférez --force-with-lease, qui échoue si le dépôt distant a changé depuis votre dernier fetch.
Lire l'historique
git log --oneline --graph --decorate -20 # compact graph with branch and tag labels
git log -p -- src/Order.java # every change to one file, with diffs
git log --author=alice --since=2.weeks
git show a1b2c3d # one commit: metadata plus diff
git blame src/Order.java # which commit last touched each line
git diff main...feature # what feature adds relative to the merge base
La forme à trois points de la dernière ligne mérite d'être connue : A..B signifie « les commits accessibles depuis B mais pas depuis A », tandis que A...B dans git diff signifie « les changements de B depuis qu'il a divergé de A » — exactement ce qu'il vous faut pour relire une branche.
Annuler sans risque
Git propose plusieurs façons d'annuler, et le bon choix dépend de deux questions : le changement a-t-il été committé, et a-t-il été partagé ?
Abandonner des modifications non committées du working tree. git restore <file> remplace le fichier par sa version dans l'index. La modification est perdue pour de bon — elle n'a jamais été committée —, alors vérifiez d'abord avec git diff.
Désindexer sans perdre ses modifications. git restore --staged <file> recopie dans l'index la version du dernier commit et laisse le working tree intact.
Corriger le dernier commit. git commit --amend remplace le commit de tête par un nouveau commit qui contient l'index actuel (avec un message éventuellement modifié). Aucun problème avant le push ; après, cela réécrit un historique partagé.
Faire reculer une branche. git reset déplace le pointeur de la branche courante vers un autre commit. Avec --soft, l'index et le working tree conservent les changements ultérieurs (pratique pour fusionner les derniers commits en un seul) ; avec --mixed, le mode par défaut, l'index est réinitialisé mais les fichiers ne sont pas touchés ; avec --hard, tout est aligné sur le commit cible et le travail non committé est détruit.
git reset --soft HEAD~3 # collapse the last three commits into staged changes
git commit -m "Implement discount codes"
Annuler un commit partagé. git revert <hash> crée un nouveau commit qui applique le changement inverse. L'historique est préservé, rien de ce que les autres ont récupéré ne devient invalide, et c'est la seule manière correcte d'annuler quelque chose sur main une fois pushé.
Récupérer des commits « perdus ». Chaque fois que HEAD bouge, Git note sa position précédente dans le reflog. Après un mauvais reset ou une branche supprimée, git reflog liste les positions récentes, et git reset --hard HEAD@{2} ou git switch -c rescued <hash> ramène les commits. Les objets inaccessibles sont conservés au moins 30 jours par défaut : presque tout ce qui a été committé est donc récupérable dans ce délai.
Le stash
git stash enregistre les modifications du working tree et de l'index sur une pile et nettoie le working tree, pour que vous puissiez changer de branche, corriger quelque chose d'urgent, puis revenir. git stash pop restaure le stash le plus récent et le supprime ; git stash list montre ce qui est mis de côté ; git stash push -m "wip: pricing" -- src/pricing ne met de côté que certains chemins, avec un libellé. Les stashes sont eux aussi des commits, et un stash dont le pop provoque un conflit laisse en place à la fois le stash et les marqueurs de conflit : rien n'est perdu.
Ignorer des fichiers et configurer Git
Un fichier .gitignore liste les motifs des fichiers que Git ne doit pas suivre — résultats de build, répertoires de dépendances, réglages d'éditeur, secrets. Les motifs s'appliquent relativement au répertoire du fichier ; un / en tête ancre le motif à ce répertoire, un / final ne cible que des répertoires, et ! inverse le motif.
# Build output
/target/
/build/
node_modules/
# IDE and OS noise
.idea/
*.iml
.vscode/
.DS_Store
# Never commit secrets
.env
*.pem
Deux pièges reviennent souvent. .gitignore n'a aucun effet sur les fichiers déjà suivis — il faut d'abord les retirer avec git rm --cached. Et une entrée d'ignore n'est pas un contrôle de sécurité : un fichier committé une seule fois reste dans l'historique tant que celui-ci n'est pas réécrit, et tout secret qu'il contenait doit être renouvelé.
La configuration existe à trois niveaux — système, global (~/.gitconfig) et dépôt (.git/config) —, chacun prenant le pas sur le précédent. Les réglages à appliquer sur chaque nouvelle machine :
git config --global user.name "Alice Martin"
git config --global user.email [email protected]
git config --global init.defaultBranch main
git config --global pull.rebase true
git config --global push.autoSetupRemote true # first push needs no -u
git config --global core.autocrlf input # on Linux/macOS; "true" on Windows
git config --global rerere.enabled true # remember how you resolved conflicts
Les tags
Un tag signale qu'un commit est important, en général une release. Les tags légers (git tag v2.4.0) sont de simples pointeurs ; les tags annotés (git tag -a v2.4.0 -m "Release 2.4.0") sont des objets avec un auteur, une date et un message, et peuvent être signés. Utilisez des tags annotés pour les releases : ils portent leur provenance et git describe les privilégie. Les tags ne sont pas pushés par défaut — git push origin v2.4.0 ou git push --tags —, et contrairement aux branches ils sont censés être définitifs : déplacer un tag que d'autres ont déjà récupéré sème une confusion durable.
Écrire des commits qui valent la peine d'être lus
Un commit est une unité de changement et un message destiné à la personne qui lira l'historique un an plus tard, souvent en pleine recherche de bug avec bisect. Limitez chaque commit à un seul changement logique, pour pouvoir le revert ou le cherry-pick isolément. Rédigez la ligne de sujet à l'impératif (« Add retry to payment client »), en une cinquantaine de caractères au plus, et mettez le pourquoi dans le corps : quel problème existait, pourquoi cette approche, quelles alternatives ont été écartées. Le diff montre déjà ce qui a changé ; le message est là pour expliquer ce que le diff ne peut pas dire.
git bisect est la récompense des petits commits bien décrits. À partir d'un commit connu comme bon et d'un commit connu comme mauvais, il extrait le commit du milieu, vous (ou un script) indiquez s'il est bon ou mauvais, et il converge vers le commit fautif en un nombre logarithmique d'étapes. Il n'est utile que si chaque commit compile et ne fait qu'une seule chose.
Erreurs fréquentes
- Committer des fichiers générés et des dépendances.
target/,node_modules/et les fichiers.classalourdissent le dépôt et provoquent des conflits sans fin. Ignorez-les dès le premier commit. git add .depuis le mauvais répertoire. Cela indexe tout ce qui se trouve sous le répertoire courant, y compris des fichiers que vous ne vouliez pas inclure. Relisezgit statusavant de committer.- Faire un force-push sur une branche partagée. Les push de vos collègues sont refusés, leurs branches locales divergent et quelqu'un finit par re-pusher l'ancien historique. N'utilisez
--force-with-leaseque sur vos branches personnelles. - Faire un pull avec des modifications non committées. Un merge ou un rebase sur des fichiers modifiés produit des conflits déroutants. Committez ou faites un stash d'abord.
- Voir
reset --hardcomme la solution à tout. Il détruit le travail non committé ; utilisez plutôt le stash, et le reflog pour revenir à des commits. - Committer des secrets « temporairement ». Ils restent dans l'historique. Renouvelez immédiatement le secret exposé et utilisez
git filter-repos'il faut purger l'historique. - Des commits géants. Un commit de 60 fichiers intitulé « changes » est impossible à relire, à revert ou à bisecter. Indexez par morceaux logiques.
Points clés à retenir
- Git stocke des snapshots immuables, adressés par leur contenu et reliés en graphe ; les branches sont des pointeurs mobiles dans ce graphe et
HEADindique où vous vous trouvez. - Un changement vit dans le working tree, l'index ou le dépôt ;
status,diffetdiff --stagedvous disent lequel, etaddconstruit délibérément le prochain commit. fetchmet à jour les branches de suivi distantes sans toucher à votre travail ;pullintègre en plus ;pushrefuse d'écraser des commits que vous n'avez pas vus.- Annulez le travail non committé avec
restore, remaniez les commits locaux avecresetetamend, annulez les commits partagés avecrevert, et récupérez à peu près tout avecreflog. - Excluez les fichiers générés avec
.gitignore, réglez globalementpull.rebaseet les options de fin de ligne, et utilisez des tags annotés pour les releases. - Ce sont des commits petits, à objectif unique, avec des messages qui en expliquent la raison, qui rendent plus tard l'historique, bisect et la revue de code réellement utiles.
Sources et lectures complémentaires
Questions fréquentes
- Quelle est la différence entre git fetch et git pull ?
- fetch télécharge les nouveaux commits et met à jour les branches de suivi distantes (comme origin/main) sans toucher à votre branche ni à votre working tree. pull, c'est un fetch suivi d'un merge ou d'un rebase dans votre branche courante. Faites d'abord un fetch si vous voulez examiner ce qui a changé avant de l'intégrer ; un pull quand vous êtes prêt à intégrer tout de suite.
- J'ai committé sur la mauvaise branche. Comment réparer ?
- Si vous n'avez pas encore pushé, notez le hash du commit dans git log, passez sur la bonne branche et lancez git cherry-pick avec ce hash, puis revenez sur la mauvaise branche et lancez git reset --hard HEAD~1 pour y supprimer le commit. Si la mauvaise branche a déjà été pushée, préférez git revert afin de ne pas réécrire un historique que d'autres ont peut-être déjà récupéré.
- git reset --hard est-il dangereux ?
- Il supprime définitivement les modifications non committées du working tree et de l'index — elles n'ont jamais fait partie d'un commit, donc rien ne peut les récupérer. Les commits que reset laisse derrière lui ne sont pas supprimés immédiatement : git reflog les liste encore pendant au moins 30 jours par défaut, et vous pouvez revenir dessus avec un reset. Avant un reset --hard, lancez git status et mettez de côté avec git stash tout ce que vous pourriez vouloir garder.
- Pourquoi Git affiche-t-il « detached HEAD » ?
- HEAD pointe normalement vers un nom de branche, et committer fait avancer la branche. Si vous faites un checkout d'un commit ou d'un tag précis, HEAD pointe directement vers ce commit. Les commits créés dans cet état n'appartiennent à aucune branche et finiront supprimés par le garbage collector, sauf si vous lancez git switch -c une-branche pour en créer une à cet endroit.