La pression de cet article après un bon gros titre putaclic comme ça, j’vous dit pas.

En fin d’études, 2013, j’ai utilisé git pour la première fois.

svn était encore bien accroché, donc la question qui revenait souvent c’était

Quel intérêt par rapport à *svn*?

Pour le coup… Je n’avais pas su répondre à l’époque. _Étant très habitué à svn j’avais fait l’erreur d’utiliser git comme *svn.

De l’eau a coulé sous les ponts et j’ai maintenant beaucoup moins de mal à répondre à cette question.

De mon point de vue, les 2 avantages majeurs sont :

Je ne vais pas parler du mode distribué (ou très très peu).

Le gros sujet ici, c’est le système de branche, un système qui permet de faire tout et n’importe quoi.

Git est un super outil, mais c’est aussi un outil compliqué.

Et comme n’importe quel outil, on ne va pas -forcément- chercher comment il fonctionne tant qu’il fonctionne.

Pire, avec git il y a plein de GUIs qui permettent de ne pas se soucier de ce qui se passe derrière les rideaux…

Les devs qui utilisent sourcetree, gitkraken, etc, sans comprendre les mécaniques derrière.:
meme généré

Pour parer à ce problème, des flows ont été créés.

Le plus populaire était celui là : https://nvie.com/posts/a-successful-git-branching-model/.

Ce n’est pas mal comme flow.

Il implique une gestion de branche correcte, mais il laisse quand même la porte ouverte à* un beau bordel*.

Et ce n’est pas parce qu’il est mauvais ! Au contraire

Mais bien parce que les devs l’appliquent sans le comprendre !

J’ai un gros problème avec un workflow non maitrisé parcequ’on se retrouve vite avec un arbre en “plan de métro”.

Et même si, parfois, ça peut être utilie, ce n’est généralement pas le cas.

Je sais que mes propos peuvent être difficiles à visualiser, pour cela, je vais vous raconter une histoire…

🤡 C’est l’histoire d’un projet

Bob est en chargé d’un projet.

Il se dit qu’il va utiliser git parce que c’est l’outil à la mode.

Bob ne va pas trop chercher comment tout cela fonctionne… à partir du moment ou cela fonctionne.

On considère ici que l'on fera des commits de merge.
Donc pas de fast-forward même s'ils sont possibles, git merge --no-ff
On considère que Bob et Alice utilisent un dépôt distant de type github, gitlab, bitbucket ou autre.
Ce dépot distant est unique et sera appelé origin.
On considère aussi que les branches ne sont jamais mergées en local, mais tout le temps via le dépôt distant et un principe de pull request / merge request.
Dans les illustrations qui vont suivre, les ronds représentent des commits.
Les étiquettes de couleur, quant à elles, représentent des références, vers des branches par exemple, qu'elles soient locales ou distantes (sur origin).

🥇 First commit

Au début tout se passe bien. Bob fait le fameux “First commit”, ici bleu.

First commit

⤴️ Premières features

Comme Bob n’est pas un sauvage, il va tout de suite partir sur un système de branches pour les features, fix, et autres.

Il crée donc une première branche qui part du commit first commit pour réaliser sa feature verte

Mais Bob veut aller vite.

Il fait donc 2 features sur la même branche, les features verte et orange.

First merge 1

Bob ouvre une pull request pour sa première branche.

Alice fait la review.

Au départ, elle tilte sur le fait qu’il y ait 2 features dans une branche. Cela prend donc un peu de temps.

Mais après quelques échanges, elle accepte la PR, la branche feat/verte de Bob est mergée dans develop.

Le commit jaune est créée.

First merge 2

🏎 Pas le temps d’niaiser

En attendant qu’Alice vérifie la PR feat/verte dont on parle juste avant, et donc avant que les features verte et orange ne soient mergées, Bob a créé une autre branche pour avancer sur la feature rouge.

Malin ce Bob.

