Git
Chapters ▾ 2nd Edition

7.8 Utilitaires Git - Fusion avancée

Fusion avancée

La fusion avec Git est généralement plutôt facile. Puisque Git rend facile la fusion d’une autre branche plusieurs fois, cela signifie que vous pouvez avoir une branche à très longue durée de vie que vous pouvez mettre à jour au fil de l’eau, en résolvant souvent les petits conflits plutôt que d’être surpris par un énorme conflit à la fin de la série.

Cependant, il arrive quelques fois des conflits compliqués. À la différence d’autres systèmes de contrôle de version, Git n’essaie pas d’être plus intelligent que de mesure pour la résolution des conflits. La philosophie de Git, c’est d’être malin pour déterminer lorsque la fusion est sans ambiguïté mais s’il y a un conflit, il n’essaie pas d’être malin pour le résoudre automatiquement. De ce fait, si vous attendez trop longtemps pour fusionner deux branches qui divergent rapidement, vous rencontrerez des problèmes.

Dans cette section, nous allons détailler ce que certains de ces problèmes peuvent être et quels outils Git vous offre pour vous aider à gérer ces situations délicates. Nous traiterons aussi quelques types de fusions différents, non-standard, ainsi que la manière de mémoriser les résolutions que vous avez déjà réalisées.

Conflits de fusion

Bien que nous avons couvert les bases de la résolution de conflits dans Conflits de fusions (Merge conflicts), pour des conflits plus complexes, Git fournit quelques outils pour vous aider à vous y retrouver et à mieux gérer les conflits.

Premièrement, si c’est seulement possible, essayer de démarrer d’un répertoire de travail propre avant de commencer une fusion qui pourrait engendrer des conflits. Si vous avez un travail en cours, validez-le dans une branche temporaire ou remisez-le. Cela vous permettra de défaire tout ce que vous pourrez essayer. Si vous avez des modifications non sauvegardées dans votre répertoire de travail quand vous essayez une fusion, certaines des astuces qui vont suivre risque de vous faire perdre ce travail.

Parcourons ensemble un exemple très simple. Nous avons un fichier Ruby super simple qui affiche « hello world ».

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

hello()

Dans notre dépôt, nous créons une nouvelle branche appelée whitespace et nous entamons la transformation de toutes les fins de ligne Unix en fin de lignes DOS, ce qui revient à modifier chaque ligne, mais juste avec des caractères invisibles. Ensuite, nous changeons la ligne « hello world » en « hello mundo ».

$ git checkout -b whitespace
Basculement sur la nouvelle branche 'whitespace'

$ unix2dos hello.rb
unix2dos: converting file hello.rb to DOS format ...
$ git commit -am 'converted hello.rb to DOS'
[whitespace 3270f76] converted hello.rb to DOS
 1 file changed, 7 insertions(+), 7 deletions(-)

$ vim hello.rb
$ git diff -w
diff --git a/hello.rb b/hello.rb
index ac51efd..e85207e 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,7 @@
 #! /usr/bin/env ruby

 def hello
-  puts 'hello world'
+  puts 'hello mundo'^M
 end

 hello()

$ git commit -am 'hello mundo change'
[whitespace 6d338d2] hello mundo change
 1 file changed, 1 insertion(+), 1 deletion(-)

À présent, nous rebasculons sur master et nous ajoutons une documentation de la fonction.

$ git checkout master
Basculement sur la branche 'master'

$ vim hello.rb
$ git diff
diff --git a/hello.rb b/hello.rb
index ac51efd..36c06c8 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello world'
 end

$ git commit -am 'document the function'
[master bec6336] document the function
 1 file changed, 1 insertion(+)

Et maintenant, nous essayons de fusionner notre branche whitespace et nous allons générer des conflits dûs aux modifications de fins de ligne.

$ git merge whitespace
Fusion automatique de hello.rb
CONFLIT (contenu) : Conflit de fusion dans hello.rb
La fusion automatique a échoué ; réglez les conflits et validez le résultat.

