Git de l'intérieur

Tutoriel pour apprendre le fonctionnement interne de Git

Mary Rose Cook

Traducteur

Chrtophe

Ce tutoriel va vous expliquer le fonctionnement de git. Il suppose que vous comprenez suffisamment son fonctionnement pour l'utiliser pour contrôler vos versions de projets.

Commentez

- Titre : Git de l'intérieur

- Auteur : Mary Rose Cook

- Traducteur : Chrtophe

- Parution : 10 juin 2018

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2018 Mary Rose Cook. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- 1. Introduction

- 2. Création d'un projet

- 3. Initialisation du dépôt

- 4. Ajouter quelques fichiers

- 5. Faire un commit (une validation)

- 6. Créer un arbre

- 7. Création d'un objet commit

- 8. Pointer la branche actuelle sur le nouveau commit

- 9. Faire un commit qui n'est pas le premier commit

- 10. Chargement d'un commit

- 11. Créer une branche

- 12. Basculer vers une branche

- 13. Basculement d’une branche incompatible avec la copie de travail

- 14. Fusionner un ascendant

- 15. Fusionner un descendant

- 16. Fusionner deux commits de différentes lignées

- 17. Fusionner deux commits de deux lignées différentes qui modifient tous les deux le même fichier

- 18. Supprimer un fichier

- 19. Copie d'un dépôt

- 20. Lier un dépôt à un autre

- 21. Aller chercher une branche depuis un dépôt distant

- 22. fusion FETCH_HEAD

- 23. Tirer une branche d'un dépôt distant

- 24. Cloner un dépôt

- 25. Pousser une branche vers une branche spécifique dans un dépôt distant

- 26. Cloner un dépôt nu

- 27. Pousser une branche vers un dépôt nu

- 28. Notes

- 29. Résumé

- 30. Notes de la rédaction

1 - Introduction

Notes de lecture :

Pour simplifier la lecture, l'auteur a pris des libertés avec les formats des fichiers internes de git. Elle présente ces fichiers comme s'ils étaient stockés en clair sur le disque (lisibles avec un éditeur de texte), mais ce n'est pas le cas.

Après l'exécution de la première commande git add data/lettre.txt, le blob obtenu ne sera pas lisible depuis un éditeur. En effet, le format de fichier réel contient des caractères non imprimables dans son entête, il est de plus compressé avec zlib après la génération de l'empreinte SHA1.

Si vous souhaitez vérifier vous-même le contenu des fichiers au fil de la lecture, les commandes suivantes (internes à git) vous aideront grandement.

Depuis la racine du dépôt :

- lister tous les fichiers objets disponibles : $ find .git/objects -type f ;
- afficher le contenu d'un fichier objet : $ git cat-file 2e65efe2a145dda7ee51d1741299f848e5bf752e -p ;
- afficher le type d'un fichier objet (tree, blob, commit ou tag) : $ git cat-file 2e65efe2a145dda7ee51d1741299f848e5bf752e -t ;
- afficher le contenu du fichier index : $ git ls-files --stage.

Dernier point, l'auteur de l'article part du principe que vous êtes sur une machine de type Unix avec un bash standard. Si vous êtes sur un système Windows, assurez-vous d'utiliser le git bash pour Windowsen ligne de commande. Il n'est pas possible de réaliser ce tutoriel depuis un terminal Windows basique.

L'équipe de traduction.

Ce tutoriel va vous expliquer le fonctionnement de git. Vous pouvez regarder cette vidéo à la place de la version tutoriel si vous préférez.

Le tutoriel suppose que vous comprenez suffisamment git pour l'utiliser pour contrôler les versions de vos projets. Il se concentre sur la structure en graphe que sous-entend git, et de quelle façon ses propriétés dictent le comportement de git. De base, vous construisez votre analyse sur le concret plutôt que sur l'expérimentation de l'API. Ce modèle plus vrai vous donnera une meilleure compréhension de ce que git aura effectué.

Ce document est structuré sous forme d'une série de commandes git sur un seul projet. À certains intervalles, il y aura des observations sur la structuration graphique sur laquelle git est construit. Ces observations illustreront une propriété du graphique et le comportement produit par cette propriété.

Après lecture, si vous souhaitez aller plus loin avec git, vous pourrez jeter un œil sur ce code source fortement documenté de mon implémentation de git en JavaScript.

2 - Création d'un projet

~ $ mkdir alpha
~ $ cd alpha

L'utilisateur crée le dossier alpha pour son projet.

~/alpha $ mkdir data
~/alpha $ printf 'a' > data/lettre.txt

Il rentre dans le dossier alpha et crée un dossier nommé data. Dedans, il crée un fichier nommé lettre.txt qui contient « a ». Le dossier alpha ressemble à ceci :

alpha

└── data

└── lettre.txt

3 - Initialisation du dépôt

~/alpha $ git init
Initialized empty Git repository

git init transforme le dossier courant en dépôt git. Pour cela, il crée un dossier .git et écrit des fichiers à l’intérieur. Ces fichiers définissent toute la configuration du dépôt et l'historique du projet. Il s'agit juste de fichiers ordinaires, rien de magique dedans. L'utilisateur peut les lire et les éditer avec un éditeur de texte ou avec le shell. Autrement dit, l'utilisateur peut lire et éditer l'historique de son projet aussi facilement que les fichiers de celui-ci.

