Aller au contenu

Fonctionnement de git⚓︎

Exploration initiale⚓︎

Pour comprendre comment fonctionne git, il peut être intéressant de jeter un coup d'oeil sous le capot. Tout se trouve dans le dossier .git, il suffit donc d'explorer.

Commençons par créer un dépôt vide (répertoire test), et naviguer dedans.

# Création d'un dépôt vide, dans le répertoire test
git init test
# Navigation dans ce dossier
cd test

Nous pouvons voir que ce dépôt ne contient qu'un répertoire "caché" : .git

Listons le contenu de ce répertoire .git

$ ls -al .git/
total 7
drwxrwxr-x 7 jmbarbier jmbarbier 10 juin  30 18:27 ./
drwxrwxr-x 3 jmbarbier jmbarbier  3 juin  30 18:27 ../
drwxrwxr-x 2 jmbarbier jmbarbier  2 juin  30 18:27 branches/
-rw-rw-r-- 1 jmbarbier jmbarbier 92 juin  30 18:27 config
-rw-rw-r-- 1 jmbarbier jmbarbier 73 juin  30 18:27 description
-rw-rw-r-- 1 jmbarbier jmbarbier 21 juin  30 18:27 HEAD
drwxrwxr-x 2 jmbarbier jmbarbier 15 juin  30 18:27 hooks/
drwxrwxr-x 2 jmbarbier jmbarbier  3 juin  30 18:27 info/
drwxrwxr-x 4 jmbarbier jmbarbier  4 juin  30 18:27 objects/
drwxrwxr-x 4 jmbarbier jmbarbier  4 juin  30 18:27 refs/

Nous allons explorer chacun de ces éléments.

Le fichier HEAD : c'est un simple fichier texte, qui contient le texte ref: refs/heads/main

$ cat .git/HEAD
ref: refs/heads/main

Le fichier config est aussi un fichier texte, au format toml. Il contient quelques lignes pouvant varier selon les systèmes.

