Chapitre 4 sur 13

La puissance des branches Git

Laisser un commentaire

Les branches sont présentes dans la plupart des systèmes de versioning et constituent une notion centrale de Git. Elles offrent une très grande souplesse de développement en permettant d’aller et venir entre différentes versions d’un projet, de faire des tests et de collaborer.

Git encourage le travail avec les branches en rendant instantané la création, la fusion et la suppression de branches. Par conséquent, bien comprendre et maîtriser la gestion des branches est indispensable à un usage efficace de Git.

Ré-introduction aux branches

Nous avons déjà rapidement vu l’usage des branches dans les commandes indispensables de Git. Nous allons ici nous attacher à bien comprendre leur fonctionnement avant d’aller plus loin dans leur usage. Comprendre comment foncrtionne cet outil nous permettra par la suite d’en avoir un usage éclairé.

Dans le premier chapitre, nous avons expliqué que Git est un système de fichiers stockant le contenu d’un répertoire en trois types : blob, tree et commit ; chacun étant référencé par une emprunte SHA-1.

Chaque état successif, les commits, sont représentés par un état à un instant t, un snapshot. Git ne stocke par per se les modifications effectuées, mais l’ensemble des éléments constituants un instantané. Si entre deux instantané certains éléments ne sont pas modifés, cela ne change rien pour Git, il stocke simplement les références des éléments du projet.

Données des commits
On visualise ici la structure de plusieurs commits

Ainsi, si un fichier est dans le même état entre dix commits, son emprunte SHA-1 sera la même pour ces dix commits. Chaque fichier et chaque répertoire dans un état t est consultable via son emprunte SHA-1.

Instantanés de Git
Git ne stocke que des références vers les fichiers, aucune donnée n'est dupliquée

HEAD le pointeur de Git

Afin de savoir ce que le répertoire de travail doit refléter, Git maintient un pointeur nommé HEAD. Dans Git, les pointeurs s’appellent références ou refs. Ces pointeurs sont stockés dans le répertoire .git/refs.

ls .git/refs
heads    remotes    tags

On constate qu’il existe trois types de références :

L’objet HEAD de Git est donc un pointeur vers une de ces références. Lorsque l’on change de branche, Git sait quel est l’état à restaurer en allant chercher la référence vers l’état souhaité dans heads, remotes ou tags. Lorsque l’on fait git switch feature_branch, HEAD pointera vers .git/refs/heads/feature_branch.

HEAD est ainsi une référence vers une autre référence. On le vérifie facilement en affichant le contenu de HEAD.

# On est ici sur la branche master, peut importe le projet
cat .git/HEAD
ref: refs/heads/master

# refs/heads/master contient une référence SHA-1
cat .git/refs/heads/master
1f80958ea9be7064f735b0d9c7c50993ff198aa0

Il y a un seul cas dans lequel HEAD contient directement une emprunte SHA-1 et non une référence, c’est lorsque Git est dans un état nommé detached HEAD. Dans cet état, HEAD ne pointe pas vers un objet référencé tel qu’une branche ou un tag, mais vers un commit en particulier.

Cette possibilité nous permet de revenir à l’état de n’importe quel commit, d’observer l’état des choses à cet instant et de restaurer ou apporter des modifications à partir de ce point. Nous verrons cela plus loin dans ce chapitre.

Créer et naviguer entre les branches

Nous l’avons vu dans le chapitre sur les commandes indispensables, créer une nouvelle branche est assez aisé. Lorsque l’on créé une branche, cette dernière est toujours une copie de la branche depuis laquelle elle est créée. Ainsi, si l’on créé feature depuis la branche master, les deux branches sont en tout point identiques jusqu’à ce qu’une des deux soit modifiée.

Schéma montrant deux branches distrinctes

On le voit dans le schéma ci-dessus, la branche nouvellement créée est une copie exacte de la branche source, mais leurs existances sont par la suite totalement indépendantes.

git branch <branchname>

Cette commande ne nous place pas sur la branche en question, il faut donc ensuite utiliser switch pour aller sur la branche.

git switch <branchname>

Lorsque l’on veut créer une branche et directement s’y placer, avoir à taper deux commandes successives est fastidieux. Avec switch, il est possible de créer la branche et s’y déplacer en une seule fois.