Pour cette branche, il avait besoin d’un tout petit bout de code de la feature verte. Donc, il est reparti du commit vert et non pas du commit bleu. (Normal, le commit jaune n’existait pas à l’époque).

Premier conflit

Une fois que la feature rouge est finie Bob ouvre une pull request.

Malheureusement, entre temps, Alice avait validé la PR feat/verte.

Cette branche avait donc été mergée et le commit jaune existait… (voir ci-dessous).

Et le problème dans cette histoire, c’est que dans la feature rouge et dans la feature orange Bob a modifié le même fichier, au même endroit.

Cela implique que dans le commit jaune, la modification du fichier vennant du commit orange est présente.

Or, dans la PR de Bob pour la feature rouge, on demande à git de modifier le même fichier au même endroit.

On a donc un conflit.

Premier clonflit 2

Bon rien de catastrophique, c’est même plutôt courant sur git.

Bob ouvre sa PR, on résout cela dans le commit de merge.

Ce sera le commit violet.

Conflit 3

🤝 Alice à la rescousse (ou comment éviter les conflits)

Bob s’est quand même fait remonter les bretelles.

Alice, qui a dû s’occuper de la PR rouge, n’avait aucune envie de résoudre les conflits de_ Bob_.

Alors pour les prochaines fois, Bob est prié de bien vouloir les corriger avant !

Bien compris” dit-il !

Facile, il me suffit de merger develop dans ma branche, résoudre les conflits et faire une PR par la suite.

Quelle bonne idée.

Grâce à cela, Alice n’aura pas à résoudre les conflits, qui seront résolus dans le commit violet.

Son travail sera de valider la PR depuis le commit violet et cela créera le commit gris.

Conflit de merge

😏 Oops, un petit fix

Dans la suite, on utilise sur les mêmes couleurs, mais on ne parle pas des mêmes commits/branches, on va se dire qu'on est quelques temps plus tard._

Les aventures de Bob ne s’arrêtent pas là.

Au cours du projet, une feature verte a été mergée un peu trop vite

(Bob a fait un self-validate de sa PR verte).

Ce qui devait arriver arriva, il a introduit un bug dans develop.

Introduction de bug

Alice, en pullant develop, l’a remarqué et a tout de suite alerté Bob.

Réactif, Bob est reparti de sa branche verte.

Il a rapidement créé un fix, jaune, ouvert une PR et mergé ce dernier dans develop, créant ainsi le commit rouge pour corriger tout cela.

Correction du bug

Ouf.

👿 C’est une urgence pour la démo

Bob, était préssé par Carole la PO / Chef de projet / Scrum Master de son projet.

Elle s’était engagée (oui, elle), auprès du client, il fallait faire la feature rouge pour la déployer avant la démo le lendemain matin.

Comme la dernière fois, il avait besoin d’un bout de vert et n’avait pas le temps d’attendre la validation d’Alice.

En plus quand il a vu qu’il avait créé un bug et qu’il a dû le corriger, il était bien content de ne pas avoir attendu.

Au moins, la feature rouge était prête à temps.

Merge du fix

✌️ Player 2 has entered the game

Ainsi s’achèvent (presque) les aventures de Bob au pays de git.

Fort heureusement, il n’était pas seul sur le projet, et avec Alice, ils n’étaient pas peu fiers du travail accompli.

Quant au dépôt git ? Qui s’en soucie vraiment ? À la fin il était plutôt propre non ?

Il ressemblait peut-être à ça ? Ou peut-être pire.

💩 On arrête de faire du sale

Mais alors, comment est-ce que Bob peut éviter tout cela ?

On a identifié plusieurs problèmes:

Finalement, on veut réécrire l’histoire de ce qui s’est passé.

Ça tombe bien il existe un outil super pour ça: le rebase

L'idée ici est de jouer avec l'historique git. Une autre façon de faire est de merger en utilisant la statégie "squash" en fast forward. On n'en parlera pas ici car ce n'est pas le sujet.