# Sur un mac
$ cat .git/config
[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
    ignorecase = true
    precomposeunicode = true
# Sur un linux
$ cat .git/config
[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true

Le fichier description est aussi un fichier texte. Comme son nom l'indique, il est destiné à contenir le nom du projet, mais on peut tout à fait ne jamais le toucher.

$ cat .git/description
Unnamed repository; edit this file 'description' to name the repository.

Le dossier hooks contient des exemples de scripts qui peuvent être appelés à chaque étape du cycle de vie d'un dépôt.

$ ls .git/hooks
applypatch-msg.sample*     pre-applypatch.sample*     pre-rebase.sample*         sendemail-validate.sample*
commit-msg.sample*         pre-commit.sample*         pre-receive.sample*        update.sample*
fsmonitor-watchman.sample* pre-merge-commit.sample*   prepare-commit-msg.sample*
post-update.sample*        pre-push.sample*           push-to-checkout.sample*

Le dossier info ne contient qu'un seul fichier, le fichier exclude. Ce fichier permet d'exclure certains fichiers du suivi de version, comme les fichiers .gitignore, mais sans être lui-même enregistré dans le suivi de version.

cat  .git/info/exclude
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~

Le dossier objects ne contient que deux répertoires vides : info et pack

ls -l .git/objects
.git/objects/info:
total 0

.git/objects/pack:
total 0

De même, le dossier refs ne contient que deux répertoires vides : heads et tags

ls -l .git/refs/*
.git/refs/heads:
total 0

.git/refs/tags:
total 0

La commande git status affiche :

$ git status
Sur la branche main

Aucun commit

rien à valider (créez/copiez des fichiers et utilisez "git add" pour les suivre)

Nous allons voir comment évolue le contenu de ce répertoire .git au fur et à mesure que l'on travaille avec ce dépôt.

Premier commit⚓︎

Nous allons rajouter un fichier dans le dépôt, un fichier test qui contient le texte hello.

echo "hello" > test

La commande git status affiche alors

$ git status
Sur la branche main

Aucun commit

Fichiers non suivis:
  (utilisez "git add <fichier>..." pour inclure dans ce qui sera validé)
    test

aucune modification ajoutée à la validation mais des fichiers non suivis sont présents (utilisez "git add" pour les suivre)

Le contenu du dossier .git n'a pas du tout changé. Si nous utilisons la commande suggérée : git add test

git add test

git status nous confirme que quelque chose a changé :

$ git status
Sur la branche main

Aucun commit

Modifications qui seront validées :
  (utilisez "git rm --cached <fichier>..." pour désindexer)
    nouveau fichier : test

En listant les fichiers, find .git, nous voyons au milieu des fichiers et dossiers déjà présents précédemment 2 nouveautés :

$ find .git
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
.git/index

Ces 2 fichiers sont des fichiers binaires. Des commandes git existent pour pouvoir lire leur contenu plus facilement :

  • git ls-files -s : lit le fichier index (entre autres)
  • git cat-file -p <hash> : lit le contenu des fichiers dans objects. Le hash est constituĂ© des 2 lettres du rĂ©pertoire + le nom de fichier (ici: ce013625030ba8dba906f756967f9e9ca394464a). Si il n'y a pas d'ambiguitĂ©, on peut ne donner que les premières lettres du hash (minimum 4). La mĂŞme commande avec -s nous donnera la taille et -t le type.

RĂ©sultat de ces deux commandes :

$ git cat-file -p ce013625
hello
$ git cat-file -s ce013625
6
$ git cat-file -t ce013625
blob
$ git ls-files -s
100644 ce013625030ba8dba906f756967f9e9ca394464a 0   test

On voit que le contenu du fichier test a été copié dans la base des objets (le nom de chaque objet est un hash associé au contenu), et que le fichier index référence le fichier test comme correspondant à l'objet ce013625...

flowchart LR
ce01[blob:ce01:hello]

Nous allons maintenant faire le commit pour enregistrer cet Ă©tat :

$ git commit -m "commit 1"
[main (commit racine) bdb7563] commit 1
 1 file changed, 1 insertion(+)
 create mode 100644 test

Git nous confirme que le commit a bien été effectué, et nous donne un résumé des modifications.

La commande git status nous indique que tout est propre :

$ git status
Sur la branche main
rien Ă  valider, la copie de travail est propre

Le contenu du dossier .git a changé : dans le dossier objects, de nouveaux fichiers sont apparus :

$ find .git/objects -type f
.git/objects/bd/b7563d98b04292eaaa047ac8c5d64998e6d18a # nouveau
.git/objects/31/d73eb4914a8ddb6cb0e4adf250777161118f90 # nouveau
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a

Nous pouvons inspecter le contenu de ces fichiers, en commencant par celui dont le hash correspond à celui indiqué dans le message de commit :

$ git cat-file -t bdb7
commit
$ git cat-file -p bdb7
tree 31d73eb4914a8ddb6cb0e4adf250777161118f90
author BARBIER Jean-Matthieu <jm.barbier@solidev.net> 1719766117 +0200
committer BARBIER Jean-Matthieu <jm.barbier@solidev.net> 1719766117 +0200

commit 1

Ce fichier contient toutes les informations nécessaires pour retrouver l'état du dépôt à ce moment précis, ainsi que des informations sur l'auteur et le commiteur du commit, la date, et le message associé.

Ce commit indique un tree, qui est un autre objet git, permettant de représenter l'arborescence des fichiers à ce moment précis. C'est le 3ème objet qui est apparu dans le dossier objects.

git cat-file -t 31d7
tree
$ git cat-file -p 31d7
100644 blob ce013625030ba8dba906f756967f9e9ca394464a    test

On voit que ce tree contient une seule entrée, permettant d'associer le fichier test à l'objet ce013625...

flowchart LR
ce01[blob:ce01:hello]
31d7[tree:31d7]
bdb7[commit:bdb7:commit 1]
bdb7 --> 31d7
31d7 -- test --> ce01

Un autre fichier est apparu dans le dossier refs/heads :

$ cat .git/refs/heads/main
bdb7563d98b04292eaaa047ac8c5d64998e6d18a

# Rappel
$ cat .git/HEAD
ref: refs/heads/main

Ce fichier nous indique le hash du dernier commit sur la branche main, et le fichier HEAD nous indique la branche sur laquelle nous sommes actuellement.

flowchart LR
ce01[blob:ce01:hello]
31d7[tree:31d7]
bdb7[commit:bdb7:commit 1]
bdb7 --> 31d7
31d7 -- test --> ce01
HEAD --> refs/heads/main
refs/heads/main --> bdb7

Petit coup d'oeil sur le fichier index :

$ git ls-files -s
100644 ce013625030ba8dba906f756967f9e9ca394464a 0       test

Deuxième commit⚓︎

Nous allons :

  • rajouter un fichier test2 contenant le mot world, dans le mĂŞme rĂ©pertoire.
  • rajouter un fichier test3 contenant le mot test, dans un sous-rĂ©pertoire dossier.
  • modifier le fichier test pour mettre une majuscule Ă  hello.
echo "world" > test2
mkdir dossier
echo "world" > dossier/test3
echo "Hello" > test

La commande git status nous indique que test2 et le contenu de dossier sont non suivis, et que test a été modifié.

$ git status
Sur la branche main
Modifications qui ne seront pas validées :
  (utilisez "git add <fichier>..." pour mettre à jour ce qui sera validé)
  (utilisez "git restore <fichier>..." pour annuler les modifications dans le répertoire de travail)
        modifié :         test

Fichiers non suivis:
  (utilisez "git add <fichier>..." pour inclure dans ce qui sera validé)
        dossier/
        test2

aucune modification n'a été ajoutée à la validation (utilisez "git add" ou "git commit -a")

Sur la branche main Fichiers non suivis: (utilisez "git add ..." pour inclure dans ce qui sera validé) dossier/

Comme précédemment, rien n'a changé dans le dossier `.git` pour l'instant.

Nous allons ajouter ces modifications Ă  l'index.

```bash
$ git add test test2 dossier/test3

La commande git status nous indique que les fichiers sont prêts à être commités.

$ git status
Sur la branche main
Modifications qui seront validées :
  (utilisez "git restore --staged <fichier>..." pour désindexer)
        nouveau fichier : dossier/test3
        modifié :         test
        nouveau fichier : test2

En listant les objets dans le dossier .git, nous voyons que 2 nouveaux objets sont apparus :

$ find .git/objects -type f
.git/objects/bd/b7563d98b04292eaaa047ac8c5d64998e6d18a
.git/objects/e9/65047ad7c57865823c7d992b1d046ea66edf78 # nouveau
.git/objects/31/d73eb4914a8ddb6cb0e4adf250777161118f90
.git/objects/cc/628ccd10742baea8241c5924df992b5c019f71 # nouveau
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a

Ces 2 nouveaux objets sont les objets correspondant aux contenus Hello et world.

$ git cat-file -t e965
blob
$ git cat-file -p e965
Hello
$ git cat-file -t cc62
blob
$ git cat-file -p cc62
Hello

Le fichier index a aussi été mis à jour :

$ git ls-files -s
100644 ce013625030ba8dba906f756967f9e9ca394464a 0       dossier/test3
100644 e965047ad7c57865823c7d992b1d046ea66edf78 0       test
100644 cc628ccd10742baea8241c5924df992b5c019f71 0       test2

Nous allons maintenant faire le commit :

$ git commit -m "commit 2"
[main 166e718] commit 2
 3 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 dossier/test3
 create mode 100644 test2

Ă€ nouveau, des objets sont apparus dans le dossier objects :

.git/objects/bd/b7563d98b04292eaaa047ac8c5d64998e6d18a
.git/objects/e9/65047ad7c57865823c7d992b1d046ea66edf78
.git/objects/31/d73eb4914a8ddb6cb0e4adf250777161118f90
.git/objects/a5/cac10b4a8614c94e77bf34b2a887a879e6a9bd # nouveau
.git/objects/cc/628ccd10742baea8241c5924df992b5c019f71
.git/objects/16/6e71833a2cfb74d9d22807a597b0a1a2634364 # nouveau
.git/objects/0a/5033915bbf715a2b1f67b077ba195b12302b28 # nouveau
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a

Ces 3 nouveaux objets sont les objets du commit, et des 2 tree associés permettant d'enregistrer l'arborescence des fichiers à ce moment précis.

$ git cat-file -t 166e
commit
$ git cat-file -p 166e
tree a5cac10b4a8614c94e77bf34b2a887a879e6a9bd
parent bdb7563d98b04292eaaa047ac8c5d64998e6d18a
author BARBIER Jean-Matthieu <jm.barbier@solidev.net> 1719769536 +0200
committer BARBIER Jean-Matthieu <jm.barbier@solidev.net> 1719769536 +0200

commit 2

Le parent de ce commit est le commit précédent, et le tree associé est le nouvel objet a5cac10b4a8614c94e77bf34b2a887a879e6a9bd.

$ git cat-file -t a5ca
tree
$ git cat-file -p a5ca
040000 tree 0a5033915bbf715a2b1f67b077ba195b12302b28    dossier
100644 blob e965047ad7c57865823c7d992b1d046ea66edf78    test
100644 blob cc628ccd10742baea8241c5924df992b5c019f71    test2

Ce tree contient 3 entrées, permettant d'associer les fichiers test, test2 et le dossier dossier à leur objet respectif.

$ git cat-file -t 0a50
tree
$ git cat-file -p 0a50
100644 blob ce013625030ba8dba906f756967f9e9ca394464a    test3

Le fichier refs/heads/main a été mis à jour :

$ cat .git/refs/heads/main
166e71833a2cfb74d9d22807a597b0a1a2634364

On peut tracer le graphe des objets git :

flowchart LR
bdb7[commit:bdb7:commit 1]
e965[blob:e965:Hello]
31d7[tree:31d7]
a5ca[tree:a5ca]
cc62[blob:cc62:world]
0a50[tree:0a50]
ce01[blob:ce01:hello]
166e[commit:166e:commit 2]

bdb7 --> 31d7
31d7 -- test --> ce01

166e -- parent --> bdb7
166e -- tree --> a5ca
a5ca -- dossier --> 0a50
a5ca -- test --> e965
a5ca -- test2 --> cc62
0a50 -- test3 --> ce01

HEAD --> refs/heads/main
refs/heads/main --> 166e

On peut voir que les commits sont des objets qui contiennent des références vers des tree, qui contiennent des références vers des objets blob ou d'autres tree. C'est ce mécanisme qui permet de retrouver l'état du dépôt à un moment donné, et de suivre l'évolution des fichiers.

Branche et nouveau commit⚓︎

Une branche est un pointeur vers un état du dépôt.

Nous allons créer une nouvelle branche issue-1, et faire un commit sur cette branche.

$ git branch issue-1

Cette commande crée un nouveau fichier dans le dossier refs/heads :

find .git/refs -type f
.git/refs/heads/main
.git/refs/heads/issue-1

Le contenu de ce fichier est le hash du commit sur lequel pointe la branche :

$ cat .git/refs/heads/issue-1
166e71833a2cfb74d9d22807a597b0a1a2634364

En revanche le fichier HEAD n'a pas changé :

$ cat .git/HEAD
ref: refs/heads/main

Nous allons maintenant rendre la branche issue-1 active :

$ git checkout issue-1

Le fichier HEAD a été mis à jour :

$ cat .git/HEAD
ref: refs/heads/issue-1

Nous allons maintenant rajouter un fichier test4 contenant le mot issue, et faire un commit sur cette branche.

$ echo "issue" > test4
$ git add test4
$ git commit -m "commit 3"

[issue-1 932f923] commit 3
 1 file changed, 1 insertion(+)
 create mode 100644 test4

Des objets sont apparus dans le dossier objects :

$ ls -altr .git/objects/*/*
-r--r--r-- 1 jmbarbier jmbarbier 139 juin  30 18:48 .git/objects/bd/b7563d98b04292eaaa047ac8c5d64998e6d18a
-r--r--r-- 1 jmbarbier jmbarbier  49 juin  30 18:48 .git/objects/31/d73eb4914a8ddb6cb0e4adf250777161118f90
-r--r--r-- 1 jmbarbier jmbarbier  21 juin  30 19:37 .git/objects/e9/65047ad7c57865823c7d992b1d046ea66edf78
-r--r--r-- 1 jmbarbier jmbarbier  21 juin  30 19:37 .git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
-r--r--r-- 1 jmbarbier jmbarbier  21 juin  30 19:37 .git/objects/cc/628ccd10742baea8241c5924df992b5c019f71
-r--r--r-- 1 jmbarbier jmbarbier 106 juin  30 19:45 .git/objects/a5/cac10b4a8614c94e77bf34b2a887a879e6a9bd
-r--r--r-- 1 jmbarbier jmbarbier 169 juin  30 19:45 .git/objects/16/6e71833a2cfb74d9d22807a597b0a1a2634364
-r--r--r-- 1 jmbarbier jmbarbier  50 juin  30 19:45 .git/objects/0a/5033915bbf715a2b1f67b077ba195b12302b28
-r--r--r-- 1 jmbarbier jmbarbier  21 juin  30 20:02 .git/objects/78/77337e0ffb58fda84644dea72ffae13e394d94 # nouveau
-r--r--r-- 1 jmbarbier jmbarbier 132 juin  30 20:02 .git/objects/e6/24812b89d4c38a5f59fb477665fc50eb477ee9 # nouveau
-r--r--r-- 1 jmbarbier jmbarbier 170 juin  30 20:02 .git/objects/93/2f9235b01b90343e78647d4f89e4879e6d6446 # nouveau

Ces 3 fichiers correpondents :

  • au commit commit 3 pour le fichier 93/2f9235b01b90343e78647d4f89e4879e6d6446
  • au tree associĂ© pour le fichier a5/cac10b4a8614c94e77bf34b2a887a879e6a9bd
  • au contenu du fichier test4 pour le fichier e6/24812b89d4c38a5f59fb477665fc50eb477ee9
$ git cat-file -t 932f
commit
$ git cat-file -p 932f
tree e624812b89d4c38a5f59fb477665fc50eb477ee9
parent 166e71833a2cfb74d9d22807a597b0a1a2634364
author BARBIER Jean-Matthieu <jm.barbier@solidev.net> 1719770529 +0200
committer BARBIER Jean-Matthieu <jm.barbier@solidev.net> 1719770529 +0200

commit 3

$ git cat-file -t e624
tree
$ git cat-file -p e624
040000 tree 0a5033915bbf715a2b1f67b077ba195b12302b28    dossier
100644 blob e965047ad7c57865823c7d992b1d046ea66edf78    test
100644 blob cc628ccd10742baea8241c5924df992b5c019f71    test2
100644 blob 7877337e0ffb58fda84644dea72ffae13e394d94    test4


$ git cat-file -t 7877
blob
$ git cat-file -p 7877
issue

flowchart TB
bdb7[commit:bdb7:commit 1]
e965[blob:e965:Hello]
31d7[tree:31d7]
a5ca[tree:a5ca]
cc62[blob:cc62:world]
0a50[tree:0a50]
ce01[blob:ce01:hello]
166e[commit:166e:commit 2]
932f[commit:932f:commit 3]
e624[tree:e624]
7877[blob:7877:issue]

bdb7 --> 31d7
31d7 -- test --> ce01

166e -- parent --> bdb7
166e -- tree --> a5ca
a5ca -- dossier --> 0a50
a5ca -- test --> e965
a5ca -- test2 --> cc62
0a50 -- test3 --> ce01

932f -- parent --> 166e
932f -- tree --> e624
e624 -- test4 --> 7877



HEAD --> refs/heads/issue-1
refs/heads/issue-1 --> 932f
refs/heads/main --> 166e

On voit ici que la branche "main" existe toujours, mais que la branche "issue-1" pointe sur un commit différent. On peut basculer d'une branche à l'autre, et faire évoluer chaque branche indépendamment. Si on veut fusionner les modifications de la branche "issue-1" dans la branche "main", on peut faire un merge.

$ git checkout main
$ git merge issue-1
Mise Ă  jour 166e718..932f923
Fast-forward
 test4 | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 test4

git log --oneline --graph --all

A ce stade, les branches "main" et "issue-1" pointent sur le mĂŞme commit, et on peut supprimer la branche "issue-1".

$ git branch -d issue-1