Abandonner une fusion

Nous avons ici plusieurs options. Une première consiste à sortir de cette situation. Vous ne vous attendiez peut-être pas à rencontrer un conflit et vous ne souhaitez pas encore le gérer, alors vous pouvez simplement faire marche arrière avec git merge --abort.

$ git status -sb
## master
UU hello.rb

$ git merge --abort

$ git status -sb
## master

L’option git merge --abort essaie de vous ramener à l’état précédent la fusion. Les seuls cas dans lesquels il n’y parvient pas parfaitement seraient ceux pour lesquels vous aviez déjà auparavant des modifications non validées ou non remisées dans votre répertoire de travail au moment de la fusion. Sinon, tout devrait se passer sans problème.

Si, pour une raison quelconque, vous vous trouvez dans une situation horrible et que vous souhaitez repartir à zéro, vous pouvez aussi lancer git reset --hard HEAD ou sur toute autre référence où vous souhaitez revenir. Souvenez-vous tout de même que cela va balayer toutes les modifications de votre répertoire de travail, donc assurez-vous de n’avoir aucune modification de valeur avant.

Ignorer les caractères invisibles

Dans ce cas spécifique, les conflits sont dûs à des espaces blancs. Nous le savons parce que le cas est simple, mais cela reste assez facile à déterminer dans les cas réels en regardant les conflits parce que chaque ligne est supprimée à une ligne puis réintroduite à la suivante. Par défaut, Git voit toutes ces lignes comme modifiées et il ne peut pas fusionner les fichiers.

La stratégie de fusion par défaut accepte quand même des arguments, et certains d’entre eux traitent le cas des modifications impliquant les caractères blancs. Si vous vous rendez compte que vous avez de nombreux conflits de caractères blancs lors d’une fusion, vous pouvez simplement abandonner la fusion et en relancer une en utilisant les options -Xignore-all-space ou -Xignore-space-change. La première option ignore complètement tous les espaces tandis que la seconde traite les séquences d’un ou plusieurs espaces comme équivalentes.

$ git merge -Xignore-all-space whitespace
Fusion automatique de hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

Puisque dans ce cas, les modifications réelles n’entraient pas en conflit, une fois les modifications d’espaces ignorées, tout fusionne parfaitement bien.

Ça sauve la vie si vous avez dans votre équipe une personne qui reformate tout d’espaces en tabulations ou vice-versa.

Re-fusion manuelle d’un fichier

Bien que Git gère le pré-traitement d’espaces plutôt bien, il existe d’autres types de modifications que Git ne peut pas gérer automatiquement, mais dont la fusion peut être scriptable. Par exemple, supposons que Git n’ait pas pu gérer les espaces et que nous ayons dû résoudre le problème à la main.

Ce que nous devons réellement faire est de passer le fichier que nous cherchons à fusionner à travers dos2unix avant d’essayer de le fusionner réellement. Comment pourrions-nous nous y prendre ?

Premièrement, nous entrons dans l’état de conflit de fusion. Puis, nous voulons obtenir des copies de la version locale (ours), de la version distante (theirs, celle qui vient de la branche à fusionner) et de la version commune (l’ancêtre commun depuis lequel les branches sont parties). Ensuite, nous voulons corriger au choix la version locale ou la distante et réessayer de fusionner juste ce fichier.

Obtenir les trois versions des fichiers est en fait assez facile. Git stocke toutes ces versions dans l’index sous formes d’étapes (stages), auxquelles chacune y a un nombre associé. Stage 1 est l’ancêtre commun, stage 2 est notre version, stage 3 est la version de MERGE_HEAD, la version qu’on cherche à fusionner (theirs).

Vous pouvez extraire une copie de chacune de ces versions du fichier en conflit avec la commande git show et une syntaxe spéciale.

