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 puis réintroduite modifiée. 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 tous les 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 forme d’étapes (stages), chacune associĂ©e Ă  un nombre. 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 137. 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 138. 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 139. 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 140. 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 141. 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_branch 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). Nous 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
scroll-to-top