🔀 Anatomie d’un rebase (interactif)

Je vais volontairement être réducteur ici, mais, en gros.

Le git rebase c’est un outil qui va permettre de réécrire l’histoire d’une branche. Pour cela, il va déplacer le commit de départ d’une branche vers un autre commit.

Mais cela ne s’arrête pas là, on peut aller encore plus loin en faisant un rebase interactif !

Cela va nous permettre beaucoup plus de choses:

Et cela va nous aider pour garder un dépôt propre qui tient plus d’une ligne de TGV que du métro parisien.

Pour faire cela, le rebase va dupliquer les commits de votre branche un a un.

Puis il va supprimer les anciens.

On est donc sur une opération destructrice 💥 et il faut faire attention.

De plus, comme on supprime les commits, les références changent. Donc git va avoir tendance à être un peu perdu (notemment avec les branches sur vos dépôts distants).

Mais pas de panique, nous sommes des professionnels (et on va voir quelques exemples pour comprendre tout cela)

Comment se passe un rebase ?

On se place sur un commit (ou une branche plus généralement), disons la référ_ence à rebaser_.

Ensuite, on définit la référence d’où on veut partir (quel commit ou quelle branche), appelons là référence de départ. Et pour finir on exécute la commande de rebase

# On se met sur la référence à rebaser
git checkout "reference-à-rebaser"
# On rebase
git rebase -i "reference-de-départ"

Le -i est là pour spécifier qu’on veut un rebase “interactif”.

Grâce à cela, git va faire la liste des commits qu’il y a entre les 2 et nous laisser choisir quoi en faire.

Si cette option n’est pas spécifiée, git va juste prendre tous les commits et les déplacer.

Le fait de spécifier -i ajoute une étape à notre rebase. En effet git va faire la liste et ouvrir votre éditeur par défaut pour vous la présenter.

Par exemple, si on a 3 commits sur notre branche, git affichera cela:

pick 22d839f message du commit 1
pick 3f31c4b message du commit 2
pick 4f2a285 message du commit 3

# Rebase 6d846a1..2b3055d onto 2b3055d (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.

C’est notre responsabilité de décider quoi faire en remplaçant les pick par un des mots-clés expliqués dans commands.

Une fois cela fait, on sauvegarde (généralement, c’est vim qui est utilisé alors on sauvegarde en faisant ESC + :wq + ENTER) et git rebase fait le reste.

Maintenant qu’on a posé les bases, voyons quelques exemples.

🦿 Le cas du multi-feature sur la même branche

Constat de départ

On part de ce constat là:

git checkout -b feat/verte
git commit -m "vert"
git commit -m "orange"
git push origin feat/verte

First merge

Bob a donc 2 commits, vert et orange, sur sa branche feat/verte, et origin est à jour sur sa branche (origin/feat/verte est au même niveau que feat/verte)

Or Bob aurait dû la séparer en 2 et faire 2 PR. Une pour la feature verte et une pour orange. Alors comment réparer ça ?

Créer une branche feat/orange

La première étape va être de créer une branche pour orange, appelons là feat/orange.

# Bob se remet sur le commit orange
git checkout "orange"
# Depuis ce commit, Bob créé une nouvelle branche
git checkout -b feat/orange 
# Bob push sa nouvelle branche
git push origin feat/orange

Checkout

Replacer feat/vert

# Bob se remet sur sa branche feat/vert
git checkout feat/vert
# Bob va forcer la branche "verte" à se remettre sur le commit vert (pour connaitre la sh1, il aura fait un git log --oneline)
git reset --hard "sha1-commit-vert"
# Bob push la branche verte en "forcant" parce que le dépot est en avance
git push -f origin feat/vert

Push du dépot

Pourquoi l’option -f pour le git push est nécessaire ici ?

Sur le dépôt distant, la branche verte est en “avance” par rapport à celle locale car sur le commit feat/orange.

