git mergetool
. Le plus simple à configurer est kdiff3. Il suffit de le télécharger et installer, puis de rajouter ceci dans votre fichier .gitconfig :
[diff]
tool = kdiff3
keepBackup = false
prompt = false
[merge]
tool = kdiff3
keepBackup = false
prompt = false
[mergetool "kdiff3"]
path = C:/Program Files (x86)/KDiff3/kdiff3.exe
trustExitCode = false
Remplacez “C:/Program Files (x86)/KDiff3/kdiff3.exe” par le bon chemin selon votre système, bien entendu.
kdiff3 a tendance à laisser trainer des fichiers *.orig après le merge, et ça peut se régler dans Configure/Options > Directory Merge > Backup Files (*.orig).
Ainsi votre prochain merge ouvrira tranquilement kdiff3 pour chaque conflit.
L’open source c’est fantastique. Github c’est merveilleux. On parle de batbelt, et paf, quelques jours plus tard on a notre première contribution.
Ce qui est sympa avec cette lib, c’est que ce sont des fonctions simples et courtes, axées sur des tâches très précises. Le code est donc généralement intéressant à lire, mais pas trop compliqué, et on tombe souvent sur des cas d’école.
C’est le cas de cette modification de la fonction dmerge()
. Originalement, dmerge
permet de créer un dictionnaire à partir de la fusion de de 2 autres dicos, et ça marchait comme ça:
def dmerge(d1, d2):
d = {}
d.update(d1)
d.update(d2)
return d
Facile à comprendre, rapide, simple, et le comportement par défaut est intuitif : si une clé est en double, la clé du deuxième dico écrase la première.
Comme le précisait Etienne, ce comportement est néanmoins un parmi de nombreux possibles. On pourrait aussi vouloir:
Pour cela, notre contributeur a proposé qu’on puisse passer en paramètre une fonction qui choisit quelle valeur garder en cas de doublon. Le code devient donc :
def dmerge(d1, d2, merge_func=lambda v1, v2: v2):
d = {}
d.update([(k, v) for k, v in d1.iteritems() if k not in d2])
d.update([(k, v) for k, v in d2.iteritems() if k not in d1])
d.update([(k, merge_func(v, d2[k])) for k, v in d1.iteritems() if k in d2])
return d
Avoir une valeur par défaut nous permet de garder le comportement initial. Pour cela on utilise une lambda. C’est ce qu’on appelle l’injection de dépendance.
Comme j’ai bien faire mumuse avec Python, je me suis demandé : “quelle est la stratégie de merge la plus rapide ?”. J’ai donc fait plusieurs tests de merge, avec plusieurs algos. Voici ce que ça donne:
from itertools import chain
def dmerge1(d1, d2, merge_func=None):
"""
Mon premier essai, en virant la lambda et en utilisant itertools.
"""
d = {}
if merge_func is None:
d.update(d1)
d.update(d2)
return d
for k, v in chain(d1.iteritems(), d2.iteritems()):
if k in d1 and k in d2:
d[k] = merge_func(d1[k], d2[k])
else:
d[k] = v
return d
def dmerge2(d1, d2, merge_func=lambda v1, v2: v2):
"""
Le code du PR original
"""
d = {}
d.update([(k, v) for k, v in d1.iteritems() if k not in d2])
d.update([(k, v) for k, v in d2.iteritems() if k not in d1])
d.update([(k, merge_func(v, d2[k])) for k, v in d1.iteritems() if k in d2])
return d
def dmerge3(d1, d2, merge_func=None):
"""
Un mélange du code original et de mon premier essai.
"""
d = {}
d.update(d1)
if merge_func is None:
d.update(d2)
return d
for k, v in d2.iteritems():
if k in d:
d[k] = merge_func(d[k], v)
else:
d[k] = v
return d
def dmerge4(d1, d2, merge_func=None):
"""
Le code original, en virant la lambda
"""
d = {}
if merge_func is None:
d.update(d1)
d.update(d2)
return d
d.update([(k, v) for k, v in d1.iteritems() if k not in d2])
d.update([(k, v) for k, v in d2.iteritems() if k not in d1])
d.update([(k, merge_func(v, d2[k])) for k, v in d1.iteritems() if k in d2])
return d
if __name__ == "__main__":
import random
import timeit
print('Generate test dicts')
# pour que le test soit juste, il faut créer plusieurs types de dicos:
# des longs, et des courts avec plein de collisions ou moins
d1 = {random.randint(0, 100): 'd1' for x in xrange(10000)}
d2 = {random.randint(0, 100): 'd2' for x in xrange(10000)}
d3 = {random.randint(0, 10000000): 'd1' for x in xrange(1000000)}
d4 = {random.randint(0, 10000000): 'd2' for x in xrange(1000000)}
merge_to_list = lambda a, b: [a, b]
# ensuite il faut s'assurer que toutes ces fonctions retournent bien
# la même chose
print("Testing returns value all match")
assert (dmerge1(d1, d2) == dmerge2(d1, d2)
== dmerge3(d1, d2) == dmerge4(d1, d2))
assert (dmerge1(d1, d2, merge_to_list) == dmerge2(d1, d2, merge_to_list)
== dmerge3(d1, d2, merge_to_list) == dmerge4(d1, d2, merge_to_list))
assert (dmerge1(d3, d4) == dmerge2(d3, d4)
== dmerge3(d3, d4) == dmerge4(d3, d4))
assert (dmerge1(d3, d4, merge_to_list) == dmerge2(d3, d4, merge_to_list)
== dmerge3(d3, d4, merge_to_list) == dmerge4(d3, d4, merge_to_list))
assert (dmerge1(d1, d4) == dmerge2(d1, d4)
== dmerge3(d1, d4) == dmerge4(d1, d4))
assert (dmerge1(d1, d4, merge_to_list) == dmerge2(d1, d4, merge_to_list)
== dmerge3(d1, d4, merge_to_list) == dmerge4(d1, d4, merge_to_list))
# enfin on lance l'évaluation du temps d'éxécution avec timeit()
print("Start timing")
# ce code est exécuté une fois par appel de timeit
# notez l'astuce 'from __main__ import x' qui importe du code
# du fichier en cours, ce qui sert rarement
setup = '''from __main__ import (dmerge1, dmerge2, dmerge3, dmerge4,
d1, d2, d3, d4, merge_to_list)'''
# ensuite on fait des appels à timeit :
# - le premier paramètre est le code à mesurer: il faut qu'il
# soit le plus simple et court possible
# - le second et le code d'initialisation avant le test (hors mesure)
# - le 3e est le nombre de fois que le code va être appelé.
# on va tester chaque fonction pour chaque type de dico, une fois
# avec l'approche par défaut, et une fois avec une fonction de merge
# personnalisée
print "Lots of collisions"
print "Default merge strategy"
print "1", timeit.timeit("dmerge1(d1, d2)", setup=setup, number=1000000)
print "2", timeit.timeit("dmerge2(d1, d2)", setup=setup, number=1000000)
print "3", timeit.timeit("dmerge3(d1, d2)", setup=setup, number=1000000)
print "4", timeit.timeit("dmerge4(d1, d2)", setup=setup, number=1000000)
print "Custom merge strategy"
print "1", timeit.timeit("dmerge1(d1, d2, merge_to_list)",
setup=setup, number=100000)
print "2", timeit.timeit("dmerge2(d1, d2, merge_to_list)",
setup=setup, number=100000)
print "3", timeit.timeit("dmerge3(d1, d2, merge_to_list)",
setup=setup, number=100000)
print "4", timeit.timeit("dmerge4(d1, d2, merge_to_list)",
setup=setup, number=100000)
# le nombre de répétitions est bien plus faible ici car sinon le test
# est très très long
print "Long dictionaries"
print "Default merge strategy"
print "1", timeit.timeit("dmerge1(d3, d4)", setup=setup, number=100)
print "2", timeit.timeit("dmerge2(d3, d4)", setup=setup, number=100)
print "3", timeit.timeit("dmerge3(d3, d4)", setup=setup, number=100)
print "4", timeit.timeit("dmerge4(d3, d4)", setup=setup, number=100)
print "Custom merge strategy"
print "1", timeit.timeit("dmerge1(d3, d4, merge_to_list)",
setup=setup, number=100)
print "2", timeit.timeit("dmerge2(d3, d4, merge_to_list)",
setup=setup, number=100)
print "3", timeit.timeit("dmerge3(d3, d4, merge_to_list)",
setup=setup, number=100)
print "4", timeit.timeit("dmerge4(d3, d4, merge_to_list)",
setup=setup, number=100)
print "Mixed dictionaries"
print "Default merge strategy"
print "1", timeit.timeit("dmerge1(d1, d4)", setup=setup, number=100)
print "2", timeit.timeit("dmerge2(d1, d4)", setup=setup, number=100)
print "3", timeit.timeit("dmerge3(d1, d4)", setup=setup, number=100)
print "4", timeit.timeit("dmerge4(d1, d4)", setup=setup, number=100)
print "Custom merge strategy"
print "1", timeit.timeit("dmerge1(d1, d4, merge_to_list)",
setup=setup, number=100)
print "2", timeit.timeit("dmerge2(d1, d4, merge_to_list)",
setup=setup, number=100)
print "3", timeit.timeit("dmerge3(d1, d4, merge_to_list)",
setup=setup, number=100)
print "4", timeit.timeit("dmerge4(d1, d4, merge_to_list)",
setup=setup, number=100)
# Et voici le résultat que ça nous ressort. On voit clairement
# que la 3eme fonction donne les meilleurs perfs, du coup
# c'est celle qu'on a choisit
## Generate test dicts
## Testing returns value all match
## Start timing
## Lots of collisions
## Default merge strategy
## 1 19.9299340248
## 2 148.185166121
## 3 21.2276539803
## 4 21.2074358463
## Custom merge strategy
## 1 30.646312952
## 2 18.522135973
## 3 14.0125968456
## 4 18.5139119625
## Long dictionaries
## Default merge strategy
## 1 84.4819910526
## 2 383.444111109
## 3 80.7273669243
## 4 86.0287930965
## Custom merge strategy
## 1 294.41114521
## 2 377.38009119
## 3 154.505481005
## 4 256.771039963
## Mixed dictionaries
## Default merge strategy
## 1 19.9574320316
## 2 87.1410660744
## 3 19.3570361137
## 4 19.524998188
## Custom merge strategy
## 1 60.6157000065
## 2 86.3876900673
## 3 59.0331327915
## 4 87.0504939556
## [Finished in 2494.7s]
Le test complet a pris en tout 2494.7s, soit 41 minutes, pour tourner sur ma machine, et je l’ai lancé plusieurs fois avec différentes modifs au fil de la journée. Il faut vraiment est un geek pour aimer faire ce genre de connerie. Parce que soyons honnête, tout le monde s’en branle des perfs de cette fonction :-)
Il faut savoir aussi qu’après coup j’ai réalisé que son code utilisait des listes en intention, et non des expressions génératrices, ce qui fait qu’il faut attendre que toute la liste soit générée avec que update()
fasse son travail.
Il y avait donc des cas supplémentaires à tester et j’avais mergé son code dans batbelt. Arf, l’optimisation n’a jamais de fin ^^
Bref, j’ai relancé un test avec avec des listes en intentions (et même avec des expressions ternaires ce qui donne des trucs capilotractés):
def dmerge1(d1, d2, merge_func=None):
"""
Une version de l'ancien dmerge3 qui utilise une expression génératrice.
"""
d = {}
d.update(d1)
if merge_func is None:
d.update(d2)
return d
d.update((k, (v if k not in d else merge_func(d[k], v))) for k, v in d2.iteritems())
return d
def dmerge2(d1, d2, merge_func=lambda v1, v2: v2):
"""
La version du proposée par notre gentil contributeur, mais
avec des expressions génératrices à la place des listes en
intention.
"""
d = {}
d.update((k, v) for k, v in d1.iteritems() if k not in d2)
d.update((k, v) for k, v in d2.iteritems() if k not in d1)
d.update((k, merge_func(v, d2[k])) for k, v in d1.iteritems() if k in d2)
return d
def dmerge3(d1, d2, merge_func=None):
"""
La version la plus rapide du test précédent.
"""
d = {}
d.update(d1)
if merge_func is None:
d.update(d2)
return d
for k, v in d2.iteritems():
if k in d:
d[k] = merge_func(d[k], v)
else:
d[k] = v
return d
def dmerge4(d1, d2, merge_func=None):
"""
La version proposée par notre contributeur, optimisée comme un connard.
"""
d = {}
if merge_func is None:
d.update(d1)
d.update(d2)
return d
d.update((k, v) for k, v in d1.iteritems() if k not in d2)
d.update((k, v) for k, v in d2.iteritems() if k not in d1)
d.update((k, merge_func(v, d2[k])) for k, v in d1.iteritems() if k in d2)
return d
## Generate test dicts
## Testing returns value all match
## Start timing
## Lots of collisions
## Default merge strategy
## 1 12.2690660954
## 2 97.121183157
## 3 12.1084120274
## 4 12.6807589531
## Custom merge strategy
## 1 8.73903894424
## 2 12.0493769646
## 3 8.00074601173
## 4 11.4764301777
## Long dictionaries
## Default merge strategy
## 1 54.5233860016
## 2 218.695598841
## 3 70.213809967
## 4 61.8348701
## Custom merge strategy
## 1 103.258821964
## 2 217.720175982
## 3 96.5204670429
## 4 214.480421066
## Mixed dictionaries
## Default merge strategy
## 1 19.169850111
## 2 66.9599928856
## 3 19.4371211529
## 4 19.5183057785
## Custom merge strategy
## 1 64.7940750122
## 2 66.9712991714
## 3 58.5918440819
## 4 67.5281140804
## [Finished in 1619.3s]
La bonne nouvelle pour moi, c’est que l’algo 3 est toujours le plus rapide, j’ai pas à re pusher.
Mais il est intéressant de constater que globalement les 4 tests mettent 1619.3s à s’exécuter. Globalement utiliser des expressions génératrices boost bien les perfs.
Mettez le code suivant du le fichier .git/hooks/post-merge de votre repo en local :
#!/bin/bash
# On met ici tous les fichiers (ou pattern de nom) qu'on veut surveiller
# changez les pour les adapter à votre projet
files=('settings.py' 'migrations');
# on récupère tous les noms de fichiers modifiés depuis le dernier merge
modified_files=`git diff "HEAD@{1}" --name-only`
# on boucle sur chaque nom de fichier surveillé
for watched_file in "${files[@]}"; do
# on liste tous les fichiers modifiés qui correspondent à ce nom de
# fichier surveillé
modified_watched_files=(`echo "$modified_files" | grep $watched_file`)
# si le nombre de fichiers correspondant est plus grand que 0
if [ ${#modified_watched_files[@]} ]; then
# pour chaque fichier qui correspond, on affiche un avertissement
# en rouge
for modified_watched_file in "${modified_watched_files[@]}"; do
echo -e "\e[41m "$modified_watched_file" has changed \033[0m"
done
fi
done
Le hook post-merge est exécute après git pull (et seulement si il n’y a pas eu de conflit). Il va afficher une bonne grosse alerte en rouge pour chaque fichier surveillé qui a été modifié entre avant et après le pull.
Notez au passage la syntaxe intuitive de bash pour utiliser des tableaux. On dirait presque que le mec qui a codé bash était un pote du gars qui a codé git.
]]>Après git pull
:
git checkout --theirs .
Pour garder toutes les modifs des autres.
OU
git checkout --ours .
Pour garder les siennes.
On peut remplacer .
(le point) par un chemin de fichier pour être plus selectif.
Ces commandes vont juste mettre les fichiers voulus dans la copie de travail. Pour terminer le merge, il faut faire évidement un petit add
et un commit
.
Git merge
.
Les dents se serrent. Les fesses aussi. Est-ce que je vais tout pêter ?
Quand on ne connait pas Git, on a peur de perdre son travail parcequ’on assimile le fait qu’on ne peut plus trouver son travail avec sa destruction. En fait, Git ne supprime rien (du moins, si aucune référence ne pointe sur les données – ce qui n’arrive jamais pour votre histo – vous avez 2 bonnes semaines devant vous).
Donc, si vos données sont commitées (les fichiers modifiés non commités ne comptent pas, évidement), ils sont quelques part dans les méandre du répository.
On peut utiliser cela à son avantage
Git branch
, le point de sauvegarde avant d’attaquer le boss finalQuand vous avez un truc tendu à faire (genre un merge de mamouth), la stratégie du débutant est de faire un gros copier/coller du repo. Et si ça merde, on supprime, et renomme, et hop, c’est tout neuf.
Ca marche (on l’a tous fait), mais c’est un peu con.
Il existe une stragétie beaucoup plus maline: simplement créer une branche
git branch point_de_sauvegarde
git merge mamouth
Le 1 est très important, si vous êtes débutant et que vous l’oubliez, vous êtes dead. Il faudrait faire une option dans git qui interdit un merge sur une copie de travail en cours de modification sous peine de choc électrique par le clavier (ou juste automatiquement, mais c’est moins fun).
Si le merge a marché: bingo, on peut supprimer la sauvegarde avec git branch -d point_de_sauvegarde
et commiter le merge git commit -m "Je l'ai faiiiiiiiiiiit"
.
Sinon, on revient au point de sauvegarde: git reset --hard point_de_sauvegarde
puis git branch -d point_de_sauvegarde
Et voilà, comme si rien ne s’était passé !
Ca marche d’abord en agissant prudement, et en ne faisant pas de merge sur une copie de travail en cours de modification. C’est la moitié du boulot.
Ensuite, on créé une nouvelle branche. Une branche n’est qu’un pointeur, ce n’est rien d’autre qu’un panneau disant “ici c’est ‘point de sauvegarde'”. Ce n’est pas un bras d’un arbre comme dans SVN. C’est une putain d’étiquette toute simple.
Donc en créant une branche, on met juste une étiquette sur le commit que l’on veut garder. Les commits de l’historique de Git sont INALTERABLES. Même avec un rebase, contrairement à la croyance populaire, on ne peut pas les supprimer. Le seul moyen de supprimer un commit est de le laisser sans aucun accès (sans étiquette ni parent avec étiquette) pendant 2 semaines ou de forcer le nettoyage avec git gc
.
En clair, vous ne pouvez pas perdre un commit. Votre collègue ne peut pas vous niquer un commit par accident. La seule chose à faire si vous tenez à un commit, c’est de mettre un moyen pour le retrouver facilement. Ici, on lui met une étiquette.
Puis finalement, si ça marche, on supprime l’étiquette.
Si ça ne marche pas, on demande un hard reset depuis le point de sauvegarde. Un hard reset remet à plat votre copie de travail (les fichiers du disque), et l’index.
Cela marche pour une seule raison: git merge
ne commit pas.
Si le merge est un fast forward, Git va juste se déplacer d’une case en avant. Si le merge est plus complexe, il va vous demander de commiter.
Dans le cas un, reset vous fait juste reculer d’une case. Dans le cas deux, tant que vous ne commitez pas, le hard reset annule tout ce qui a été fait, et le nouveau commit n’est jamais créé.
git branch point_de_sauvegarde
.git commit
(parfois inutile, mais ça ne coûte rien).git reset --hard point_de_sauvegarde
.Une fois que vous êtes à l’aise avec cette idée, vous réaliserez qu’en fait le point de sauvegarde n’est pas du tout obligatoire. Puisque votre merge est accessible, tous ses parents le sont. Avec git log
, vous pouvez retrouver l’ID du parent et faire la même chose:
git reset --hard 1234abcd
Avec 1234abcd comme id du parent :-)
Si ça ne vous parles pas, restez avec le point de sauvegarde, c’est une bonne technique.
]]>