Git repose principalement sur les commits : vous indexez des commits, vous en créez, vous visualisez les anciens commits et vous transférez des commits entres les dépôts en utilisant différentes commandes Git. La majorité de ces commandes sont basées sur un commit, sous une forme ou une autre, et plusieurs d'entre elles acceptent une référence de commit en tant que paramètre. Par exemple, vous pouvez utiliser git checkout pour visualiser un ancien commit en transmettant une empreinte de commit ou pour basculer entre les branches en transmettant un nom de branche.

Il existe plusieurs façons de faire référence à un commit.

En comprenant les différentes méthodes de référencement d'un commit, vous pouvez exploiter au mieux ces puissantes commandes. Dans ce chapitre, nous décrirons le fonctionnement interne des commandes courantes comme git checkout, git branch et git push en étudiant les différentes méthodes de référencement d'un commit.

Nous verrons également comment récupérer des commits qui semblent « perdus » en y accédant via le mécanisme reflog de Git.

Empreintes

Le moyen le plus direct pour référencer un commit est d'utiliser l'empreinte SHA-1. Celle-ci constitue l'identifiant unique de chaque commit. Vous pouvez trouver l'empreinte de tous vos commits dans la sortie git log.

commit 0c708fdec272bc4446c6cabea4f0022c2b616eba
Author: Mary Johnson <mary@example.com>
Date:   Wed Jul 9 16:37:42 2014 -0500

    Message de commit

Lorsque vous transmettez le commit à d'autres commandes Git, vous devez uniquement spécifier suffisamment de caractères pour identifier le commit de façon unique. Par exemple, vous pouvez inspecter le commit ci-dessus avec git show en lançant la commande suivante :

git show 0c708f

Il est parfois nécessaire de résoudre une branche, un tag ou une autre référence indirecte dans l'empreinte de commit correspondante. Pour ce faire, vous pouvez utiliser la commande git rev-parse. Le code suivant renvoie l'empreinte du commit pointé par la branche master :

git rev-parse master

Il est particulièrement utile lorsque vous écrivez des scripts personnalisés qui acceptent une référence de commit. Au lieu d'analyser la référence de commit manuellement, vous pouvez laisser git rev-parse normaliser l'entrée pour vous.

Réfs

Une réf est un moyen indirect de référencer un commit. Vous pouvez la considérer comme un alias convivial d'une empreinte de commit. C'est le mécanisme interne de Git pour représenter les branches et les tags.

Les réfs sont stockées sous forme de fichiers texte normaux dans le répertoire .git/refs, où .git se nomme généralement .git. Pour explorer les réfs présentes dans l'un de vos dépôts, accédez à .git/refs. Vous devriez voir la structure suivante. Celle-ci contiendra toutefois des fichiers différents en fonction des branches, des tags et des remotes présents dans votre dépôt :

.git/refs/
heads/
master
some-feature
remotes/
origin/
master
tags/
v0.9

Le répertoire heads définit toutes les branches locales de votre dépôt. Chaque nom de fichier correspond au nom de la branche en question et, à l'intérieur du fichier, vous trouverez une empreinte de commit. Cette empreinte est l'emplacement de la pointe de la branche. Pour le vérifier, essayez de lancer les deux commandes suivantes à partir de la racine du dépôt Git :

# Générez le contenu du fichier `refs/heads/master` :
cat .git/refs/heads/master

# Inspectez le commit à la pointe de la branche `master` :
git log -1 master

L'empreinte renvoyée par la commande cat doit correspondre à l'ID de commit affiché par git log.

Pour modifier l'emplacement de la branche master, Git doit simplement changer le contenu du fichier refs/heads/master. De la même manière, il suffit d'écrire une empreinte de commit dans un nouveau fichier pour créer une nouvelle branche. C'est l'une des raisons pour lesquelles les branches Git sont si légères par rapport aux branches SVN.

Le répertoire tags fonctionne exactement de la même manière, sauf qu'il contient des tags au lieu de branches. Le répertoire remotes liste tous les dépôts distants que vous avez créés avec git remote en tant que sous-répertoires distincts. À l'intérieur de chacun d'entre eux, vous trouverez toutes les branches distantes qui ont été fetchées de votre dépôt.

Spécifier les réfs

