Révisions GIT: épisode 3

Après avoir passé en revue les bases de GIT, ainsi que les commandes de base, ce troisième épisode va traiter des branches. Cette fonctionnalité est probablement l’une des plus importante de GIT car, en permettant une « parallélisation » des développements, elles facilitent le travail en équipe.

Révisions GIT: épisode 3

Les branches

Globalement, créer une branche, c’est créer une nouvelle trajectoire, isolée de la branche principale. Les branches peuvent être utilisées pour

  • Paralléliser le développement de plusieurs fonctionnalités (et éviter que le développement de l’une n’interfère sur l’autre),
  • Démarrer un développement exploratoire, dont l’issue est incertaine,
  • Ou corriger un bug,
  • Organiser le travail d’équipes de développement.

Les branches constituent l’une des fonctionnalités les plus importantes de GIT. Nous avons 5 principales opérations sur les branches

  • Liste des branches existantes,
  • Création d’une nouvelle branche,
  • Changement de la branche active,
  • Fusion de deux branches,
  • Suppression d’une branche.

Liste des branches existantes

$ git branch
    branche-1
    branche-2
    * branche-3
    main

La branche « active » est celle sur laquelle pointe HEAD, autrement dit, c’est la branche dans laquelle s’effectuera le prochain commit. Elle est indiquée par la couleur verte, et par le préfixe *.

Figure 1 : Liste des branches
Figure 1 : Liste des branches
.

Création d’une branche

La syntaxe est simple :

$ git branch <nom de la nouvelle branche>

L’outil alerte si le nom de la branche existe déjà.

Figure 2 : Création d’une branche
Figure 2 : Création d’une branche

La commande git branch <branche> créé une branche, mais ne la rend pas active.

Dans la figure 2, nous avons bien une branche qui vient d’être créée, mais le pointeur HEAD pointe toujours sur le dernier commit de la branche principale.

La création d’une branche se fait à partir de la branche sur laquelle nous nous trouvons. Dans la figure 1, la branche branche3 n’est pas issue de main, mais bien de branche2.

Changement de la branche active

La commande checkout

La syntaxe est la suivante : git checkout <nom de la branche>. Exemple :

$ git branch
    * main
    branche1

$ git checkout branche1
    Switched to branch 'branche1'

$ git branch
    main
    * branche1

Nous retrouvons donc la fameuse commande checkout, cette fois, avec une troisième syntaxe. Au passage, cela permet de revenir à la définition d’une branche : une branche selon GIT est une étiquette qui pointe vers un commit. Voilà pourquoi nous retrouvons cette commande checkout qui permet de déplacer le pointeur HEAD, d’un commit vers un autre.

Figure 3 : La commande checkout
Figure 3 : La commande checkout
Il est possible de changer de branche, tout en la créant :

$ git checkout -b <nouvelle branche>  # Création de la branche et checkout.

Les effets de la commande

Comme indiqué dans la figure 3, la commande checkout, dans ce cas, n’affecte pas le répertoire de travail, ni l’index (staging area). Si vous avez des modifications en cours, la commande le rappellera (ici file2.txt).

$ git status                        # Nous sommes dans la branche main, et le fichier file2.txt est modifié (suivi, non-indexé)
    On branch main
    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:   file2.txt

$ git checkout branche1             # Changement de branche, la commande nous signale l'existence de la modification
    Switched to branch 'branche1'
    M    file2.txt

$ git status                        # Le status montre que même après le changement de branche, la modification est toujours là
    On branch branche1
    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:   file2.txt

Il y a une petite nuance à apporter sur ce qui vient d’être dit : la commande a un effet sur l’arborescence que vous manipulez. Si vous êtes dans une branche dans laquelle vous avez créé plusieurs répertoires, et fichiers (que vous les ayez commités ou pas), ces fichiers et répertoires disparaîtront de votre arborescence de travail, lors du checkout vers une autre branche. Ils réapparaîtront lorsque vous reviendrez sur cette branche.

Figure 4 : Effet du changement de branche sur notre arborescence de travail
Figure 4 : Effet du changement de branche sur notre arborescence de travail

