Ceci est le quatrième article d’une série consacrée aux commandes de Git, le système de gestion de révisions décentralisé. Le sujet de cet article est la commande
git rebase
, qui permet de déplacer, réordonner et modifier des commits, et donc des branches, dans l’arborescence de révisions.
Retrouvez les précédents articles de la série :
Dans cet article, nous allons :
- comprendre l’utilité du déplacement de commits et de branches ;
- montrer comment déplacer une branche dans deux cas différents ;
- apprendre à fusionner des commits ;
- évoquer quelques usages avancés.
Quelle est l’utilité du rebase ?
Les raisons de vouloir déplacer ou réordonner un groupe de commits, ou branche, sont variées :
- volonté de conserver un historique de révision linéaire, donc facile à comprendre et analyser ;
- éviter que le graphe d’historique ne ressemble à un plat de spaghettis, c’est aussi faciliter le debug, notamment avec git bisect ;
- intégrer de nouveaux commits dans la branche courante ;
- déplacer une branche créée au mauvais endroit ;
- compresser de nombreux commits locaux correspondant à différentes étapes d’un développement en un seul, propre, avant de pousser sur le dépôt central.
Déplacer une Branche
Nous allons voir ici une utilisation de base de la commande rebase
. Prenons un projet d’exemple, où nous avons deux branches : la principale, master
, et une autre contenant des développements en rapport avec une nouvelle fonctionnalité, appelée featureF
. Le graphe de révision est le suivant :
A---B---C---D master \ E---F---G featureF
Mais nous nous apercevons que la branche featureF
aurait dû être tirée depuis le commit D
. Nous allons donc « déplacer » la branche, pour lui faire intégrer ces commits et faire « comme si » elle avait été créée à partir de D
, le dernier commit de master.
Point important : la branche courante est featureF
.
git rebase master
Cette commande est équivalente à utiliser un identifiant de commit :
git rebase D
Le résultat est le suivant :
A---B---C---D master \ E'---F'---G' featureF
Il est possible d’obtenir le même résultat sans avoir à se placer au préalable sur featureF
en précisant un argument supplémentaire :
git rebase master featureF
Déplacer une branche sur une autre branche
Considérons l’état suivant :
A---B---C---D master \ E---F---G featureF \ H---I featureG
Nous avons une branche featureG
, créée à partir de featureF
, elle-même créée à partir de master
. Il s’agit d’une erreur, car featureG
aurait dû être créée à partir de master
et ne dépend pas de featureF
. Nous allons donc déplacer featureG
sans ses commits communs avec featureF
, c’est-à-dire les commits E
, F
et G
pour la « raccrocher » à master
:
git rebase --onto master featureF featureG
Le résultat est le suivant :
A---B---C---D master \ \ \ H'---I' featureG \ E---F---G featureF
Là encore il est possible d’utiliser des identifants de commits en lieu et place du nom des branches.
Fusionner des commits
git rebase
permet également de réordonner, modifier, fusionner ou encore séparer des commits. Nous allons ici nous pencher sur un usage courant, qui consiste à vouloir fusionner une série de petits commits. Par exemple, ce besoin peut se présenter après un développement compliqué, qui a nécessité plusieurs commits (étapes) intermédiaires avant d’arriver au résultat final. Une fois le résultat final atteint, seul celui-ci va intéresser le reste de l’équipe. En effet, personne ne sera intéressé par un état incohérent (fonctionnalités incomplètes, présence d’anomalies, compilation en échec…). Prenons l’exemple suivant :
* 875fdcc (HEAD -> myAwesomeFeature) Add button on UI * d1fa7e0 Fix error on database request * 4b2ddd3 Part 2, error on database request * 4a3e967 Part 1 * 32dee61 More work, does not compile * 20bd297 Create sample database structure | * afc62ce (origin/master, master) Add powerful script |/ * 3dfe024 Modify a.txt * e48e446 Initial commit
Nous venons de terminer le développement de la fonctionnalité myAwesomeFeature
et nous nous retrouvons avec tous les commits (6) intermédiaires qui nous ont été utiles pendant le développement, mais plus maintenant. Nous souhaitons donc compresser tous nos commits en un seul, afin de ne pas polluer l’historique de révision et fournir à l’équipe uniquement des informations utiles. Pour ce faire, nous allons indiquer à git sur quel ensemble de commits nous souhaitons travailler. Il s’agit des 6 derniers :
git rebase -i HEAD~6
Explication :
-i
est l’option interactive, qui va laisser l’utilisateur dire quel type de modification (fusion, découpage, suppression, modification…) il souhaite appliquer aux commits considérés ;HEAD~6
est un indicateur signifiant « 6 commits avantHEAD
».
À la validation de la commande, un éditeur de texte va s’ouvrir avec le contenu suivant :
pick 20bd297 Create sample database structure pick 32dee61 More work, does not compile pick 4a3e967 Part 1 pick 4b2ddd3 Part 2, error on database request pick d1fa7e0 Fix error on database request pick 875fdcc Add button on UI # Rebase 3dfe024..875fdcc onto 3dfe024 (6 command(s)) # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # d, drop = remove commit # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out
Tout ce qui est commenté après les premières lignes est une aide.
Les 6 premières lignes correspondent aux 6 commits sélectionnés au lancement de la commande. Ces derniers sont ordonnés du plus ancien au plus récent. Le verbe pick
en préfixe indique l’action qui sera effectuée sur ces commits. Si nous enregistrons et quittons ce fichier en l’état, rien ne sera modifié. Si l’on veut compresser plusieurs commits en un seul, il faut remplacer l’action pick
par une autre. Celle qui nous intéresse est l’action squash
, qui indique qu’un commit doit être fusionné dans le précédent. Nous modifions donc le fichier en conséquence :
pick 20bd297 Create sample database structure squash 32dee61 More work, does not compile squash 4a3e967 Part 1 squash 4b2ddd3 Part 2, error on database request squash d1fa7e0 Fix error on database request squash 875fdcc Add button on UI
Le squash
de la ligne 2 indique à git « fusionne ce commit avec le précédent (ligne 1) ». Ligne 3, un nouveau squash
indique de fusionner le commit avec celui de la ligne 2, lui-même fusionné avec la ligne 1 ; de même avec les autres. In fine, les 6 commits vont donc être rassemblés en un seul.
Puis nous enregistrons et quittons. Un nouvel éditeur apparait, affichant les informations suivantes :
# This is a combination of 6 commits. # The first commit's message is: Create sample database structure # This is the 2nd commit message: More work, does not compile # This is the 3rd commit message: Part 1 # This is the 4th commit message: Part 2, error on database request # This is the 5th commit message: Fix error on database request # This is the 6th commit message: Add button on UI
Comme nous sommes en train de fusionner 6 commits, disposant chacun de leur commentaire, Git nous demande quel est le commentaire à conserver pour le commit de fusion. Nous décidons de garder le dernier en supprimant les autres lignes.
Un fois enregistré, l’opération de rebase
est effectuée, et le résultat est le suivant :
* e1a15c4 (HEAD -> myAwesomeFeature) Add button on UI | * afc62ce (origin/master, master) Add powerful script |/ * 3dfe024 Modify a.txt * e48e446 Initial commit
Nous pouvons constater que nos 6 commits se sont transformés en un seul. Il peut maintenant être intégré à master
par la méthode de notre choix.
Autres opérations possibles
Nous venons d’utiliser le mode interactif de la commande rebase
pour fusionner des commits. Comme mentionné plus haut, il est possible d’effectuer des modifications de nature différente grâce à ce mode interactif. Nous avons vu les verbes pick
et squash
, mais il existe également les opérations suivantes :
fixup
: fonctionne de la même manière quesquash
, en excluant directement le message de commit ;reword
: permet de renommer le message de commit sur lequel il est appliqué ;edit
: conserve le commit, mais lorsque la commanderebase
va s’exécuter, elle va s’arrêter à ce commit pour permettre une édition, comme par exemple ajouter un fichier oublié, puis continuer dès que nous l’indiquerons (avec ungit rebase --continue
) ;exec
: permet d’exécuter une commande shell entre deux lignes de commit ;drop
: supprime le commit.
Pour un exemple avancé, un screencast dédié à la commande rebase
est disponible sur Xebia TV.
Conclusion
Nous venons de voir que grâce à la commande rebase
, il est possible de réagencer un ensemble de commits de toutes les manières imaginables : déplacer des groupes de commits, les réordonner, les fusionner, les modifier… Cette commande est très complète, mais il faut garder à l’esprit qu’elle modifie l’historique, et donc ne doit être appliquée que sur des commits locaux, donc avant le push
. S’il est tout de même possible de modifier une partie de l’historique déjà poussé, via un git push --force
, cela est à éviter dans la majorité des situations, car cela causera des problèmes aux personnes ayant déjà récupéré les commits modifiés.
À bientôt pour le prochain article de cette série !