# L'option -c, --create dans sa version longue
git switch -c <branchname>

Par défaut, la branche est créée à partir de la branche courante. Toutefois, il est possible de préciser la source depuis laquelle devrait être créée la nouvelle branche. Cela peut être une autre branche, un tag ou un commit en particulier.

git branch <branchname> <source>

Afficher les branches

Nous l’avons déjà abordé, dans sa forme la plus simple, afficher les branches se résume à git branch. Cependant, on peut faire bien plus. De nouveau, on prend pour exemple le repo Git emojis hook, dans lequel j’ai créé la branche hooks-distribution.

# Afficher simplement les branches et indique la branche courante
git branch
  hooks-distribution
* master

# Afficher les branches avec la référence du dernier commit et le message de commit
git branch -v
  hooks-distribution 1f80958 💄 update licence year
* master             1f80958 💄 update licence year

# Afficher en plus l'association avec les branches distantes
git branch -vv
  hooks-distribution 1f80958 💄 update licence year
* master             1f80958 [origin/master] 💄 update licence year

Les deux commandes verbeuses nous indiquent que les deux branches sont au même niveau, elles partagent leur dernier commit 1f80958. Par ailleurs, la dernière commande nous indique immédiatement que hooks-distribution ne suit pas une branche distante. On en déduit qu’elle n’a pas encore été poussée. Dans le cas où des modifications ont été faites localement ou sur la branche distante, la version -vv indiquerait que la locale est en avance sur la distante ou inversement.

Vous pouvez également afficher les branches distantes avec l’option -r ou -a. Ces deux options ont respectivement pour versions longues --remotes et --all. La première n’affiche que les branches distantes tandis que la seconde affiche à la fois les branches locales et distantes.

git branch -r
  origin/HEAD -> origin/master
  origin/master

Par ailleurs, lorsque vous avez beaucoup de branches, une option assez intéressante qui fonctionne aussi bien pour les branches locales que distantes est --list. Passée sans argument, cette option n’apporte rien. Cependant, elle permet de filtrer les branches via les patterns classiques.

# On obtiendra ici la liste de toutes les branches ayant fix- comme préfixe
git branch -vv --list 'fix-*'

Enfin, quelques autres options s’avèrent fréquemment utiles :

Renommer une branche

Il arrive qu’on veuille renommer une branche en cours de route. Trouver un nom correct est toujours un excercice difficile et seuls les imbéciles ne changent pas d’avis. On utilisera la commande branch pour cette tâche.

git branch -m [<oldname>] <newname>

L’ancien nom est facultatif si l’on souhaite renommer la branche courante.

Supprimer une branche

Nous l’avons déjà rapidement évoqué, pour supprimer une branche, il faut exécuter branch -d ou l’option --delete dans sa version longue. En outre, il faut être sur une branche différente de la branche à supprimer.

git branch -d <branch>

Toutefois, cette option refusera de s’exécuter si la branche à effacer n’est pas soit mergée dans la branche courante, soit pushée dans son upstream. Il est malgré tout possible de forcer Git à supprimer ladite branche.

Il faudra pour cela passer l’option --force en plus de --delete ou l’option -D à la place de -d, laquelle est un raccourci pour --delete --force.

De plus, il est possible que vous ayez une ou plusieurs branches de suivi n’ayant aucun équivalent local. Ce sont des branches récupérées par git fetch ou lors du clone initial, mais que vous n’avez jamais utilisées localement. Il faut pour cela spécifiquement indiquer à Git que vous désirez supprimer une tracking branch.

# L'option -r ou --remote dans sa version longue
git branch -rd <remote>/<branch>

Cette commande ne supprime que la branche de tracking locale, pas la branche distante. Si cette dernière existe toujours sur l’upstream, elle sera recréée au prochain fetch ou pull, à moins que vous ne récupériez expressément que certaines branches ou que vous ne configuriez votre projet pour ne suivre que certaines branches.

Fusionner des branches

Lors du travail avec les branches, il arrive un moment où l’on souhaite que le travail réalisé sur la branche intègre ou ré-intègre une autre branche. Plusieurs cas sont possibles selon les divergeances entre les branches.

Nous l’avons mentionné plusieurs fois, lors de la création d’une branche, elle est en tout point semblable à la branche depuis laquelle elle a été créée. Dans ce cas, il n’y a rien à fusionner, elles sont déjà identiques.

