Les branches : de simples étiquettes
Par Maxime Bréhin • Publié le 3 juin 2022
• 5 min
Tu viens de l’univers SVN ou équivalent et tu as peur de faire des branches ? Ou tu découvres seulement la gestion de versions et tu souhaites apprendre comment gérer tes branches avec Git ? Cet article est fait pour toi !
Je t’explique ici ce que sont les branches, la facilité avec laquelle on peut les manipuler, et pourquoi on peut les créer et les supprimer sans crainte.
Cet article fait partie de notre série sur le glossaire Git.
Tu préfères une vidéo ?
Si tu es du genre à préférer regarder que lire pour apprendre, on a pensé à toi :
Une simple étiquette
On a vu dans l’article sur les objets Git qu’un historique est constitué de commits que se référencent à la chaîne.
Ici, c2 pointe sur son commit parent c1 qui pointe sur son commit parent c0 :
c0 <- c1 <- c2
La notion de branche n’est qu’un complément à cela pour permettre de désigner un emplacement en lui attribuant un nom. On appose alors une étiquette sur le dernier commit (le plus récent) de l’historique censé représenter la branche. En d’autres termes, une branche n’est qu’un pointeur sur un commit.
Ici la branche dev
pointe sur le commit c2 et désigne donc implicitement la “chaîne” jusqu’à c0 :
dev
/
c0 <- c1 <- c2
Ce qu’on appelle alors la branche est la cascade des références de commits (jusqu’au commit racine, qui n’a plus de commit parent).
Ces étiquettes ont la particularité d’évoluer lorsqu’on produit de nouveaux commits alors qu’elles constituent la branche active, ou lorsqu’on souhaite les déplacer ailleurs (annulation de commits etc.). Elles servent également à indiquer au garbage collector les chaînes de commits à préserver (ça fonctionne aussi avec les tags). Enfin, elles peuvent aussi servir au partage via les dépôts distants.
Dans le diagramme ci-après, le commit c3 a été produit depuis l’état de c2. Il pointe donc sur son parent c2 et l’étiquette de branche dev
a évolué pour le référencer :
dev
/
c0 <- c1 <- c2 <- c3
Performance et tranquillité
Ce fonctionnement par référence est extrêmement pratique à bien des égards :
- la performance est accrue : on ne manipule que des pointeurs ;
- on peut garder l’esprit tranquille quand on effectue nos opérations : jamais un commit ou objet Git ne sera supprimé, au pire il sera déréférencé.
La création d’une branche ne représente rien de plus que la création d’un fichier unique sur le disque. La branche est disponible localement et pourra être partagée plus tard (ou pas, si on veut réaliser un travail « privé »).
Le renommage d’une branche signifie uniquement le renommage de son fichier descripteur.
La suppression d’une branche consiste en la suppression de ce fichier, donc du pointeur, mais en aucun cas des commits qu’elle désignait. Là aussi, c’est du coup très rapide. On a en prime la capacité de récupérer un historique potentiellement perdu (déréférencé / invisible dans le log classique).
Pour donner un contre-exemple, lorsqu’on demande à SVN de créer une branche, il va (sur le serveur SVN) dupliquer l’intégralité de l’historique et des fichiers désignés et les affecter à cette branche. On est donc soumis à la latence réseau en plus d’une duplication encombrante. Il en va de même pour la suppression et les autres manipulations.
Cycle de vie d’une branche : les cernes des branches
Comme dans nos articles sur les objets Git et HEAD je trouve intéressant de regarder sous le capot la manière dont sont traitées nos branches pour mieux les comprendre.
(En botanique, les cernes sont les cercles / anneaux de croisance d’un arbre 🌳.)
Initialisation d’un projet
À l’initialisation d’un projet, aucun commit ni branche n’existe. Une branche par défaut sera créée au premier commit. Par convention cette branche s’appelle main
(auparavant master
, ce renommage ayant eu lieu suite au mouvement Black Lives Matter).
On trouve cette référence de branche active / courante via notre HEAD :
# On regarde le contenu du fichier HEAD avec la commande `cat`
> cat .git/HEAD
ref: refs/heads/main
Les fichiers des étiquettes de branches sont stockés dans le répertoire .git/refs/heads/
et portent le nom des branches. Pour l’instant ce répertoire est vide car je n’ai rien dans le projet.
Si je crée un commit, j’observe l’apparition du fichier .git/refs/heads/main
. Et si je regarde son contenu je m’aperçois qu’il ne contient qu’une ligne de 40 caractères : un identifiant de commit (SHA-1).
> cat .git/refs/heads/main
377758e1b6a31850f0307abb1ac5f039da53e17f
Évolution d’une branche
Si je continue et réalise d’autres commits, je constate que ce fichier enregistre la référence du dernier commit produit :
> git commit …
> cat .git/refs/heads/main
9c118f78bc49f1be85085e708e61503a9d787c51
> git commit …
> cat .git/refs/heads/main
dd4d06db3420782a6b63fa5a067d82d42af9ec7d
Créer une autre branche
Puisque les branches ne sont que de simples étiquettes, rien ne m’empêche d’en créer autant que je veux, où je veux. Généralement je crée une branche depuis mon emplacement actuel pour démarrer un travail spécifique :
# Soit en 2 temps, création, "activation"
git branch feat/specific-stuff
git switch feat/specific-stuff
# Soit les 2 en un
git switch --create=feat/specific-stuff
J’ai délibérément choisi ce nom avec un séparateur slash /
pour te montrer que dans le répertoire .git/refs/heads/
on obtient un sous-répertoire feat/
qui contient un fichier specific-stuff
. Ce type de nommage me permet de regrouper mes branches par sous-ensembles thématiques mais me contraint par la même occasion en m’empêchant de créer une branche feat
. Je trouve cette approche avantageuse car elle nous évite de partir dans les méandres des schémas de branches trop profonds. Si le sujet du nommage des branches t’intéresse, je t’invite à lire cet autre article.
Revenons à nos moutons : notre branche créée possède la même référence que notre branche main
, à savoir le dernier commit produit dd4d06db3420782a6b63fa5a067d82d42af9ec7d
. J’ai précisé au-dessus l’avoir activée. Ça signifie que HEAD pointe dessus et que si je réalise de nouveaux commits, cette nouvelle branche évoluera, mais pas main
.
Renommer une branche
Admettons que ma branche feat/specific-stuff
aurait dû s’appeler feat/stuff
. Je peux changer son nom comme suit :
git branch -m feat/specific-stuff feat/stuff
Supprimer une branche
Supprimer une branche signifie supprimer une étiquette, pas les commits. Ceux-ci seront déréférencés, sauf si une autre branche (ou étiquette) désigne un historique contenant ma branche.
L’opération de suppression consiste à supprimer le fichier de pointeur localement, c’est donc très rapide :
git branch -d branch-name
Git possède un garde-fou très utile puisqu’il nous empêche normalement de supprimer une étiquette de branche si les commits qu’elle désigne risquent d’etre déréférencés 😮💨. Il nous affichera alors un message nous expliquant que la branche n’est pas fusionnée et qu’on va « perdre » des commits, nous proposant de forcer éventuellement cette opération (git branch -D branch-name
ou git branch -d -f branch-name
). Et tant qu’à faire, il nous interdira toujours de supprimer la branche sur laquelle on se trouve (bûcheron suicidaire, passe ton chemin 🪚🪵).
Fusionner des branches
La notion de fusion consiste généralement à marquer la fin d’un travail. Pour ce qui est des branches, la fusion implique de faire évoluer une branche en intégrant le travail d’une autre branche. On fait donc évoluer une des branches quand l’autre reste à sa place.
Prenons une branche main
et une branche dev
dont on vient de terminer le travail et qu’on souhaite intégrer à main
:
main <- HEAD
/
*--*--*
\
*--*
\
dev
Une fois la fusion réalisée (avec la branche main
active, on git merge dev
), seule l’étiquette main
a changé :
main <- HEAD
/
*--*--*---*
\ /
*--*
\
dev
La fusion est un vaste sujet qui demande un article dédié. Ce qu’il faut retenir, c’est qu’au niveau des branches ça représente une fois de plus un simple déplacement d’étiquette.
Résumons tout ça
Tu l’as compris, les branches sont faciles à utiliser. Les opérations se faisant à 99% localement (toutes sauf les interactions de partage, en fait), la performance est optimale. Cerise sur le gâteau : on a la garantie, même en cas de mauvaise manipulation, de ne rien perdre réellement. Bon, après, il va falloir travailler sur ta capacité à retrouver ce que tu as perdu, principalement avec le reflog.