Donc

  • Si vous êtes dans une branche A, et que vous modifiez un fichier dans un sous-répertoire qui n’existe pas dans une branche B
  • Si vous passez à la branche B, le répertoire disparaîtra, et votre modification avec.

Heureusement, la commande le signale :

$ git status
    On branch feature                   # Nous sommes dans la branche feature
    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:   folder1/file5.txt   # Le fichier file5.txt est modifié (il se trouve dans le répertoire folder1)

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

$ git checkout master                   # Nous tentons de passer de la branche feature a la branche master.
    error: Your local changes to the following files would be overwritten by checkout:
            folder1/file5.txt
    Please commit your changes or stash them before you switch branches.
    Aborting

La commande stash

Dans le cas que nous venons de décrire, nous souhaitons changer de branche, mais les modifications en cours nous en empêchent. Il nous faut valider (commit) ces modifications pour être autoriser à changer de branche. Mais que faire si nous ne sommes pas prêts ?

GIT nous offre une solution pour ce genre de cas : la commande git stash. Cette commande permet de stocker temporairement nos modifications en cours (dans le répertoire de travail), et de les réappliquer ultérieurement.

Reprenons l’exemple précédent : nous ne pouvons pas quitter la branche feature.

$ git status
    On branch feature
    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:   folder1/file5.txt

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

$ git stash
    Saved working directory and index state WIP on feature: a1396b0 folder

$ git status
    On branch feature
    nothing to commit, working tree clean

$ git checkout master
    Switched to branch 'master'

$ git status
    On branch master
    nothing to commit, working tree clean

Une fois effectuer les changements dans la branche master, nous pouvons revenir à la branche feature, et réappliquer les modifications initiales :

$ git checkout feature          # On retourne dans la branche feature
    On branch feature
    nothing to commit, working tree clean

$ git stash pop                 # On récupère les modifications sauvegardées précédemment
    On branch feature
    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:   folder1/file5.txt

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

La commande git stash pop applique les modifications sauvegardées dans notre espace de travail, puis supprime « la sauvegarde ». Il est possible d’appliquer les modifications, et les conserver pour les réappliquer ultérieurement, avec la commande git stash apply.

Par défaut, la commande git stash s’applique aux changements

  • De la zone staging (index),
  • Sur les fichiers suivis, modifiés, mais non indexés (tracked) du répertoire de travail

La commande ne s’applique ni sur les fichiers non-suivis, ni sur les fichiers ignorés, à moins d’utiliser les paramètres -u, et -a :

  • L’option -u (ou --include-untracked) inclut les fichiers non-suivis,
  • L’option -a (ou --all) inclut tous les fichiers, y compris les fichiers ignorés.

Figure 5 : Les différentes options de la commande stash
Figure 5 : Les différentes options de la commande stash

Gérer plusieurs stashes

Exécuter git stash à plusieurs reprises, va créer plusieurs stashes. Ces éléments sont stockés sous la forme d’une pile LIFO (Last In, First Out).

CommandeDescription
git stash listAfficher la liste des stashes
git stash popRécupère les modifications correspondant au dernier stash
git stash save "<message>"Sauvegarde les modifications, en annotant le stash
$ git stash pop stash@{<numéro>}Récupère les modifications du stash correspondant à <numéro>
$ git stash show [-p]Affiche le résumé d’un stash (les modifications). -p donnera tous les détails
git stash listAfficher la liste des stashes
git stash clearSupprime l’ensemble de la pile de stashes
git slash drop <identifiant>Supprime le stash désigné par l’identifiant
git stash branch add-stylesheet <identifiant>Créer une branche à partir des modifications du stash désigné par l’identifiant

Les lignes suivantes mettent en pratique les commandes que nous venons de lister :

# Vérification de la situation. Nous avons 1 fichier modifié, mais non indexé
$ git status
    On branch feature
    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:   folder1/file5.txt

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

# Sauvegarde des modifications en cours (dont notre fichier file5.txt)
$ git stash
    Saved working directory and index state WIP on feature: a1396b0 folder