Schéma montrant deux branches pointants vers le même commit

Cela peut être le cas lorsque rien n’a été commité sur l’une ou l’autre des branches depuis la création de la branche, mais aussi si un merge a déjà été effectué, ou si une branche tierce a été mergée dans les deux branches, ou encore si les mêmes commits ont été appliqués sur les deux branches. De multiples raisons peuvent mener à cet état. Les branches sont déjà identiques, il n’y a donc rien à faire.

Lorsque seulement une des branches possède des commits que l’autre n’a pas, il faut alors répercuter ces commits sur la branche cible. Ce type de merge s’appelle fast-forward, c’est à dire qu’il suffit d’appliquer les commits présents sur l’une pour que les deux soient à nouveau semblables. Pour qu’un merge soit fast-forwardable, il faut que la branche la plus avancée ait un ancêtre direct avec la branche cible de la fusion.

Schéma montrant deux branches compatibles avec un merge fast forward

Dans ce schéma, le commit “C” de la branche master est un ancêtre direct de la branche hooks-distribution car il s’agit du dernier commit de master et qu’il est égalemement présent dans l’historique de hooks-distribution.

Maintenant, j’effectue des modifications directement sur la master. On se retrouve dans une situation où master et hooks-distribution n’ont plus d’ancêtre commun direct.

Schéma montrant deux branches compatibles avec un merge fast forward

Les deux branches ont toujours des ancêtres communs (A, B et C sont présents dans les deux branches), mais ils ne sont plus directs. Dans ce cas, Git procèdera à une fusion de type three-way merge. Ce type de fusion utilise trois versions d’un fichier pour effectuer le merge : la version originale, la version A et la version B.

La version A est la version dans laquelle on veut merger, on se réfère en général aux modifications de cette version en tant que Ours, nos modifications, tandis que la version à merger, la B, constitue les modifications nommées Theirs, les leurs. Cela indépendamment de l’auteur réel des modifications, qui peut être la même personne.

Dans le cas d’un two-way merge, le VCS fait un diff et constate des différences entre les deux fichiers, il ne peut pas en déduire lesquelles sont à intégrer et lesquelles sont à rejeter. En comparant les versions A et B avec la version originale – cela se fait en trouvant l’ancêtre le plus proche commun aux deux versions – on peut déduire ce qui est à intégrer ou non.

Prenons un exemple : la version de base est un fichier texte contenant 10 lignes. La version A supprime la première ligne, tandis que la version B ajoute une ligne à la fin. En comparant A et B à la base, Git sait alors que la version fusionnée aura la première ligne de supprimée ainsi qu’une ligne supplémentaire.

Utiliser le merge

Pour illustrer ce que nous venons d’aborder, nous allons travailler avec le dépôt Git emojis hook. À ce stade, le dépôt contient deux branches – master et hooks-distribution – lesquelles sont au même niveau.

J’ai besoin de mettre à jour l’année dans LICENCE.md. Je créé donc une branche licence, effectue mes modifications et fusionne cette dernière avec la master lorsque j’ai terminé.

# On utilise switch pour créer et se placer en un coup sur "licence"
git switch -c licence
Switched to a new branch 'licence'

# On effectue nos modifications et commite ces dernières
# On saute ici la case indexation et passe directement le msg de commit (options -a et -m)
# :docs: est remplacé par 📖, c'est tout l'objet de Git emojis hook !
git commit -am ":docs: update licence year"
[licence 6fb75bc] 📖 update licence year
 1 file changed, 1 insertion(+), 1 deletion(-)

# C'est parti pour le merge
# On se replace d'abord sur la branche cible
git switch master
Switched to branch 'master'
Your branch is up to date with 'origin/master'

# On invoque ensuite merge
git merge licence
Updating 1f80958..6fb75bc
Fast-forward
 LICENCE.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

Sans suprise, Git nous indique ici que le merge est fast-forward. La modification est simplissime. J’ai ici utilisé une branche pour l’exemple, mais dans un usage courant, on effectuerait ce genre de modification directement sur la master, sans passer par une branche intermédiaire. Enfin, comme nous n’avons plus besoin de licence, on la supprime.

git branch -d licence
Deleted branch licence (was 6fb75bc).