Si vous essayez de push, git va le remarquer et refuser le push en vous incitant à pull d’abord pour récupérer les modifications.

Cela est une très mauvaise idée de faire cela.

Ça reviendrait à faire un merge de feat/orange dans feat/vert, ce qui n’est absolument pas ce que l’on veut.

Ce que nous voulons, c’est FORCER feat/vert à se déplacer sur le commit vert, d’où le git push -f.

C’est un bel exemple qui montre que quand on sait comment cela fonctionne on ne doit pas croire tout ce que git nous dit.

Première PR verte

Une fois cela fait, on ouvre la PR verte et Bob attend qu’elle soit validée et mergée. Il laisse orange tel quel pour l’instant.

PR ouverte

Rebaser feat/orange

Maintenant que la PR verte est mergée, Bob va rebaser orange.

# Bob se remet sur sa branche feat/orange
git checkout feat/orange
# Bob va mettre à jour son dépot pour develop (qui contient le commit jaune)
git fetch
# Ensuite il va rebaser sa branche feat/orange sur origin/develop
git rebase -i origin/develop
# Et pusher sa branche
git push -f origin feat/orange

Rebase visible

Alors, techniquement qu’est-ce qui s’est passé ici ?

Pour commencer, git a recréé un autre commit orange, il ne l’a pas “déplacé”.

Rebase visible

D’ailleurs, c’est pour cela qu’on doit push -f, *sinon, sur origin, nous aurons encore l’ancien commit *orange, git refusera donc le push en nous disant de récupérer les modifications, comme expliqué précédement.

J’insiste sur le sujet car c’est l’erreur la plus répandue. Chaque fois que je parle de rebase, on m’explique qu’on a “cassé un dépôt” en l’utilisant et c’est généralement à cause de ça !

Il faut bien comprendre que le contenu des 2 commits est exactement le même ! Donc si on pull, c’est la catastrophe, chaque ligne modifiée est considérée comme un conflit.

Le contenu des 2 commits oranges est exactement le même, mais ce sont bien 2 commits différents.

Donc d’un point de vue git c’est normal qu’il refuse le push !

Pour lui on est à la fois en avance d’un commit (le nouveau orange) et en retard d’un commit (l’ancien orange).

C’est donc parce qu’on sait que l’ancien commit orange ne sert plus à rien qu’on peut l’écraser avec le nouveau avec un git push -f.

Rebase visible

Une fois qu’on en est là, techniquement, plus aucune “référence” facile d’accès (comme un tag ou une branche) n’est reliée au commit feature orange original. D’un certain point de vue il est “perdu” (bon ce n’est pas vraiment le cas, mais c’est une autre histoire)

Rebase visible

Une PR et on y va

Partant de là, il ne reste plus qu’à ouvrir une PR et on aura un arbre qui ressemblera à cela

Merge

🌪 Avancer sur une feature avec une branche pas mergée

Dans cette situation, finalement, c’est très semblable avec ce qu’on a vu ci-dessus.

Bob a créé une branche feat/rouge</span> depuis feat/verte</span> et a travaillé dessus.

Conflit

Dans un cas où nous n’avons pas de conflit, on reproduit la méthode vue avant et tout se passe bien

# Bob se met sur sa branche violette
git checkout feat/violet
# Bob se met à jour
git fetch
# Bob rebase sa branche
git rebase -i origin/develop
# Bob push
git push -f origin feat/violet

Arbre git - conflit

💥 Le cas d’un conflit

Le problème dans la situation précédente, et nous l’avons vu, c’est si Bob modifie le même fichier dans feat/orange et feat/violet et que cela crée un conflit.

(ce n’est pas toujours le cas, mais pour les besoins de l’article c’est le cas)

Arbre git - conflit

Dans cette situation, on pourrait se dire qu’on n’a qu’a merger origin/develop dans notre branche.