# Nous avons bien cette sauvegarde
$ git stash list
    stash@{0}: WIP on feature: a1396b0 folder

# Création d'un fichier file6.txt
$ echo a > file6.txt

# La commande status nous montre bien le file6.txt, non-suivi
$ git status
    On branch feature
    Untracked files:
    (use "git add <file>..." to include in what will be committed)
            file6.txt
    nothing added to commit but untracked files present (use "git add" to track)

# Sauvegarde de nore répertoire de travail, et de la zone d'index.
$ git stash -u
    Saved working directory and index state WIP on feature: a1396b0 folder

# Nous avons bien deux sauvegardes
$ git stash list
    stash@{0}: WIP on feature: a1396b0 folder
    stash@{1}: WIP on feature: a1396b0 folder

# Creation et indexation du fichier file7.txt
$ echo y > file7.txt
$ git add file7.txt

# Le fichier est dans la zone d'indexation (prêt pour le commit)
$ git status
    On branch feature
    Changes to be committed:
    (use "git restore --staged <file>..." to unstage)
            new file:   file7.txt

# Nous sauvegardons cette modification, en lui donnant une description
$ git stash save "Add y in the file7.txt"
    Saved working directory and index state On feature: Add y in the file7.txt

# Nous voyons bien la description dans la liste des sauvegardes
$ git slash list
    stash@{0}: On feature: Add x in the file7.txt
    stash@{1}: WIP on feature: a1396b0 folder
    stash@{2}: WIP on feature: a1396b0 folder

# Nous souhaitons récupérer le fichier file6.txt
$ git stash drop stash@{1}
    Dropped stash@{1} (2ebf490ba5238004467ecd098d9ac3c81c933ef5)

# Le fichier 6 est récupéré, il ne nous reste que deux sauvegardes
$ git stash list
    stash@{0}: On feature: Add x in the file7.txt
    stash@{1}: WIP on feature: a1396b0 folder

# Nous utilisons la sauvegarde 1 pour créer une branche
$ git stash branch new-feature stash@{1}
Switched to a new branch 'new-feature'
On branch new-feature
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:   folder1/file5.txt

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

Fusion de deux branches avec la commande merge

A un moment donné, si nous sommes satisfaits de nos modifications, nous pouvons les intégrer à la branche principale. On parle de fusion de branches (merge). Lors d’une fusion, GIT va intégrer toutes les modifications contenues sur chaque branche dans une seule et même arborescence.

Le processus de fusion habituel est

  • On se place d’abord sur la branche qui va recevoir toutes les modifications,
  • On effectue la fusion
  • A partir de ce moment, toutes les modifications de la branche feature sont intégrées à la branche main.

Figure 5: Fusion de deux branches
Figure 5: Fusion de deux branches

Dans ce cas, l’opération de fusion créé un nouveau commit, qui a deux parents. Il ne remplace pas le commit courant.

Une fusion correspond à l’intégration de toutes les modifications effectuées sur les deux branches que nous souhaitons fusionner. GIT est capable d’identifier les modifications dans les différents fichiers, et de les agréger. Dans certains cas, cependant, GIT ne peut pas gérer les “conflits”, qu’il faut alors résoudre manuellement.

Pour cela GIT interrompt le processus de fusion, et ajoute des marqueurs dans les fichiers à fusionner. Nous devons éditer les fichiers manuellement afin de résoudre ces conflits.

Ce travail manuel est assez rébarbatif lorsqu’il est fait avec l’outil GIT en ligne de commande. Il vaut mieux effectuer ce genre d’opération, soit avec un outil embarqué dans votre environnement de développement, soit depuis un serveur git centralisé comme Github.

Figure 6 : Analyse des différences dans Visual Studio
Figure 6 : Analyse des différences dans Visual Studio
La commande git merge --abort ne peut être exécutée qu’après un git merge conduisant à de conflits non résolus automatiquement. Cette commande tentera alors d’annuler l’opération de fusion, et de revenir à la situation initiale. Cette tentative peut conduire éventuellement à la perte de vos modifications en cours.

Exécuter git merge en ayant des modifications non commitées est fortement déconseillé

Résumé

