Git #4 : les conflits
Il y a à mon sens (et c’est un avis très personnel) 3 verrous à l’apprentissage de Git : comprendre la notion de commit, savoir utiliser correctement les branches, et gérer les conflits.
Un conflit a lieu lorsque l’on fusionne deux versions (branches, commits…) d’un projet possédant des modifications incompatibles. Globalement, il s’agit de modifications sur les mêmes lignes d’un même fichier. Git est capable de résoudre de lui même les conflits les plus simples, comme la modification d’un même fichier mais à deux endroits distincts. En revanche, lorsqu’un fichier a été modifié dans deux branches différentes aux mêmes lignes, seuls les développeurs concernés peuvent résoudre le conflit résultant.
Dans le cas d’un conflit, la fusion de deux branches (comme un merge
) va se solder par un échec et un message de Git nous invitant à résoudre les conflits avant de poursuivre la fusion.
Création du conflit Git
Pour des besoins de démonstration, créons artificiellement un conflit. Deux branches seront utilisées : conflict-branch
et master
. Le fichier conflict-file.txt
est créé dans chaque branche, avec la ligne line from conflict branch
sur la branche conflict-branch
et line from master
sur master
.
Voici un petit tour du repository après cela.
# Voyons dans un premier temps la différence au niveau des commits $ git log master e58a6b9 (HEAD -> master) create conflict file on master Thu Feb 4 11:57:26 2021 +0100 Jules Chevalier <jchevalier@peaks.fr> 480b8fd (origin/master, origin/HEAD) Merge branch 'python-hello-world' Wed Oct 14 15:27:22 2020 +0200 Jules Chevalier <jchevalier@peaks.fr> $ git log conflict-branch e48246c (conflict-branch) create conflict file on conflict branch Thu Feb 4 11:51:34 2021 +0100 Jules Chevalier <jchevalier@peaks.fr> 480b8fd (origin/new-feature, origin/master, origin/HEAD) Merge branch 'python-hello-world' Wed Oct 14 15:27:22 2020 +0200 Jules Chevalier <jchevalier@peaks.fr> # Maintenant plus en détail les commits qui diffèrent entre les deux branches $ git show e58a6b9 e58a6b9 (HEAD -> master) create conflict file on master diff --git a/conflict-file.txt b/conflict-file.txt @@ -0,0 +1 @@ +line from master $ git show e48246c e48246c (conflict-branch) create conflict file on conflict branch diff --git a/conflict-file.txt b/conflict-file.txt @@ -0,0 +1 @@ +line from new branch</jchevalier@peaks.fr></jchevalier@peaks.fr></jchevalier@peaks.fr></jchevalier@peaks.fr>
Maintenant que tout est en place, déchaînons les cieux : il est temps de fusionner les branches. Comme précédemment, on revient sur master
pour rapatrier les modifications de conflict-branch
, créant ainsi un conflit puisque la même ligne du même fichier a été modifiée dans les deux branches.
$ git checkout master Switched to branch 'master' $ git merge conflict-branch CONFLICT (add/add): Merge conflict in conflict-file.txt Auto-merging conflict-file.txt Automatic merge failed; fix conflicts and then commit the result.
Résolution de conflit
Il s’agit maintenant de résoudre le conflit, autrement dit de départager les versions qui s’affrontent. Pour faciliter ce travail, git modifie le fichier problématique en ajoutant les deux possibilités contradictoires, au développeur de faire le choix. Pour représenter ces possibilités, Git utilise des marqueurs de conflits
.
$ cat conflict-file.txt <<<<<<< HEAD line from master ======= line from new branch >>>>>>> conflict-branch
Ici, Git indique que deux versions sont en conflit : la première, HEAD
(qui représente la position actuelle du repository, ici master
),
et la seconde venant de conflict-branch
. Pour résoudre le conflit, il faut choisir la version que l’on souhaite garder et enlever les marqueurs de conflit, ce qui donnera ceci.
$ cat conflict-file.txt line from new branch
Ensuite, git status
nous indique la marche à suivre.
$ cat conflict-file.txt line from master $ git status On branch master Your branch is ahead of 'origin/master' by 1 commit. (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 ..." to mark resolution) both added: conflict-file.txt
Git nous donne deux nouvelles indications : fix conflicts and run git commit
et use git add
to mark resolution. C’est exactement la procédure que nous allons suivre : utiliser git add
pour marquer la résolution de notre conflit sur le fichier conflict-file.txt puis terminer la résolution des conflits avec git commit
.
Note: Git propose au moment du
git commit
de modifier le commit de merge, ce qui n’est pas nécessaire.
$ git add conflict-file.txt $ git commit [master 8eada8a] Merge branch 'conflict-branch' $ git status On branch master Your branch is ahead of 'origin/master' by 3 commits. (use "git push" to publish your local commits) nothing to commit, working tree clean $ git log 8eada8a (HEAD -> master) Merge branch 'conflict-branch' Thu Feb 4 14:46:48 2021 +0100 Jules Chevalier <jchevalier@peaks.fr> e58a6b9 create conflict file on master Thu Feb 4 11:57:26 2021 +0100 Jules Chevalier <jchevalier@peaks.fr> e48246c (conflict-branch) create conflict file Thu Feb 4 11:51:34 2021 +0100 Jules Chevalier <jchevalier@peaks.fr> 480b8fd (origin/new-feature, origin/master, origin/HEAD) Merge branch 'python-hello-world' Wed Oct 14 15:27:22 2020 +0200 Jules Chevalier <jchevalier@peaks.fr> </jchevalier@peaks.fr></jchevalier@peaks.fr></jchevalier@peaks.fr></jchevalier@peaks.fr>
La branche master
est maintenant à jour ! En tout, on se retrouve avec 3 nouveaux commits : le commit déjà présent sur master
, le commit rapatrié depuis la branche conflict-branch
, et pour finir le fameux commit de merge. Il s’agit d’un commit spécial, qui porte les modifications liées à la résolution du conflit. Son message de commit permet de situer la fusion des branches dans l’historique, d’où l’intérêt de ne pas le modifier.
Comme toujours, il ne reste qu’à envoyer la nouvelle version de master
sur le serveur en utilisant git push
et la fusion sera complètement effective.
Les outils de merge de Git
Bon, soyons honnêtes, changer à la main un conflit d’une ligne en supprimant 3 lignes et 2 balises, ça passe. Mais lorsque l’on a à faire à un vrai conflit, réparti sur plusieurs fichiers contenants chacun plusieurs zones conflictuelles… Il nous faut un outil graphique plus adapté pour la résolution d’un tel conflit.
Il en existe un certain nombre, avec un fonctionnement similaire : afficher côte à côte les différentes versions en conflits pour aider l’utilisateur à régler « visuellement » le conflit, en affichant également la version « finale » qui sera conservée après la résolution du conflit. Certains outils proposent en plus la version « ancêtre commun », qui représente le dernier commit en commun avec les deux branches en conflit (dans notre exemple, il s’agit du commit 480b8fd Merge branch 'python-hello-world'
). Ces trois versions permettent d’avoir une vision de l’ensemble du conflit, avec à la fois les deux versions proposées, mais aussi la version « d’origine » pour mieux comprendre les changements. Enfin, ces outils permettent de valider graphiquement la version à garder et de passer rapidement d’un conflit au suivant.
Git, conclusion
La résolution des conflits peut (sans mauvais jeu de mot) poser problème. Au delà de la difficulté de choisir la bonne version à garder, une mauvaise résolution peut casser le code existant, par exemple en laissant traîner des marqueurs de conflits qui rendent le code inutile…
En utilisant des outils graphiques pour régler efficacement et visuellement les conflits, on échappe au pire en s’assurant de ne pas perdre de code. Reste à avoir une idée claire de l’intention des auteurs des différentes versions du projet afin de conserver la bonne version du code…