Nous aurions quelque chose qui ressemblerait à

Arbre git - conflit

Mais c’est exactement ce qu’on veut éviter de faire avec les rebase. On ne va donc pas partir sur cette option !

Dans ce cas, le rebase va nous alerter qu’il y a un conflit et nous demander de le corriger avec un beau message bien explicite.

CONFLICT (add/add): Merge conflict in >>FILE_NAME<<
Auto-merging >>FILE_NAME<<
error: could not apply >>SH1<<... violet
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply >>SH1<<... violet

Ce que nous dit git ici:

  1. il y a un conflit dans le fichier FILE_NAME,
  2. 2 ajouts au même endroit (add/add)
  3. Les commits problématiques SH1

Et qu’on a plusieurs options

Bob va prendre l’option 1, il sait ce qu’il a fait, il peut corriger le conflit.

Notez que le conflit qui apparait serait apparu quoi qu'il arrive, c'est le fameux conflit qu'Alice a dû gérer dans notre situation de départ.
# Bob regarde quel fichier pose problème car il n'a pas lu le message
git status
# Bob voit que le problème est sur le fichier FILE_NAME
# Bob ouvre le fichier et corrige le conflit manuellement
# Bob va maintenant ajouter le fichier fraichement corrigé 
git add FILE_NAME
# Bob fait savoir a git que le conflit est résolut et que le rebase peut reprendre
git rebase --continue
# Bob push 
git push -f origin feat/violet

Arbre git - conflit

Après le rebase, (mais avant le push) Bob a corrigé les conflits dans sa branche locale. Comme expliqué plus haut, une fois que Bob aura git push -f sa branche, le commit feature rouge original “disparaitra”

L’avantage ici:

  1. On n’a pas de conflit au moment de la PR ! Donc Alice n’aura pas à résoudre les problèmes que Bob a causés
  2. Mais surtout : on n’a pas besoin de faire un merge de develop dans notre feat/violet pour récupérer les modifications ! (C’est tout l’avantage !!)

On passe donc de ça:

Arbre git - conflit

A ça:

Arbre git - rebase

Propre

☄️ Un petit fix ?

Dans cette situation, Bob avait mergé “trop tôt” et créé un problème dans develop.

Déjà ça n’aurait pas dû arriver, on ne merge pas si ce n’est pas testé.

Maintenant, que celui ou celle à qui ça n’est jamais arrivé jette la première pierre à Bob.

Personne.

Donc comment on fait ?

DISCLAIMER : Ici on va être un peu dangereux, mais c'est aussi pour montrer qu'on peut faire tout et n'importe quoi et qu'il faut rester maitre de son dépôt.

Ce qu’on veut éviter c’est ça:

Arbre git - merges moches

Remontons le fil.

Au moment où Alice alerte Bob, nous en sommes là:

Arbre git

Voici ce que Bob et Alice décident de faire

Remettre develop au bon endroit

# Bob remet de suite develop à l'état du commit bleu
git push -f origin "sha1-commit-bleu":develop
# Bob récupère l'info sur son dépot local
git fetch

Arbre git - conflit

Faire les fix en local

Bob fait ses fix en local jusqu’à éliminer le problème, mais ne push pas (observez origin/feat/verte</span> et feat/verte</span>)

Arbre git - fix

Une fois arrivé ici, le fix fonctionne. Bob va donc faire un rebase et merger tout cela:

Réécrire l’histoire

Depuis le début, on travaille sur 1 commit.

Lorsqu’on fait un git rebase -i, git affiche un “résumé” de ce qui va se passer dans l’éditeur selectionné (souvent vim ou nano), il faut alors “valider” cela pour continuer.

Dans les situations précédentes, on se contentait de sauvegarder et retour, ici cela va être un peu différent.

Quand Bob a fait son git rebase -i origin/develop, voila ce que git lui affiche:

