Contrôle de version (Git)
Les systèmes de contrôle de version (VCS) sont des outils utilisés pour suivre les modifications apportées au code source (ou à d’autres collections de fichiers et de dossiers). Comme leur nom l’indique, ces outils permettent de conserver un historique des modifications ; ils facilitent en outre la collaboration. Les VCS suivent les modifications apportées à un dossier et à son contenu dans une série d’instantanés, où où chaque instantané encapsule l’état complet des fichiers/dossiers d’un répertoire de premier niveau. Les VCS conservent également des métadonnées telles que la personne qui a créé chaque instantané, les messages associés à chaque instantané, etc.
Pourquoi le contrôle de version est-il utile ? Même lorsque vous travaillez seul, il peut vous permettre de consulter d’anciennes snapshots d’un projet, de garder une trace des raisons pour lesquelles certaines modifications ont été effectuées, de travailler sur des branches parallèles du développement, et bien plus encore. Lorsque vous travaillez à plusieurs, c’est un outil inestimable pour voir ce que d’autres personnes ont modifié, ainsi que pour résoudre les conflits dans le cadre d’un développement simultané.
Les VCS modernes vous permettent également de répondre facilement (et souvent automatiquement) à des questions telles que :
- Qui a écrit ce module ?
- Quand cette ligne particulière de ce fichier particulier a-t-elle été éditée ? Par qui ? Pourquoi a-t-elle été éditée ?
- Au cours des 1000 dernières révisions, quand/pourquoi un test unitaire particulier a-t-il échoué ?
Bien qu’il existe d’autres systèmes de contrôle de version, Git est la norme de facto en matière de contrôle de version. Cette bande dessinée de XKCD comic illustre la réputation de Git :
Parce que l’interface de Git est une grande abstraction, apprendre Git de haut en bas (en commençant par son interface / son interface en ligne de commande) peut conduire à beaucoup de confusion. Il est possible de mémoriser une poignée de commandes et de les considérer comme des incantations magiques, et de suivre l’approche de la bande dessinée ci-dessus chaque fois que quelque chose ne va pas.
Bien que l’interface de Git soit laide, sa conception et ses idées sous-jacentes sont belles. Alors qu’une interface laide doit être mémorisée, une belle conception peut être comprise. C’est pourquoi nous expliquons Git de manière ascendante, en commençant par son modèle et en couvrant ensuite l’interface en ligne de commande. Une fois le modèle compris, les commandes peuvent être mieux comprises en termes de manipulation du modèle sous-jacent.
Modèle de Git
Il existe de nombreuses approches ad hoc pour le contrôle des versions. Git a un modèle modèle bien pensé qui permet de bénéficier de toutes les fonctionnalités intéressantes du contrôle de version, comme la conservation de l’historique, la prise en charge des branches et la collaboration.
Snapshots
Git modélise l’historique d’une collection de fichiers et de dossiers au sein d’un répertoire de top niveau comme une série de snapshots. Dans la terminologie de Git, un fichier est appelé “blob”, et il est vu comme un tas d’octets. Un répertoire est appelé “tree”, et il associe des noms à des blobs ou à des trees (les répertoires peuvent donc contenir d’autres répertoires). Une snapshot est le tree de top niveau qui est suivi. Par exemple, nous pourrions avoir une arborescence comme suit :
<root> (tree)
|
+- foo (tree)
| |
| + bar.txt (blob, contenu = "hello world")
|
+- baz.txt (blob, contenu = "git est un outil formidable")
L’arbre de top niveau contient deux éléments, un tree “foo” (qui contient lui-même un élément, un blob “bar.txt”), et un blob “baz.txt”.
Modélisation de l’historique : liaisons des snapshots
Comment un système de contrôle des versions doit-il relier les snapshots ? Un modèle simple consisterait à avoir un historique linéaire. Un historique serait une liste de snapshots classés dans le temps. Pour de nombreuses raisons, Git n’utilise pas un modèle aussi simple.
Dans Git, un historique est un graphe acyclique dirigé (DAG) de snapshots. Cela peut sembler être un mot mathématique compliqué, mais ne vous laissez pas intimider. Tout ce que cela signifie, c’est que chaque instantané dans Git fait référence à un ensemble de “parents”, les snapshots qui l’ont précédé. Il s’agit d’un ensemble de parents plutôt que d’un seul parent (comme ce serait le cas dans un historique linéaire) car une snapshot peut descendre de plusieurs parents, par exemple, en raison de la combinaison (merge) de deux branches parallèles de développement.
Git appelle ces snapshots des “commit”. La visualisation de l’historique des commits peut ressembler à quelque chose comme ceci :
o <-- o <-- o <-- o
^
\
--- o <-- o
Dans l’image ASCII ci-dessus, les o
correspondent à des commits individuels (instantanés). Les flèches pointent vers le parent de chaque commit (il s’agit d’une relation “vient auparavant”, et non “vient postérieurement”). Après le troisième commit, l’historique se divise en deux branches distinctes. Cela peut correspondre, par exemple, à deux fonctionnalités distinctes développées en parallèle, indépendamment l’une de l’autre. À l’avenir, ces branches peuvent être fusionnées pour créer un nouvel instantané qui intègre les deux fonctionnalités, produisant un nouvel historique qui ressemble à ceci, avec le nouveau commit de fusion en gras :
o <-- o <-- o <-- o <---- o
^ /
\ v
--- o <-- o
Dans Git, les commits sont immuables. Cela ne signifie pas que les erreurs ne peuvent pas être corrigées, cependant ; c’est juste que les “éditions” de l’historique des commits créent en fait des commits entièrement nouveaux, et les références (voir ci-dessous) sont mises à jour pour pointer vers les nouveaux.
Modèle de données, sous forme de pseudocode
Il peut être instructif de voir le modèle de données de Git écrit en pseudo-code :
// un fichier est un ensemble d'octets
type blob = array<byte>
// un répertoire contient des fichiers et des répertoires nommés
type tree = map<string, tree | blob>
// un commit a des parents, des métadonnées et l'arbre de top niveau
type commit = struct {
parents: array<commit>
author: string
message: string
snapshot: tree
}
Il s’agit d’un modèle d’historique simple et clair.
Objets et adressage du contenu
Un “objet” est un blob, un arbre ou un commit :
type object = blob | tree | commit
Dans le stockage de données de Git, tous les objets sont adressés en fonction de leur contenu par leur hash SHA-1.
objects = map<string, object>
def store(object):
id = sha1(object)
objects[id] = object
def load(id):
return objects[id]
Les blobs, les trees et les commits sont unifiés de cette manière : ce sont tous des objets. Lorsqu’ils font référence à d’autres objets, ils ne les contiennent pas réellement dans leur représentation sur disque, mais y font référence par leur hachage.
Par exemple, l’arbre de l’exemple de structure de répertoire ci-dessus (visualisé à l’aide de la fonction git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d
), ressemble à :
100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85 baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87 foo
Le tree lui-même contient des pointeurs vers son contenu, baz.txt (un blob) et foo (un tree). Si nous regardons le contenu adressé par le hash correspondant à baz.txt avec git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85
, nous obtenons ce qui suit :
git is wonderful
Références
Désormais, toutes les snapshots peuvent être identifiées par leur hachage SHA-1. Cela n’est pas pratique, car les humains ne sont pas doués pour se souvenir de chaînes de 40 caractères hexadécimaux.
La solution de Git à ce problème est d’utiliser des noms lisibles par l’homme pour les hashs SHA-1, appelés “références”. Les références sont des pointeurs vers les commits. Contrairement aux objets, qui sont immutables, les références sont mutables (elles peuvent être mises à jour pour pointer vers un nouveau commit). Par exemple, la référence master
pointe généralement vers le dernier commit de la branche principale de développement.
references = map<string, string>
def update_reference(name, id):
references[name] = id
def read_reference(name):
return references[name]
def load_reference(name_or_id):
if name_or_id in references:
return load(references[name_or_id])
else:
return load(name_or_id)
Grâce à cela, Git peut utiliser des noms lisibles par l’homme comme master
pour se référer à une snapshot particulière dans l’historique, au lieu d’une longue chaîne hexadécimale.
Un détail est que nous voulons souvent avoir une notion de “l’endroit où nous sommes actuellement” dans l’historique, de sorte que lorsque nous prenons une nouvelle snapshot, nous savons à quoi elle est relative (comment nous définissons le champ parents
du commit). Dans Git, cet “endroit où nous sommes actuellement” est une référence spéciale appelée “HEAD”.
Repositories
Enfin, nous pouvons définir ce qu’est (en gros) un dépôt Git : il s’agit des objets
et des références
.
Sur le disque, tout ce que Git stocke, ce sont des objets et des références : c’est tout ce qu’il y a dans le modèle de données de Git. Toutes les commandes Git
se traduisent par une manipulation du DAG de commit par l’ajout d’objets et l’ajout/mise à jour de références.
Chaque fois que vous tapez une commande, pensez à la manipulation qu’elle effectue sur la structure de données graphique sous-jacente. Inversement, si vous essayez d’apporter un type particulier de changement au DAG des livraisons, par exemple “supprimer les changements non commit et faire pointer la ref ‘master’ sur le commit 5d83f9e
”, il y a probablement une commande pour le faire (par exemple, dans ce cas, git checkout master; git reset --hard 5d83f9e
).
Zone d’attente
Il s’agit d’un autre concept orthogonal au modèle de données, mais qui fait partie de l’interface de création de commits.
L’une des façons d’implémenter la snapshot telle que décrite ci-dessus est d’avoir une commande “create snapshot” qui crée une nouvelle snapshot basée sur l’état actuel du répertoire de travail. Certains outils de contrôle de version fonctionnent ainsi, mais pas Git. Nous voulons des snapshots propres, et il n’est pas toujours idéal de faire une snapshot à partir de l’état actuel. Par exemple, imaginez un scénario dans lequel vous avez implémenté deux fonctionnalités distinctes, et vous voulez créer deux commits distincts, où le premier introduit la première fonctionnalité, et le suivant introduit la seconde fonctionnalité. Ou imaginez un scénario dans lequel vous avez ajouté des instructions de débogage (print) partout dans votre code, ainsi qu’une correction de bugs ; vous voulez valider la correction de bugs tout en supprimant toutes les instructions de débogage.
Git s’adapte à ces scénarios en vous permettant de spécifier quelles modifications doivent être incluses dans la prochaine snapshot grâce à un mécanisme appelé “staging area” (zone d’attente).
Interface de ligne de commande Git
Pour éviter de dupliquer des informations, nous n’allons pas expliquer les commandes ci-dessous en détail. Consultez le très recommandé Pro Git pour plus d’informations, ou regardez la vidéo de présentation.
Bases
git help <command>
: obtenir de l’aide pour une commande gitgit init
: créer un nouveau repo git, dont les données sont stockées dans le répertoire.git
.git status
: vous indique ce qui se passegit add <filename>
: ajouter des fichiers à la zone d’attentegit commit
: créer un nouveau commit- Écrivez de bons commentaires qui ont du sens!
- Encore plus de raisons d’écrire de bons commentaires qui ont du sens!
git log
: montre un historique des logsgit log --all --graph --decorate
: visualise l’historique sous la forme d’un DAGgit diff <filename>
: montre les changements que vous avez effectués par rapport à la zone d’attentegit diff <revision> <filename>
: montre les différences dans un fichier entre des snapshotsgit checkout <revision>
: met à jour HEAD et la branche actuelle
Branchement et merge
git branch
: affiche les branchesgit branch <name>
: crée une branchegit checkout -b <name>
: crée une branche et y accède- identique que
git branch <name>; git checkout <name>
- identique que
git merge <revision>
: fusionne dans la branche courantegit mergetool
: utilise un outil sophistiqué pour aider à résoudre les conflits de mergegit rebase
: rebase un ensemble de patches sur une nouvelle base
Remotes
git remote
: list des remotesgit remote add <name> <url>
: ajoute un remotegit push <remote> <local branch>:<remote branch>
: envoie des objets à la branche remote, et met à jour la référence remotegit branch --set-upstream-to=<remote>/<remote branch>
: établit une correspondance entre la branche locale et la branche remotegit fetch
: récupère des objets/références d’une branche remotegit pull
: même chose quegit fetch; git merge
git clone
: télécharge le repository à partir d’une branche remote
Annuler
git commit --amend
: éditer le contenu/message d’un commitgit reset HEAD <file>
: rétablit l’état d’un fichiergit checkout -- <file>
: rejette les modifications
Advanced Git
git config
: git est très personnalisablegit clone --depth=1
: clone superficiel, sans l’historique complet des versionsgit add -p
: staging interactifgit rebase -i
: rebasement interactifgit blame
: montre qui a édité quelle ligne en derniergit stash
: supprimer temporairement les modifications dans le répertoire de travailgit bisect
: historique de recherche binaire (par exemple pour les régressions).gitignore
: spécifie les fichiers intentionnellement à ignorer
Divers
- GUIs: il existe de nombreux clients GUI pour git. Personnellement, nous ne les utilisons pas et utilisons plutôt l’interface en ligne de commande.
- Intégration au shell: il est très pratique d’avoir un statut Git dans le prompt du shell (zsh, bash). Souvent inclus dans des frameworks comme Oh My Zsh. Oh My Zsh.
- Intégration de l’éditeur: de la même manière que ci-dessus, des intégrations pratiques avec de nombreuses fonctionnalités. fugitive.vim est l’intégration standard pour Vim.
- Workflows: nous vous avons enseigné le modèle de données, ainsi que quelques commandes de base ; nous ne vous avons pas dit quelles pratiques suivre lorsque vous travaillez sur de grands projets beaucoup d’approches différentes.
- GitHub: Git n’est pas GitHub. GitHub a une façon spécifique de contribuer au code d’autres projets, appelée pull requests.
- Autres fournisseurs Git: GitHub n’est pas un cas particulier : il existe de nombreux hébergeurs de dépôts Git, comme GitLab et BitBucket.
Resources
- La lecture de Pro Git est fortement recommandée. En parcourant les chapitres 1 à 5, vous devriez apprendre l’essentiel de ce dont vous avez besoin pour utiliser Git de manière efficace, maintenant que vous comprenez le modèle de données. Les derniers chapitres contiennent des informations intéressantes et avancées.
- Oh Shit, Git!?! est un petit guide sur la façon de se remettre de certaines erreurs courantes de Git.
- Git pour les informaticiens est une courte explication du modèle de données de Git, avec moins de pseudocode et plus de diagrammes que les notes ce cours.
- Git depuis le début explication détaillée de l’implémentation de Git au-delà du modèle de données, pour les curieux.
- Comment expliquer git en quelques mots
- Apprendre le branchement Git est un jeu par navigateur qui vous apprend le Git.
Exercices
- Si vous n’avez pas d’expérience avec Git, essayez de lire les deux premiers chapitres de Pro Git ou suivez un tutoriel tel que Learn Git Branching. Pendant que vous travaillez, faites le lien entre les commandes Git et le modèle de données.
- Clonez le dépôt du site web de la classe.
- Explorer l’historique des versions en le visualisant sous la forme d’un graphique.
- Qui a été la dernière personne à modifier
README.md
? (Indice: utilisergit log
avec un argument). - Quel était le commentaire associé à la dernière modification de la ligne
collections:
de_config.yml
? (Indice: utilisergit blame
etgit show
).
- Une erreur fréquente lors de l’apprentissage de Git est de commit des fichiers volumineux qui ne devraient pas être gérés par Git ou d’ajouter des informations sensibles. Essayez d’ajouter un fichier à un repository, d’effectuer quelques commits, puis de supprimer ce fichier de l’historique (vous pouvez consulter cette rubrique).
- Cloner un dépôt depuis GitHub, et modifier un de ses fichiers existants. Que se passe-t-il lorsque vous faites
git stash
? Que voyez-vous lorsque vous exécutezgit log --all --oneline
? Exécutezgit stash pop
pour annuler ce que vous avez fait avecgit stash
. Dans quel scénario cela peut-il être utile ? - Comme beaucoup d’outils en ligne de commande, Git fournit un fichier de configuration (ou fichier point) appelé
~/.gitconfig
. Créez un alias dans~/.gitconfig
pour que lorsque vous lancezgit graph
, vous obteniez la sortie degit log --all --graph --decorate --oneline
. Des informations sur les alias git peuvent être trouvées ici. - Vous pouvez définir des motifs d’ignorance globaux dans
~/.gitignore_global
après avoir exécutégit config --global core.excludesfile ~/.gitignore_global
. Faites cela, et configurez votre fichier gitignore global pour ignorer les fichiers temporaires spécifiques à un système d’exploitation ou à un éditeur, comme.DS_Store
. - Faites un fork du répertoire du site web de la classe, trouvez une faute de frappe ou une autre amélioration que vous pouvez apporter, et soumettez une demande de modification (pull request) sur GitHub (vous voulez peut-être jeter un oeil ici). Veuillez ne soumettre que des PR utiles (ne nous spammez pas, s’il vous plaît !). Si vous ne trouvez pas d’amélioration à apporter, vous pouvez ignorer cet exercice.
Sous licence CC BY-NC-SA.