Git est un projet très intéressant : il est basé sur un modèle simple, et fourni plein d’outils pour manipuler celui-ci avec facilité.

Encore une fois, il existe plein de documentations sur le sujet, donc je ne vais tout ré-expliquer ici. Cependant, je vais vous faire un micro-résumé parce que ça me fait plaisir (j’insiste).

Infos utiles

Pour commencer, gros changement depuis SCCS : Git stocke des états, pas des changements. Cela veut dire qu’il stocke tous les fichiers de chaque commit, dans leur intégralité, permettant donc d’y accéder facilement à n’importe quel moment. N’ayez pas pour autant peur d’un gâchis d’espace : grâce à son modèle (et à un peu de triche) il parvient à maintenir des tailles de dépôts comparable (souvent inférieure) à celles des dossiers de travail Subversion ! Bien entendu, s’il on compare un dépôt Git et un dépôt Subversion, la différence est encore plus frappante.

Autre point qu’il est bon de le savoir : tous les objets manipulés par Git sont référencés par leur SHA1, soit un hash cryptographique. Il est donc (quasiment) impossible de remplacer un contenu par un autre. De plus il est possible de signer (cryptographiquement) les tags de Git. Exemple concret : les serveurs de Linux ont été pénétrés en septembre 2011, et donc les attaquants ont eu accès au dépôt principal de l’OS. Grâce à ce mécanisme sécurisé, nous savons qu’aucune altération du dépôt n’a pu être effectuée. Ils auraient pu (dans le pire des cas) supprimer des informations. Mais — étant donné que Git est décentralisé — tout était déjà copier dans les autres dépôts (ne serait-ce que sur le PC perso de Linus), donc là encore aucune perte possible. Donc Git est sécurisé de part son modèle… ce qui est tout de même rassurant :-)

Les objets

Je vais donc vous parler des objets constituant le modèle de Git : blob, tree, commit et tag.

Tous les objets sont stockés de la même manière : gzippé dans un fichier nommé par la somme SHA1 du contenu (non compressé). Vous pouvez jeter un coup d’œil si le cœur vous en dit : leur chemin de fichier est de la forme $GIT_DIR/objects/aa/bbbb, où aa représente les deux premiers caractères du SHA1, et bbb les trente autres.

  • blob
  • Un blob est juste un contenu de fichier. Git calcule la somme SHA1 de ce contenu et s’en sert pour nommer le fichier dans lequel il stocke le blob, gzippé. Toutes les versions de tous les fichiers commités dans votre projet se trouvent dans le dépôt local sous forme de blob. Étant donné que seul le contenu est pris en compte, il même blog peut être utilisé pour représenté plusieurs fichiers, du moment que ceux-ci ont exactement le même contenu. C’est ainsi qu’un fichier non modifié par un commit réutilisera le même blob que son commit parent.

  • tree

  • De même qu’un blob est la représentation d’un (contenu de) fichier, le tree est la représentation d’un dossier. Pour faire simple, c’est un fichier dans lequel Git stocke des tuples (sha1, droits, nom). De même que pour le blob, seul le contenu du dossier est sauvegardé, par son propre nom (ni ses propres attributs) : il pourra donc être réutilisé par les prochain commits.

  • commit

  • La représentation d’un commit (en plus il a le même nom, c’est fou non ?). Il contient les méta-données (date, nom et email de l’auteur — qui a créé le commit — ainsi que du commiteur — qui l’a commité en dernier via un cherry-pick, rebase, commit --amend ou autre), les références des commits parents, une référence de tree et le message. Ainsi que je le disais, un commit contient donc un état et non juste un diff.

Ces 3 objets suffisent à représenter votre historique. Cependant, d’autres ont été ajouté pour ajouter des fonctionnalités sympathiques.

  • tag
  • Présents depuis la première version. Il contient des méta-données (date, nom et email de l’auteur), un référence de commit, un message et optionnellement une signature cryptographique.

  • note

  • Les notes sont des sortes d’extensions des messages de commits. Leur intérêt est de pouvoir être ajoutées et modifiées sans toucher au commit lui-même (donc sans modifier l’historique).

Les références

Une référence est une string servant à désigner un objet (le plus souvent un commit).

Git est assez flexible sur les référence : elle peuvent être globales (SHA1, un nom de branche ou de tag, etc) ou relatives (parent d’un SHA1 ou d’une branche, etc).