$ git show :1:hello.rb > hello.common.rb
$ git show :2:hello.rb > hello.ours.rb
$ git show :3:hello.rb > hello.theirs.rb

Si vous voulez rentrer un peu plus dans le dur, vous pouvez aussi utiliser la commande de plomberie ls-files -u pour récupérer les SHA-1 des blobs Git de chacun de ces fichiers.

$ git ls-files -u
100755 ac51efdc3df4f4fd328d1a02ad05331d8e2c9111 1	hello.rb
100755 36c06c8752c78d2aff89571132f3bf7841a7b5c3 2	hello.rb
100755 e85207e04dfdd5eb0a1e9febbc67fd837c44a1cd 3	hello.rb

La syntaxe :1:hello.rb est juste un raccourcis pour la recherche du SHA-1 de ce blob.

À présent que nous avons le contenu des trois étapes dans notre répertoire de travail, nous pouvons réparer manuellement la copie distante pour résoudre le problème d’espaces et re-fusionner le fichier avec la commande méconnue git merge-file dont c’est l’exacte fonction.

$ dos2unix hello.theirs.rb
dos2unix: converting file hello.theirs.rb to Unix format ...

$ git merge-file -p \
    hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb

$ git diff -w
diff --cc hello.rb
index 36c06c8,e85207e..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,8 -1,7 +1,8 @@@
  #! /usr/bin/env ruby

 +# prints out a greeting
  def hello
-   puts 'hello world'
+   puts 'hello mundo'
  end

  hello()

À ce moment, nous avons un fichier joliment fusionné. En fait, cela fonctionne même mieux que l’option ignore-all-space parce que le problème d’espace est corrigé avant la fusion plutôt que simplement ignoré. Dans la fusion ignore-all-space, nous avons en fait obtenu quelques lignes contenant des fins de lignes DOS, ce qui a mélangé les styles.

Si vous voulez vous faire une idée avant de finaliser la validation sur ce qui a réellement changé entre un côté et l’autre, vous pouvez demander à git diff de comparer le contenu de votre répertoire de travail que vous êtes sur le point de valider comme résultat de la fusion avec n’importe quelle étape. Détaillons chaque comparaison.

Pour comparer votre résultat avec ce que vous aviez dans votre branche avant la fusion, en d’autres termes, ce que la fusion a introduit, vous pouvez lancer git diff --ours

$ git diff --ours
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index 36c06c8..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -2,7 +2,7 @@

 # prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

Donc nous voyons ici que ce qui est arrivé à notre branche, ce que nous introduisons réellement dans ce fichier avec cette fusion, n’est qu’une ligne modifiée.

Si nous voulons voir le résultat de la fusion modifiée depuis la version distante, nous pouvons lancer git diff --theirs. Dans cet exemple et le suivant, nous devons utiliser -w pour éliminer les espaces parce que nous le comparons à ce qui est dans Git et non pas notre version nettoyée hello.theirs.rb du fichier.

$ git diff --theirs -w
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index e85207e..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello mundo'
 end

Enfin, nous pouvons voir comment le fichier a été modifié dans les deux branches avec git diff --base.

$ git diff --base -w
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index ac51efd..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,8 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

À ce point, nous pouvons utiliser la commande git clean pour éliminer les fichiers supplémentaires maintenant inutiles que nous avons créés pour notre fusion manuelle.

$ git clean -f
Suppression de hello.common.rb
Suppression de hello.ours.rb
Suppression de hello.theirs.rb

Examiner les conflits

Peut-être ne sommes-nous pas heureux de la résolution actuelle, ou bien l’édition à la main d’un côté ou des deux ne fonctionne pas correctement et nécessite plus de contexte.

Modifions un peu l’exemple. Pour cet exemple, nous avons deux branches à longue durée de vie qui comprennent quelques commits mais créent des conflits de contenu légitimes à la fusion.