La figure 7 donne un exemple d’utilisation de deux branches simultanément.

Figure 7 : Exemple de flow avec deux branches
Figure 7 : Exemple de flow avec deux branches
L’important est de bien identifier où l’on se trouve lorsque l’on effectue des modifications.

  1. Création de la branche
  2. Comme nous n’avons pas changé de branche, les modifications sont toujours effectuées dans la branche main
  3. Pour développer dans la branche feature nous devons explicitement le demander
  4. La branche active étant maintenant feature, toutes les modifications seront “enregistrées” dans cette branche
  5. Mais rien ne nous empêche de revenir à la branche main,
  6. Et y enregistrer des modifications

Commentaire : Il ne s’agit que d’un exemple

  • En pratique, il est déconseillé de développer directement dans la branche main. On développe dans des branches, que l’on fusionne avec la branche main à chaque fin de cycle de développement,
  • Et il est bien sur conseillé d’avoir des sessions de travail par branche. Si vous devez changer de branche régulièrement, vous ferez, immanquablement une erreur à un moment donné.

Retour sur l’état de « tête détachée »

Dans le chapitre précédent, nous avons parlé de l’état de tête détachée (detached head). Nous allons revenir sur cet état, et nous allons en profiter pour mieux comprendre l’utilisation des commandes log, et reflog.

Situation de départ, nous n’avons qu’une seule branche main. Dans cette branche, nous avons deux commits :

$ git log
    commit 8169912adcfe5612389e69501f4192db09a85c25 (HEAD -> main)
    Author: Emmanuel
    Date:   Tue Apr 20 21:42:23 2021 +0200

        Second commit

    commit 1b975d5493080c428fa8421aab72eb52e994cd9f
    Author: Emmanuel
    Date:   Tue Apr 20 21:41:23 2021 +0200

        Initial release

Figure 8 : Situation initiale
Figure 8 : Situation initiale

Maintenant, créons la situation de tête détachée, en faisant pointer HEAD

$ git checkout 1b975d5
    Note: switching to '1b975d5'.

    You are in 'detached HEAD' state. You can look around, make experimental
    changes and commit them, and you can discard any commits you make in this
    state without impacting any branches by switching back to a branch.

    If you want to create a new branch to retain commits you create, you may
    do so (now or later) by using -c with the switch command. Example:

    git switch -c <new-branch-name>

    Or undo this operation with:

    git switch -

    Turn off this advice by setting config variable advice.detachedHead to false

    HEAD is now at 1b975d5 Initial release

La commande nous signale la situation de façon claire.

Figure 9 : Après la commande checkout
Figure 9 : Après la commande checkout

Continuons nos modifications sur le fichier :

$ echo c > file1.txt
$ git status
    HEAD detached at 1b975d5
    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:   file1.txt

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

Nous effectuons un commit

$ git commit -m -a "3rd commit"
    [detached HEAD e5245ff] 3rd commit

   1 file changed, 1 insertion(+), 1 deletion(-)

Effectuons une nouvelle modification pour obtenir le commit 89cd6ad.

Figure 10 : Modification après la création d’un tête détachée
Figure 10 : Modification après la création d’un tête détachée

Si nous regardons la liste des commits avec la commande git log:

$ git log
    commit 89cd6ad35b505a42df8b98b3d29ef6f939df6c13 (HEAD)
    Author: Emmanuel
    Date:   Tue Apr 20 22:01:33 2021 +0200

        4th commit

    commit e5245ffea64186bdc958c0253fc95f664e3977d2
    Author: Emmanuel
    Date:   Tue Apr 20 21:48:18 2021 +0200

        3rd commit

    commit 1b975d5493080c428fa8421aab72eb52e994cd9f
    Author: Emmanuel
    Date:   Tue Apr 20 21:41:23 2021 +0200

        Initial release

Ici, nous ne voyons plus le second commit. Pour voir l’ensemble des commits, il faut utiliser la commande git reflog

$ git reflog
    89cd6ad (HEAD) HEAD@{0}: commit: 4th commit
    e5245ff HEAD@{1}: commit: 3rd commit
    1b975d5 HEAD@{2}: checkout: moving from main to 1b975d5
    8169912 (main) HEAD@{3}: commit: Second commit
    1b975d5 HEAD@{4}: commit (initial): Initial release

