on contextualise¶
on a vu tout à l’heure un exemple de fusion (merge) qui s’est passé sans douleur ni surprise - il faut dire qu’on avait tout bien choisi pour :)
mais toutes les fusions ne se comportent pas de la même manière, et dans ce notebook très court on va essayer de couvrir un peu tous les cas de figure :
- d’abord les cas simples, où il n’y a rien à faire (le fast-forward)
- et ensuite au contraire, le cas compliqué, la fusion échoue à cause d’un conflit
rappels¶
on rappelle que la forme générale de la fusion c’est
git merge <to-merge>
et que l’effet escompté, c’est qu’on se retrouve à la fin sur
un commit qui contienne à la fois le code courant et celui du commit to-merge
(que comme d’habitude on peut exprimer comme un nom de branche, un SHA1, le résultat d’une navigation avec les opérateurs ~
et ^
, etc…)
le fast-forward¶
une fusion fast-forward, c’est le cas où le commit courant est déjà un parent (transitivement) de to-merge
; comme par exemple ici:
- la branche courante est
main
(et c’est pour ça qu’elle est encadrée en rouge), et on fusionne avec le commitdevel
- on cherche donc à créer un commit qui contienne à la fois de code de
main
et dedevel
- mais, attendez : le commit
A
est un parent deC
- et donc par définition
C
vérifie déjà la bonne propriétéC
contient main (qui est un de ses parents)C
contientdevel
(c’estdevel
)
et du coup dans ce cas de figure, la fusion n’a même pas besoin de créer un commit
il suffit de faire avancer la branche courante (d’où le terme de fast-forward)
(bien sûr ça signifie aussi de modifier l’index et les fichiers;
il faut toujours lancer la fusion sur un dépôt propre)
comment savoir si on est dans le cas du fast-forward ?¶
la règle qui permet de savoir si on est, ou pas, dans le cas d’un fast-forward, est simple :
si (et seulement si) il existe une chaine de parenté entre les deux commits (celui ou on est, et celui qu’on veut fusionner),
alors on est dans un fast-forward, il n’est pas besoin de créer un commit, il en existe déjà un qui matérialise la fusion
ici dans le premier exemple on avait to-merge=C → B → HEAD=A
, donc pas besoin de créer un commit, simplement besoin d’avancer la branche courante
dans l’autre sens
juste pour la curiosité, le cas se présente assez peu en pratique, mais que se passe-t-il d’après vous lorsque dans l’autre sens, on essaie de fusionner un parent ?
la réponse : rien du tout ! pourquoi ?
une “vraie fusion”¶
dans tous les autres cas - c’est-à-dire quand il n’existe pas de lien de parenté entre les deux commits - on ne peut pas trouver un commit existant qui convienne, on doit donc créer un nouveau commit
pour réaliser ça, git a recours à des outils tiers (diff
et diff3
) qui reposent sur le fait que les contenus sont textuels
Mais reprenons : la fusion fonctionne plutôt très bien avec le texte, tant que les deux modifications impactent des endroits différents dans les fichiers;
mais à l’impossible nul n’est tenu ! et si les deux branches touchent le même endroit du code, on atteint les limites de l’outil : la fusion échoue avec un conflit
mon premier conflit¶
allons-y, on va provoquer cette situation en pratique :
on va repartir d’un dépôt vierge, pour pouvoir expérimenter tranquillement, et on va commencer par y créer un fichier unique
on va créer deux branches distinctes qui touchent au même endroit du fichier
du coup la fusion va échouer en signalant un conflit
préparation¶
- je commence par créer un dépôt ad hoc; on a vu déjà toutes ces commandes :
mkdir my-first-conflict
cd my-first-conflict
git init
- je crée un fichier témoin, que j’appelle
form.txt
, avec ce contenu :
prof #1 Nom :
prof #1 Note :
---
prof #2 Nom :
prof #2 Note :
---
total :
- je crée un premier commit
git add form.txt
git commit -m"le formulaire vierge"
les deux branches¶
ce qu’on veut faire, c’est simuler deux changements faits en même temps par deux personnes différentes; disons qu’on a deux profs, Minerva McGonagall et Albus Dumbledore, qui remplissent chacun leur partie
pour simplifier on va dire que
- McGonagall utilise la branche
main
, et - Dumbledore utilise une branche
dumbledore
Ils partent donc tous les deux du formulaire vide, ils remplissent chacun leur partie au mieux, et leurs versions respectives du fichier sont
cat form-mcgonagall.txt
prof #1 Nom : McGonagall
prof #1 Note : 12
---
prof #2 Nom :
prof #2 Note :
---
total : 12
cat form-dumbledore.txt
prof #1 Nom :
prof #1 Note :
---
prof #2 Nom : Dumbledore
prof #2 Note : 15
---
total : 15
sauriez-vous simuler ce scénario ?
pour McGonagall, c’est simple, comme on est sur main il suffit de modifier form.txt
(1ère version, note 12) et de commiter
cp form-mcdonagall.txt form.txt
git add form.txt
git commit -m"notes mcgonagall"
pour la deuxième version, on a besoin de créer la branche dumbledore
et de revenir en arrière; on peut faire en un seul coup avec le raccourci git switch -c
git switch -c dumbledore HEAD~
rappel: git switch -c = git branch + git switch
pour rappel, on peut aussi décomposer, et le faire pas à pas en deux fois
# créer la branche
git branch dumbledore HEAD~
# et y aller
git switch dumbledore
à ce stade, le formulaire est à nouveau vide (car on est revenu en arrière)
on remplit alors la fiche dans la version dumbledore, et on commite exactement comme au dessus;
cp form-dumbledore.txt form.txt
git add form.txt
git commit -m"notes dumbledore"
et à ce stade le repo ressemble à ceci
$ git log --all --graph --oneline
* 34fa617 (HEAD -> dumbledore) notes dumbledore
| * 65b135a (main) notes mcgonagall
|/
* 6f201cc le formulaire vierge
le merge¶
à ce stade on a le choix :
- de se mettre sur
main
et de fusionnerdumbledore
- ou l’inverse, rester sur
dumbledore
, et fusionnermain
c’est relativement équivalent, mais en général on préfère se mettre sur main
, et c’est ce qu’on va faire ici
$ git switch main
Switched to branch 'main'
on peut maintenant essayer de fusionner
$ git merge dumbledore
Auto-merging form.txt
CONFLICT (content): Merge conflict in form.txt
Automatic merge failed; fix conflicts and then commit the result.
ouh là, il n’a pas l’air content !
ce qui se passe, c’est ceci
- les changements faits dans la zone “chacun chez soi” (en vert sur le dessin) peuvent être fusionnés sans souci
- par contre comme les deux branches ont modifié la ligne de total chacune de leur côté
la fusion ne sait pas quelle version retenir
voilà, on a créé un conflit, et du coup tout s’arrête…
conflit: état du repo¶
dans quel état est notre dépôt à ce stade ?
on n’a pas créé le commit de fusion, évidemment
git status
nous dit ceci$ git status On branch main 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: form.txt no changes added to commit (use "git add" and/or "git commit -a")
on apprend comme ça que c’est dans
form.txt
que se situe le souci
(bon nous on n’a qu’un seul fichier, mais quand il y en a 200 c’est une information intéressante)et
git merge
nous a laissé les conflits annotés directement dans le code
avec cette forme qui est facile à voir visuellement<<<<<<< HEAD total : 12 ⩶⩶ total : 15 >>>>>>> dumbledore
noter enfin qu’à ce stade, on ne peut plus utiliser la commande
git commit
pour créer un nouveau commit
il faut d’abord retourner dans un état propre: nettoyer le repo
nettoyer - option 1: revenir en arriére¶
pour cela, on a principalement deux choix :
- soit on décide que tout bien réfléchi, c’est beaucoup trop compliqué, on ne va pas s’en sortir, on veut juste tout défaire et revenir à l’état avant le
merge
, et pour ça on fait juste
$ git merge --abort
$ git status
On branch main
nothing to commit, working tree clean
ouf, on a tout effacé, on est exactement comme avant le merge
nettoyer - option 2: résoudre le conflit¶
- soit on décide de gérer, c’est-à-dire de passer sur les conflits (nous on n’en a qu’un) et de décider quoi faire;
faisons-le; nous ici on veut dire que le total, ça n’est ni 12 ni 15, mais 27
pour résoudre le conflit, on doit utiliser un éditeur de code; dans vs-code par exemple on verrait ceci