# Git indique que notre branche locale est en avance sur master
git branch -vv
 hooks-distribution 0d8ba1b 📖 add hooksPath information
*  master             6fb75bc [origin/master: ahead 1] 📖 update licence year

Nous allons maintenant modifier le README.md afin d’ajouter des indications sur la mise en place des hooks. Je procède donc aux modifications adéquates et je commite ces dernières.

# Les modifications ont été effectutées
# On affiche l'historique de notre projet avec git log
# On utilise l'option --all pour voir toutes les branches
# Et l'option --graph pour visualiser la structure des branches
git log --oneline --graph --all
* 0d8ba1b (HEAD -> hooks-distribution) 📖 add hooksPath information
| * 6fb75bc (master) 📖 update licence year
|/
* 1f80958 (origin/master, origin/HEAD) 💄 update licence year

Cette visualisation du log avec --graph nous montre clairement que les deux branches ont subi des modifications et l’état d’avancement de chacune d’elles :

# De nouveau on s'assure d'être sur la bonne branche
# Le git branch nous confirme que les pointeurs de nos deux branches sont à des commits différents
git branch -vv
  hooks-distribution 0d8ba1b 📖 add hooksPath information
* master             6fb75bc [origin/master: ahead 1] 📖 update licence year

# On lance le merge
git merge hooks-distribution

# Comme le merge n'est pas fast-forward
# Git va lancer un éditeur de texte pour saisir un message de merge
Merge branch 'hooks-distribution'
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

# Après validation du message
Merge made by the 'recursive' strategy.
 README.md | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

On peut de nouveau contrôler nos branches et notre historique, ils reflètent le merge.

# Les deux banches sont à des commits différents
# La master possède en effet le commit de merge que la branche source n'a pas
git branch -vv
  hooks-distribution 0d8ba1b 📖 add hooksPath information
* master             d01b8cf [origin/master: ahead 3] Merge branch 'hooks-distribution'

# On peut de nouveau afficher le log afin d'en voir d'avantage
git log --oneline --all --graph
*   d01b8cf (HEAD -> master) Merge branch 'hooks-distribution'
|\
| * 0d8ba1b (hooks-distribution) 📖 add hooksPath information
* | 6fb75bc 📖 update licence year
|/
* 1f80958 (origin/master, origin/HEAD) 💄 update licence year

Le “graphique” nous montre cette fois-ci clairement que le commit 0d8ba1b est fusionné avec la master. On peut donc procéder à la suppression de la branche mergée s’il n’y a rien d’autre que l’on veut y faire.

Annuler ou éditer un merge

Si l’on veut annuler le merge, après avoir commenté la première ligne dans l’éditeur, il suffira de relancer merge avec l’option --abort. Git nous replace alors dans l’état d’avant merge.

Dans le cas où on n’entre aucun message de merge, tant que git merge --abort n’a pas été exécutée, toutes les modifications sont présentes dans l’index et peuvent être commitées. Il faudra alors entrer un message de commit (qui est un merge commit). Cela nous laisse l’opportunité d’inspecter et/ou amender les modifications avant de les valider.

Ce comportement peut également être invoqué directement avec l’option --no-commit. Si vous souhaitez sauter l’étape de l’éditeur, deux solutions s’offrent à vous :

Le merge commit, c’est pas automatique

Vous avez certainement remarqué que Git vous demande d’entrer un message pour le commit de merge seulement quand ce dernier n’est pas fast-forward. Il est possible d’indiquer à merge que vous souhaitez entrer un message de commit dans tous les cas, il s’agit de l’option --no-ff.

L’usage du message de commit a ses avantages et ses inconvénients, nous y reviendrons en détails dans un prochain chapitre.

Résolution des conflits

Il y a des cas dans lesquels Git ne sait pas définir de lui-même quelles modifications il doit appliquer. C’est ce qui arrive lorsqu’un ou plusieurs fichiers sont modifiés au(x) même(s) endroit(s). Dans cette situation, il est impossible pour Git de savoir quelles sont les modifications à garder et quelles sont les modifications à jeter. La stratégie du three-way merge n’est d’aucun secours.

Lorsqu’il se retrouve face à cette situation, Git n’a d’autre choix que de vous demander de résoudre vous-même les conflits. Pour illustrer cela, nous allons modifier le README.md au même endroit, à la fois dans master et dans une branche test.