Le dossier alpha ressemble maintenant à ceci :

alpha
|-- data
|-- lettre.txt
|-- .git
|-- objects
etc...

Le dossier .git et son contenu concernent git. Tous les autres fichiers sont collectivement désignés comme la copie de travail. Ce sont les fichiers de l'utilisateur.

4 - Ajouter quelques fichiers

~/alpha $ git add data/lettre.txt

L'utilisateur lance git add data/lettre.txt. Ceci a deux effets :

Premièrement, il crée un nouveau fichier blob (Binary Large Object) dans le dossier .git/objects/.

Ce fichier blob contient le contenu compressé (NdT : à l’aide de zlib) de data/lettre.txt. Son nom est dérivé du hashage de son contenu. « Hasher » un texte signifie générer une empreinte. Cette empreinte a la particularité d’être d’une taille fixe et réduite, avec une entropie suffisante pour identifier de manière unique le texte d’origine.

Les deux premiers caractères sont utilisés en tant que dossier dans la base de données d'objets : .git/objects/2e/. Le reste du hash est utilisé en tant que nom de fichier blob qui contient le contenu du fichier ajouté : .git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e.

Notez comment ajouter un fichier à git sauve son contenu dans le dossier objects. Son contenu sera toujours en sécurité dans git si l'utilisateur supprime data/lettre.txt du répertoire de travail.

Deuxièmement : git add ajoute le fichier à l'index. L'index est juste une liste qui contient chaque fichier dont git doit garder trace. Cet index est stocké en tant que fichier dans .git/index.  Chaque ligne du fichier correspond à un fichier suivi avec le hash de son contenu au moment où il a été ajouté. Voici l'index après l'appel de la commande git add :

data/lettre.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e

L'utilisateur crée un fichier nommé data/nombre.txt qui contient 1234.

~/alpha $ printf '1234' > data/nombre.txt

Le répertoire de travail ressemblera à ceci :

alpha
|--data
|-- lettre.txt
|-- nombre.txt

L'utilisateur ajoute les fichiers à git :

~/alpha $ git add data

La commande git add crée un objet blob qui contient le contenu de data/nombre.txt. Il ajoute une entrée à l'index pour data/nombre.txt qui pointe sur ce blob. Voici l'index après un second appel de la commande git add :

data/lettre.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e
data/nombre.txt 274c0052dd5408f8ae2bc8440029ff67d79bc5c3

Notez que seuls les fichiers du répertoire data sont listés dans l'index, bien que l'utilisateur ait lancé git add data. Le dossier data n'est pas listé séparément.

~/alpha $ printf '1' > data/nombre.txt
~/alpha $ git add data

Quand l'utilisateur a créé data/nombre.txt, il voulait (en fait) taper « 1 » et non « 1234 ». Il a fait la correction et ajouté de nouveau le fichier à l'index. Cette commande a créé un nouveau blob avec le nouveau contenu, et a mis à jour l'entrée d'index pour data/nombre.txt pour pointer sur ce nouveau blob.

5 - Faire un commit (une validation)

~/alpha $ git commit -m 'a1'
[master (root-commit) 774b54a] a1

L'utilisateur fait le commit a1. git va alors afficher certaines données à propos du commit. Ces données auront du sens prochainement.

La commande commit s’exécute en trois étapes. Elle crée un arbre (tree graph) pour représenter le contenu de la version du projet étant validé. Elle crée un objet commit. Elle fait pointer la branche courante sur le commit créé.

6 - Créer un arbre

git enregistre l'état courant du projet en créant un arbre depuis l'index. Cet arbre enregistre la position et le contenu de chaque fichier du projet. Un état (ou instantané) est composé de deux types d'objets : les blobs et les arbres.

Les blobs sont stockés par git add. Ils représentent le contenu des fichiers.

Les arbres sont générés quand un commit est fait. Un arbre représente un dossier dans la copie de travail.

Ci-dessous, un objet arbre qui enregistre le contenu du dossier data pour le nouveau commit :

100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e lettre.txt
100664 blob 56a6051ca2b02b04ef92d5150c9ef600403cb1de nombre.txt

La première ligne enregistre tout ce qui est nécessaire pour reproduire data/lettre.txt. La première partie donne l'état des permissions. La deuxième partie indique que le contenu de cette entrée est représenté par un blob plutôt qu'un arbre. La troisième partie donne le hash du blob. La quatrième partie donne le nom du fichier.

La seconde ligne enregistre la même chose pour data/nombre.txt.

Voici ci-dessous l'arbre (tree) pour alpha, qui est le répertoire racine du projet :

040000 tree 0eed1217a2947f4930583229987d90fe5e8e0b74 data

L'unique ligne de cet arbre pointe sur l'arbre data :

Dans le graphe ci-dessus, la racine de l'arbre (le root) pointe sur l'arbre data. L'arbre data pointe sur les blobs data/lettre.txt et data/nombre.txt.

7 - Création d'un objet commit

git commit crée un objet commit après création de l'arbre. L'objet commit est juste un autre fichier texte dans .git/objects :

tree ffe298c3ce8bb07326f888907996eaa48d266db4
author Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500

a1

La première ligne pointe sur l'arbre. Le hash concerne l'objet tree qui représente la racine de la version en cours, c’est-à-dire le dossier alpha. La dernière ligne est le message de commit.

