Git refs: An overview
Découvrez les bases de Git avec ce tutoriel sur le thème de l'espace.
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 Date: Wed Jul 9 16:37:42 2014 -0500 Some commit message
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 main
:
Ressource connexe
Comment déplacer un dépôt Git complet
DÉCOUVRIR LA SOLUTION
Découvrir Git avec Bitbucket Cloud
git rev-parse main
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/ main some-feature remotes/ origin/ main 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 :
# Output the contents of `refs/heads/main` file: cat .git/refs/heads/main # Inspect the commit at the tip of the `main` branch: git log -1 main
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 main
, Git doit simplement changer le contenu du fichier refs/heads/main
. De la même manière, il suffit d'écrire une empreinte de commit dans un nouveau fichier pour créer une 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.
Specifying refs
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.
Packed refs
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/main 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.
Special refs
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 fichierHEAD
avant les changements radicaux effectués.MERGE_HEAD
: le(s) commit(s) que vous mergez dans la branche courante avecgit 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 main
:
git checkout main cat .git/HEAD
Cela génère ref: refs/heads/main
, ce qui signifie que HEAD
pointe vers la réf refs/heads/main
. C'est de cette manière que Git détermine la branche main
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 sous la forme suivante : [+]
<src>
:
<dst>
. Le paramètre <src>
correspond à la branche source dans le dépôt local et le paramètre <dst>
à la branche cible 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 main
vers le dépôt distant origin
comme un git push
ordinaire , mais elle utilise qa-main
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 main:refs/heads/qa-main
You can also use refspecs for deleting remote branches. This is a common situation for feature-branch workflows that push the feature branches to a remote repo (e.g., for backup purposes). The remote feature branches still reside in the remote repo after they are deleted from the local repo, so you get a build-up of dead feature branches as your project progresses. You can delete them by pushing a refspec that has an empty parameter, like so:
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 main
. Pour récupérer uniquement la branche main
, modifiez la ligne fetch
pour correspondre à ce qui suit :
[remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/main:refs/remotes/origin/main
Vous pouvez aussi configurer git push
de façon similaire. Par exemple, si vous souhaitez toujours pusher la branche main
vers qa-main
dans le remote origin
(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/main:refs/remotes/origin/main push = refs/heads/main:refs/heads/qa-main
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.
Relative refs
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.
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 :
# Only list commits that are parent of the second parent of a merge commit git log HEAD^2 # Remove the last 3 commits from the current branch git reset HEAD~3 # Interactively rebase the last 3 commits on the current branch git rebase -i HEAD~3
The reflog
Le reflog est le filet de sécurité de Git. Il enregistre chacun des changements que vous effectuez dans votre dépôt, que vous ayez commité un instantané ou non. Vous pouvez le considérer comme un historique chronologique de toutes vos actions dans le dépôt local. Pour afficher le reflog, exécutez la commande git reflog
. Celle-ci devrait générer quelque chose de similaire à ce qui suit :
400e4b7 HEAD@{0}: checkout: moving from main to HEAD~2 0e25143 HEAD@{1}: commit (amend): Integrate some awesome feature into `main` 00f5425 HEAD@{2}: commit (merge): Merge branch ';feature'; ad8621a HEAD@{3}: commit: Finish the feature
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
feature
dans la branchemain
. - Avant de faire un commit d'un instantané
La syntaxe HEAD{
vous permet de référencer les commits stockés dans le reflog. Son fonctionnement ressemble beaucoup à celui des références HEAD~
dans la section précédente, mais le
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: moving to HEAD~3 298eb9f HEAD@{1}: commit: Some other commit message bbe9012 HEAD@{2}: commit: Continue the feature 9cb79fa HEAD@{3}: commit: Start a new feature
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é.
Résumé
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.
Partager cet article
Thème suivant
Lectures recommandées
Ajoutez ces ressources à vos favoris pour en savoir plus sur les types d'équipes DevOps, ou pour les mises à jour continues de DevOps chez Atlassian.