La référence la plus connue est sans nul doute « master », qui est le nom de la branche créée par défaut par Git. Ensuite vient « origin/master » qui est la branche remote.

Concernant les noms de branche ou tags, leur vraie forme est « refs/heads/master » pour les branches locales, « refs/remotes/origin/master » pour les branches distantes et « refs/tags/1.0 » pour les tags. Mais Git vous permet de ne pas écrire le « refs/ » ni « heads/ » ou « tags/ ». Cependant sachez que vous pouvez utiliser la forme complète pour éviter les ambiguïtés.

Pourquoi cette forme étrange (« refs/heads/master » et non « master » tout simplement) ? La réponse est simple : c’est le chemin du fichier dans lequel Git stocke la référence. (Là je m’attends à un « Heeeeiiiiing ? »). Je reprends : votre sacro-sainte branche master n’est qu’un fichier dont le chemin est $GIT_DIR/refs/heads/master et qui contient un SHA1 (celui du commit sur lequel la branche pointe). Vous pouvez modifier le SHA1 à la main dans ce fichier : cela modifiera votre branche (à la manière d’un git reset --soft).

Les références relatives utilisent une syntaxe décrite dans le manuel (git help rev-parse), donc je ne vais pas toutes les reprendre. Cependant voici un aperçu :

<ref>@(<date>)
remonte l’historique de <ref> pour trouver le commit avec la date voulu (accepte des dates globales et relatives).
<ref>^ ou <ref>^1 ou <ref>~1
1er parent de <ref>
<ref>^2
2e parent de <ref> (donc <ref> est un merge)
<ref>~2 ou <res>^1^1
1er parent du 1er parent de <ref>
:/<text>
remonte l’histoire pour trouver le commit donc le message contient <text>

Et j’en passe une belle flopée :-)

En plus de ces références statiques 1, Git fourni quelques références magiques, dont la valeur dépendra du contexte. Encore une fois, ce sont de simples fichiers stockés tels quels dans le dossier $DIR_DIR, et qui contiennent une référence. En voici quelques un :

HEAD
le commit courant. Le plus utile. Exemple d’utilisation : HEAD~1 désigne le commit parent.
ORIG_HEAD
l’ancien commit courant. Dit comme ça c’est étrange mais en fait c’est tout à fait logique : c’est lui qui vous sauvera la vie dans le cas d’un rebase foireux. Après un rebase (ou un merge), ORIG_HEAD désigne le commit référencé par HEAD avant cette opération.
FETCH_HEAD
contient les références récupérées par le dernier fetch. J’avoue que je vous le mets plus pour éviter de faire une liste de deux éléments, car je m’en sers relativement jamais en fait…

*[TGCM]: Ta Gueule C’est Magique

Les packs

Vous vous rappelez que j’ai dit que Git ne stockait pas de diffs ? J’ai menti :-)

Stocker un objet pour chaque version de chaque fichier commité — plus les dossiers et les commits — représente un grand nombre de fichiers à créer, et la plupart des systèmes de fichiers n’aiment pas trop. De plus, même si gzip est efficace — surtout en ce qui concerne les fichiers textes — séparer les fichiers réduit les chances d’optimiser la compression.

Git stocke donc bien tous les objets décrits dans la section Les objets, mais il se permet aussi d’en regrouper une (grosse) partie dans des fichiers nommés « packs ».

Un pack contient donc plusieurs objets (en fait plein serait plus juste), et utilise des techniques de compression sophistiqués afin de gagner un maximum de place. Ainsi il va stocker des différentiels là où il estime que ce sera utile.

Chaque pack est constitué de 2 fichiers : le contenu et un index permettant de retrouver rapidement les objets. Ils sont stockés dans $GIT_DIR/objects/pack.

Les références aussi ont droit à un pack, pour celles qui ne sont pas souvent modifier : elles sont dans $GIT_DIR/packed-refs, qui est un fichier texte non compressé. La syntaxe est très simple : « <sha1> <nom> ». Il est possible d’effectuer des modifications directement dans ce fichier, elles seront prises en compte immédiatement par Git.

Conclusion

J’espère avoir été clair, n’hésitez pas à me dire si certaines parties sont floues (voir fausse ☺).

Prochain et dernier article de cette série : notes sur la configuration.


  1. Ça n’est pas un vrai concept de Git, surtout qu’une référence de branche évolue au cours du temps, mais je m’en fou : c’est mon article alors TGCM.