Au passage, notons la différence entre les commandes git log, et git reflog:

  • git log liste les commits relatifs à la branche en cours (donc il remonte l’historique en partant du pointeur HEAD),
  • git reflog nous donne l’historique des « mouvements » du pointeur HEAD.

Figure 11 : Différence entre git log, et git reflog
Figure 11 : Différence entre git log, et git reflog
Reprenons notre développement : La situation n’est pas encore catastrophique, mais que se passe-t-il si nous tentons de revenir sur la branche main ?

$ git checkout main
    Warning: you are leaving 2 commits behind, not connected to
    any of your branches:

    89cd6ad 4th commit
    e5245ff 3rd commit

    If you want to keep them by creating a new branch, this may be a good time
    to do so with:

    git branch <new-branch-name> 89cd6ad

    Switched to branch 'main'

Là encore, les messages de GIT sont clairs. Nous avons bien deux commits qui ne sont plus rattachés à rien.

Figure 12 : Tête détachée, après retour à la branche main
Figure 12 : Tête détachée, après retour à la branche main

$ git log
    commit 8169912adcfe5612389e69501f4192db09a85c25 (HEAD -> main)
    Author: Emmanuel
    Date:   Tue Apr 20 21:42:23 2021 +0200

        Second commit

    commit 1b975d5493080c428fa8421aab72eb52e994cd9f
    Author: Emmanuel
    Date:   Tue Apr 20 21:41:23 2021 +0200

        Initial release

Donc là, nous avons perdus 2 commits sur les 4. La commande git reflog nous donne

$ git reflog
    8169912 (HEAD -> main) HEAD@{0}: checkout: moving from 89cd6ad35b505a42df8b98b3d29ef6f939df6c13 to main
    89cd6ad HEAD@{1}: commit: 4th commit
    e5245ff HEAD@{2}: commit: 3rd commit
    1b975d5 HEAD@{3}: checkout: moving from main to 1b975d5
    8169912 (HEAD -> main) HEAD@{4}: commit: Second commit
    1b975d5 HEAD@{5}: commit (initial): Initial release

Pour terminer notre périple : si nous souhaitons conserver ces commits, en les identifiants clairement, nous avons la possibilité de les rattacher à une branche.

$ git branch nouvbranche 89cd6ad   # Creation d'une branche qui va pointer sur le 4ieme commit
$ 
$ git reflog
    8169912 (HEAD -> main) HEAD@{0}: checkout: moving from 89cd6ad35b505a42df8b98b3d29ef6f939df6c13 to main
    89cd6ad (nouvbranche) HEAD@{1}: commit: 4th commit
    e5245ff HEAD@{2}: commit: 3rd commit
    1b975d5 HEAD@{3}: checkout: moving from main to 1b975d5
    8169912 (HEAD -> main) HEAD@{4}: commit: Second commit
    1b975d5 HEAD@{5}: commit (initial): Initial release

Ce qui donne graphiquement :

Figure 13 : Création d’une nouvelle branche à partir des commits non attachées
Figure 13 : Création d’une nouvelle branche à partir des commits non attachées
Nous sommes maintenant revenu à une situation « propre », avec l’ensemble de nos commits attachés à une branche.

J’ai été un peu long sur ces explications, mais je pense que c’est un cas très intéressant pour mieux comprendre la notion de pointeur, et de branche.

Avance rapide, et vraie fusion

Revenons aux fusions, en entrant un peu plus dans le détail. Précédemment, j’ai dit que la fusion créait systématiquement un commit de fusion. Ce n’est pas tout à fait vrai. Lorsque nous invoquons la commande git merge, nous avons deux cas de fusion : le fast-forward (avance rapide), et le true merge (vrai fusion).

Plaçons-nous dans la situation suivante :

  • Nous avons une branche main, avec déjà un certain historique,
  • Nous décidons l’implémentation d’une nouvelle fonctionnalité : nous avons créé pour cela une branche feature, dans laquelle nous avons effectué des modifications,
  • Entre temps, des utilisateurs nous ont signalé des erreurs. Pour gérer la correction, nous avons donc créé une branche patch, pour les corriger.