$ git log --graph --oneline --decorate --all
* f1270f7 (HEAD, master) update README
* 9af9d3b add a README
* 694971d update phrase to hola world
| * e3eb223 (mundo) add more tests
| * 7cff591 add testing script
| * c3ffff1 changed text to hello mundo
|/
* b7dcc89 initial hello world code

Nous avons maintenant trois commits uniques qui n’existent que sur la branche master et trois autres sur la branche mundo. Si nous essayons de fusionner la branche mundo, nous obtenons un conflit.

$ git merge mundo
Fusion automatique de hello.rb
CONFLIT (contenu): Conflit de fusion dans hello.rb
La fusion automatique a échoué ; réglez les conflits et validez le résultat.

Nous souhaitons voir ce qui constitue le conflit de fusion. Si nous ouvrons le fichier, nous verrons quelque chose comme :

#! /usr/bin/env ruby

def hello
<<<<<<< HEAD
  puts 'hola world'
=======
  puts 'hello mundo'
>>>>>>> mundo
end

hello()

Les deux côtés de la fusion on ajouté du contenu au fichier, mais certains commits ont modifié le fichier au même endroit, ce qui a causé le conflit.

Explorons quelques outils que vous avez à disposition pour déterminer comment ce conflit est apparu. Peut-être le moyen de résoudre n’est-il pas évident. Il nécessite plus de contexte.

Un outil utile est git checkout avec l’option --conflict. Il va re-extraire le fichier et remplacer les marqueurs de conflit. Cela peut être utile si vous souhaitez éliminer les marqueurs et essayer de résoudre le conflit à nouveau.

Vous pouvez passer en paramètre à --conflict, soit diff3 soit merge (le paramètre par défaut). Si vous lui passez diff3, Git utilisera une version différente des marqueurs de conflit, vous fournissant non seulement les versions locales (ours) et distantes (theirs), mais aussi la version « base » intégrée pour vous fournir plus de contexte.

$ git checkout --conflict=diff3 hello.rb

Une fois que nous l’avons lancé, le fichier ressemble à ceci :

#! /usr/bin/env ruby

def hello
<<<<<<< ours
  puts 'hola world'
||||||| base
  puts 'hello world'
=======
  puts 'hello mundo'
>>>>>>> theirs
end

hello()

Si vous appréciez ce format, vous pouvez le régler comme défaut pour les futur conflits de fusion en renseignant le paramètre merge.conflictstyle avec diff3.

$ git config --global merge.conflictstyle diff3

La commande git checkout peut aussi accepter les options --ours et --theirs, qui peuvent servir de moyen rapide de choisir unilatéralement une version ou une autre sans fusion.

Cela peut être particulièrement utile pour les conflits de fichiers binaires où vous ne pouvez que choisir un des côté, ou des conflits où vous souhaitez fusionner certains fichiers depuis d’autres branches - vous pouvez fusionner, puis extraire certains fichiers depuis un côté ou un autre avant de valider le résultat.

Journal de fusion

Un autre outil utile pour la résolution de conflits de fusion est git log. Cela peut vous aider à obtenir du contexte ce qui a contribué aux conflits. Parcourir un petit morceau de l’historique pour se rappeler pourquoi deux lignes de développement ont touché au même endroit dans le code peut s’avérer quelque fois très utile.

Pour obtenir une liste complète de tous les commits uniques qui ont été introduits dans chaque branche impliquée dans la fusion, nous pouvons utiliser la syntaxe « triple point » que nous avons apprise dans Triple point.

$ git log --oneline --left-right HEAD...MERGE_HEAD
< f1270f7 update README
< 9af9d3b add a README
< 694971d update phrase to hola world
> e3eb223 add more tests
> 7cff591 add testing script
> c3ffff1 changed text to hello mundo

Voilà une belle liste des six commits impliqués, ainsi que chaque ligne de développement sur laquelle chaque commit se trouvait.

Néanmoins, nous pouvons simplifier encore plus ceci pour fournir beaucoup plus de contexte. Si nous ajoutons l’option --merge à git log, il n’affichera que les commits de part et d’autre de la fusion qui modifient un fichier présentant un conflit.