Lorsque vous transmettez une réf à une commande Git, vous pouvez définir le nom complet de la réf, ou utiliser un nom abrégé et laisser Git rechercher une réf correspondante. Vous devez être déjà familiarisé avec les noms abrégés pour les réfs, puisque vous l'utilisez à chaque fois que vous appelez une branche par son nom.

git show some-feature

L'argument some-feature dans la commande ci-dessus est en réalité le nom abrégé de la branche. Git corrige cela en refs/heads/some-feature avant de l'utiliser. Vous pouvez également spécifier la réf complète sur la ligne de commande :

git show refs/heads/some-feature

Ceci évite toute ambiguïté quant à l'emplacement de la réf. C'est notamment nécessaire si vous avez un tag et une branche nommés some-feature. Toutefois, si vous utilisez les conventions de dénomination appropriées, l'ambiguïté entre les tags et les branches ne pose généralement aucun problème.

Nous verrons d'autres noms de réf complets dans la section Refspecs.

Réfs compressées

Pour les dépôts volumineux, Git effectuera périodiquement un nettoyage de type garbage collection pour supprimer les objets inutiles et compresser les réfs dans un fichier unique afin d'optimiser les performances. Vous pouvez forcer cette compression avec la commande garbage collection :

git gc

Cette commande déplace tous les fichiers de tag et de branche présents dans le dossier refs vers un fichier unique nommé packed-refs et situé au-dessus du répertoire .git. Si vous ouvrez ce fichier, vous trouverez un mappage des empreintes de commit vers les réfs :

00f54250cf4e549fdfcafe2cf9a2c90bc3800285 refs/heads/feature
0e25143693cfe9d5c2e83944bbaf6d3c4505eb17 refs/heads/master
bb883e4c91c870b5fed88fd36696e752fb6cf8e6 refs/tags/v0.9

À l'extérieur, la fonctionnalité Git normale ne sera pas affectée. Néanmoins, si vous vous demandez pourquoi le dossier .git/refs est vide, sachez que c'est là que se trouvaient les réfs.

Réfs spéciales

Outre le répertoire refs, des réfs spéciales sont également situées dans le répertoire .git de niveau supérieur. Celles-ci sont répertoriées ci-dessous :

  • HEAD : le commit/la branche actuellement extrait(e).
  • FETCH_HEAD : la branche la plus récente fetchée dans un dépôt distant.
  • ORIG_HEAD : une référence de sauvegarde du fichier HEAD avant les changements radicaux effectués.
  • MERGE_HEAD : le(s) commit(s) que vous mergez dans la branche courante avec git merge.
  • CHERRY_PICK_HEAD – Le commit que vous sélectionnez.

Ces réfs sont toutes créées et mises à jour par Git lorsque cela s'avère nécessaire. Par exemple, la commande git pull exécute d'abord git fetch, qui met à jour la référence FETCH_HEAD. Elle lance ensuite git merge FETCH_HEAD pour finir de faire un pull des branches fetchées vers le dépôt. Bien évidemment, vous pouvez toutes les utiliser comme n'importe quelle autre réf, ce que vous avez déjà fait, j'en suis sûr, avec HEAD.

Le contenu de ces fichiers diffère en fonction du type et de l'état de votre dépôt. La réf HEAD peut contenir une réf symbolique, qui est simplement une référence à une autre réf au lieu d'une empreinte de commit, ou une empreinte de commit. Examinons par exemple le contenu de HEAD lorsque vous êtes sur la branche master :

git checkout master
cat .git/HEAD

Cela génère ref: refs/heads/master, ce qui signifie que HEAD pointe vers la réf refs/heads/master. C'est de cette manière que Git détermine la branche master en cours d'extraction. Si vous passez à une autre branche, le contenu de HEAD est mis à jour pour refléter la nouvelle branche. Mais si vous extrayez un commit au lieu d'une branche, HEAD contient une empreinte de commit au lieu d'une réf symbolique. Ainsi, Git sait qu'il est à l'état « HEAD détachée ».

Dans la plupart des cas, HEAD est la seule référence que vous utilisez directement. Les autres sont généralement utiles si vous écrivez des scripts de niveau inférieur qui doivent être intégrés dans le fonctionnement interne de Git.

Refspecs

Une refspec fait correspondre une branche dans le dépôt local avec une branche dans le dépôt distant. Ainsi, les branches distantes peuvent être gérées en utilisant des commandes Git locales, et il est possible de configurer un comportement avancé pour git push et git fetch.