Figure 14 : Situation initiale
Figure 14 : Situation initiale

Le fast-forward

Notre correction étant prête, nous souhaitons l’incorporer à notre branche principale. Utilisons donc la commande merge :

$ git checkout main
    Switched to branch 'main'

$ git merge patch
    Updating e31b465..291892b
    Fast-forward
     file2.xt | 2 +
     1 file changed, 2 insertions(+)

Figure 15: Fusion de la branche patch, cas d’un foast-forward
Figure 15: Fusion de la branche patch, cas d’un foast-forward

Ici, le commit G est un descendant direct du commit C de la branche main. Il n’y a pas de modifications divergentes à fusionner. GIT se contente alors d’avancer le pointeur HEAD. Cette opération s’appelle un fast-forward (avance rapide), comme indiqué dans la réponse de la commande merge.

Supprimer maintenant la branche patch dont nous n’avons plus besoin.

Figure 16: Suppression d’une branche après un fast-forward
Figure 16: Suppression d’une branche après un fast-forward

Le true merge

Maintenant que l’application fonctionne de nouveau sans erreur, nous reprenons le fil de notre développement. Notre nouvelle fonctionnalité est prête, les modifications de la branche feature peut être incorporées à la branche principale. Si nous effectuons une fusion, nous obtenons

$ git checkout main
    Switched to branch 'main'

$ git merge feature
    Merge made by the 'recursive' strategy.
    file2.txt |    1 +
    1 file changed, 1 insertion(+)

Dans cas, GIT va réaliser une fusion à trois sources (three-way merge): le dernier commit de la branche main (G), le dernier commit de la branche feature (F), et le premier ancêtre commun (C). L’outil va créer un nouveau commit, appelé commit de fusion (merge commit), et va déplacer le pointeur head sur celui-ci.

Figure 17 : Fusion de la branche feature, cas d’un true merge
Figure 17 : Fusion de la branche feature, cas d’un true merge

Nous parlons dans ce cas, d’une vraie fusion (true merge). Nous pouvons maintenant supprimer la branche feature, dont nous n’avons plus besoin.

Figure 18 : Suppression d’une branche après un true merge
Figure 18 : Suppression d’une branche après un true merge

Influencer le comportement de GIT

Dans les deux cas précédents, nous avons laissé GIT décider de la statégie de fusion. Comme vous pouvez le constater avec les figures figure 16 et figure 18, appliquer l’une ou l’autre des méthodes de laisse pas le même historique. Pour diverses raisons, notamment lorsque l’on travaille en groupe, notre volonté peut être de

  • Garder un historique des modifications, pour privilégier la traçabilité,
  • Ou, au contraire, proposer un historique propre et linéaire pour une compréhension plus aisée.

Il est possible d’influencer la stratégie de fusion avec le paramètre --no-ff, qui comme son nom l’indique, demande à GIT de ne pas réaliser de fast-forward.

$ git checkout main
    Switched to branch 'main'

$ git merge patch --no-ff

Fusion de deux branches avec la commande rebase

Jusqu’à présent, nous nous sommes basés sur des exemples, où une branche de développement, issue d’une branche principale, “avance” plus vite que sa branche mère. Mais que se passe-t-il, si l’historique de la branche principale évolue régulièrement, en parallèle de notre branche de développement ?

L’exemple

Reprenons notre exemple précédent. A la fin du fast-forward, nous avons :

Figure 19 : Branche principale active
Figure 19 : Branche principale active

Lors du développement de la nouvelle fonctionnalité, nous découvrons que le patch est nécessaire au son fonctionnement. Pour remédier à cette situation, nous pouvons considérer une fusion, non pas de la branche de feature vers la branche main, mais au contraire, de la branche main vers la branche feature, pour ensuite continuer nos développements :

Figure 20 : Fusion de la branche main vers la branche feature
Figure 20 : Fusion de la branche main vers la branche feature