$ git log --oneline --left-right --merge
< 694971d update phrase to hola world
> c3ffff1 changed text to hello mundo

Si nous lançons cela avec l’option -p à la place, vous obtenez les diffs limités au fichier qui s’est retrouvé en conflit. Cela peut s’avérer vraiment utile pour vous donner le contexte nécessaire à la compréhension de la raison d’un conflit et à sa résolution intelligente.

Format de diff combiné

Puisque Git indexe tous les résultats de fusion couronnés de succès, quand vous lancez git diff dans un état de conflit de fusion, vous n’obtenez que ce qui toujours en conflit à ce moment. Il peut s’avérer utile de voir ce qui reste à résoudre.

Quand vous lancez git diff directement après le conflit de fusion, il vous donne de l’information dans un format de diff plutôt spécial.

$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,11 @@@
  #! /usr/bin/env ruby

  def hello
++<<<<<<< HEAD
 +  puts 'hola world'
++=======
+   puts 'hello mundo'
++>>>>>>> mundo
  end

  hello()

Ce format s’appelle « diff combiné » (combined diff) et vous fournit deux colonnes d’information sur chaque ligne. La première colonne indique que la ligne est différente (ajoutée ou supprimée) entre la branche « ours » et le fichier dans le répertoire de travail. La seconde colonne fait de même pour la branche « theirs » et la copie du répertoire de travail.

Donc dans cet exemple, vous pouvez voir que les lignes <<<<<<< et >>>>>>> sont dans la copie de travail mais n’étaient dans aucun des deux côtés de la fusion. C’est logique parce que l’outil de fusion les a collés ici pour donner du contexte, mais nous devrons les retirer.

Si nous résolvons le conflit et relançons git diff, nous verrons la même chose, mais ce sera un peu plus utile.

$ vim hello.rb
$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

Ceci nous montre que « hola world » était présent de notre côté mais pas dans la copie de travail, que « hello mundo » était présent de l’autre côté mais pas non plus dans la copie de travail et que finalement, « hola mundo » n’était dans aucun des deux côtés, mais se trouve dans la copie de travail. C’est particulièrement utile lors d’une revue avant de valider la résolution.

Vous pouvez aussi l’obtenir depuis git log pour toute fusion pour visualiser comment quelque chose a été résolu après coup. Git affichera ce format si vous lancez git show sur un commit de fusion, ou si vous ajoutez une option --cc à git log -p (qui par défaut ne montre que les patchs des commits qui ne sont pas des fusions).

$ git log --cc -p -1
commit 14f41939956d80b9e17bb8721354c33f8d5b5a79
Merge: f1270f7 e3eb223
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Sep 19 18:14:49 2014 +0200

    Merge branch 'mundo'

    Conflicts:
        hello.rb

diff --cc hello.rb
index 0399cd5,59727f0..e1d0799
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

Défaire des fusions

Comme vous savez créer des commits de fusion à présent, vous allez certainement en faire par erreur. Un des grands avantages de l’utilisation de Git est qu’il n’est pas interdit de faire des erreurs, parce qu’il reste toujours possible (et très souvent facile) de les corriger.

Les commits de fusion ne font pas exception. Supposons que vous avez commencé à travailler sur une branche thématique, que vous l’avez accidentellement fusionnée dans master et qu’en conséquence votre historique ressemble à ceci :

_Commit_ de fusion accidentel.
Figure 138. Commit de fusion accidentel

Il existe deux façons d’aborder ce problème, en fonction du résultat que vous souhaitez obtenir.

Correction des références

Si le commit de fusion non désiré n’existe que dans votre dépôt local, la solution la plus simple et la meilleure consiste à déplacer les branches pour qu’elles pointent où on le souhaite. La plupart du temps, en faisant suivre le git merge malencontreux par un git reset --hard HEAD~, on remet les pointeurs de branche dans l’état suivant :