la bonne nouvelle, c’est qu’il a détecté la présence d’un conflit; il nous propose même des options toutes faites (Accept current change…) pour choisir l’une ou l’autre des deux versions
la mauvaise nouvelle, c’est qu’aucune des deux ne convient, et ce qu’on va faire simplement c’est d’écrire la ligne comme elle doit être, ça donne ceci:

à ce stade :
comme
git status
nous indique toujours qu’on a un Unmerged path, on va indiquer à git que le conflit est résolu, en faisantgit add form.txt
et là on peut concrétiser le merge en faisant
git merge --continue # ou encore si on préfère` git commit
petit détail, on ne peut pas passer de message sur la ligne de commande avec -m ici
et remarquez qu’on aurait tout aussi bien pu faire tout simplementgit commit -m"fusion après conflit résolu à la main"
et tout le monde est content
git log --oneline --graph * 4e94d65 (HEAD -> main) fusion après conflit résolu à la main |\ | * 065c80e (dumbledore) notes dumbledore * | 975505c notes mcgonagall |/ * 2230fb9 le formulaire vierge
et
git status On branch main nothing to commit, working tree clean git log --
et
cat form.txt prof #1 Nom : McGonagall prof #1 Note : 12 ‒‒‒ prof #2 Nom : Dumbledore prof #2 Note : 15 ‒‒‒ total : 27
résumé¶
à retenir à propos de ce notebook :
lorsque les deux commits à fusionner sont parents l’un de l’autre, la fusion n’a pas besoin de créer un nouveau commit;
c’est ce qu’on appelle une fusion fast-forwarddans le cas contraire, la création d’un commit est nécessaire
- les modifications disjointes peuvent être fusionnées automatiquement
- lorsque deux modifications changent la même zone de code par contre, la fusion atteint ses limites:
- on doit alors résoudre les conflits
- puis finaliser la fusion à la main en appelant
git commit