Git au quotidien
Ce que ce chapitre apporte
- Mettre un projet sous Git dès sa première ligne, avec un .gitignore utile.
- Lire git status et savoir dans quelle zone se trouve chaque modification.
- Composer des commits par intention plutôt que par moment de la journée.
- Relire un historique, retrouver quand une ligne a changé et pourquoi.
- Choisir la bonne opération d'annulation selon l'endroit où se trouve la modification.
- Mettre de côté un travail en cours pour traiter une urgence.
- Expliquer ce que calcule un diff.
Le premier jour
Trois commandes, une fois pour toutes.
Puis un .gitignore, tout de suite, avant le premier commit. Ce qu'il contient : tout ce qui se régénère et tout ce qui ne regarde que vous.
# Produits de construction
__pycache__/
*.pyc
build/
dist/
# Dépendances installées
node_modules/
.venv/
# Réglages locaux et secrets
.env
.vscode/settings.json
# Bruit du système
.DS_Store
.gitignore ne concerne que les fichiers que Git ne suit pas encore. Un fichier déjà commité continue d'être suivi, quoi qu'on écrive ensuite.Il faut le retirer explicitement du suivi avec
git rm --cached fichier, puis commiter ce retrait. Et s'il contenait un secret, le secret reste lisible dans l'historique : il est compromis, il faut le changer.
La boucle de la journée
git status est la commande la plus utile de tout l'outil et la moins utilisée des débutants. Elle dit trois choses à la fois : sur quelle branche vous êtes, ce qui est modifié mais pas indexé, ce qui est indexé et attend le commit. La plupart des situations qui semblent inextricables se démêlent en la lisant.
git diff --staged montre exactement ce qui va entrer dans le commit. Trente secondes de relecture attrapent l'affichage de débogage oublié, le fichier de configuration personnel ajouté par mégarde, et la moitié du fichier collée deux fois.C'est aussi le moment où l'on se rend compte que le commit mélange deux sujets, et qu'il vaut mieux en faire deux.
Une semaine de travail ordinaire donne un historique linéaire, où chaque commit correspond à une intention.
Relire l'histoire
Un historique ne sert que si on sait l'interroger.
git log -S mérite d'être connue : elle répond à « quand cette fonction est-elle apparue » ou « qui a supprimé cet appel », questions auxquelles une recherche dans le code actuel ne peut pas répondre, puisque justement le code a disparu.
git blame a mauvaise réputation à cause de son nom. Son usage réel n'est pas de désigner un coupable mais de retrouver le commit qui a introduit une ligne, donc son message, donc la raison.
Ce que calcule un diff
Un diff n'est pas une comparaison ligne à ligne, sinon l'insertion d'une ligne au début décalerait tout et rendrait les deux fichiers entièrement différents. L'outil cherche la plus longue sous-suite commune aux deux versions : ce qui reste, de part et d'autre, est ce qui a été supprimé et ajouté.
Deux lignes ajoutées, aucune supprimée, cinq lignes communes reconnues malgré le décalage. C'est ce calcul qui permet à Git de fusionner deux branches sans rien demander tant qu'elles ne touchent pas aux mêmes endroits.
Annuler : quatre situations, quatre réponses
C'est le sujet où l'on se trompe, parce que la question « comment annuler » n'a de sens qu'accompagnée de « annuler quoi, et est-ce déjà publié ».
| Ce qu'on veut défaire | Où c'est | Commande |
|---|---|---|
| Une modification du fichier, pas encore indexée | répertoire de travail | git restore fichier |
| Une modification indexée, pas encore commitée | index | git restore --staged fichier |
| Le dernier commit, non publié | dépôt local | git commit --amend |
| Un commit déjà publié | dépôt distant | git revert a1b2c3d |
Les deux premières lignes concernent du travail en cours et ne touchent pas à l'historique. Les deux dernières sont d'une autre nature.
git commit --amend remplace le dernier commit par un nouveau, avec une nouvelle empreinte. Utile pour rattraper un message mal écrit ou un fichier oublié, à condition que personne n'ait récupéré le commit d'origine.
git revert ne supprime rien : il crée un commit qui applique l'inverse de celui qu'on désigne. L'histoire garde trace de l'erreur et de sa correction, ce qui est exactement ce qu'on veut sur une branche partagée.
git reset --hard déplace la branche en arrière et écrase le répertoire de travail. Tout ce qui n'était pas commité est perdu, sans confirmation.Sur une branche partagée, le résultat est pire : votre historique et celui du dépôt distant divergent, et la remise en état demande une intervention pour tout le monde. Sur une branche partagée, on annule avec
revert.
git reflog liste tous les endroits où HEAD est passé, y compris ceux qu'aucune branche ne référence plus. Un commit « perdu » par un mauvais reset s'y retrouve, et git checkout sur son empreinte le ramène.Cela ne vaut que pour ce qui a été commité au moins une fois. Un travail jamais commité, lui, ne laisse aucune trace.
Mettre de côté
Une urgence arrive alors que le travail en cours n'est pas en état d'être commité.
Le réflexe alternatif consiste à commiter un état provisoire, avec un message explicite, puis à le corriger plus tard par --amend ou à le réorganiser avant publication. Les deux se valent tant que le résultat publié reste propre. Ce qui ne se fait pas, c'est de laisser un commit wip dans l'historique commun.
Exercices type
git status montre un fichier à la fois « modifié » et « indexé ». Comment est-ce possible ?
Parce que ce sont deux zones distinctes, et que le fichier a été modifié après avoir été indexé.
L'index contient l'état du fichier au moment du git add. Le répertoire de travail contient l'état actuel. Si vous avez modifié le fichier depuis, les deux diffèrent, et Git le signale dans les deux rubriques.
Le commit n'enregistrera que la version indexée. Pour enregistrer la modification récente, il faut refaire un git add.
Vous avez commité un fichier de configuration contenant un mot de passe. Que faire, dans quel ordre ?
D'abord changer le mot de passe. Le dépôt a pu être poussé, cloné, sauvegardé : le secret doit être considéré comme compromis, et c'est la seule action qui règle vraiment le problème.
Ensuite retirer le fichier du suivi : git rm --cached config.env, l'ajouter au .gitignore, commiter.
Enfin, seulement si nécessaire, réécrire l'historique pour effacer les anciennes versions. C'est une opération lourde qui change toutes les empreintes et oblige chacun à reprendre son dépôt. Elle ne dispense pas de la première étape.
Quelle différence entre revert et reset ?
revert ajoute un commit qui applique l'inverse d'un commit existant. L'historique s'allonge, rien n'est réécrit, et tout le monde peut récupérer la correction normalement.
reset déplace le pointeur de branche en arrière. Les commits qui suivaient ne sont plus atteignables par la branche : l'historique est réécrit.
Le critère de choix est la publication. Sur une branche partagée, revert. Sur un travail local que personne n'a vu, reset est légitime et souvent plus propre.
Pourquoi un diff n'est-il pas une simple comparaison ligne par ligne ?
Parce qu'une seule ligne insérée en tête décalerait toutes les suivantes, et la comparaison position par position déclarerait le fichier entièrement modifié.
L'algorithme cherche donc la plus longue sous-suite commune aux deux versions. Ce qui en fait partie est inchangé, le reste se répartit en suppressions et en ajouts.
C'est ce qui rend les diffs lisibles, et c'est la même notion qui permet à une fusion de savoir que deux modifications portent sur des endroits différents.
Un commit a été poussé avec un message erroné. Peut-on le corriger ?
Techniquement oui, par git commit --amend suivi d'une poussée forcée. Pratiquement, cela réécrit l'historique commun : quiconque avait récupéré l'ancien commit se retrouve avec une histoire divergente, et la remise en ordre coûte du temps à tout le monde.
Sur une branche partagée, on laisse le message tel quel, quitte à préciser dans le commit suivant.
Sur une branche personnelle que personne d'autre n'utilise, la correction est sans risque.
À quoi sert git stash qu'un commit provisoire ne ferait pas ?
À rien que le commit provisoire ne fasse, et c'est le point : les deux sont acceptables.
stash a l'avantage de ne rien laisser dans l'historique, ce qui évite d'avoir à nettoyer ensuite. Il a l'inconvénient d'être une pile invisible dans git log, facile à oublier, et non partagée entre machines.
Le commit provisoire a l'avantage d'être visible et sauvegardé. Il oblige à ne pas le publier tel quel.
La méthode
git initet.gitignoreavant la première ligne de code, pas après.- Lis
git statusavant toute opération, et relis-le après quand tu doutes. - Relis
git diff --stagedavant chaque commit. - Un commit par intention. Si le message contient « et », il y a probablement deux commits.
- Avant d'annuler, situe : travail, index, commit local, commit publié. La commande en découle.
revertsur ce qui est publié, jamaisreset.- En cas de panique,
git reflogavant toute autre manipulation.
En résumé
- Le
.gitignorene s'applique qu'aux fichiers pas encore suivis. git statussitue chaque modification dans l'une des trois zones.- Un commit se compose par intention, pas par fin de journée.
git log -Sretrouve les commits qui ont ajouté ou retiré un texte, même disparu.- Un diff repose sur la plus longue sous-suite commune entre les deux versions.
- Annuler dépend de l'endroit :
restore,restore --staged,--amend,revert. revertajoute un commit inverse ;resetréécrit l'historique.git reflogretrouve des commits qu'aucune branche ne référence plus.- Un secret commité est compromis : on le change, on ne se contente pas de l'effacer.
Et ensuite ? Tout cela vaut pour un dépôt personnel. À plusieurs, il faut décider qui pousse quoi, comment on relit, et comment on tranche un conflit. C'est le chapitre suivant.