8 - Pointer la branche actuelle sur le nouveau commit

Finalement, la commande commit fait pointer la branche courante sur le nouvel objet commit.

Quelle est la branche courante ? git va dans le fichier HEAD dans .git/HEAD et trouve :

ref: refs/heads/master

Ceci dit que HEAD pointant sur master, master est la branche courante.

HEAD et master sont tous les deux des refs (références). Un ref est un label utilisé par git ou l'utilisateur pour identifier un commit spécifique.

Le fichier qui représente la ref master n'existe pas, car c'est le premier commit du dépôt. git crée le fichier .git/refs/heads/master et positionne son contenu sur le hash de l'objet commit :

74ac3ad9cde0b265d2b4f1c778b283a6e2ffbafd

Si vous saisissez ces commandes git, telles que vous les lisez, le hash de votre commit a1 sera différent du mien. Les objets conteneurs comme les blobs et les trees ont toujours la même valeur de hash. Mais pas les commit, car ils incluent les dates et les noms de leurs créateurs.

Ajoutons HEAD et master au graphe git :

HEAD pointe sur master et master pointe sur le commit a1.

9 - Faire un commit qui n'est pas le premier commit

Voici ci-dessous le graphe git après le commit a1. La copie de travail et l'index sont inclus.

Notez que la copie de travail, l'index, et le commit a1 ont tous le même contenu pour data/lettre.txt et data/nombre.txt. L'index et le commit HEAD utilisent tous les deux des hashs pour se référer aux objets blobs, mais le contenu de la copie de travail est stocké en tant que texte dans un emplacement différent.

~/alpha $ printf '2' > data/nombre.txt

L'utilisateur positionne le contenu de data/nombre.txt sur 2. Ceci met à jour la copie de travail, mais laisse les commits index et HEAD tels quels.

~/alpha $ git add data/nombre.txt

L'utilisateur ajoute le fichier à git. Ceci ajoute un blob contenant 2 au dossier objects. Il pointe l'entrée index pour data/nombre.txt sur le nouveau blob.

~/alpha $ git commit -m 'a2'
[master f0af7e6] a2

L'utilisateur effectue un commit. Les étapes du commit sont les mêmes que précédemment.

Premièrement, un nouvel arbre est créé pour représenter le contenu de l'index.

L'entrée d'index pour data/nombre.txt est changé. L'ancien arbre data ne reflète plus l'état indexé du dossier data. Un nouvel objet arbre data doit être créé :

100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e lettre.txt
100664 blob d8263ee9860594d2806b0dfd1bfd17528b0ba2a4 nombre.txt

Il y a une nouvelle valeur de hash pour l'arbre data. Un nouvel arbre root doit être créé pour enregistrer ce hash :

040000 tree 40b0318811470aaacc577485777d7a6780e51f0b data

Deuxièmement, un nouvel objet commit est créé.

tree ce72afb5ff229a39f6cce47b00d1b0ed60fe3556
parent 774b54a193d6cfdd081e581a007d2e11f784b9fe
author Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500

a2

La première ligne de l'objet commit pointe sur un nouvel objet arbre racine. La seconde ligne pointe sur a1 : le commit parent. Pour trouver le commit parent, git est allé dans HEAD, puis master, et a trouvé le hash de commit de a1.

Troisièmement, le contenu du fichier de branche master est positionné sur le hash du nouveau commit.

Propriété de graphe : le contenu est stocké comme un arbre d'objets. Cela signifie que seules les différences sont stockées dans la base d'objets. Regardez le graphe ci-dessus, le commit a2 réutilise le blob a qui a été fait avant le commit a1. Similairement, si un dossier entier ne change pas d'un commit à l'autre, son arbre et tous les blobs et arbres en dessous de lui peuvent être réutilisés. Généralement, il n'y a que quelques changements d'un commit à un autre. Cela signifie que git peut stocker un gros historique de commit dans un faible espace.

Propriété de graphe : chaque commit a un parent. Cela signifie qu'un dépôt peut stocker l'historique d'un projet.

Propriété de graphe : les refs sont des points d'entrée d'une partie d'un historique d'un commit ou d'un autre. Cela signifie que les commit peuvent recevoir des noms significatifs. L'utilisateur organise son travail en lignées significatives pour son projet avec des refs concrètes comme fix-pour-bug-376. git utilise des refs symboliques comme HEAD, MERGE_HEAD, FETCH_HEAD pour aider les commandes qui manipulent l'historique de commit.

Propriété de graphe : les éléments dans le dossier objects sont non modifiables. Cela signifie qu'un contenu est édité, non supprimé. Chaque partie de contenu est toujours ajoutée et chaque commit est quelque part dans le dossier objects (3).

Propriété de graphe : les refs sont modifiables. Par conséquent, la signification d'un ref peut changer. Le commit pointé par master peut être la meilleure version du projet à un moment, mais assez tôt, il sera remplacé par un nouveau et meilleur commit.

La copie de travail et les commits pointés par une référence sont directement accessibles, mais pas les autres commits. Cela signifie que l’historique récent est facile à rappeler, mais aussi qu’il change plus souvent. Autrement dit, git dispose d’une mémoire nécessitant une gymnastique plus ou moins complexe pour pouvoir être accédée en fonction de la profondeur souhaitée.