# On visualise nos deux branches avec chacune des modifs
# L'option -p permet de voir les modifications
git log --oneline --all -p
8187e6c (HEAD -> master) 💄 pluralize hook (hooks)
diff --git a/README.md b/README.md
index 76ac715..fdf8f23 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Git emojis hook
+# Git emojis hooks

 A simple git hook to provide strong guidelines for commit message with emojis.

a94eb54 (test) 💄 capitalize title
diff --git a/README.md b/README.md
index 76ac715..d50b473 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Git emojis hook
+# Git Emojis Hook

 A simple git hook to provide strong guidelines for commit message with emojis.

On constate donc que sur les deux branches, la première ligne de README.md a été modifiée. Voyons ce que donne le merge.

# Nous sommes sur la master
git merge test
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

Git nous indique l’échec du merge automatiue à cause de conflits que nous devons manuellement résoudre avant la poursuite du merge. À ce stade, nous pouvons toujours effectuer un git merge --abort afin de revenir à l’état d’avant commit.

Cependant, si nous voulons poursuivre, il faut résoudre les conflits, les indexer et finaliser la fusion.

# On commence par afficher les fichiers ayant des conflits
git status
On branch master
Your branch is ahead of 'origin/master' by 4 commits.
  (use "git push" to publish your local commits)

You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
    both modified:   README.md

no changes added to commit (use "git add" and/or "git commit -a")

Nous n’avons ici modifié qu’un seul fichier, il est donc normal de ne pas avoir de modifications à d’autres endroits. Il est néanmoins courant lors de fusions plus conséquentes d’avoir des conflits en plusieurs endroits dans plusieurs fichiers. Il suffit ensuite de se rendre dans les fichiers en question afin de visualiser les marqueurs de conflits.

cat README.md
<<<<<<< HEAD
# Git emojis hooks
=======
# Git Emojis Hook
>>>>>>> test

A simple git hook to provide strong guidelines for commit message with emojis.

Les conflits sont matérialisés par les lignes et flèches :

Lorsque la fusion est non triviale, il peut être avantageux de visualiser la version d’un fichier d’une ou de l’autre branche. Pour cela, on peut utiliser la commande restore.

Cette dernière commande, de même que checkout, permet de restaurer un ou plusieurs fichiers du répertoire de travail (ici dans l’état d’avant merge). L’option --theirs permet de restaurer le fichier tel qu’il est sur la branche source du merge tandis que l’option --ours permet de restaurer le fichier tel qu’il est sur la branche cible du merge.

git restore --ours README.md
cat README.md
# Git emojis hooks

A simple git hook to provide strong guidelines for commit message with emojis.

# Maintenant, faisons l'inverse
git restore --theirs README.md
cat README.md
# Git Emojis Hook

A simple git hook to provide strong guidelines for commit message with emojis.

Pour restaurer le fichier dans son état de fusion avec les marqueurs de merge, on utilisera de nouveau restore (ou checkout) avec l’option -m ou son équivalent en forme longue : --merge.

On choisit alors la ligne que l’on veut conserver (parfois les deux) et on efface les marqueurs de conflit pour indiquer à Git que ce dernier est résolu. On git add alors le ou les fichiers dont le ou les conflits sont résolus.

git add README.md

# Une fois que tous les conflits sont résolus
# On peut alors commiter de manière classique
# Ou utiliser l'option --continue
# On aura alors plus qu'à entrer notre message de merge commit
git merge --continue
Merge branch 'test'

# Conflicts:
#       README.md
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
#       .git/MERGE_HEAD
# and try again.


# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch master
# Your branch is ahead of 'origin/master' by 4 commits.
#   (use "git push" to publish your local commits)
#
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
#       modified:   README.md
#

Stratégies de fusion

Git possède de nombreuses stratégies permettant de fusionner une branche dans une autre. Nous n’allons pas toutes les détailler ici, vous pouvez vous référer à la doc[en] pour plus de détails.

La stratégie par défaut est la stratégie recursive. Cette stratégie permet d’effectuer des three-way merge tout en garantissant le moins de conflits possibles. Par ailleurs cette stratégie peut recevoir des paramètres lui indiquant comment résoudre automatiquement les conflits :