et nous pouvons poursuivre le développement sur la branche feature, puis incorporer cette branche à notre branche principale (en mode true merge)

Figure 21 : Développement de la fonctionnalité
Figure 21 : Développement de la fonctionnalité

Comme vous pouvez le constater, l’historique de développement devient un peu compliqué. Comprendre cette historique avec les commandes git log va devenir extrêmement complexe.

Nous avons une autre possibilité d’intégrer le patch dans la branche de développement, en utilisant la commande rebase, dont la syntaxe est tout à fait similaire, à celle de la commande merge :

$ git checkout feature
$ git rebase main

Figure 22 : Commande rebase
Figure 22 : Commande rebase

La commande rebase « réécrit » l’historique de la branche en créant de nouveaux commits (E’, F’ dans le cas de la figure 22) pour chaque commit de la branche d’origine. Nous avons donc maintenant :

  • Les modifications de la nouvelle fonctionnalité, incluant le patch,
  • Un historique beaucoup plus propre que celui obtenu avec la commande merge.

A partir de là, nous pouvons continuer le développement de la fonctionnalité, avant de l’intégrer dans la branche principale :

Figure 23 : Développement après le rebase, puis fusion en true merge ou fast-forward
Figure 23 : Développement après le rebase, puis fusion en true merge ou fast-forward

Pour résumer

La commande git rebase peut être utilisée lorsque nous sommes dans les situations suivantes :

  • Certaines fonctionnalités de la branche principale sont pertinentes pour notre développement,
  • La base de départ de notre branche de développement est jugée obsolète,
  • Les deux branches divergent trop, et la future fusion risque d’être complexe à exécuter.

Utiliser la commande git merge est possible, mais comporte deux principaux inconvénients

  • En fonction des cas, cette opération peut rendre la lecture de l’historique relativement complexe à comprendre,
  • Le commit issu de la fusion des deux branches peut intégrer un grand nombre de modifications comparé à son prédécesseur, ce qui peut rendre, là encore, les choses difficiles à comprendre.

La commande git rebase permet donc d’intégrer l’historique de la branche principale, tout en conservant un historique simple.

  • Les avantages
    • L’historique du projet est plus propre,
    • L’historique étant linéaire, il est plus facile à remonter,
  • Les inconvénients
    • On perd certains points de contexte : les moments de divergence, et de convergence (les merges),
    • La commande peut potentiellement poser des problèmes vis-à-vis des dépôts distants (que nous verrons dans le chapitre suivant),
    • La commande merge ne touche pas aux commits existants. La commande rebase réécrit l’historique : elle peut être potentiellement dangereuse.

Suppression d’une branche

La commande pour supprimer une branche est la suivante :

$ git branch -d <nom de la branche à supprimer>

A noter que l’on peut également utiliser le paramètre -D qui est équivalent à -d -f (--delete --force)

Figure 24: Suppression d’une branche
Figure 24: Suppression d’une branche
Les commits de la branche feature ne sont pas effacés. Ils subsistent, et peuvent être listés grâce à la commande git reflog, qui référence l’historique du pointeur HEAD.

Stratégie de fusion et flux de travail

Avec ce chapitre, nous commençons à voir qu’utiliser GIT n’est pas qu’une question technique, mais c’est également une question de méthodologie, surtout lorsque l’on travaille en groupe, avec des dépôts distants.

Il est important de définir des conventions, et une méthodologie commune de travail sans quoi,

  • Il y aura toujours un risque de perte des modifications,
  • L’historique de ces modifications deviendra illisible, et la traçabilité deviendra quasiment impossible.

Conclusion

Nous venons d’aborder la notion de branche dans GIT. Nous en avons profité pour “jouer” un peu avec les pointeurs. Les épisodes 2 et 3 montrent bien ce que j’annonçais dans l’épisode 1 : bien comprendre l’outil, c’est avant tout savoir ou l’on se trouve : à la fois dans les différentes zones de stockage, et à la fois dans notre arbre de commits.

Nous allons continuer notre apprentissage de GIT, en abordant, dans le prochain épisode, les dépôts distants.

Références

Commentaires