La copie de travail est le point d'historique le plus facile à rappeler, car il est à la racine du dépôt. Le rappeler ne nécessite pas de commande git. C'est aussi le point le plus bas de l'historique permanent. L'utilisateur peut faire une douzaine de versions d'un fichier, mais git n'enregistrera aucun d'eux tant qu'ils ne sont pas ajoutés.

Le commit sur lequel pointe HEAD est très facile à rappeler. Il est à l'extrémité de la branche qui est vérifiée. Pour voir son contenu, l'utilisateur peut juste faire un stach puis examiner la copie en cours(4). En même temps, HEAD est la ref la plus fréquemment changée.

Le commit sur lequel pointe concrètement un ref est facile à rappeler. L'utilisateur peut facilement quitter cette branche. La pointe de la branche change moins souvent que HEAD, mais suffisamment souvent pour que le nom d'une branche soit à changer.

Il est difficile de rappeler un commit qui n'est pointé par aucune ref. Plus l'utilisateur va loin dans une ref, plus il sera difficile pour lui de construire le sens du commit. Mais plus on va en arrière, moins il est probable que quelqu'un ait changé l'historique depuis leur dernier examen (5).

10 - Chargement d'un commit

~/alpha $ git checkout 37888c2
You are in 'detached HEAD' state...

L'utilisateur charge le commit a2 en utilisant son hash. Si vous lancez cette commande git, elle ne fonctionnera pas. Utilisez git log pour trouver le hash de votre commit a2.

Le chargement s'effectue en quatre étapes :

- premièrement, git récupère le commit a2 ainsi que l'arbre sur lequel il pointe ;

- deuxièmement, il écrit les entrées fichier dans l’arbre de la copie de travail. Cela ne génère aucun changement. La copie de travail a déjà le contenu de l’arbre qui va être écrit, car HEAD pointait déjà dessus via master au commit a2 ;

- troisièmement, git écrit les entrées fichier de l'arbre dans l'index. Ceci, encore, ne génère pas de changement. L'index a déjà le contenu du commit a2 ;

- quatrièmement, le contenu de HEAD est positionné sur le hash du commit a2:

Positionner le contenu de HEAD sur un hash met le dépôt dans un état de HEAD détaché. Notez dans le graphe ci-dessous que HEAD pointe directement sur le commit a2, plutôt que de pointer sur master.

~/alpha $ printf '3' > data/nombre.txt
~/alpha $ git add data/nombre.txt
~/alpha $ git commit -m 'a3'
[detached HEAD 3645a0e] a3

L'utilisateur modifie le contenu de data/nombre.txt à 3 et fait un commit sur ce changement. git va à HEAD pour obtenir le parent du commit a3. Plutôt que de chercher et suivre un ref de branche, il cherche et retourne le hash du commit a2.

git met à jour HEAD pour pointer directement sur le hash du nouveau commit a3. Le dépôt est toujours dans l'état HEAD détaché. Le commit n'est pas sur une branche, car aucune ref ne pointe sur a3 ou un de ses descendants. Cela signifie qu'il est facile de le perdre.

À partir de maintenant, les arbres et les blobs seront pour la plupart omis des diagrammes de graphe.

11 - Créer une branche

~/alpha $ git branch interimaire

L'utilisateur crée une nouvelle branche nommée interimaire. Cela crée juste un nouveau fichier dans .git/refs/heads/interimaire qui contient le hash sur lequel pointe HEAD : le hash du commit a3.

Propriété de graphe : Les branches sont juste des ref et les ref sont juste des fichiers. Cela signifie que les branches git sont légères en poids.

La création de la branche interimaire (deputy dans le texte original) place le nouveau commit a3 sans encombre dans une branche. HEAD est toujours détaché, car il pointe toujours directement sur un commit.

12 - Basculer vers une branche

~/alpha $ git checkout master
Switched to branch 'master'

L'utilisateur bascule vers la branche master :

- premièrement, git récupère le commit a2 sur lequel pointe master et récupère l'arbre sur lequel pointe le commit ;

- deuxièmement, git écrit les entrées de fichier dans l'arbre de graphe dans les fichiers de la copie de travail. Cela positionne le contenu de data/nombre.txt à 2 ;

- troisièmement, git écrit les entrées de fichier dans l’arbre dans l'index. Ceci met à jour l'entrée pour data/nombre.txt sur le hash du blob 2 ;

- quatrièmement, git pointe HEAD sur master en changeant le contenu du hash vers :

ref: refs/heads/master

13 - Basculement d’une branche incompatible avec la copie de travail

~/alpha $ printf '789' > data/nombre.txt
~/alpha $ git checkout interimaire
Your changes to these files would be overwritten
by checkout:
data/nombre.txt
Commit your changes or stash them before you
switch branches.

L'utilisateur modifie accidentellement le contenu de data/nombre.txt à 789. Il essaye de basculer vers la branche interimaire. git empêche le basculement.

HEAD pointe sur master qui pointe sur a2, ou data/nombre.txt lit 2. interimaire pointe sur a3 ou data/nombre.txt lit 3. La copie de travail de data/nombre.txt lit 789. Toutes ces versions sont différentes et les différences doivent être résolues.

git pourrait remplacer la version de travail de data/nombre.txt avec la version dans le commit ayant été vérifié. Mais il évitera une perte de donnée à tout prix.