En complément, il y a parfois des modifications qui sont en réalité simplement des changements d’espaces : ajout d’espace, tabs vs spaces etc. Ces modifications peuvent se produire lorsque plusieurs développeurs travaillent avec différentes configurations qui modifient automatiquement les espaces des fichiers. Git possède alors deux options très intéressantes pour ces cas précis :

On passe l’option -s pour choisir une stratégie et l’option de celle-cli avec -X. Il n’est pas utile de spécifier la stratégie avec l’option -s lorsque l’on souhaite utiliser celle par défaut.

# Lorqu'un conflit de fusion semble être provoqué par les espaces
# On peut alors indiquer à Git de les ignorer
git merge -X ignore-space-change <branch>

Rebase, l’alternative au merge

Le rebase peut être vu comme une alternative au merge. Ces deux commandes ont pour objectif d’intégrer les modifications d’une branche à une autre. Nous l’avons vu, lorsque nous mergeons une branche dans une autre, tous les commits présents sur l’une sont ajoutés à l’autre via un seul commit de fusion.

illustration du merge

À l’inverse, le rebase redéfinit la base. Ainsi, lorsque l’on rebase la branche master sur la branche feature, Git rembobine cette dernière jusqu’au commit commun aux deux branches, puis il applique tous les commits présents sur master qui ne sont pas sur feature et enfin, les commits propres à feature sont ajoutés.

illustration du merge

La branche feature est comme déplacée à la pointe de la branche master. Cependant, les commits de la master ajoutés à la feature sont bien des nouveaux commits créés par Git, ils n’ont pas les mêmes empruntes SHA-1.

Le rebase réécrit l’historique car il ajoute des commits non pas après les commits déjà présents, mais entre le parent commun le plus proche et les commits propres à la branche rebasée. L’énorme avantage du rebase est qu’il offre un historique propre et linéaire.

En revanche, comme l’historique est modifié, cette méthode est à proscrire si les modifications sont déjà partagées sur un dépôt distant publique ou partagé. Git refusera d’ailleurs de pusher les modifications, à moins que vous n’utilisiez l’option -f, le --force.

historique modifié

Comme nous l’avons dit, l’historique est modifié et les commits sont différents, ils n’ont pas les mêmes empruntes. Si vous forcez le push, vous substituez en quelque sorte une version de la branche par une autre version. Les collaborateurs ayant cloné la branche avant que vous n’en modifiez l’historique et ayant intégré leurs modifications en local auront de grandes difficulté à fusionner avec la nouvelle version de la branche distante…

Dans une démarche collaborative, le rebase d’une branche partagée sur une branche privée locale permet d’éviter des commits de fusion et de conserver un historique propre et linéaire. On peut alors utiliser l’option --ff-only lors des merge et avoir recours à rebase si nécessaire. Nous reviendrons sur les différentes stratégies de collaboration dans un chapitre dédié.

# Intégrer les changements d'une branche sur la branche courante
git rebase <branch>

# Intégrer les changements d'une branche sur une autre
# Cela permet le rebase sans devoir se placer au préalable sur la branche cible souhaitée
git rebase <sourceBranch> <destBranch>

Rebase comme stratégie de fusion

Utiliser le merge comme stratégie lors du pull revient à créer un commit de fusion dès que la fusion n’est pas fast-forward. C’est à mon sens polluer l’historique avec des commits peu informatifs : savoir que vous avez fusionné une branche distante dans une locale est proche de zéro en terme d’info utile.

J’explique plus en détails la nécessité d’un historique propre et de l’utilité du rebase pour y parvenir dans un chapitre dédié. À mon sens, ce type de merge reflète une manipulation technique locale et devrait être évité.

Pour se faire, on peut tirer profit du rebase : git pull --rebase, c’est aussi simple que cela ! Si vous partagez mon avis et que vous souhaitez en faire votre stratégie par défaut, il suffit de configurer Git pour qu’il adopte ce comportement.

git config --global pull.rebase true

Cherry-pick

La commande cherry-pick permet d’appliquer n’importe quel commit depuis une branche vers une autre. Cette commande possède de nombreux cas d’usages : appliquer une modification commitée sur une mauvaise branche, récupérer un bugfix depuis une branche pour l’appliquer à une autre…

