Git expliqué : ce que sont réellement un commit, une branche et un merge, sous le capot
En une phrase
Git ne stocke pas une liste de modifications sur vos fichiers — il stocke des instantanés complets de tout votre projet à chaque commit, et la « magie », c'est qu'il est assez malin pour stocker des instantanés quasi identiques à très faible coût, en ne sauvegardant réellement que ce qui a changé.
Ce qu'est réellement un commit
Un commit est un instantané plus des métadonnées : qui l'a fait, quand, un message, et — c'est le point crucial — un pointeur vers le commit qui le précède. C'est tout le principe. Comme chaque commit pointe vers son parent, l'historique entier de votre projet n'est en réalité qu'une chaîne d'instantanés reliés en arrière dans le temps. Git n'a pas besoin d'un fichier « historique » séparé ; l'historique, c'est juste suivre les pointeurs.
Ce qu'est réellement une branche (ce n'est pas ce qu'on imagine)
La plupart des gens imaginent une branche comme une copie séparée de tout le projet. Ce n'est pas ça. Une branche est une étiquette minuscule et mobile — quelques octets — qui pointe simplement vers un commit dans cette chaîne. Créer une branche ne copie rien ; ça ajoute juste une nouvelle étiquette pointant là où vous êtes actuellement. Quand vous faites un nouveau commit sur cette branche, l'étiquette avance automatiquement pour pointer vers le nouveau commit. C'est pour ça que créer une branche dans Git est instantané, même sur un dépôt énorme — vous ne dupliquez pas des gigaoctets de fichiers, vous écrivez quelques octets.
Ce que fait réellement un merge
Disons que la branche A et la branche B ont toutes deux démarré depuis le commit X et ont divergé à partir de là. Un merge trouve cet ancêtre commun (X), regarde ce qui a changé sur A depuis X et ce qui a changé sur B depuis X, et combine les deux ensembles de modifications en un nouveau commit avec deux parents au lieu d'un. Si les deux branches ont modifié des lignes différentes, Git les combine automatiquement. Si elles ont modifié la même ligne différemment, Git ne peut pas deviner ce que vous voulez — c'est un conflit de merge, et ce n'est pas Git qui est cassé, c'est Git qui refuse à juste titre de choisir silencieusement un gagnant à votre place.
Ce qu'on se trompe souvent
« git pull et git fetch, c'est à peu près pareil » — non. Fetch télécharge le dernier historique depuis le dépôt distant et s'arrête là, en laissant votre propre branche totalement intacte pour que vous puissiez regarder avant de décider quoi que ce soit. Pull fait ce même fetch puis tente immédiatement de le fusionner dans votre branche courante. Fetch est la version prudente, qui regarde d'abord ; pull, c'est fetch-plus-merge en une seule étape, ce qui explique exactement pourquoi il peut vous surprendre avec un merge inattendu que vous n'aviez pas demandé.
Une nuance à connaître
Rien n'est vraiment supprimé dans Git au moment où on le croit. Même après avoir « supprimé » une branche, les commits vers lesquels elle pointait existent généralement encore dans le dépôt pendant un moment — Git ne nettoie les commits vraiment non référencés qu'après un certain temps. C'est exactement pour ça que récupérer une branche « supprimée par accident » est très souvent encore possible avec git reflog, une commande qui vaut la peine d'être connue avant de paniquer après un mauvais git branch -D.