git pourrait fusionner la version en cours avec la version ayant été vérifiée. Mais c'est compliqué. Donc git abandonne la vérification.

~/alpha $ printf '2' > data/nombre.txt
~/alpha $ git checkout interimaire
Switched to branch 'interimaire'

L'utilisateur se rend compte qu'il a accidentellement édité data/nombre.txt et refixe le contenu à 2. Il vérifia avec succès interimaire.

14 - Fusionner un ascendant

~/alpha $ git merge master
Already up-to-date.

L'utilisateur fusionne master dans interimaire. Fusionner deux branches signifie fusionner deux commit. Le premier commit est celui sur lequel pointe interimaire : le receveur. Le second commit est celui sur lequel pointe master : le donneur. Pour cette fusion, git ne fait rien. Il le reporte comme is Already up-to-date.

Propriété de graphe : Les séries de commit dans le graphe sont interprétées comme une série de changements fait dans le contenu du dépôt. Cela signifie que, dans une fusion, si le commit d'un donneur est un ascendant du commit du receveur, git ne fera rien. Ces changements ont déjà été incorporés.

15 - Fusionner un descendant

~/alpha $ git checkout master
Switched to branch 'master'

L'utilisateur bascule vers la branche master.

~/alpha $ git merge interimaire
Fast-forward

Il fusionne interimaire et master. git découvre que le commit receveur, a2, est un ascendant du commit donneur, a3. Il peut faire une fusion en « avance rapide ».

Il récupère le commit donneur et récupère l'arbre sur lequel il pointe. Il écrit les entrées de fichier dans l'arbre dans la copie de travail et l'index. Il fait une « avance rapide » sur le master pour pointer sur a3.

Propriété de graphe : Les séries de commit dans le graphe sont interprétées comme une série de changements fait dans le contenu du dépôt. Cela signifie que, dans une fusion, si le donneur est un descendant du receveur, l'historique n’est pas changé. Il y a déjà une séquence de commit décrivant les changements à faire ; la séquence de commit entre le receveur et le donneur. Mais, bien que l'historique git ne change pas, le graphe change. La ref concrète sur laquelle pointe HEAD est mise à jour pour pointer sur le commit donneur.

16 - Fusionner deux commits de différentes lignées

~/alpha $ printf '4' > data/nombre.txt
~/alpha $ git add data/nombre.txt
~/alpha $ git commit -m 'a4'
[master 7b7bd9a] a4

L'utilisateur fixe le contenu de nombre.txt à 4 et fait un commit sur master.

~/alpha $ git checkout interimaire
Switched to branch 'interimaire'
~/alpha $ printf 'b' > data/lettre.txt
~/alpha $ git add data/lettre.txt
~/alpha $ git commit -m 'b3'
[interimaire 982dffb] b3

L'utilisateur bascule vers la branche interimaire. Il modifie le contenu de data/lettre.txt à « b » et fait un commit sur interimaire.

Propriété de graphe : les commits peuvent partager des parents. Cela signifie qu’ils peuvent être créés dans l'historique de commits.

Propriété de graphe : les commits peuvent avoir plusieurs parents. Cela signifie que des lignées séparées peuvent être reliées par un commit avec deux parents : un commit de fusion.

~/alpha $ git merge master -m 'b4'
Merge made by the 'recursive' strategy.

L'utilisateur fusionne master dans interimaire.

git découvre que le receveur, b3, et le donneur, a4 sont dans des lignées différentes. Il fait un commit de fusion. Ce processus a huit étapes :

Premièrement, git écrit le hash du commit donneur dans un fichier alpha/.git/MERGE_HEAD. La présence de ce fichier informe git qu'il est dans une fusion intermédiaire.

Deuxièmement, git trouve le commit de base : l'ascendant le plus récent que les commit du donneur et du receveur ont en commun.

Propriété de graphe : les commit ont des parents. Cela signifie qu'il est possible de trouver le point de divergence entre deux lignées. git remonte en arrière depuis b3 et a4 pour trouver tous leurs ascendants partagés. C'est le commit de base.

Troisièmement, git génère les indices pour les commit de base, du receveur, du donneur, depuis leur arbre.

Quatrièmement, git génère un diff qui combine les changements faits de la base par le commit receveur et le commit donneur. Ce diff est une liste des chemins de fichier qui pointe sur un changement, un ajout, un retrait, une modification, ou un conflit.

git prend la liste de tous les fichiers apparaissant dans la base, le receveur, ou les indices donneur. Pour chacun, il compare les entrées d'index pour décider du changement à faire dans le fichier. Il écrit une entrée correspondante dans le diff. Dans ce cas, le diff a deux entrées.

La première entrée est pour data/lettre.txt. Le contenu de ce fichier est « a » dans la base, « b » dans le receveur, et « a » dans le donneur. Le contenu est différent dans la base et le receveur, mais est le même dans la base et le donneur. git voit que le contenu a été modifié par le receveur, mais pas le donneur. L'entrée diff pour data/lettre.txt est une modification, pas un conflit.

La seconde entrée dans le diff est pour data/nombre.txt. Dans ce cas, le contenu est le même dans la base et le receveur, et différent dans le donneur. L'entrée diff pour data/lettre.txt est aussi une modification.

