GIT : squasher des merges
30 May 2013Supposons 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
Ce qui donne :
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 :
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 :
Si vous avez plus simple, je suis preneur…
Merci aux membres de stackoverflow.
Je vois souvent passer ce genre d’article autour de GIT. Je n’ai jamais compris pourquoi certain se donnait autant de mal pour “cacher” des choses réelles. Pour moi il ne faut pas chercher à tordre GIT et l ‘historique de nos actions sur une branche à tout prix. Ce n’est pas parce qu’on peut le faire qu’il faut le faire. Gardez les choses simples et acceptez d’avoir des graph, des logs, des historiques un peu “complet” de temps en temps, plutôt que de vouloir nettoyer, renommer, rebase, à posteriori tout le temps.
En tant que chef de projet je préfère 100 fois relire un historique complet mais réel, que de repasser derrière quelqu’un qui m’a tout manipulé et transformé, et qui 1 fois sur 2 va juste se planter et me foutre un gros bordel.