Historique après `git reset --hard HEAD~`.
Figure 139. Historique après git reset --hard HEAD~

Nous avons détaillé reset dans Reset démystifié et il ne devrait pas être très difficile de comprendre ce résultat. Voici néanmoins un petit rappel : reset --hard réalise généralement trois étapes :

  1. Déplace la branche pointée par HEAD ; dans notre cas, nous voulons déplacer master sur son point avant la fusion (C6),

  2. Faire ressembler l’index à HEAD,

  3. Faire ressembler le répertoire de travail à l’index.

Le défaut de cette approche est qu’elle ré-écrit l’historique, ce qui peut être problématique avec un dépôt partagé. Reportez-vous à Les dangers du rebasage pour plus d’information ; en résumé si d’autres personnes ont déjà les commits que vous ré-écrivez, il vaudrait mieux éviter un reset. Cette approche ne fonctionnera pas non plus si d’autres commits ont été créés depuis la fusion ; déplacer les références des branches éliminera effectivement ces modifications.

Inverser le commit

Si les déplacements des pointeurs de branche ne sont pas envisageables, Git vous donne encore l’option de créer un nouveau commit qui défait toutes les modifications d’un autre déjà existant. Git appelle cette option une « inversion » (revert), et dans ce scénario particulier, vous l’invoqueriez comme ceci :

$ git revert -m 1 HEAD
[master b1d8379] Revert "Merge branch 'topic'"

L’option -m 1 indique quel parent est le principal et devrait être conservé. Si vous invoquez une fusion dans HEAD (git merge topic), le nouveau commit a deux parents : le premier est HEAD (C6), et le second est le sommet de la branche en cours de fusion (C4). Dans ce cas, nous souhaitons défaire toutes les modifications introduites dans le parent numéro 2 (C4), tout en conservant tout le contenu du parent numéro 1 (C6).

L’historique avec le commit d’inversion ressemble à ceci :

Historique après `git revert -m 1`.
Figure 140. Historique après git revert -m 1

Le nouveau commit ^M a exactement le même contenu que C6, et partant de là, c’est comme si la fusion n’avait pas eu lieu, mis à part que les commits qui ne sont plus fusionnés sont toujours dans l’historique de HEAD. Git sera confus si vous tentez de re-fusionner topic dans master :

$ git merge topic
Already up-to-date.

Il n’y a rien dans topic qui ne soit pas déjà joignable depuis master. Pire encore, si vous ajoutez du travail à topic et re-fusionnez, Git n’ajoutera que les modifications depuis la fusion inversée :

Historique avec une mauvaise fusion.
Figure 141. Historique avec une mauvaise fusion

Le meilleur contournement de ceci est de dé-inverser la fusion originale, puisque vous voulez ajouter les modifications qui ont été annulées, puis de créer un nouveau commit de fusion :

$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
Historique après re-fusion de la fusion annulée.
Figure 142. Historique après re-fusion de la fusion annulée

Dans cet exemple, M et ^M s’annulent. ^^M fusionne effectivement les modifications depuis C3 et C4, et C8 fusionne les modifications depuis C7, donc à présent, topic est totalement fusionnée.

Autres types de fusions

Jusqu’ici, nous avons traité les fusions normales entre deux branches qui ont été gérées normalement avec ce qui s’appelle la stratégie « récursive » de fusion. Il existe cependant d’autres manières de fusionner des branches. Traitons en quelques unes rapidement.

Préférence our ou theirs

Premièrement, il existe un autre mode utile que nous pouvons utiliser avec le mode « recursive » normal de fusion. Nous avons déjà vu les options ignore-all-space et ignore-space-change qui sont passées avec -X mais nous pouvons aussi indiquer à Git de favoriser un côté plutôt que l’autre lorsqu’il rencontre un conflit.