Propriété de graphe : Il est possible de trouver le commit de base d'une fusion. Cela signifie que si un fichier a changé depuis la base uniquement au niveau du receveur ou du donneur, git peut automatiquement résoudre la fusion pour ce fichier. Cela résout le travail que l'utilisateur doit faire.

Cinquièmement, les changements indiqués par l'entrée dans le diff sont appliqués à la copie de travail. Le contenu de data/lettre.txt est fixé à « b » et le contenu de data/nombre.txt est fixé à 4.

Sixièmement, les changements indiqués par les entrées dans le diff sont appliqués à l'index. L'entrée pour data/lettre.txt pointe sur le blob b et l'entrée pour data/nombre.txt pointe sur le blob 4.

Septièmement, l'index mis à jour est validé par un commit :

tree 20294508aea3fb6f05fcc49adaecc2e6d60f7e7d
parent 982dffb20f8d6a25a8554cc8d765fb9f3ff1333b
parent 7b7bd9a5253f47360d5787095afc5ba56591bfe7
author Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500

b4

Notez que le commit a deux parents.

Huitièmement, git pointe la branche courante, interimaire, sur le nouveau commit.

17 - Fusionner deux commits de deux lignées différentes qui modifient tous les deux le même fichier

~/alpha $ git checkout master
Switched to branch 'master'
~/alpha $ git merge interimaire
Fast-forward

L'utilisateur permute sur la branche master. Il fusionne interimaire dans master. Ceci fait une avance rapide de master au commit b4. master et interimaire pointent maintenant sur le même commit.

~/alpha $ git checkout interimaire
Switched to branch 'interimaire'
~/alpha $ printf '5' > data/nombre.txt
~/alpha $ git add data/nombre.txt
~/alpha $ git commit -m 'b5'
[interimaire bd797c2] b5

L'utilisateur vérifie interimaire. Il modifie le contenu de data/nombre.txt sur 5 et fait un commit sur interimaire.

~/alpha $ git checkout master
Switched to branch 'master'
~/alpha $ printf '6' > data/nombre.txt
~/alpha $ git add data/nombre.txt
~/alpha $ git commit -m 'b6'
[master 4c3ce18] b6

L'utilisateur change de branche pour la branche master. Il modifie le contenu de data/nombre.txt à 6 et fait un commit sur master.

~/alpha $ git merge interimaire
CONFLICT in data/nombre.txt
Automatic merge failed; fix conflicts and
commit the result.

L'utilisateur fusionne interimaire dans master. Il y a un conflit et la fusion est mise en pause. Le processus pour une fusion à conflit suit les mêmes six étapes d'un processus de fusion sans conflit ; il positionne .git/MERGE_HEAD, trouve le commit de base, génère les indices de la base, du receveur, du donneur, crée un diff, met à jour la copie de travail et met à jour l'index. À cause du conflit, les septième et huitième étapes de commit et l'étape de mise à jour de ref ne sont jamais effectuées. Retournons dans ces étapes et voyons ce qu'il se passe.

Premièrement, git écrit le hash du commit du donneur dans un fichier .git/MERGE_HEAD.

Deuxièmement, git trouve le commit de base, b4.

Troisièmement, git génère les indices pour les commit de base, receveur et donneur.

Quatrièmement, git génère un diff qui combine les changements faits à la base par le commit receveur et le commit donneur. Ce diff est une liste de chemins des fichiers qui pointent sur un changement : ajout, suppression, modification, ou conflit.

Dans ce cas, le diff contient seulement une entrée data/nombre.txt. L'entrée est marquée comme un conflit, car le contenu de data/nombre.txt est différent dans le receveur, le donneur, et la base.

Cinquièmement, les changements indiqués par les entrées du diff sont appliqués à la copie de travail. Pour une zone en conflit, git écrit les deux versions dans le fichier de la copie de travail. Le contenu de data/nombre.txt est maintenant :

<<<<<<< HEAD
6
=======
5
>>>>>>> interimaire

Sixièmement, les changements indiqués par les entrées du diff sont appliqués à l'index. Les entrées dans l'index sont uniquement identifiées par une combinaison de leur chemin de fichier et niveau. L'entrée pour un fichier non en conflit a un niveau de 0. Avant cette fusion, l'index ressemblait à ceci :

0 data/lettre.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
0 data/nombre.txt 62f9457511f879886bb7728c986fe10b0ece6bcb

Après la fusion, le diff est écrit dans l'index, l'index ressemble à ceci :

0 data/lettre.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
1 data/nombre.txt bf0d87ab1b2b0ec1a11a3973d2845b42413d9767
2 data/nombre.txt 62f9457511f879886bb7728c986fe10b0ece6bcb
3 data/nombre.txt 7813681f5b41c028345ca62a2be376bae70b7f61

L'entrée pour data/lettre.txt au niveau 0 est la même qu'avant la fusion. L'entrée pour data/nombre.txt au niveau 0 est parti. Il y a trois nouvelles entrées dans cette place. L'entrée pour le niveau 1 a le hash de base du contenu de data/nombre.txt. L'entrée pour le niveau 2 a le hash du contenu du receveur de data/nombre.txt. L'entrée pour le niveau 3 a le hash du contenu du donneur data/nombre.txt. La présence de ces trois entrées dit à git que data/nombre.txt est en conflit.

La fusion se met en pause.

~/alpha $ printf '11' > data/nombre.txt
~/alpha $ git add data/nombre.txt

