Bien réussir un merge avec Git

Alexandre Garnier @zigarn

Git, les merges et vous

Qui n'a jamais utilisé Git ?

Qui n'a jamais fait de merge ?

Qui n'a jamais eu de problème de merge ?

Mise en place de l'environnement

  1. Installer Git (si ce n'est pas déjà fait)
  2. Cloner le projet de travail :
    
    git clone https://bitbucket.org/zigarn/git-merge-workshop-workspace.git
    							
  3. Rentrer dans le dossier :
    
    cd git-merge-workshop-workspace
    							
  4. Lancer le projet :
    
    ./run.sh
    							

Avant de démarrer un merge

Faire place nette !


git status
git stash --include-untracked
					

Qu'est-ce qu'un merge ?

On peut simplement voir ça simplement comme la fusion du contenu d'un dossier dans un autre.

Mais avec Git ?

Ben c'est pareil : simplement la fusion du contenu d'une branche dans celui d'une autre.

Avec en plus enregistrement du point de fusion.

Un merge simple

  1. Se positionner sur la branche cible :
    
    git checkout branch-A-1
    							
  2. Observer le contenu
  3. Y merger une branche :
    
    git merge origin/branch-A-2
    							
  4. Observer le résultat

Bilan

  • Tout s'est bien passé !
  • Git fait automatiquement le commit du résultat du merge.
    On peut éviter ce comportement avec l'option --no-commit
    
    git reset --hard HEAD@{1} # Rembobiner avant le merge
    git merge --no-commit origin/branch-A-2
    git status
    git diff --cached
    git commit
    								

Alors pourquoi avoir peur du merge ?

Parce que ça provoque des conflits !

Mais en vrai beaucoup moins souvent qu'on ne se l'imagine.

Un merge moins simple

  1. Se positionner sur la branche cible :
    
    git checkout branch-B-1
    							
  2. Observer le contenu
  3. Y merger une branche :
    
    git merge --no-commit origin/branch-B-2
    							
  4. Observer le résultat et commiter :
    
    git status
    git diff --cached
    git commit
    							

Bilan

  • Tout s'est encore bien passé !
  • Et pourtant on a modifié le même fichier.

    Chaque modification possède un contexte.

    Si les contextes sont disjoints, il n'y a pas de conflit.

    
    --- a/run.sh
    +++ b/run.sh
    @@ -2,7 +2,7 @@
    
     echo "Hello World 1!"
     echo "Hello World 2!"
    -echo "Hello World 3!"
    +echo "Hello everybody!"
     echo "Hello World 4!"
     echo "Hello World 5!"
     echo "Hello World 6!"
    										
    
    --- a/run.sh
    +++ b/run.sh
    @@ -17,6 +17,6 @@
     echo "Hello World 15!"
     echo "Hello World 16!"
     echo "Hello World 17!"
    -echo "Hello World 18!"
    +echo "Hello nobody!"
     echo "Hello World 19!"
     echo "Hello World 20!"
    										

Un "vrai" merge

  1. Se positionner sur la branche cible :
    
    git checkout branch-C-1
    							
  2. Observer le contenu
  3. Y merger une branche :
    
    git merge --no-commit origin/branch-C-2
    							
    Enfin un conflit !
  4. Observer le résultat :
    
    git status
    cat run.sh
    							

Un peu de théorie

  • Un merge est la réconciliation de 2 évolutions parallèles.
  • Ces 2 évolutions ont un ancêtre commun.
  • Cet ancêtre commun est souvent appelé la BASE.
  • Le résultat des évolutions dans les 2 branches sont appelées OURS et THEIRS.

En termes Git

OURS
Notre branche courante branch-C-1 ou HEAD
THEIRS
La branche à merger origin/branch-C-2 ou MERGE_HEAD
BASE

git merge-base branch-C-1 origin/branch-C-2
# Ou plus joliment
git describe --all --always $(git merge-base branch-C-1 origin/branch-C-2)
							

Voir les évolutions dans chaque branche

  • Évolutions entre BASE et OURS
    
    git diff $(git merge-base branch-C-1 origin/branch-C-2)..branch-C-1
    							
  • Évolutions entre BASE et THEIRS
    
    git diff $(git merge-base branch-C-1 origin/branch-C-2)..origin/branch-C-2
    							

Voir le contenu dans chaque branche

Git donne accès au contenu en conflit dans chaque branche.

  • Contenu dans BASE
    
    git show :1:run.sh
    							
  • Contenu dans OURS
    
    git show :2:run.sh
    							
  • Contenu dans THEIRS
    
    git show :3:run.sh
    							

Marqueurs de conflit


#!/bin/sh

<<<<<<< HEAD
echo "Hello World from branch-C-1!"
=======
echo "Hello World from branch-C-2!"
>>>>>>> branch-C-2
					
  • Le contenu en conflit dans OURS
    
    <<<<<<< HEAD
    echo "Hello World from branch-C-1!"
    =======
    							
  • Le contenu en conflit dans THEIRS
    
    =======
    echo "Hello World from branch-C-2!"
    >>>>>>> branch-C-2
    							

Marqueurs de conflit diff3

  1. Annuler le merge :
    
    git merge --abort
    							
  2. Configurer des marqueurs de diff en mode diff3 :
    
    git config --local merge.conflictStyle diff3
    							
  3. Relancer le merge :
    
    git merge origin/branch-C-2
    							

Marqueurs de conflit diff3 (suite)

  • Le contenu en conflit dans OURS
    
    <<<<<<< HEAD
    echo "Hello World from branch-C-1!"
    ||||||| merged common ancestors
    							
  • Le contenu en conflit dans BASE
    
    ||||||| merged common ancestors
    echo "Hello World!"
    =======
    							
  • Le contenu en conflit dans THEIRS
    
    =======
    echo "Hello World from branch-C-2!"
    >>>>>>> branch-C-2
    							

Résoudre le conflit (enfin)

  1. Simplement éditer les fichiers en conflit et choisir le contenu resultant du merge :
    
    #!/bin/sh
    
    echo "Hello World from both branch-C-1 and branch-C-2!"
    							
  2. Indiquer le conflit comme résolu en l'indexant :
    
    git add run.sh
    							
  3. Commiter :
    
    git commit
    							

Et voilà !

Utiliser un outil de résolution externe

  • Git permet d'utiliser un mergetool pour nous aider à résoudre les conflits.
  • Mon préféré est kdiff3 car multi-plateforme et en style diff3.
  • Mais il en existe un grand nombre de préconfigurés (emerge, meld, p4merge, tortoisemerge, vimdiff, winmerge, ...).
  • Vous pouvez même en configurer un nouveau avec mergetool.<tool>.cmd

Utilisation du mergetool

  1. Rembobiner avant le merge :
    
    git reset --hard HEAD@{1}
    							
  2. Configurer kdiff3 en mergetool :
    
    git config --local merge.tool kdiff3
    							
  3. Relancer le merge :
    
    git merge origin/branch-C-2
    							
  4. Résoudre les conflits :
    
    git mergetool
    							

Mergetool avec kdiff3

Mergetool et .orig

  • L'utilisation de mergetool provoque la création de fichiers en .orig.
  • Ce sont tout simplement des sauvegardes du fichier avec les marqueurs de conflit afin de pouvoir reprendre la résolution.
  • Il est possible de simplement les ignorer :
    
    echo '*.orig' >> .git/info/exclude
    							

D'ailleurs, si je me suis trompé dans la résolution de mon merge ?

  1. Simplement re-merger un fichier pour revenir au marqueurs de conflit :
    
    git checkout -m run.sh
    							
  2. Résoudre de nouveau le conflit, l'indéxer et committer :
    
    vi run.sh
    git add run.sh
    git commit
    							

Types de conflit

  • both modified : le plus classique
  • deleted by them/us : modification sur une branche et suppression sur l'autre
  • both added : ajout avec contenu différent sur chaque branche
  • autres, mais vraiment ésotériques

Types de conflit

  1. Se positionner sur la branche cible :
    
    git checkout branch-D-1
    							
  2. Observer le contenu
  3. Y merger une branche :
    
    git merge --no-commit origin/branch-D-2
    							
  4. Observer le résultat, résoudre les conflits et commiter :
    
    git status
    # solve conflicts
    git add --update
    git commit
    							

Merge fast-forward

Dans le cas où OURS==BASE

  • Il n'y a pas de divergences, de modifications concurrentes
  • Git le détecte
  • Il va simplement déplacer OURS sur THEIRS
  • Mais on peut forcer la création d'un commit de merge avec l'option --no-ff

Merge fast-forward (suite)

  1. Se positionner sur la branche cible :
    
    git checkout branch-E-1
    							
  2. Y merger une branche :
    
    git merge origin/branch-E-2
    							
  3. Observer le résultat
  4. Rembobiner :
    
    git reset --hard HEAD@{1}
    							
  5. Y merger une branche en --no-ff:
    
    git merge --no-ff origin/branch-E-2
    							
  6. Observer le résultat

Pas de conflit != pas de problème

  • Il peut y avoir des conflits "fonctionnels"
    Par exemple : appel à une méthode sur une branche et suppression de cette méthode sur une autre.
    On touche à 2 zones non conflictuelles mais pourtant plus rien de fonctionne !
  • Il peut y avoir d'autres types de problèmes par la résolution automatique des merges
  • C'est pourquoi il faut toujours vérifier le résultat d'un merge
    
    git diff HEAD^..HEAD # Regarder que le diff semble correct
    ./launch_tests.sh # Lancer sa suite de tests
    							

Merge problématique sans conflit

  1. Se positionner sur la branche cible :
    
    git checkout branch-F-1
    							
  2. Y merger une branche :
    
    git merge --no-commit origin/branch-F-2
    							
  3. Observer le résultat
  4. Lancer le projet :
    
    ./run.sh
    							
  5. Observer le résultat

Conclusion

J'espère que vous avez moins peur des merges

Merci