Chapitre 4 sur 13
La puissance des branches Git
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.
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.
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 :
heads
sont les pointeurs vers le dernier snapshot d’une branche (l’emprunte du tree).remotes
sont les pointeurs vers les branches upstream. Cela permet à Git de se “souvenir” l’état de la branche distante lors de la dernière synchro.tags
sont des pointeurs vers des tags. Nous en verrons le fonctionnement plus loin dans ce chapitre. Un tag permet en quelque sorte de nommer un commit pour facilement pouvoir revenir à l’état de ce commit par la suite.
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.
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 :
--contains <commit>
affiche toutes les branches contenant un commit donné,--no-contains
fait le contraire de la précédente,--no-merge
affiche les branches qui n’ont pas été mergées avec la branche courante (on peut lui passer un commit en argument),--merge
fait le contraire de la précédente.
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.
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.
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.
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 :
- origin/master n’est pas à jour avec la master locale, respectivement aux commits
1f80958
et6fb75bc
, - la branche ayant reçu le dernier commit est hooks-distribution avec le commit
0d8ba1b
.
# 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 :
- accepter le message de merge par défaut, dans ce cas, vous pouvez utiliser l’option
--no-edit
, - vous utilisez l’option
-m
en lui passant un message de commit, exactement comme on peut le faire aveccommit
.
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 :
<<<<<<< HEAD
la branche récipendaire, c’est la branche sur laquelle on est,=======
la séparation entre les deux versions,>>>>>>> branche_a_fusionner
la branche cible de la fusion.
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 :
ours
choisi toujours les modifications de la branche cible (celle sur laquelle on est),theirs
choisi toujours les modifications de la branche source.
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 :
ignore-all-space
ignore les changements de quantité d’espaces. Les espaces en fin de ligne et considère que toutes les séquences d’un ou plusieurs espaces comme équivalentes.ignore-all-space
ignore tout bonnement les espaces dans l’algorithme de comparaison.
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.
À 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.
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
.
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
:
--edit
afin de définir le message du commit,--strategy
et-X
afin de choisir une stratégie de merge et ses options en cas de conflits,--no-commit
ou-n
permet d’appliquer les changements mais ne les commite pas automatiquement.
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 !