L'utilisateur intègre le contenu des deux versions en conflit en fixant le contenu de data/nombre.txt à 11. Il ajoute le fichier à l'index. git ajoute un blob contenant 11. Ajouter un fichier en conflit dit à git que le conflit est résolu. git retire les entrées de data/nombre.txt pour les niveaux 1,2,3 depuis l'index. Il ajoute une entrée pour data/nombre.txt au niveau 0 avec le hash du nouveau blob. L'index lit maintenant :

0 data/lettre.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
0 data/nombre.txt 9d607966b721abde8931ddd052181fae905db503

~/alpha $ git commit -m 'b11'
[master 251a513] b11

Septièmement, l'utilisateur effectue le commit. git voit .git/MERGE_HEAD dans le dépôt, qui signifie qu'une fusion est en cours. Il vérifie l'index et trouve qu'il n'y a pas de conflit. Il crée un nouveau commit, b11, pour enregistrer le contenu de la résolution de la fusion. Il supprime le fichier ./git/MERGE_HEAD. Ceci complète la fusion.

Huitièmement, git fait pointer la branche courante master sur le nouveau commit.

18 - Supprimer un fichier

Ce diagramme du graphe de git inclut l'historique de commit, les arbres et les blobs pour le dernier commit, et la copie de travail et l'index :

~/alpha $ git rm data/lettre.txt
rm 'data/lettre.txt'

L'utilisateur demande à git de supprimer data/lettre.txt. Le fichier est supprimé de la copie de travail. L'entrée est supprimée de l'index.

~/alpha $ git commit -m '11'
[master d14c7d2] 11

L'utilisateur fait un commit. Comme partie du commit, comme à chaque fois, git construit un arbre qui représente le contenu de l'index. data/lettre.txt n'est pas inclus dans l'arbre, car il n'est pas dans l'index.

19 - Copie d'un dépôt

~/alpha $ cd ..
~ $ cp -R alpha bravo

L'utilisateur copie le contenu du dépôt alpha/ dans le dossier bravo/. Ceci produit la structure de répertoire suivante :

|-- alpha
|   |-- data
|        |-- nombre.txt
|-- bravo
|-- data
|-- nombre.txt

Il y a maintenant un autre graphe git dans le dossier bravo :

20 - Lier un dépôt à un autre

~ $ cd alpha
~/alpha $ git remote add bravo ../bravo

L'utilisateur retourne dans le dépôt alpha. Il configure bravo comme un dépôt distant dans alpha. Ceci ajoute quelques lignes dans le fichier alpha/.git/config :

[remote "bravo"]
url = ../bravo/

Ces lignes spécifient qu'il y a un dépôt distant nommé bravo dans le dossier ../bravo.

21 - Aller chercher une branche depuis un dépôt distant

~/alpha $ cd ../bravo
~/bravo $ printf '12' > data/nombre.txt
~/bravo $ git add data/nombre.txt
~/bravo $ git commit -m '12'
[master 94cd04d] 12

L'utilisateur va dans le dépôt bravo. Il modifie le contenu de data/nombre.txt à 12 et fait un commit du changement sur master dans bravo.

~/bravo $ cd ../alpha
~/alpha $ git fetch bravo master
Unpacking objects: 100%
From ../bravo
* branch master -> FETCH_HEAD

L'utilisateur va dans le dépôt alpha. Il amène master depuis bravo dans alpha. Ce processus a quatre étapes.

Premièrement, git récupère le hash du commit sur lequel pointe master sur bravo. C'est le hash du commit 12.

Deuxièmement, git fait une liste de tous les objets dont le commit 12 dépend : l'objet commit lui-même, les objets dans son arbre, les commit ascendants du commit 12 et les objets dans leurs arbres. Il supprime de cette liste tous les objets que la base de données alpha a déjà, il copie le reste dans alpha/.git/objects/.

Troisièmement, le contenu concret du fichier ref dans alpha/.git/refs/remotes/bravo/master est positionné sur le hash du commit 12.

Quatrièmement, le contenu de alpha/.git/FETCH_HEAD est positionné sur :

94cd04d93ae88a1f53a4646532b1e8cdfbc0977f branch 'master' of ../bravo

Ceci indique que la plus récente commande fetch apporte le commit 12 de master depuis bravo.

Propriété de graphe : les objets peuvent être copiés. Cela signifie que l'historique peut être partagé entre plusieurs dépôts.

Propriété de graphe : un dépôt peut stocker des refs de branche distante comme alpha/.git/refs/remotes/bravo/master. Cela signifie qu'un dépôt peut enregistrer localement l'état d'une branche d'un dépôt distant. Il est à jour au moment de l'apport, mais sera périmé si la branche distante change.

22 - fusion FETCH_HEAD

~/alpha $ git merge FETCH_HEAD
Updating d14c7d2..94cd04d
Fast-forward

L'utilisateur fusionne FETCH_HEAD. FETCH_HEAD est juste une nouvelle ref. Elle pointe sur le commit 12, le donneur. HEAD pointe sur le commit 11, le receveur. git fait une fusion en avance rapide et fait pointer master sur le commit 12.

23 - Tirer une branche d'un dépôt distant

~/alpha $ git pull bravo master
Already up-to-date.

