Le versioning#
Quel est le problème qu’on cherche à résoudre?#
Le logiciel (et le code source dont il est constitué) a une particularité fondamentale : il change tout le temps. Même un programme simple est rapidement modifié - pour corriger un bug, ajouter une fonctionnalité, améliorer la performance ou simplement rendre le code plus lisible. Et très vite, une question apparaît : comment savoir ce qui a changé, quand, et pourquoi ?
Sans système de versioning, le développement logiciel devient fragile. On copie des fichiers, on renomme des dossiers (« version2 », « version_finale », « version_finale_bis »), on échange du code par courriel ou sur un disque partagé. Ces solutions fonctionnent… jusqu’au moment où elles ne fonctionnent plus : on perd une modification importante, on ne sait plus quelle version est la bonne, ou deux personnes écrasent mutuellement leur travail.
Le versioning n’est pas une simple sauvegarde. Sauvegarder, c’est garder une photo du code à un instant donné. Versionner, c’est garder la mémoire de son évolution. Un bon système de versioning permet de revenir en arrière, de comprendre l’historique d’un projet, de travailler à plusieurs sans se gêner, et d’expérimenter sans casser ce qui fonctionne.
À mesure que les projets grossissent et que le travail devient collectif, le versioning devient indispensable. Il structure la façon dont on travaille, dont on collabore, et même dont on réfléchit au code. C’est ce problème — gérer le changement — qui a conduit à la création des systèmes de contrôle de versions, et en particulier de Git, que nous allons étudier dans ce chapitre.
Bref historique des systèmes de gestion de versions#
Première génération : local et fichier unique#
SCCS (Source Code Control System, 1972) — Bell Labs
- Premier système de versioning, créé par Marc Rochkind
- Stockage par deltas (différences) pour économiser l’espace
- Un seul fichier à la fois, un seul utilisateur à la fois
- Verrouillage exclusif pour éviter les conflits
RCS (Revision Control System, 1982) — Walter Tichy, Purdue
- Amélioration de SCCS, plus rapide grâce aux deltas inversés (stocke la version courante en entier, reconstruit les anciennes)
- Toujours limité à des fichiers individuels
- Reste local à une machine
Deuxième génération : centralisé et projets entiers#
CVS (Concurrent Versions System, 1986–1990) — Dick Grune, puis Brian Berliner
- Construit au-dessus de RCS mais gère des arborescences de fichiers
- Premier système vraiment concurrent : plusieurs développeurs peuvent modifier simultanément
- Modèle client-serveur avec un dépôt central
- Faiblesses notoires : pas de commits atomiques, gestion pénible des renommages et des branches
Perforce (1995) — commercial
- Très performant sur de gros dépôts binaires
- Encore utilisé dans l’industrie du jeu vidéo et les grandes entreprises
Subversion (SVN) (2000) — CollabNet
- Conçu explicitement pour corriger les défauts de CVS
- Commits atomiques, versioning des répertoires, renommages suivis
- Toujours centralisé : le serveur est la source de vérité unique
- Numérotation linéaire des révisions (r1, r2, r3…)
Troisième génération : distribué#
BitKeeper (1998–2000) — Larry McVoy
- Premier système distribué utilisé à grande échelle (noyau Linux de 2002 à 2005)
- Licence propriétaire controversée, ce qui a mené à la création de Git
GNU Arch / Bazaar / Darcs (début 2000)
- Expérimentations diverses sur le versioning distribué
- Darcs introduit une approche par patches commutatifs (théorie des patches)
Git (2005) — Linus Torvalds
- Créé en quelques semaines après la rupture avec BitKeeper
- Objectifs : vitesse, intégrité des données, support massif des branches
- Caractéristiques clés :
- Chaque clone est un dépôt complet avec tout l’historique
- Stockage par snapshots (pas deltas) avec déduplication via SHA-1
- Branches ultra-légères (simples pointeurs)
- Travail hors ligne total
- Modèle de staging (index) avant commit
- Devenu le standard de facto grâce à GitHub (2008)
Mercurial (2005) — Matt Mackall
- Lancé la même semaine que Git, mêmes motivations
- Interface plus simple, concepts similaires
- Utilisé par Facebook, Mozilla (historiquement)
- Moins dominant aujourd’hui, mais toujours actif
Tableau récapitulatif#
| Système | Année | Modèle | Granularité | Particularité |
|---|---|---|---|---|
| SCCS | 1972 | Local | Fichier | Premier système, deltas |
| RCS | 1982 | Local | Fichier | Deltas inversés |
| CVS | 1990 | Centralisé | Projet | Accès concurrent |
| SVN | 2000 | Centralisé | Projet | Commits atomiques |
| Git | 2005 | Distribué | Projet | Snapshots, branches légères |
| Mercurial | 2005 | Distribué | Projet | Interface épurée |
L’évolution suit une logique claire : d’abord résoudre le problème du suivi pour un fichier, puis pour un projet, puis permettre la collaboration, et enfin décentraliser pour la résilience et la flexibilité.
Un système de gestion des versions moderne et extrêmement populaire : git#
Fonction de hachage#
Pour se construire un bon modèle mental du fonctionnement de git, il est essentiel de bien comprendre tout d’abord la notion de fonction de hachage.
Une fonction de hachage est une fonction mathématique qui prend en entrée un nombre, et qui retourne un autre nombre. Une fonction de hachage ne peut pas être n’importe quelle fonction par contre (comme par exemple $f(x) = mx + b$), elle doit avoir certaines caractéristiques :
Déterminisme : Pour une même donnée en entrée, vous obtiendrez toujours exactement le même hash en sortie.
Rapidité : Le calcul du hash doit être presque instantané.
Effet d’avalanche : Si vous changez ne serait-ce qu’une virgule ou une majuscule dans le texte d’origine, le hash produit sera totalement différent.
Irréversibilité (Hachage cryptographique) : Il est impossible de retrouver la donnée d’origine à partir du hash. C’est un sens unique.
Résistance aux collisions : Il est extrêmement improbable que deux entrées différentes produisent le même hash.
Le premier critère implique qu’en dépit des apparences, un hash n’est pas un nombre aléatoire, il est important de le réaliser. Le dernier critère implique que l’image de la fonction (l’ensemble de ses valeurs possibles) doit être extrêmement grand (car s’il ne l’était pas, les collisions seraient fréquentes). La fonction de hachage SHA-1, utilisée couramment par git, produit par exemple des valeurs de 160 bits, ce qui correspond à $2^{160}$ valeurs possibles, ce qui est un nombre vraiment très grand, mais tout de même plus petit que ceux produits par la fonction SHA-256, aussi utilisée par les versions plus modernes de git, et dont la taille de l’image se rapproche du nombre estimé d’atomes dans l’univers (environ $2^{266}$, ou $10^{80}$).
Il est important de comprendre que le but d’une fonction de hachage, dans le contexte de git, est de calculer un nombre unique, à partir d’un “objet”. Cet “objet” (au sens digital) peut être de plusieurs natures :
- Une chaîne de caractères
- Un fichier (ou document)
- Un groupe de plusieurs fichiers
- Une arborescence (récursive) de fichiers
- Etc.
Chacun de ces objets peut être considéré comme étant un nombre, avec les conversions nécessaires. C’est ici qu’entrent en jeu les qualités particulières que nous avons énumérées ci-haut : pour un objet donné (un fichier par exemple), la valeur de hash doit être unique (résistance aux collisions), et toujours la même, sans exception (déterminisme). Si on ne change ne serait-ce qu’un bit d’un fichier, la valeur doit changer (effet d’avalanche). L’implémentation de cette fonction (dans un langage comme Python, avec la librairie hashlib par exemple doit être suffisamment rapide, parce que git l’utilisera extrêmement souvent, étant donné qu’il s’agit d’une de ses opérations fondamentales.
Implémentation de SHA-256 en Python pur (sans la libraire hashlib), pour les curieux
# Implémentation pédagogique de SHA-256 en Python pur (sans hashlib)
def rotation_droite(valeur, decalage):
"""
Rotation circulaire à droite sur 32 bits
"""
return ((valeur >> decalage) | (valeur << (32 - decalage))) & 0xFFFFFFFF
def sha256(message: bytes) -> str:
"""
Calcule le hash SHA-256 d'un message (bytes)
et retourne une chaîne hexadécimale
"""
# Valeurs initiales du hash
# (32 premiers bits des parties fractionnaires
# des racines carrées des 8 premiers nombres premiers)
H = [
0x6a09e667,
0xbb67ae85,
0x3c6ef372,
0xa54ff53a,
0x510e527f,
0x9b05688c,
0x1f83d9ab,
0x5be0cd19,
]
# Constantes de ronde
# (32 premiers bits des parties fractionnaires
# des racines cubiques des 64 premiers nombres premiers)
K = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
]
# --- Prétraitement (padding) ---
longueur_originale = len(message) * 8 # en bits
message += b'\x80' # ajout du bit 1
# Ajout de zéros jusqu'à atteindre 448 bits modulo 512
while (len(message) * 8) % 512 != 448:
message += b'\x00'
# Ajout de la longueur originale sur 64 bits
message += longueur_originale.to_bytes(8, 'big')
# --- Traitement par blocs de 512 bits ---
for debut_bloc in range(0, len(message), 64):
bloc = message[debut_bloc:debut_bloc + 64]
W = [0] * 64 # planification du message
# Les 16 premiers mots proviennent directement du bloc
for i in range(16):
W[i] = int.from_bytes(bloc[i*4:(i+1)*4], 'big')
# Extension des 16 mots en 64
for i in range(16, 64):
s0 = (
rotation_droite(W[i-15], 7)
^ rotation_droite(W[i-15], 18)
^ (W[i-15] >> 3)
)
s1 = (
rotation_droite(W[i-2], 17)
^ rotation_droite(W[i-2], 19)
^ (W[i-2] >> 10)
)
W[i] = (W[i-16] + s0 + W[i-7] + s1) & 0xFFFFFFFF
# Initialisation des registres de travail
a, b, c, d, e, f, g, h = H
# --- Boucle principale de compression ---
for i in range(64):
S1 = (
rotation_droite(e, 6)
^ rotation_droite(e, 11)
^ rotation_droite(e, 25)
)
choix = (e & f) ^ (~e & g)
temp1 = (h + S1 + choix + K[i] + W[i]) & 0xFFFFFFFF
S0 = (
rotation_droite(a, 2)
^ rotation_droite(a, 13)
^ rotation_droite(a, 22)
)
majorite = (a & b) ^ (a & c) ^ (b & c)
temp2 = (S0 + majorite) & 0xFFFFFFFF
h = g
g = f
f = e
e = (d + temp1) & 0xFFFFFFFF
d = c
c = b
b = a
a = (temp1 + temp2) & 0xFFFFFFFF
# Ajout du résultat du bloc au hash courant
H = [
(x + y) & 0xFFFFFFFF
for x, y in zip(H, [a, b, c, d, e, f, g, h])
]
# Conversion finale en hexadécimal
return ''.join(f'{valeur:08x}' for valeur in H)
# Exemple d'utilisation
if __name__ == "__main__":
print(sha256(b"hello world"))Une valeur de hash particulière pourrait être par exemple :
2123f2435e1dbe255a323c90e97e38f759fe8946
ce qui correspond à un nombre hexadécimal (en base 16 donc, qui s’exprime avec
les dix chiffres 0 à 10, ainsi que les 6 premières lettres de l’alphabet, de a
à f) de 40 “chiffres”. Chaque caractère hexadécimal de cette chaîne étant
équivalent à 4 bits, nous avons donc un nombre de $40 \times 4 = 160$ bits, qui
permet d’exprimer donc $2^{160}$ valeurs possibles.
Faites-vous une idée concrète du fonctionnement d’une fonction de hachage à l’aide de cette applet interactive :
Finalement, une manière de comprendre le rôle que peut jouer une fonction de hachage est en tant qu’une sorte de “signature” : une signature représente une personne ou un document de manière unique, sans aucune ambiguïté possible. Il est également possible de se représenter un hash en tant que pointeur vers quelque chose, ce qui est la métaphore utilisée par git, comme nous le verrons concrètement.
La notion de hachage est utilisée de manière centrale par git, mais il s’agit d’une notion fondamentale en informatique. Elle est particulièrement importante dans le monde de la cryptographie, qui est la science de l’échange et de la manipulation de “secrets”. Elle joue également un rôle fondateur dans des domaines plus récents et émergents, comme les cryptomonnaies (Bitcoin est l’exemple le plus fameux) et de manière plus générale, la notion de chaîne de blocs (blockchain en anglais).
Les fondements de git#
Git est un outil pour la ligne de commande qui utilise un modèle de données particulier pour représenter et manipuler le contenu et l’évolution historique de l’ensemble de fichiers d’un projet de développement logiciel. Nous allons l’étudier tout en l’utilisant concrètement. Je vous recommande d’entrer les commandes qui vont suivre dans votre propre ligne de commande au moment de la lecture, afin de rendre plus concrète les notions que nous verrons.
La première notion importante de git est celle de dépôt (repository en anglais), qui correspond à un répertoire particulier, et qui constitue un projet au sens de git. Ce répertoire peut déjà exister (et contenir des fichiers) ou être créé pour l’occasion et donc être vide. Nous allons donc tout d’abord créer un nouveau répertoire :
$ mkdir mon_premier_depot
$ cd mon_premier_depot
$ ls -al
total 0
drwxr-xr-x@ 2 cjauvin staff 64 Jan 13 16:54 ./
drwxr-x---+ 75 cjauvin staff 2400 Jan 13 16:54 ../Notre répertoire n’est pas encore un dépôt git, mais avec cette commande il le devient :
$ git init
Initialized empty Git repository in /Users/cjauvin/mon_premier_depot/.git/On peut constater que git a ajouté un sous-répertoire spécial dans notre répertoire :
$ ls -al
total 0
drwxr-xr-x@ 3 cjauvin staff 96 Jan 13 16:58 ./
drwxr-x---+ 75 cjauvin staff 2400 Jan 13 16:54 ../
drwxr-xr-x@ 9 cjauvin staff 288 Jan 13 17:10 .git/Ce répertoire contient toutes les données dont git aura besoin pour gérer notre dépôt. On peut le voir comme une sorte de base de données aussi, qui est pour le moment vide :
$ ls -al .git
total 24
drwxr-xr-x@ 9 cjauvin staff 288 Jan 13 17:10 ./
drwxr-xr-x@ 3 cjauvin staff 96 Jan 13 16:58 ../
-rw-r--r--@ 1 cjauvin staff 137 Jan 13 16:58 config
-rw-r--r--@ 1 cjauvin staff 73 Jan 13 16:58 description
-rw-r--r--@ 1 cjauvin staff 21 Jan 13 16:58 HEAD
drwxr-xr-x@ 16 cjauvin staff 512 Jan 13 16:58 hooks/
drwxr-xr-x@ 3 cjauvin staff 96 Jan 13 16:58 info/
drwxr-xr-x@ 4 cjauvin staff 128 Jan 13 16:58 objects/
drwxr-xr-x@ 4 cjauvin staff 128 Jan 13 16:58 refs/Commençons notre exploration en créant tout d’abord un premier fichier :
$ echo "allo!" >> toto.txtÀ ce stade, l’état du fichier est “untracked”, c’est-à-dire qu’il ne fait pas partie du dépôt (même si git a tout de même remarqué sa présence) :
$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
toto.txt
nothing added to commit but untracked files present (use "git add" to track)On peut ajouter le fichier dans le “staging area” de git, qui constitue une sorte d’espace de travail temporaire, qui sert à accumuler les changement que nous voudrons “committer” plus tard :
$ git add toto.txtUne fois le fichier ajouté, la même commande permet de constater le changement d’état. Pour le moment, le nouveau fichier est dans le staging area, prêt à être committé :
$ git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: toto.txtOn est maintenant prêt à faire notre premier commit, qui est simplement l’ajout officiel de ce fichier dans la base de données git :
$ git commit -m "Premier commit"
[main (root-commit) 1359fb1] Premier commit
1 file changed, 1 insertion(+)
create mode 100644 toto.txtUne fois ce premier commit effectué, notre dépôt git est démarré en bonne et due forme, et on peut commencer à l’inspecter en profondeur pour en comprendre le fonctionnement et le modèle de données. Il est important de savoir que les commandes qui vont suivre sont très rarement utilisées dans l’usage quotidien de git (pratiquement jamais en fait). On s’en sert ici simplement en tant qu’outils pédagogiques.
Premier type d’objet fondamental git : le blob#
Le modèle de données de Git est constitué de quatre types fondamentaux d’objets.
Le premier type est le blob, qui correspond aux données données binaires
compressées d’un fichier donné (ici toto.txt) :
$ git ls-tree -r HEAD
100644 blob 4c7d057645ac149446d1289aaa6f9fd74e91ce13 toto.txtNotre premier blob a donc comme identifiant le hash
4c7d057645ac149446d1289aaa6f9fd74e91ce13. Voici comment git calcule le hash du
blob, à partir du fichier original qu’on a ajouté :
$ git hash-object toto.txt
4c7d057645ac149446d1289aaa6f9fd74e91ce13Cette valeur particulière devrait être la même pour vous, dans votre
environnement, car elle n’est pas une valeur aléatoire, elle est entièrement
déterministe. La seule chose qui pourrait changer la donne serait que votre git
soit configuré pour utiliser une autre fonction de hachage que SHA1, ce qui
est possible.
Si on entre la valeur allo! dans l’applet interactif ci-haut, on n’obtient pas
cette valeur particulière de hash pourtant (essayez-le, avec la fonction
SHA1).. pourquoi donc? Parce que git hache en fait un peu plus que simplement
le contenu du fichier : il hache la valeur blob <taille>\0<contenu>, où
<contenu> correspond dans notre cas à allo!\n (le \n est un caractère
spécial pour le retour de chariat, newline en anglais) et la <taille>, en
nombre de caractères, est donc 6. Donc si on entre la valeur blob 6\0allo!\n
dans l’applet, le même hash que celui calculé par git devrait apparaître.
Ce blob est en fait un fichier, qui est sauvegardé dans la base de données
spéciale de git (dans le répertoire caché .git). Le chemin vers ce fichier (de
blob) est créé à partir du hash, qui est séparé en deux parties (la première
correspondant à son suffixe de deux lettres) :
$ ll .git/objects/
total 0
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 12:16 4c/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 12:16 9e/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 12:16 cf/
drwxr-xr-x@ 2 cjauvin staff 64B Jan 15 12:16 info/
drwxr-xr-x@ 2 cjauvin staff 64B Jan 15 12:16 pack/
$
$ ll .git/objects/4c
total 8
-r--r--r--@ 1 cjauvin staff 21B Jan 15 12:16 7d057645ac149446d1289aaa6f9fd74e91ce13Contrairement à toto.txt, qui contient du texte, le blob n’est pas lisible,
car il est constitué de données binaires compressées :
cat .git/objects/4c/7d057645ac149446d1289aaa6f9fd74e91ce13
xK��OR0cH���W�+�⏎Essayons maintenant de modifier notre fichier texte en y ajoutant une ligne :
$ echo "bonjour" >> toto.txt
$ cat toto.txt
allo!
bonjourEt vérifions l’effet sur git avec la commande git status :
$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: toto.txt
no changes added to commit (use "git add" and/or "git commit -a")Pour voir le changement précis que git détecte on peut utiliser git diff :
$ git diff
diff --git a/toto.txt b/toto.txt
index 4c7d057..48bed0c 100644
--- a/toto.txt
+++ b/toto.txt
@@ -1 +1,2 @@
allo!
+bonjourQue se passerait-il si on tentait de committer notre changement à ce point?
$ git commit -m "Dexuième commit"
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: toto.txt
no changes added to commit (use "git add" and/or "git commit -a")Il n’y a rien (encore) à committer, car le changement n’a pas été ajouté
dans le staging area avec la commande git add, dont on peut tout de suite
voir l’effet avec git status :
$ git add toto.txt
$
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: toto.txtVoici le modèle mental qu’il faut préférablement avoir des trois “endroits” importants qui sont impliqués dans le pipeline de création d’un commit :
Nous sommes maintenant prêt pour le deuxième commit :
$ git commit -m "Deuxième commit"
[main 34c85ab] Deuxième commit
1 file changed, 1 insertion(+)
$
$ git status
On branch main
nothing to commit, working tree cleanExaminons ce qui s’est passé au niveau des objets git :
git ls-tree -r HEAD
100644 blob 48bed0c35d92c60539832929142859e3f5ae6eda toto.txtOn remarque que toto.txt est maintenant associé à ce qui semble être un blob
différent, car le hash est différent (48bed0c35d92c60539832929142859e3f5ae6eda
alors qu’on avait 4c7d057645ac149446d1289aaa6f9fd74e91ce13 avant). Que
s’est-il passé? Un nouveau blob a été créé, correspondant à la nouvelle version
du fichier. Si on regarde la liste des objets, on peut voir notre nouveau blob
et son contenu :
$ ll .git/objects/
total 0
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 14:57 34/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 14:53 38/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 14:39 48/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 12:16 4c/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 14:53 88/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 12:16 9e/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 12:16 cf/
drwxr-xr-x@ 2 cjauvin staff 64B Jan 15 12:16 info/
drwxr-xr-x@ 2 cjauvin staff 64B Jan 15 12:16 pack/
$
$ cat .git/objects/48/bed0c35d92c60539832929142859e3f5ae6eda
xK��OR04aH���W�J����/-�O⏎Mais ce qu’il est fondamental de remarquer et de comprendre, c’est que notre
blob précédent (4c7d057645ac149446d1289aaa6f9fd74e91ce13) est toujours là, il
n’a pas disparu ! Ceci veut donc dire que tous les stages de transformation d’un
fichier sont sauvegardés en tant que fichiers complets et indépendants. Git
ne gère donc pas les “différences” entre les fichiers, mais conserve plutôt
l’état complet de chaque version (en anglais on parle souvent de “snapshot”,
soit l’état précis d’un fichier à un certain point dans une chaîne de
changements), de manière à pouvoir “calculer” la différence, au moment où c’est
nécessaire. Est-ce que ceci n’entraîne pas un coût supplémentaire par contre, en
terme d’espace disque? Si on a un fichier de 100 lignes et qu’on fait 10 commits
qui ajoutent chacun 1 ligne, nous aurons au final 10 blobs de plus de 100 lignes
(un de 100, un de 101, etc). Il serait plus compact de ne sauvegarder que les
différences successives. Pourtant, git choisit de fonctionner avec des
snapshots, parce que ça lui permet d’opérer de manière plus rapide et efficace,
il s’agit d’un compromis. Les comparaisons et la navigation entre les versions
d’un fichier sont donc, avec git, particulièrement efficaces, ce qui a
certainement joué un rôle dans son adoption généralisée spectaculaire. Il faut
mentionner également qu’il y a un niveau plus “bas” que les blobs, les
packfiles, où le contenu des blobs est compressé efficacement (et donc la
redondance éliminée), mais comme ce niveau est plus complexe et moins facile
d’accès, il ne fera donc pas partie de notre analyse.
Le fait d’avoir un mécanisme comme la fonction de hachage permet à git de
rapidement répondre à la question : est-ce qu’un fichier a changé ou non? Si le
hash d’un fichier est identique aujourd’hui à ce qu’il était hier, nous avons la
certitude, par définition, que le fichier n’a pas changé. Car le moindre
changement (ne serait-ce que l’ajout dune virgule) produit une valeur de hash
complètement différente. Par contre, cette différence, au niveau des valeurs du
hash, ne disent rien au sujet de ce qui a changé ! Elle dit SEULEMENT que
quelque chose a changé. Si git veut savoir exactement ce qui a changé (pour par
exemple le fournir à l’utilisateur de la commande git diff), il doit le
calculer en temps réel, au moment du besoin. On pourrait penser que ceci est
coûteux et inefficaces, mais dans les faits ça ne l’est pas, car un algorithme
pour déterminer la différence entre deux fichiers textes peut être exécuté très
rapidement. Le modèle de donnés de git est entièrement conçu de manière à
optimiser les cas s’usage les plus courants.
Poursuivons notre analyse du modèle de données de git en nous demandant ensuite : qu’est-ce qu’un commit, au juste? Jusqu’à maintenant, nous avons créé deux commits, en séquence :
$ git log
commit 34c85abbbbee1c21c34da2b5ed126cb888fcb498 (HEAD -> main)
Author: Christian Jauvin <cjauvin@gmail.com>
Date: Thu Jan 15 14:57:15 2026 -0500
Deuxième commit
commit 9e6072c8df904f021474a6173cc1b17b2908c0f1
Author: Christian Jauvin <cjauvin@gmail.com>
Date: Thu Jan 15 12:16:21 2026 -0500
Premier commitLe premier commit est associé au hash 9e6072c8df904f021474a6173cc1b17b2908c0f1.
On peut utiliser git cat-file pour décrire le contenu de ce commit :
$ git cat-file -p 9e6072c8df904f021474a6173cc1b17b2908c0f1
tree cff192ac57ced2fea4977bcf9dcc6c3590af9cbd
author Christian Jauvin <cjauvin@gmail.com> 1768497381 -0500
committer Christian Jauvin <cjauvin@gmail.com> 1768497381 -0500
Premier commitDeuxième type d’objet git fondamental : le tree#
On voit que le commit contient un “tree” (un arbre), qui est le deuxième type
fondamental d’objet dans le modèle de données de git que nous voyons, après le
blob. Si un blob est un fichier, alors le tree est un répertoire (une
arborescence récursive de fichiers). Si on inspecte notre tree avec git cat-file,
on constate qu’il ne contient qu’un seul blob, celui qui correspond à notre fichier toto.txt :
$ git cat-file -p cff192ac57ced2fea4977bcf9dcc6c3590af9cbd
100644 blob 4c7d057645ac149446d1289aaa6f9fd74e91ce13 toto.txtPour se faire une meilleure idée du fonctionnement des trees, complexifions un peu la structure de notre projet en ajoutant des fichiers et des répertoires :
$ mkdir src
$ touch src/app.js
$ mkdir src/components
$ touch src/components/menu.jsLa commande git add effectuée sur un répertoire ajoute récursivement au
staging area tous les fichiers se trouvant à n’importe quel niveau sous le
répertoire, d’un coup :
$ git add src
$
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: src/app.js
new file: src/components/menu.jsOn peut maintenant faire notre troisième commit :
$ git commit -m "Troisième commit"
[main 2e6fdee] Troisième commit
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 src/app.js
create mode 100644 src/components/menu.jsOn peut récupérer le hash de notre troisième et dernier commit en faisant :
$ git log -1
commit 2e6fdee379fbe0d258b2d5f04e7cdd0b4d5cbccb (HEAD -> main)
Author: Christian Jauvin <cjauvin@gmail.com>
Date: Thu Jan 15 19:05:29 2026 -0500
Troisième commitInspectons tout d’abord ce commit :
$ git cat-file -p 2e6fdee379fbe0d258b2d5f04e7cdd0b4d5cbccb
tree fb1722eff4b73668ddec99b3452d692b1e4aa9ba
parent 34c85abbbbee1c21c34da2b5ed126cb888fcb498
author Christian Jauvin <cjauvin@gmail.com> 1768521929 -0500
committer Christian Jauvin <cjauvin@gmail.com> 1768521929 -0500
Troisième commitInspectons ensuite son tree (dont le hash est spécifié à la première ligne) :
$ git ls-tree -r -t fb1722eff4b73668ddec99b3452d692b1e4aa9ba
040000 tree 6edfc5b49bc2a7de3402cd734abfee5c4a0c42bd src
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 src/app.js
040000 tree e0408d67c1864db2c5bfae47d89933d6c714b810 src/components
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 src/components/menu.js
100644 blob 48bed0c35d92c60539832929142859e3f5ae6eda toto.txtOn constate la nature récursive du tree, qui est lui-même composé de blobs et de sous-trees.
Il faut comprendre que les trees sont des “objets” git au même sens que les blobs (seulement d’un type différent), et qu’ils sont stockés sous la forme de fichiers binaires, au même endroit que les blobs :
$ ll .git/objects/
total 0
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 19:05 2e/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 14:57 34/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 14:53 38/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 16 15:40 3d/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 14:39 48/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 12:16 4c/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 16 15:40 4d/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 16 15:40 69/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 19:05 6e/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 16 15:37 76/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 16 15:37 82/
drwxr-xr-x@ 4 cjauvin staff 128B Jan 16 15:37 88/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 12:16 9e/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 16 15:37 c7/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 12:16 cf/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 19:05 e0/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 19:02 e6/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 16 15:37 e7/
drwxr-xr-x@ 3 cjauvin staff 96B Jan 15 19:05 fb/
drwxr-xr-x@ 2 cjauvin staff 64B Jan 15 12:16 info/
drwxr-xr-x@ 2 cjauvin staff 64B Jan 15 12:16 pack/Les objets git sont organisés sur le disque en fonction du préfixe de deux chiffres de leur hash.
Le diagramme qui suit introduit une idée importante à comprendre avec git : si cela n’introduit pas d’ambiguïté, on peut référer à un hash (qui est une valeur très longue et difficile à lire ou manipuler) avec seulement son préfixe, soit une valeur plus courte. On peut ainsi faire référence à
fb1722eff4b73668ddec99b3452d692b1e4aa9baavec son préfixe plus court des 6 premiers chiffres, soitfb1722, car il n’y a aucun autre objet, dans notre dépôt git, qui a ce même préfixe (si c’était le cas, git produirait un message d’erreur pour s’en plaindre). Ce préfixe peut être d’une longueur autre que 6, mais on préfère en général une longueur qui maximise la lisibilité, selon le contexte. Il est à noter que ce système de préfixe simplifié fonctionne également avec tous les outils git de la ligne de commande.
La notion d’arbre git permet d’introduire une notion importante, fondamentale à
git : l’arbre de Merkle. Un
arbre de Merkle est un type d’arbre particulier où chaque noeud a un hash qui
est fonction de (qui inclut, ou prend en considération, donc) tout ce qui est
“dessous”. Si on change un élément de l’arbre (un blob par exemple) alors
l’effet est que les hash de tous les éléments “au-dessus” (les ancêtres) vont
devoir changer aussi (car leur valeur dépend de tout ce qu’ils ont sous eux).
Examinons un exemple concret pour s’en convaincre, modifions tout d’abord un
fichier au milieu de notre arborescence. Profitons-en pour apprendre que l’ajout
de -a à la commande git commit (en plus de -m qu’on avait déjà, pour faire
donc -am) permet de sauter l’étape d’ajouter le changement au staging area
(git add) quand on veut aller un peu plus rapidement :
$ echo "alert('allo!')" >> src/app.js
$
$ git commit -am "Quatrième commit"
[main 3dd71fb] Quatrième commit
1 file changed, 1 insertion(+)Il est intéressant de noter en passant que l’arbre de Merkle est la structure de données qui est au coeur de la cryptomonnaie (comme Bitcoin) et de la chaîne de blocs (blockchain). Git et les cryptomonnaies constituent donc deux exemples fameux et extrêmement influents de cette technologie particulière.
Notons tout d’abord le hash du commit qu’on vient de produire :
$ git log -1
commit 3dd71fb4950c1be35ec335cd0ac40f3b5807303b (HEAD -> main)
Author: Christian Jauvin <cjauvin@gmail.com>
Date: Fri Jan 16 15:40:53 2026 -0500
Quatrième commitExaminons ensuite le commit et son tree, et notons qu’on peut utiliser son préfixe court de 4 chiffres (3dd7) car il n’est pas ambigu :
$ git cat-file -p 3dd7
tree 6954fc566d4abe842409c9265ca5bb94044af1c4
parent 2e6fdee379fbe0d258b2d5f04e7cdd0b4d5cbccb
author Christian Jauvin <cjauvin@gmail.com> 1768596053 -0500
committer Christian Jauvin <cjauvin@gmail.com> 1768596053 -0500
Quatrième commit
$
$ git ls-tree -r -t 6954
040000 tree 4d74086f34491859490bd4a3695876e23798b8f0 src
100644 blob 76aa73d7349a507efaf3206cd21428d4452eae9e src/app.js
040000 tree e0408d67c1864db2c5bfae47d89933d6c714b810 src/components
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 src/components/menu.js
100644 blob 48bed0c35d92c60539832929142859e3f5ae6eda toto.txtOn peut constater le fait intéressant que seuls les arbres 6954fc et 4d74086 ont changés,
parce qu’ils sont les ancêtres du blob 76aa73, que notre commit a changé. On constate
donc clairement qu’un arbre git est un arbre de Merkle au sens classique.
Troisième type d’objet git fondamental : le commit#
Tournons maintenant notre attention vers les commits, qui sont le troisième type d’objet git fondamental.
Chaque fois qu’un commit est créé (avec la commande git commit), un nouvel
objet (de type commit) est ajouté à la base de données de git.
Examinons encore une fois notre commit le plus récent :
$ git cat-file -p 3dd7
tree 6954fc566d4abe842409c9265ca5bb94044af1c4
parent 2e6fdee379fbe0d258b2d5f04e7cdd0b4d5cbccb
author Christian Jauvin <cjauvin@gmail.com> 1768596053 -0500
committer Christian Jauvin <cjauvin@gmail.com> 1768596053 -0500
Quatrième commitUn commit a tout d’abord un hash (comme tous les objets git) : 3dd71fb4950c1be35ec335cd0ac40f3b5807303b,
ou encore 3dd7, son préfixe équivalent, plus facile à manipuler. On constate ensuite que
le commit contient trois choses :
Un pointeur (identifiant, sous la forme d’un hash),
6954, vers un arbre qui représente (de manière récursive) les fichiers du dépôt dans un état particulier, un moment dans le temps.Un pointeur (c-à-d un hash) vers le commit parent (le commit qui a précédé ce commit particulier, soit
2e6f). Nous verrons qu’il est possible pour un commit d’avoir plusieurs parents, dans certaines circonstances.Des métadonnées diverses, comme la date et l’heure de création du commit, l’adresse de courriel du committeur, etc.
La présence du pointeur vers un commit parent suggère un concept fondamental avec git : les commits forment une chaîne :
Chaque commit a donc un hash qui “pointe” vers son commit prédécesseur (nous verrons plus loin qu’il est possible pour un commit d’avoir plus d’un parents, et en quoi c’est utile), pour former une chaîne.
Jusqu’ici, git ne nous permet que d’évoluer de manière linéaire, étant donné que
n’avons qu’une chaîne de commits. Pourtant, le développement logiciel, surtout
s’il est effectué par une équipe, est tout sauf linéaire. Des embranchements
peuvent être nécessaires dans le processus d’évolution du code source. Pour
illustrer cela, imaginons que les 4 commits que nous avons jusqu’à maintenant
correspondent à la version 1 (v1) d’une application qu’on a mise en ligne, et
qui est utilisée par le grand public. Mais étant donné qu’on perd rarement
rarement du temps en développement logiciel, le travail a déjà commencé pour
concevoir la version 2 du même logiciel (qu’on prévoit mettre en ligne plus
tard). Deux commits ont donc été ajoutés à notre chaîne, qui constituent le
début du travail sur la version 2. Notez qu’à partir d’ici, nous allons utiliser
des noms de commit plus simples pour les diagrammes, donc le commit C1 va
correspondre 9e60, C2 à 34c8, et ainsi de suite. Nos deux nouveaux commits
pour la version 2 de l’application seraient donc C5 et C6
Maintenant imaginons ce qui se passerait si un bogue était découvert dans la
version 1, présentement en ligne (et qui correspond au commit C4 (3dd7) dans
notre exemple) :
Le problème est donc que la chaîne de commits est déjà rendue plus loin, au
deuxième commit de la version 2, soit le commit C6 ! Il faudrait produire un
commit C7, qui serait une correction de C4, mais qui n’inclurait pas les
changements introduits par la version 2 (C5 et C6). Comment serait-il
possible de résoudre un tel problème. Arrêtez-vous un instant pour y réfléchir
si vous le désirez, avant de prendre connaissance de la réponse, car il s’agit
d’un problème fondamental et intéressant !
La branche#
La façon dont git permet de résoudre ce problème est à l’aide d’une branche :
Donc au moment où le travail sur la version 1 est complété (au commit C4
donc), on doit créer une nouvelle branche (nommée v2), pour le travail qui
doit être effectué sur la nouvelle version 2. De cette manière, quand un
problème survient avec la version 1, il sera toujours possible de le résoudre en
ajoutant simplement un commit (C7) à la branche main, correspondant à la
version 1.
Il est souvent dit que créer une branche est sans contredit une des opérations les plus simples et les moins coûteuses de git :
$ git branch v2En comparaison, les systèmes de versioning plus anciens que git faisaient en sorte que cette opération était beaucoup plus coûteuse et complexe. La raison qui fait en sorte que la branche est si simple, avec git, est le fait qu’elle est techniquement extrêmement simple : ce n’est même pas un “objet fondamental” au sens où le blob, le tree et le commit le sont.. il s’agit plutôt simplement d’un fichier qui “pointe” vers le hash d’un commit particulier :
$ cat .git/refs/heads/v2
3dd71fb4950c1be35ec335cd0ac40f3b5807303bOn voit ici que le fichier .git/refs/heads/v2, dont le nom correspond à notre
nouvelle branche v2, contient simplement le commit 3dd7 (ou encore, C4,
dans nos diagrammes, pour simplifier), qui est l’endroit de départ (ou
l’embranchement) pour notre nouvelle branche.
Mais si une branche est si simple, qu’elle n’est qu’une étiquette qui pointe vers un commit, comment peut-elle fonctionner? Nous sommes présentement dans l’état suivant :
Si nous ajoutons un nouveau commit dans cet état, comment git fera pour savoir dans
quelle branche l’ajouter? Nous devons donc introduire un nouveau concept : HEAD, qui
est un fichier qui “pointe” vers la branche active, celle dont nous voulous nous servir :
$ cat .git/HEAD
ref: refs/heads/mainVoici donc la représentation réelle de notre état actuel :
Git manipule donc deux types de pointeur :
- Un pointeur vers un commit (soit une branche)
- Un pointeur vers une branche (soit
HEAD)
Si on utilise cette commande :
$ git branch
* main
v2git nous dit que la branche courante est toujours main (qui est la branche par
défaut, créée au moment de la création du dépôt), avec la petite étoile (*).
Si nous ajoutions un commit à ce moment-ci, il serait donc dans la branche
main. Mais comme nous voulons maintenant travailler dans la branche v2, nous
devons utiliser cette nouvelle commande :
$ git switch v2
Switched to branch 'v2'
$
$ git branch
main
* v2pour nous retrouver maintenant dans cet état (avec lequel le pointeur HEAD pointe simplement
vers la nouvelle branche v2) :
Continuons donc notre expérience de pensée, et imaginons que nous sommes en train de travailler
dans le contexte de la version 2, et que nous ajoutons deux nouveaux commits (C5 et C6 dans notre
notation simplifiée pour les diagrammes) :
$ echo "aa" >> toto.txt
$ git commit -am "Cinquième commit (premier de v2)"
[v2 b1d097c] Cinquième commit (premier de v2)
1 file changed, 1 insertion(+)
$
$ echo "bb" >> toto.txt
$
$ git commit -am "Sixième commit (deuxième de v2)"
[v2 daa06a6] Sixième commit (deuxième de v2)
1 file changed, 1 insertion(+)Notre dépôt est maintenant comme ceci :
Notez que la structure du diagramme pourrait aisément suggérer que nous sommes
toujours dans une structure linéaire (une chaîne), et on pourrait se demander :
en quoi est-ce différent du modèle précédent, à une seule branche? La clé est
d’avoir le bon modèle mental : une branche n’est qu’un pointeur ! Et notre
diagramme montre que nous avons deux pointeurs, donc deux branches : main et
v2. Les commits C1 à C4 sont communs aux deux branches, et les commits
C5 et C6 appartiennent seulement à la branche v2. Et comme HEAD montre
(pointe) que nous nous trouvons sur la branche v2, tout nouveau commit s’y
retrouverait également.
Si on reprend le cours de notre expérience de pensée, on arrive au moment où un
bogue serait découvert avec la version 1 ! Que peut-on faire, avec la solution? Si
on ne fait qu’ajouter un commit (pour la solution), elle se retrouvera à suivre C6 dans la
branche v2, ce qui n’est pas ce que l’on veut (car la version 1 n’inclut pas encore les nouveautés
de la version 2). La solution est donc de changer tout d’abord de branche :
$ git switch main
Switched to branch 'main'
$
$ git branch
* main
v2ce qui ne fait que déplacer le pointeur HEAD vers la branche main, là où se trouve notre
fameux bogue :
Nous sommes donc maintenant en position de fournir la solution à notre bogue, et de faire en sorte qu’elle se retrouve à l’endroit logique là où elle devrait être :
$ echo "solution" >> toto.txt
$
$ git commit -am "Septième commit (bugfix v1)"
[main f229f82] Septième commit (bugfix v1)
1 file changed, 1 insertion(+)Et avec ceci, nous nous retrouvons donc dans la situation suivante :
qui correspond exactement à la configuration souhaitée que nous avons décrite plus haut (prenez le temps de vous en convaincre, car c’est important).
Le merge (de deux branches)#
Poursuivons notre scénario imaginé : une fois le bogue dans la version 1
corrigé, il se trouve que la version 2 est maintenant prête, et nous aimerions
la mettre en ligne, pour que les utilisateurs puissent l’utiliser. Ceci veut
donc dire que nous aimerions que le travail effectué dans les commits C5 et
C6, de la branche v2, soit intégré à la version 1, dont l’état le plus
récent se trouve dans notre commit actuel, C7. La commande pour faire cela
est git merge :
$ git merge v2
Auto-merging toto.txt
CONFLICT (content): Merge conflict in toto.txt
Automatic merge failed; fix conflicts and then commit the result.Que s’est-il passé? Il semble y avoir un problème ! Le merge a causé un
“conflit”, une incohérence entre la version du fichier toto.txt de la branche
v2, et celle de la branche main. Git nous informe qu’il n’a pas pu résoudre
lui-même ce conflit (très souvent il est en mesure de le faire, mais pas cette
fois) et que nous devons donc intervenir manuellement, avant de pouvoir
poursuivre. La commande git diff permet de mieux comprendre le problème :
diff --cc toto.txt
index 650d061,d521aa0..0000000
--- a/toto.txt
+++ b/toto.txt
@@@ -1,3 -1,4 +1,8 @@@
allo!
bonjour
++<<<<<<< HEAD
+solution
++=======
+ aa
+ bb
++>>>>>>> v2La syntaxe de cette sortie n’est pas si évidente, mais en gros, ce qui se trouve
entre <<<<<<< HEAD et ======= correspond à une version (de la branche
main), et ce qui se trouve entre ======= et >>>>>>> à l’autre version (de
la branche v2). Ces deux parties sont en contradiction, dans le sens qu’elles
occupent le même endroit dans le fichier, et git ne sait pas comment choisir. Il
faut donc éditer le fichier nous-même, et décider ce qu’on veut garder.
Dans ce cas particulier, éditez le fichier (vous pouvez par exemple utiliser les
éditeurs vim ou nano, sur la ligne de commande) pour qu’il ait l’air de ceci
:
$ cat toto.txt
allo!
bonjour
solution
aa
bbUne fois le fichier modifié, git nous informe très clairement de la situation :
$ git status
On branch main
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: toto.txtIl faut donc ajouter le fichier dont on a résolu les conflits, et ensuite on peut faire notre commit, ce qui complétera le merge :
$ git add toto.txt
$
$ git commit -m "Huitième commit (merge)"
[main 55f1243] Huitième commit (merge)Si on utilise maintenant la commande git log, la totalité de l’évolution de la
branche main (incluant son passage temporaire dans la branche v2) devient
claire :
$ git log
commit 55f1243bb2bd319d2bfefc8e9040dba986456285 (HEAD -> main)
Merge: f229f82 daa06a6
Author: Christian Jauvin <cjauvin@gmail.com>
Date: Wed Jan 21 09:40:53 2026 -0500
Huitième commit (merge)
commit f229f82b53ca118d69daa1cccbbe7a296da87758
Author: Christian Jauvin <cjauvin@gmail.com>
Date: Tue Jan 20 11:15:54 2026 -0500
Septième commit (bugfix v1)
commit daa06a6ceb25c2d008b491b1faba20ce4f5b16d5 (v2)
Author: Christian Jauvin <cjauvin@gmail.com>
Date: Tue Jan 20 10:45:38 2026 -0500
Sixième commit (deuxième de v2)
commit b1d097c40b6b69dc342cdac2bb09c11bbe853fc0
Author: Christian Jauvin <cjauvin@gmail.com>
Date: Tue Jan 20 10:45:19 2026 -0500
Cinquième commit (premier de v2)
commit 3dd71fb4950c1be35ec335cd0ac40f3b5807303b
Author: Christian Jauvin <cjauvin@gmail.com>
Date: Fri Jan 16 15:40:53 2026 -0500
Quatrième commit
commit 2e6fdee379fbe0d258b2d5f04e7cdd0b4d5cbccb
Author: Christian Jauvin <cjauvin@gmail.com>
Date: Thu Jan 15 19:05:29 2026 -0500
Troisième commit
commit 34c85abbbbee1c21c34da2b5ed126cb888fcb498
Author: Christian Jauvin <cjauvin@gmail.com>
Date: Thu Jan 15 14:57:15 2026 -0500
Deuxième commit
commit 9e6072c8df904f021474a6173cc1b17b2908c0f1
Author: Christian Jauvin <cjauvin@gmail.com>
Date: Thu Jan 15 12:16:21 2026 -0500
Premier commitSi on examine plus attentivement notre dernier commit (C8 dans le diagramme), on
constate un détail intéressant :
$ git cat-file -p 55f1
tree d9b5afa4d6f37608b115b489fbca9ac5eaa4b67b
parent f229f82b53ca118d69daa1cccbbe7a296da87758
parent daa06a6ceb25c2d008b491b1faba20ce4f5b16d5
author Christian Jauvin <cjauvin@gmail.com> 1769006453 -0500
committer Christian Jauvin <cjauvin@gmail.com> 1769006453 -0500
Huitième commit (merge)Ce commit a donc deux pointeurs parents (C6 et C7) ! Ceci est dû au fait
qu’il est un commit de merge. Sur le diagramme, ceci correspond au fait que deux
flèches partent de C8, au lieu d’une seule, comme les autres commits.
Il est utile de savoir que cet aspect structural de l’évolution des commits d’un dépôt git forme un graphe orienté acyclique (ou plus communément un DAG, en anglais). Ceci est une structure intermédiaire entre un arbre (dont les noeuds ne peuvent avoir qu’un seul parent) et une graphe général, où les noeuds peuvent avoir plusieurs parents et plusieurs enfants). Un DAG ne permet donc pas les “circuits”.