Cette commande s’avère très utile dans les cas où une branche ne sera finalement pas intégalement fusionnée dans une autre ou afin de restaurer un commit perdu. Toutefois, cherry-pick n’est pas la solution à tous les problèmes et il vaut souvent mieux utiliser merge ou rebase.

# cherry pick prend en paramètre l'emprunte du commit
# on peut aussi lui passer une référence (branche, HEAD…)
git cherry-pick <sourche-sha1> [<dest-sha1>]

cherry-pick peut intégrer plusieurs commits à la fois, il faut pour cela lui passer plusieurs réfrences. Comme pour merge, on peut passer quelques options à cherry-pick :

Mettre de côté ses modifications

git stash permet de sauvegarder nos modifications sans les commiter. C’est très pratique lorsque nous devons exécuter une action qui nécessite un répertoire de travail propre, ou que nous souhaitons temporairement revenir à notre dernier état propre pour travailler sur autre chose par exemple.

Lorsque nous sommes en plein milieu d’un travail, nous ne souhaitons pas forcément commiter car notre travail n’est pas terminé et donc imporpre à un commit dans les règles de l’art.

Sauvegarder ses modifications

# Toujours dans notre répo Git emojis
# Nous avons effectué des modifications non commitées
git status
On branch master
Your branch is ahead of 'origin/master' by 6 commits.
  (use "git push" to publish your local commits)

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   README.md

no changes added to commit (use "git add" and/or "git commit -a")

# On revient à un état sans modification avec stash
git stash
Saved working directory and index state WIP on master: 88e4253 Merge branch 'test'

# Vérification de notre répertoire de travail
git status
On branch master
Your branch is ahead of 'origin/master' by 6 commits.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean

À ce stade, vous pouvez donc changer de branche, travailler sur autre chose etc. Enfin, lorsque vous le voudrez, il suffira de demander à Git afin qu’il réapplique les modifications stashées.

La stash est locale, rien n’est donc sauvegardé sur un serveur distant. Par ailleurs, par défaut, la commande stash ne prend en compte que les fichiers déjà traqués. Si vous souhaitez inclure un fichier nouvellement créé et non encore suivi par Git, passez l’option -u, --include-untracked dans sa version longue.

Afficher ses modifications

La stash est une liste, c’est à dire que vous pouvez stasher plusieurs fois et restaurer ces modifications indépendamment les unes des autres. La commande list permet de voir ce qui se trouve dans le stash.

# Nous n'avons ici qu'une seule entrée
git stash list
stash@{0}: WIP on master: 88e4253 Merge branch 'test'

Nom de stash

Par défaut, le message de stash indique “WIP” sur la branche depuis laquelle vous avez stashé et le dernier commit sur cette branche au moment du stash. Toutefois, il nous est possible de personnaliser le message de stash.

git stash save "my stash message"

Lorsque des modifications ont été placées dans la stash il y a un moment, il arrive – assez souvent – de ne plus trop savoir ce qu’elles contiennent. Cela est d’autant plus vrai si nous ne nommons pas les stashs. Avant de restaurer une stash, il est possible de simplement afficher les modifications qu’elle apporterait avec la commande show.

git stash show [stash@{n°}]

Cette commande ne liste qu’un résumé des modifications. Pour les visualiser de manière exhaustive, il faut utiliser l’option -p.

Restaurer ses modifications

Une fois nos tâches annexes terminées, on voudra certainement restaurer notre espace de travail là où nous l’avions laissé. Il y a plusieurs manières d’effectuer cela.

Par défaut, Git restaure les dernières modifications ajoutées à la stash : stash@{0}. Mais il est possible de restaurer n’importe laquelle, il suffit de le demander gentiement. En outre, deux méthodes permettent de récupérer nos modifications : pop et apply.

pop applique les modifications et les retire de la liste de stash tandis que apply applique les modifications tout en conservant l’entrée dans la stashlist.

# Restaure les modifications précédemment "stashées"
git stash pop
On branch master
Your branch is ahead of 'origin/master' by 6 commits.
  (use "git push" to publish your local commits)

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   README.md

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (507587b9086f6c1b6565ac2bddc639e80ccc6059)

# Vérification de notre stash (vide)
git stash list

On a de nouveau effectuées des modifications placées dans la stash. Nous allons restaurer la plus ancienne des deux pour l’exemple.

