gitmerge

Supposons que je souhaite ajouter une fonctionnalité à un projet sur GIT.

Je prends la version actuelle de la branche master (A), puis ajoute sur ma branche topic les commits X et Y.

    X---Y  topic
   /
--A  master

Je propose la fonctionnalité upstream (par un git request-pull ou une pull request), qui met un peu de temps à être revue.

Pendant ce temps, la branche master a avancé, et malheureusement les modifications effectuées entrent en conflit avec mon travail sur topic.

    X---Y  topic
   /
--A---B---C  master

Une fois mon code revu et accepté, les mainteneurs vont alors me demander de résoudre les conflits avec la branche master avant de merger ma branche topic.

Si j’avais eu à prendre en compte les mises à jour de master avant d’avoir rendu public mon topic, j’aurais simplement rebasé mon travail par-dessus master. Mais là, impossible.

Je dois donc merger. Très bien. Je merge et je résous les conflits.

    X---Y---M  topic
   /       /
--A---B---C  master

Mais, alors que je n’ai pas encore rendu M public, je m’aperçois qu’il y a un nouveau commit D sur master, que je veux intégrer dans topic.

    X---Y---M  topic
   /       /
--A---B---C---D  master

La solution la plus évidente est de merger à nouveau.

    X---Y---M---N  topic
   /       /   /
--A---B---C---D  master

Mais je voudrais éviter un commit de merge inutile. Pour un seul, ce n’est pas très gênant, mais si on maintient une branche suffisamment longtemps avant qu’elle ne soit mergée, ces commits inutiles vont se multiplier.

Une solution serait de revenir à Y et de le merger avec D :

git checkout topic
git reset --hard Y
git merge master
    X---Y---M'  topic
   /         \
--A---B---C---D  master

Mais dans ce cas, pour créer M', je vais devoir résoudre à nouveau les conflits que j’avais déjà résolu en créant M.

Comment éviter ce problème ?

rerere

Une solution est d’avoir activé rerere avant d’avoir résolu les conflits de M :

git config rerere.enabled true

Ainsi, lorsque je tenterai de merger à nouveau Y et D, les conflits entre Y et C seront automatiquement résolus de la même manière que précédemment.

Cependant, cette méthode a ses inconvénients.

Tout d’abord, il ne s’agit que d’un cache local de résolutions des conflits, stocké pendant une durée déterminée (par défaut à 60 jours pour les conflits résolus), ce qui est peu pratique si on clone son dépôt sur plusieurs machines (les conflits ne seront résolus automatiquement que sur certaines).

Ensuite, elle est inutilisable lorsqu’on souhaite squasher un merge conflictuel alors que rerere était désactivé lors de sa création.

Enfin, cette fonctionnalité est encore récente, et la fonction git rerere forget (pour permettre de résoudre autrement des conflits déjà résolus), a la fâcheuse tendance à segfaulter (un patch a été proposé).

Rebranchement

La solution que j’utilise est donc la suivante.

    X---Y---M---N  topic
   /       /   /
--A---B---C---D  master

Une fois obtenus les deux merges M et N, le principe est de remplacer le parent de N, qui était M, par Y, sans rien changer d’autre au contenu.

          -----
         /     \
    X---Y---M   N' topic
   /       /   /
--A---B---C---D  master

Ainsi, M devient inatteignable, et c’est exactement le résultat souhaité :

    X---Y-------N' topic
   /           /
--A---B---C---D  master

Pour faire cela, il faut déplacer le HEAD (pointant vers topic) sur Y, faire croire à GIT qu’on est en phase de merge avec D en modifiant la référence MERGE_HEAD, puis commiter :

git checkout N
git reset --soft Y
git update-ref MERGE_HEAD D
git commit -eF <(git log ..N ^D --pretty='# %H%n%s%n%n%b')

Il n’y a plus qu’à éditer le message de commit de merge.

La fin de la ligne du git commit permet de concaténer l’historique des commits intermédiaires (a priori uniquement des merges) comme lors d’un squash avec git rebase (pour pouvoir conserver les messages de merges intermédiaires, contenant nontamment les conflits).

En utilisant les références plutôt que les numéros de commit, cela donne :

git checkout feature
git reset --soft HEAD~2
git update-ref MERGE_HEAD master
git commit -eF <(git log ..HEAD@{1} ^master --pretty='# %H%n%s%n%n%b')

Si vous avez plus simple, je suis preneur…

Merci aux membres de stackoverflow.