Par défaut, quand Git rencontre un conflit entre deux branches en cours de fusion, il va ajouter des marqueurs de conflit de fusion dans le code et marquer le fichier en conflit pour vous laisser le résoudre. Si vous préférez que Git choisisse simplement un côté spécifique et qu’il ignore l’autre côté au lieu de vous laisser fusionner manuellement le conflit, vous pouvez passer -Xours ou -Xtheirs à la commande merge.

Si une des options est spécifiée, Git ne va pas ajouter de marqueurs de conflit. Toutes les différences qui peuvent être fusionnées seront fusionnées. Pour toutes les différences qui génèrent un conflit, Git choisira simplement la version du côté que vous avez spécifié, y compris pour les fichiers binaires.

Si nous retournons à l’exemple « hello world » précédent, nous pouvons voir que la fusion provoque des conflits.

$ git merge mundo
Fusion automatique de  hello.rb
CONFLIT (contenu): Conflit de fusion dans hello.rb
La fusion automatique a échoué ; réglez les conflits et validez le résultat.

Cependant, si nous la lançons avec -Xours ou -Xtheirs, elle n’en provoque pas.

$ git merge -Xours mundo
Fusion automatique de hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 test.sh  | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 test.sh

Dans ce dernier cas, au lieu d’obtenir des marqueurs de conflit dans le fichier avec « hello mundo » d’un côté et « hola world » de l’autre, Git choisira simplement « hola world ». À part cela, toutes les autres modifications qui ne génèrent pas de conflit sont fusionnées sans problème.

Cette option peut aussi être passée à la commande git merge-file que nous avons utilisée plus tôt en lançant quelque chose comme git merge-file --ours pour les fusions de fichiers individuels.

Si vous voulez faire quelque chose similaire mais indiquer à Git de ne même pas essayer de fusionner les modifications de l’autre côté, il existe une option draconienne qui s’appelle la stratégie de fusion « ours ».

Cela réalisera une fusion factice. Cela enregistrera un nouveau commit de fusion avec les deux branches comme parents, mais ne regardera même pas la branche en cours de fusion. Cela enregistrera simplement le code exact de la branche courante comme résultat de la fusion.

$ git merge -s ours mundo
Merge made by the 'ours' strategy.
$ git diff HEAD HEAD~
$

Vous pouvez voir qu’il n’y a pas de différence entre la branche sur laquelle nous étions précédemment et le résultat de la fusion.

Cela peut s’avérer utile pour faire croire à Git qu’une branche est déjà fusionnée quand on fusionne plus tard. Par exemple, disons que vous avez créé une branche depuis une branche « release » et avez travaillé dessus et que vous allez vouloir réintégrer ce travail dans master. Dans l’intervalle, les correctifs de master doivent être reportés dans la branche release. Vous pouvez fusionner la branche de correctif dans la branche release et aussi faire un merge -s ours de cette branche dans la branche master (même si le correctif est déjà présent) de sorte que lorsque fusionnerez plus tard la branche release , il n’y aura pas de conflit dû au correctif.

Subtree Merging

L’idée de la fusion de sous-arbre est que vous avez deux projets, et l’un des projets se réfère à un sous-dossier de l’autre et vice-versa. Quand vous spécifiez une fusion de sous-arbre, Git est souvent assez malin pour se rendre compte que l’un est un sous-arbre de l’autre et fusionner comme il faut.

Nous allons explorer à travers un exemple comment ajouter un projet séparé à l’intérieur d’un projet existant et ensuite fusionner le code du second dans un sous-dossier du premier.

D’abord, nous ajouterons l’application Rack à notre projet. Nous ajouterons le projet Rack en tant que référence distante dans notre propre projet puis l’extrairons dans sa propre branche :

$ git remote add rack_remote https://github.com/rack/rack
$ git fetch rack_remote
warning: no common commits
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
From https://github.com/rack/rack
 * [new branch]      build      -> rack_remote/build
 * [new branch]      master     -> rack_remote/master
 * [new branch]      rack-0.4   -> rack_remote/rack-0.4
 * [new branch]      rack-0.9   -> rack_remote/rack-0.9