pick 22d839f feature verte
pick 3f31c4b fix 1
pick 4f2a285 fix 2
pick 2b3055d fix 2 test

# Rebase 6d846a1..2b3055d onto 2b3055d (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.

Et c’est assez explicite, ce que nous voulons faire c’est que fix 1, fix 2 et fix 2 test n’apparaissent pas, et soit mergés dans vert !

Pour cela, on va modifier avant de sauvegarder en utilisant fixup

pick 22d839f feature verte
# On remplace le pick par f (pour fixup)
f 3f31c4b fix 1
# On remplace le pick par f (pour fixup)
f 4f2a285 fix 2
# On remplace le pick par f (pour fixup)
f 2b3055d fix 2 test

Bob enregistre ce fichier, le rebase se passe et créé un nouveau commit “vert” qui contient les 4 commits !!

Avant que Bob ne push sa nouvelle branche feat/verte, nous sommes donc dans cette situation

Arbre git

Le commit vert a un thème différent car il est différent.

En revanche, comme nous n’avons toujours pas push la branche feat/verte, le dépot origin a toujours l’ancienne référence.

git push -f origin feat/verte

Bob ouvre une PR, qu’Alice valide et nous avons donc ce résultat

Arbre git

On peut modifier un peu cette image pour al rendre plus propre.

En effet, les commits “vert” et merge sont “perdus” (ou disons qu’on n’a plus de pointeur pour y accéder).

On pourrait donc afficher:

Arbre git

Et tout est bien qui fini bien.

Attention cela dit:

Lorsque nous avons forcé la mise à jour de develop, c’était une opération dangereuse.

develop est une branche “partagée” et modifier son emplacement à la volée n’est pas une chose à prendre à la légère. Cela peut engendrer des soucis sur les dépôts des gens qui travaillent avec vous car leur référence est invalide !!

(C’est d’ailleurs pour cela que je n’ai jamais parlé de branche develop locale, on peut tout à fait s’en passer !!!)

Cet exemple est là pour montrer ce qu’il est possible de faire, mais il peut être très dangereux de modifier une branche “partagée” comme cela. Dans un monde où on n’est pas certain de ce que l’on fait, il vaut mieux rouvrir une nouvelle branche et faire un fix. Tant pis pour le merge foireux, c’est toujours mieux que de perdre une après-midi à remettre les dépôts des autres à jour.

🧠 En conclusion

Pourquoi faire tout cela ?

Pour l’amour du code et d’un dépôt propre ? S’il n’y a que ça.

En fait, avoir un dépôt propre, ce n’est pas juste pour se la jouer Sheldon Cooper, ça a beaucoup d’avantages.

En revanche, il faut être attentif à ce que l’on fait.

Et les GUI dans tout cela ?

Tout ce qui est expliqué ici est faisable dans les GUI, qui sont généralement très bien faits.

Mais juste comprendre sur quel bouton appuyer sans pour autant comprendre la mécanique derrière peut poser beaucoup de problèmes.

L’exemple le plus classique est celui du push refusé après rebase et d’un pull pour corriger le souci : c’est ce que le gui affichera “classiquement” et cela incite à l’erreur si on ne comprend pas pour quelles raisons cela arrive.


Au fait, vous avez remarqué que jamais Bob n’utilise de branche develop ou master en local? Je pourrais vous dire qu’il n’y a aucun interêt a avoir une copie de ces branches sur son poste :) Mais c’est un autre débat.

🔗 Liens utiles

Tout ça ne sort pas de ma tête, il existe un nombre de ressources intéressantes sur le sujet. Voici quelques-unes qui m’ont bien aidé

https://nvie.com/posts/a-successful-git-branching-model/ (oui il est bien !) https://www.youtube.com/watch?v=rt-9mPaYtKo (très intéressant) https://www.atlassian.com/fr/git/tutorials/rewriting-history/git-rebase

Merci aussi à Jean-Baptiste pour sa relecture et ses conseils :)