git stash list
stash@{0}: WIP on master: 88e4253 Merge branch 'test'
stash@{1}: WIP on master: 88e4253 Merge branch 'test'

git stash pop stash@{1}
On branch master
Your branch is ahead of 'origin/master' by 6 commits.
  (use "git push" to publish your local commits)

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   README.md

no changes added to commit (use "git add" and/or "git commit -a")
Dropped stash@{1} (84d9f21dff63c27b727013cd3677b8e596ee4c69)

Nettoyer la stash

Deux commandes permettent de faire le ménage dans la stash. La plus radicale est git stash clear, elle efface tout ce qui se trouve dans la stash. La seconde est git stash drop.

Si aucun argument n’est passé à drop, alors le dernier élément ajouté à la stash sera supprimé. Cependant, drop accèpte les mêmes arguments que pop et apply, à savoir une référence de stash. Dans ce cas, seule la stash référencée sera supprimée.

Créer une branche à partir d’une stash

Parfois, les changements amorcés dans la stash s’avèrent plus épineux que prévus et il convient alors de les déplacer dans une branche dédiée. Git a tout prévu !

# Comme dans les autres commandes, la référence de la stash est optionnelle
# Si elle n'est pas passée, la dernière entrée est considérée
git stash branch <branchname> [stash@{n°}]

Tagger les choses importantes

Non, Git ne succombe pas à la mode du hashtag mais il permet de mettre un tag sur n’importe quel commit dont on décide qu’il revet une importance particulière.

Par exemple, la plupart des projets taguent le commit reflétant une version particulière. Dans GitHub, on retrouve ainsi facilement les différentes versions d’un projet dans un onglet dédié et il est possible de les télécharger. Par exemple le projet ƒlightDom, qui propose une API fonctionnelle et légère pour travailler avec le DOM, en est actuellement à sa version version 1.3.0.

Nous allons cloner ce projet afin d’illustrer l’usage des tags.

git clone https://github.com/Buzut/flightdom.git
Cloning into 'flightdom'...
remote: Enumerating objects: 205, done.
remote: Total 205 (delta 0), reused 0 (delta 0), pack-reused 205
Receiving objects: 100% (205/205), 864.16 KiB | 1.03 MiB/s, done.
Resolving deltas: 100% (95/95), done.

# On affiche les tags
git tag
v1.0.2
v1.1.0
v1.2.0
v1.3.0

Comme avec les branches, il est possible de facilement filtrer les tags qui nous intéressent. On passe alors l’option -l avec un motif : git tag -l 1.0.*, pour récupérer tous les tags de la version 1.0.x par exemple.

Les tags permettent de facilement référencer un commit. Presque toutes les commandes acceptant une référence SHA-1 acceptent également un tag. Par exemple, si nous voulons repartir de l’état d’un tag en particulier pour y ajouter des modifications, on pourrait créer une branche depuis cet état.

# On créé la branche nommée 1.0.2 depuis le tag v1.0.2
git branch 1.0.2 v1.0.2

Il existe deux types de tags : les tags courts et les tags annotés. Fonctionnellement, ils sont assez similaires, le tag annoté permet d’entrer un message de commit en plus du tag en lui-même.

# Tag court
git tag v1.1.0

# Tag annoté
git tag -a v1.1.0 -m "description du tag"

La grosse différence entre les deux types de tags est que le tag court est techniquement similaire à une branche, ce n’est qu’un pointeur vers un commit, tandis que le tag annoté est stocké comme un objet à part entière dans la base de données de Git.

Il est bien entendu possible d’ajouter un tag après coup, il suffit pour cela de passer un hash à la commande tag.

git tag f8c2bf2 v1.3.1

L’exemple ci-dessus concerne un tag court, mais cela fonctionne de même avec un tag annoté.

Nous avons dans ce chapitre fait le tour des principaux usages des branches, tags et stashs. Dans le chapitre suivant, nous allons nous pencher sur l’historique : visualisation, recherche, navigation et modification n’auront plus de secret pour vous.

Commentaires

Rejoignez la discussion !

Vous pouvez utiliser Markdown pour les liens [ancre de lien](url), la mise en *italique* et en **gras**. Enfin pour le code, vous pouvez utiliser la syntaxe `inline` et la syntaxe bloc

```
ceci est un bloc
de code
```