Pendant longtemps, j'ai utilisé Git comme beaucoup de monde : trois commandes apprises par cœur (add, commit, push) et une vague panique dès que quelque chose sortait du chemin habituel. Un conflit de fusion, un reset tapé un peu vite, et c'était parti pour une recherche Stack Overflow à 23 h.
Ce qui m'a fait passer un cap, ce n'est pas d'apprendre plus de commandes. C'est de comprendre où vont mes fichiers quand je tape chacune d'elles. Une fois qu'on a ce modèle en tête, Git arrête d'être une boîte noire. C'est l'approche que j'ai suivie pour le cours vidéo ci-dessous, et que je détaille ici à l'écrit, avec les exemples que je ne pouvais pas caser dans la vidéo.
Git, c'est une partie de jeu vidéo avec des points de sauvegarde
L'image que j'utilise toujours : Git, c'est un système de sauvegardes pour votre projet. À chaque étape importante, vous créez un point de sauvegarde. Si vous cassez quelque chose ensuite, vous pouvez revenir en arrière, comparer deux sauvegardes, ou repartir d'un point précis pour tester autre chose.
Chaque sauvegarde s'appelle un commit. Il a un identifiant unique (le fameux hash, du genre a3f9c12), un auteur, une date et un message. Et surtout, il est immuable : une fois créé, il ne bouge plus.
Les trois zones : le modèle à avoir en tête
C'est LE point à comprendre. Git manipule vos fichiers dans trois endroits :
- Le dossier de travail : vos fichiers tels que vous les voyez et les modifiez dans votre éditeur.
- La zone d'attente (l'index, ou staging area) : là où vous préparez votre prochaine sauvegarde. Vous y placez seulement ce qui doit faire partie du commit.
- Le dépôt : l'historique, rangé dans le dossier caché
.git. Les commits y sont enregistrés pour de bon.
Dossier de travail --git add--> Zone d'attente --git commit--> Dépôt (.git)
^ |
+--------------------- git restore / checkout -----------------+
Pourquoi une zone intermédiaire ? Parce qu'elle vous permet de faire des commits propres. Vous avez modifié cinq fichiers, mais seuls deux concernent la correction de bug ? Vous n'ajoutez que ces deux-là, vous commitez, puis vous traitez le reste dans un second commit. Votre historique raconte une histoire lisible au lieu d'un fourre-tout.
Les commandes de base, dans l'ordre où on les utilise
git init : créer le dépôt
mkdir mon-projet && cd mon-projet
git init
# Initialized empty Git repository in /home/arnaud/mon-projet/.git/
Un dossier ordinaire devient un dépôt. Tout l'historique vivra dans .git. Si vous supprimez ce dossier, vous supprimez l'historique (vos fichiers, eux, restent).
git status : la boussole
Si je ne devais garder qu'un réflexe, ce serait celui-là. git status vous dit dans quelle zone se trouve chaque fichier : modifié mais pas ajouté, ajouté et prêt à être commité, ou pas suivi du tout. Je le tape avant chaque add et avant chaque commit. Ça évite 90 % des surprises.
git add : préparer la sauvegarde
git add index.html # un fichier précis
git add src/ # tout un dossier
git add . # tout ce qui a changé
git add -p # morceau par morceau, en choisissant
Le -p (patch) est sous-estimé : Git vous montre chaque bloc modifié et vous demande s'il doit partir dans le commit. Idéal quand vous avez mélangé deux sujets dans le même fichier.
git commit : enregistrer
git commit -m "Corrige le calcul de la TVA sur les remises"
Un conseil qui m'a servi des centaines de fois : écrivez le message comme si vous le lisiez dans six mois, en cherchant pourquoi quelque chose a changé. « fix » ou « wip » ne vous aideront jamais.
git log et git diff : regarder ce qui s'est passé
git log --oneline --graph --all # l'historique compact, avec les branches
git diff # ce qui a changé et n'est pas encore ajouté
git diff --staged # ce qui est dans la zone d'attente
git diff affiche les lignes supprimées en rouge et les lignes ajoutées en vert. Je le lance systématiquement avant un commit pour relire ce que je m'apprête à enregistrer.
git restore : annuler une modification
git restore fichier.py # jette les modifs du dossier de travail
git restore --staged fichier.py # retire le fichier de la zone d'attente
Attention à la première forme : les modifications non commitées sont perdues, Git ne peut pas les retrouver puisqu'elles n'ont jamais été sauvegardées.
Les branches : travailler en parallèle sans rien casser
Une branche, c'est une ligne de travail parallèle. Techniquement, c'est juste un pointeur vers un commit, ce qui explique pourquoi en créer une est instantané. Le code principal vit en général sur main, et vous testez vos idées ailleurs.
git branch nouvelle-page # crée la branche
git switch nouvelle-page # se déplace dessus
git switch -c nouvelle-page # les deux d'un coup
Le pointeur HEAD indique toujours où vous êtes. Vous croiserez aussi git checkout, l'ancienne commande qui faisait à la fois le changement de branche et la restauration de fichiers. switch et restore ont été créées pour séparer ces deux rôles, et c'est beaucoup plus clair.
git merge : réunir les branches
git switch main
git merge nouvelle-page
Si main n'a pas bougé entre-temps, Git avance simplement le pointeur (un fast-forward). Sinon, il crée un commit de fusion qui réunit les deux historiques.
Les conflits : normal, pas grave
Quand deux branches ont modifié la même ligne, Git ne choisit pas à votre place. Il marque le fichier :
<<<<<<< HEAD
const TVA = 0.20;
=======
const TVA = 0.055;
>>>>>>> nouvelle-page
Vous gardez la bonne version (ou un mélange des deux), vous supprimez les marqueurs, puis :
git add fichier.js
git commit
La première fois, ça fait peur. Après quelques dizaines, ça devient un non-événement. Si vous êtes perdu en plein milieu, git merge --abort remet tout comme avant la fusion.
Travailler à plusieurs : le dépôt distant
Votre dépôt peut avoir une copie sur un serveur : GitHub, GitLab, Gitea… Par convention, ce dépôt distant s'appelle origin.
git clone https://gitlab.com/moi/projet.git # récupère tout le dépôt
git push origin main # envoie vos commits
git fetch # récupère les nouveautés, sans toucher à votre travail
git pull # fetch + merge en une commande
Je préfère souvent git fetch suivi d'un git log origin/main : je regarde d'abord ce qui est arrivé, puis je décide comment l'intégrer. git pull fait les deux d'un coup, c'est pratique mais moins prévisible.
git stash : le tiroir
Un collègue vous demande de regarder un bug urgent, mais votre travail en cours n'est pas prêt à être commité ?
git stash # range vos modifications de côté
git switch hotfix # vous allez faire votre correction
git switch - # retour sur la branche précédente
git stash pop # vous ressortez votre travail
Niveau avancé : réécrire, réparer, enquêter
git rebase : un historique bien droit
git rebase main prend vos commits et les « repose » au bout de main, comme si vous aviez commencé votre travail à partir de la dernière version. Pas de commit de fusion, un historique linéaire.
git switch ma-branche
git rebase main
La règle d'or, que j'ai apprise à mes dépens : ne jamais rebaser une branche déjà partagée. Le rebase crée de nouveaux commits avec de nouveaux identifiants. Si un collègue avait déjà récupéré les anciens, vous venez de lui préparer une belle après-midi de résolution de conflits.
git cherry-pick : récupérer un seul commit
git cherry-pick a3f9c12
Parfait pour appliquer une correction faite sur une autre branche sans fusionner tout le reste.
git reset : reculer (avec prudence)
git reset déplace votre branche vers un ancien commit. Tout dépend du mode :
--soft: les modifications des commits annulés restent dans la zone d'attente. Pratique pour refaire un commit.--mixed(par défaut) : elles reviennent dans le dossier de travail, non ajoutées.--hard: elles sont effacées. C'est la commande qui fait le plus de dégâts quand on la tape trop vite.
git reset --soft HEAD~1 # annule le dernier commit, garde tout prêt à recommiter
git revert : annuler proprement ce qui est partagé
Si le commit est déjà poussé, on ne réécrit pas l'historique. git revert crée un nouveau commit qui fait exactement l'inverse de l'ancien. Tout le monde garde un historique cohérent.
git revert a3f9c12
git reflog : le filet de sécurité
C'est la commande que j'aurais aimé connaître plus tôt. git reflog garde la trace de tous les déplacements de HEAD, y compris après un reset --hard malheureux.
git reflog
# 4b1d2e0 HEAD@{0}: reset: moving to HEAD~3
# 9c8f7a1 HEAD@{1}: commit: Ajoute la page contact
git reset --hard 9c8f7a1 # et votre travail est de retour
Tant que votre travail a été commité au moins une fois, vous pouvez presque toujours le retrouver.
git tag et git bisect
git tag v1.0.0 pose une étiquette sur un commit, typiquement pour marquer une version. Et git bisect est un vrai détective : vous lui donnez un commit où tout marchait et un commit où le bug est présent, il coupe l'historique en deux à chaque étape jusqu'à trouver le coupable.
git bisect start
git bisect bad # le commit actuel a le bug
git bisect good v1.0.0 # cette version marchait
# Git vous place au milieu : testez, puis dites good ou bad
git bisect reset # quand c'est fini
Sur un historique de 500 commits, il faut environ 9 tests pour trouver le commit fautif. C'est magique la première fois.
L'antisèche
- Sauvegarder :
git add,git commit - Regarder :
git status,git log,git diff - Brancher :
git switch -c,git merge,git rebase - Partager :
git push,git fetch,git pull - Réparer :
git restore,git revert,git reset,git reflog
Si vous ne retenez qu'une chose : avant chaque commande qui vous fait un peu peur, tapez git status et demandez-vous dans quelle zone sont vos fichiers. Et si ça tourne mal, git reflog est presque toujours là pour vous sauver.
La suite logique, c'est d'organiser les branches à plusieurs. C'est le sujet de mon article sur Git Flow.