Une refspec est spécifiée comme [+]<src>:<dst>. Le paramètre <src> est la branche source dans le dépôt local et le paramètre <dst> est la branche de destination dans le dépôt distant. Le signe + (facultatif) permet de forcer le dépôt distant à effectuer une mise à jour sans fast-forward.

Les refspecs peuvent être utilisées avec la commande git push pour attribuer un nom différent à la branche distante. Par exemple, la commande suivante pushe la branche master vers le dépôt distant origin comme un git push ordinaire , mais elle utilise qa-master comme nom de la branche dans le dépôt origin. Elle est utile pour les équipes qualité qui doivent pusher leurs propres branches vers un dépôt distant.

git push origin master:refs/heads/qa-master

Vous pouvez aussi utiliser les refspecs pour supprimer des branches distantes. C'est une situation courante pour les workflows de branche de fonctionnalité qui pushent les branches de fonctionnalité vers un dépôt distant (p. ex. à des fins de sauvegarde). Les branches de fonctionnalité distantes sont toujours situées dans le dépôt distant après avoir été supprimées du dépôt local, donc vous obtiendrez une génération des branches de fonctionnalité mortes à mesure que votre projet évolue. Vous pouvez les supprimer en pushant une refspec avec un paramètre <src> vide, comme suit :

git push origin :some-feature

C'est très pratique, car vous n'avez pas besoin de vous connecter à votre dépôt distant et de supprimer manuellement la branche distante. Notez que depuis la version 1.7.0 de Git, vous pouvez utiliser le flag --delete plutôt que la méthode ci-dessus. Le code suivant aura le même effet que la commande ci-dessus :

git push origin --delete some-feature

En ajoutant quelques lignes dans le fichier de configuration Git, vous pouvez utiliser les refspecs pour modifier le comportement de git fetch. Par défaut, git fetch fetche toutes les branches du dépôt distant. La section suivante du fichier .git/config explique ce comportement :