$ git checkout -b rack_branch rack_remote/master
Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master.
Switched to a new branch "rack_branch"

Maintenant nous avons la racine du projet Rack dans notre branche rack_branch et notre propre projet dans la branche master. Si vous extrayez un projet puis l’autre, vous verrez qu’ils ont des racines de projet différentes :

$ ls
AUTHORS         KNOWN-ISSUES   Rakefile      contrib         lib
COPYING         README         bin           example         test
$ git checkout master
Switched to branch "master"
$ ls
README

C’est un concept assez étrange. Toutes les branches de votre dépôt n’ont pas vraiment besoin d’être des branches du même projet. C’est inhabituel, parce que c’est rarement utile, mais c’est assez facile d’avoir des branches qui contiennent des historiques totalement différents.

Dans notre cas, nous voulons tirer le projet Rack dans notre projet master en tant que sous-dossier. Nous pouvons faire cela dans Git avec la commande git read-tree. Vous en apprendrez plus sur read-tree et ses amis dans Les tripes de Git, mais pour l’instant sachez qu’elle lit l’arborescence d’une branche dans votre index courant et dans le répertoire de travail. Nous venons de rebasculer dans notre branche master, et nous tirons la branche rack dans le sous-dossier rack de notre branche master de notre projet principal :

$ git read-tree --prefix=rack/ -u rack_branch

Quand nous validons, c’est comme si nous avions tous les fichiers Rack dans ce sous-dossier – comme si les avions copiés depuis une archive. Ce qui est intéressant est que nous pouvons assez facilement fusionner les modifications d’une des branches dans l’autre. Donc, si le projet Rack est mis à jour, nous pouvons tirer en amont les modifications en basculant sur cette branche et en tirant :

$ git checkout rack_branch
$ git pull

Ensuite, nous pouvons fusionner les modifications dans notre brancher master. Nous pouvons utiliser git merge -s subtree et cela marchera bien, mais Git fusionnera lui aussi les historiques ensemble, ce que nous ne voudrons probablement pas. Pour tirer les modifications et préremplir le message de validation, utilisez les options --squash et --no-commit en complément de l’option de stratégie -s subtree :

$ git checkout master
$ git merge --squash -s subtree --no-commit rack_branch
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested

Toutes les modifications du projet Rach sont fusionnées et prêtes à être validées localement. Vous pouvez aussi faire l’inverse – faire les modifications dans le sous-dossier rack de votre branche master et ensuite les fusionner plus tard dans votre branche rack_branch pour les soumettre aux mainteneurs ou les pousser en amont.

Ceci nous donne un moyen d’avoir un flux de travail quelque peu similaire au flux de travail des sous-modules sans utiliser les sous-modules (que nous couvrirons dans Sous-modules). Npus pouvons garder dans notre dépôt des branches avec d’autres projets liés et les fusionner façon sous-arbre dans notre projet occasionnellement. C’est bien par certains côtés ; par exemple tout le code est validé à un seul endroit. Cependant, cela a d’autres défauts comme le fait que c’est un petit peu plus complexe et c’est plus facile de faire des erreurs en réintégrant les modifications ou en poussant accidentellement une branche dans un dépôt qui n’a rien à voir.

Une autre chose un peu étrange est que pour obtenir la différence entre ce que vous avez dans votre sous-dossier rack et le code dans votre branche rack_branch – pour voir si vous avez besoin de les fusionner – vous ne pouvez pas utiliser la commande diff classique. À la place, vous devez lancer git diff-tree avec la branche que vous voulez comparer :

$ git diff-tree -p rack_branch

Ou, pour comparer ce qui est dans votre sous-dossier rack avec ce qu’était la branche master sur le serveur la dernière fois que vous avez tiré, vous pouvez lancer

$ git diff-tree -p rack_remote/master