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

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 

480b8fd  (origin/master, origin/HEAD) Merge branch 'python-hello-world'
Wed Oct 14 15:27:22 2020 +0200 Jules Chevalier 

$ git log conflict-branch
e48246c  (conflict-branch) create conflict file on conflict branch
Thu Feb 4 11:51:34 2021 +0100 Jules Chevalier 

480b8fd  (origin/new-feature, origin/master, origin/HEAD) Merge branch 'python-hello-world'
Wed Oct 14 15:27:22 2020 +0200 Jules Chevalier 

# 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

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 

e58a6b9  create conflict file on master
Thu Feb 4 11:57:26 2021 +0100 Jules Chevalier 

e48246c  (conflict-branch) create conflict file
Thu Feb 4 11:51:34 2021 +0100 Jules Chevalier 

480b8fd  (origin/new-feature, origin/master, origin/HEAD) Merge branch 'python-hello-world'
Wed Oct 14 15:27:22 2020 +0200 Jules Chevalier 

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

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.

Exemple de résolution de conflit avec Meld, présentant la version finale au centre
Exemple de résolution de conflit avec Kdiff3, présentant l’ancêtre commun à gauche, et la version final en bas

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…