L'utilisateur tire master à partir du dépôt bravo dans alpha. pull est un raccourci pour « fetch and merge FETCH_HEAD », git effectue ces deux commandes et rapporte que master est déjà à jour.

24 - Cloner un dépôt

~/alpha $ cd ..
~ $ git clone alpha charlie
Cloning into 'charlie'

L'utilisateur va dans le dossier parent. Il clone alpha vers charlie. Cloner vers charlie a un résultat similaire à effectuer cp que l'utilisateur a utilisé pour produire le dépôt bravo. git crée un nouveau dossier nommé charlie. Il initialise charlie en tant que dépôt git, il ajoute alpha en tant que dépôt distant nommé origin, fait sortir origin et fusionne FETCH_HEAD.

25 - Pousser une branche vers une branche spécifique dans un dépôt distant

~ $ cd alpha
~/alpha $ printf '13' > data/nombre.txt
~/alpha $ git add data/nombre.txt
~/alpha $ git commit -m '13'
[master 3238468] 13

L'utilisateur revient dans le dépôt alpha. Il modifie le contenu de data/nombre.txt à 13 et fait un commit du master sur alpha.

~/alpha $ git remote add charlie ../charlie

Il ajoute charlie en tant que dépôt distant sur alpha.

~/alpha $ git push charlie master
Writing objects: 100%
remote error: refusing to update checked out
branch: refs/heads/master because it will make
the index and work tree inconsistent

Il pousse master vers charlie.

Tous les objets requis pour le commit 13 sont copiés dans charlie.

À ce point, le processus de push se stoppe. git, comme toujours, dit à l'utilisateur ce qui ne va pas. Il refuse de pousser les données dans une branche distante. Cela a du sens. Un push devrait mettre à jour l'index distant, et HEAD. Ceci devrait causer la confusion si quelqu'un éditait la copie de travail dans le dépôt distant.

À ce point, l'utilisateur devrait faire une nouvelle branche, fusionner le commit 13 dedans, et pousser cette branche vers charlie. Mais, ils veulent un dépôt qu'ils peuvent pousser quand ils le veulent. Ils veulent un dépôt central dans lequel ils peuvent pousser et tirer des éléments, mais que personne ne commit directement. Ils veulent quelque chose comme un GitHub distant. Ils veulent un dépôt nu.

26 - Cloner un dépôt nu

~/alpha $ cd ..
~ $ git clone alpha delta --bare
Cloning into bare repository 'delta'

L'utilisateur va dans le dossier parent. Il clone delta en tant que dépôt nu. C'est un clone ordinaire avec deux différences. Le fichier config indique que le dépôt est nu. Et les fichiers qui sont normalement stockés dans le dossier .git sont stockés dans la racine du dépôt :

delta
|-- HEAD
|-- config
|-- objects
|-- refs

27 - Pousser une branche vers un dépôt nu

~ $ cd alpha
~/alpha $ git remote add delta ../delta

L'utilisateur revient dans le dépôt alpha. il ajoute delta en tant que dépôt distant d'alpha.

~/alpha $ printf '14' > data/nombre.txt
~/alpha $ git add data/nombre.txt
~/alpha $ git commit -m '14'
[master cb51da8] 14

Il modifie le contenu de data/nombre.txt à 14 et effectue un commit pour changer le master d'alpha.

~/alpha $ git push delta master
Writing objects: 100%
To ../delta
3238468..cb51da8 master -> master

Il pousse master vers delta. La poussée se fait en trois étapes.

Premièrement, tous les objets requis pour le commit 14 dans la branche master sont copiés depuis alpha/.git/objects vers delta/objects.

Deuxièmement, delta/refs/heads/master est mis à jour pour pointer sur le commit 14.

Troisièmement, alpha/.git/refs/remotes/delta/master est positionné sur le commit 14. alpha a un enregistrement à jour de l'état de delta.

28 - Notes

1. Dans ce cas, le hash est plus long que le contenu original. Mais toutes les parties de contenu plus long que le nombre de caractères dans un hash seront exprimées de façon plus concise.

2. Il y a une chance que deux parties de contenu différentes aient le même hash. Mais cette chance est faible.

3. git prune supprime tous les objets ne pouvant être atteints depuis une ref. Si l'utilisateur lance cette commande, il pourra perdre du contenu.

4. gitstash stocke toutes les différences entre la copie actuelle et le commit HEAD dans un espace sécurisé. Elles pourront être récupérées plus tard.

5. La commande rebase peut être utilisée pour ajouter, éditer, et supprimer des commits dans un historique.

29 - Résumé

git est construit autour de graphes. Quasiment toutes les commandes git manipulent ces graphes. Pour comprendre git profondément, concentrez-vous sur les propriétés de ce graphe, pas sur les processus ou les commandes.

Pour en apprendre plus sur git, étudiez le dossier .git. Ce n’est pas effrayant. Regardez dedans. Changez le contenu des fichiers et regardez ce qu'il se passe. Créez un commit à la main. Essayez de voir à quel point vous pouvez planter un dépôt, puis réparez-le.

30 - Notes de la rédaction

Nous remercions Mary Rose Cook de nous avoir autorisés à traduire et republier son billet initialement publié ici « Git from the inside out ».

Nous remercions également Chrtophe pour sa traduction ainsi que François Dorin, Marco46 et Obsidian pour leur relecture technique et f-leb pour sa relecture orthographique.