[remote "origin"]
url = https://git@github.com:mary/example-repo.git
fetch = +refs/heads/*:refs/remotes/origin/*

La ligne fetch demande à git fetch de télécharger toutes les branches du dépôt origin. Cependant, certains workflows n'ont pas besoin de toutes les branches. Par exemple, de nombreux workflows d'intégration continue s'appuient uniquement sur la branche master. Pour récupérer uniquement la branche master, modifiez la ligne fetch pour correspondre à ce qui suit :

[remote "origin"]
url = https://git@github.com:mary/example-repo.git
fetch = +refs/heads/master:refs/remotes/origin/master

Vous pouvez aussi configurer git push de façon similaire. Par exemple, si vous souhaitez toujours pusher la branche master vers qa-master dans l'origine distante (comme nous l'avons fait auparavant), vous devrez modifier le fichier de configuration comme suit :

[remote "origin"]
url = https://git@github.com:mary/example-repo.git
fetch = +refs/heads/master:refs/remotes/origin/master
push = refs/heads/master:refs/heads/qa-master

Les refspecs vous donnent un contrôle total sur la manière dont les différentes commandes Git transfèrent les branches entre les dépôts. Elles vous permettent de renommer et de supprimer des branches dans votre dépôt local, de récupérer/pusher vers des branches avec des noms différents et de renommer git push et git fetch pour ne fonctionner qu'avec les branches de votre choix.

Réfs relatives

Vous pouvez également référencer des commits en fonction d'un autre commit. Le caractère ~ vous permet d'atteindre les commits parents. Par exemple le code suivant affiche le grand-parent de HEAD :

git show HEAD~2

Néanmoins, lorsque vous travaillez avec des commits de merge, les choses deviennent un peu plus compliquées. Les commits de merge ayant plusieurs parents, vous pouvez suivre différents chemins. Dans le cas des merges à trois branches, le premier parent est celui de la branche où vous étiez lorsque vous avez fait le merge et le second parent est celui de la branche que vous avez passée à la commande git merge.

Le caractère ~ suit toujours le premier parent d'un commit de merge. Si vous voulez suivre un autre parent, vous devez le spécifier avec le caractère ^. Par exemple, si HEAD est un commit de merge, le code suivant renvoie le second parent de HEAD.

git show HEAD^2

Vous pouvez utiliser plusieurs caractères ^ pour déplacer plusieurs générations. Par exemple, ce code affiche le grand-parent de HEAD (en supposant qu'il s'agit d'un commit de merge) qui est situé sur le second parent.

git show HEAD^2^1

Illustrant le fonctionnement de ~ et ^, la figure suivante montre comment accéder à n'importe quel commit depuis le point A en utilisant des références relatives. Dans certains cas, il existe plusieurs chemins pour atteindre un commit.

Accéder aux commits à l'aide de réfs relatives

Les réfs relatives peuvent être utilisées avec les mêmes commandes qu'une réf normale. Par exemple, toutes les commandes suivantes utilisent une référence relative :

# Répertoriez uniquement les commits parents du deuxième parent d'un commit de merge
git log HEAD^2

# Supprimez les 3 derniers commits de la branche courante
git reset HEAD~3

# EffectueZ un rebase interactif des 3 derniers commits sur la branche courante
git rebase -i HEAD~3

Reflog

The reflog is Git’s safety net. It records almost every change you make in your repository, regardless of whether you committed a snapshot or not. You can think of it as a chronological history of everything you’ve done in your local repo. To view the reflog, run the git reflog command. It should output something that looks like the following:

400e4b7 HEAD@{0}: checkout: moving from master to HEAD~2
0e25143 HEAD@{1}: commit (amend): Integrate some awesome feature into `master`
00f5425 HEAD@{2}: commit (merge): Merge branch ';feature';
ad8621a HEAD@{3}: commit: Terminer la fonctionnalité

Ceci peut se traduire comme suit :

  • Vous venez d'extraire HEAD~2.
  • Avant cette opération, vous avez modifié un message de commit.
  • Auparavant, vous avez fait un merge de la branche de fonctionnalité dans la branche master.
  • Avant de faire un commit d'un instantané

La syntaxe HEAD{<n>} vous permet de référencer les commits stockés dans le reflog. Son fonctionnement ressemble beaucoup à celui des références HEAD~<n> dans la section précédente, mais le <n> renvoie à une entrée du reflog au lieu de l'historique des commits.

Vous pouvez l'utiliser pour restaurer l'état qui, autrement, serait perdu. Supposons pas exemple que vous venez de supprimer une nouvelle fonctionnalité avec git reset. Votre reflog devrait ressembler à ce qui suit :

ad8621a HEAD@{0}: reset: Passer à HEAD~3
298eb9f HEAD@{1}: commit: Un autre message de commit
bbe9012 HEAD@{2}: commit: Continuer la fonctionnalité
9cb79fa HEAD@{3}: commit: Démarrer une nouvelle fonctionnalité

Les trois commits avant git reset sont désormais libres (dangling), ce qui signifie qu'ils ne peuvent pas être référencés à moins d'utiliser le reflog. Vous vous rendez ensuite compte que vous n'auriez pas dû supprimer tout votre travail. Dans ce cas, il vous suffit d'extraire le commit HEAD@{1} pour restaurer l'état de votre dépôt avant l'exécution de git reset.

git checkout HEAD@{1}

Vous passerez ensuite à l'état HEAD détaché. Vous pouvez créer une nouvelle branche et continuer à travailler sur votre fonctionnalité.

Summary

Vous êtes désormais familiarisé avec le référencement des commits dans un dépôt Git. Nous avons appris comment les branches et les tags sont stockés en tant que réfs dans le sous-répertoire .git, comment lire un fichier packed-refs, comment HEAD est représenté, comment utiliser les refspecs pour faire un push ou un fetch, et comment utiliser les opérateurs relatifs ~ et ^ pour traverser une hiérarchie de branche.

Nous avons également examiné le reflog, lequel permet de référencer des commits qui ne sont pas disponibles d'une autre manière. C'est une excellente méthode pour se sortir des situations où l'on se dit : « Oups, je n'aurais pas dû faire ça ».

L'objectif étant que vous soyez capable de sélectionner le commit dont vous avez besoin dans un scénario de développement donné. Il est très facile d'exploiter les compétences que vous avez acquises dans cet article avec vos connaissances Git existantes, car certaines des commandes les plus courantes acceptent les réfs comme arguments, notamment git log, git show, git checkout, git reset, git revert, git rebase, et bien d'autres.