Pendant longtemps, Max et moi on a joué à celui qui push le premier pour que l’autre se tape le merge. Parfois on est pas en forme, le terrain est trop lourd, les sangliers ont mangé des cochoneries… Bref, on perd la course, et la goutte de sueur au bord de la tempe, on se lance dans le Git merge
.
Les dents se serrent. Les fesses aussi. Est-ce que je vais tout pêter ?
Bonne nouvelle, Git ne perd rien
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 final
Quand 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
- Assurez-vous que votre copie de travail est propre (git stash au besoin). Vous devez avoir le message: “nothing to commit (working directory clean)“.
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é !
Heu, ok c’est magique, mais ça marche comment ?
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éé.
En résumé, avant de faire un truc qui vous fait peur avec git
- Verifiez que la copie de travail est propre.
git branch point_de_sauvegarde
.- Si ça marche,
git commit
(parfois inutile, mais ça ne coûte rien). - Si ça marche pas,
git reset --hard point_de_sauvegarde
.
L’astuce pour les levels 99
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.
Merci pour ces différents rappels !
Pourquoi est-ce qu’un tag ne serait pas suffisant, si c’est juste pour nommer explicitement un commit ?
Ca marche exactement pareil avec un tag. Mais les tags ne sont sémantiquement fait pour être supprimés. Une branche, c’est sémantiquement jetable. En revanche techniquement, les deux sont des étiquettes, et les deux peuvent être créés, supprimés, et checkouté de la même manière.
Oui mais une branche, à mon sens, c’est fait pour vivre (un minimum), je trouve donc personnellement le tag plus approprié (est-ce que l’abondance de tags peut nuire à un repo git ? vous avez 2 heures) :)
Je comprendrais très bien qu’on utilise un tag. Un tag point de sauvegarde a parfaitement du sens pour moi.
Par contre je ne dirais pas qu’une branche doit vivre une minimum. Je vais parfois des branches pour quelques minutes seulement.
Si c’est juste pour pouvoir annuler un merge en cours, pas besoin de tout ça :
$ git pull
# ...
# ratage de résolution de conflits
# ...
$ git reset --merge
git refuse de démarrer un merge si les fichiers concernés par le merge sont modifiés, et « git reset –-merge » ne va réinitialiser que les fichiers concernés par le merge, donc ça marche, sans point de sauvegarde (c’est juste HEAD), et sans « git stash » préalable.
(sinon, j’ai pas compris en quoi les tags ne seraient pas faits sémantiquement pour être supprimés, un « lightweight tag », c’est justement bien à ça que ça sert, non ?)
Git stash est indispensable si on veut faire un pull qui contient des fichiers modifiés.
Pour le tag, le principe sémantique est de dire “ceci est important dans mon historique”. Généralement c’est fait pour rester. Par contre dans git, les branches, contrairement à SVN, sont faites pour être très temporaires (même si on en garde certaines très longtemps pour le dev et le trunk) du fait des workflow à base de fork/commit/rebase.
Techniquement on peut utiliser un tag ou une branche, ça n’a pas vraiment d’importance. C’est juste que si tu quittes précipitement ton bureau, il y a plus de chance que tu vois ta banche “backup” à ton retour et que ça te revienne, qu’un tag (lister les tags, ça se fait une fois par mois à tout pêter).
Notez bien que ce sont des techniques pour débutants: le but est de les rassurer en leur donnant un choix facile, comprehensible, avec des éléments par défaut sains. Ce ne sont pas les plus rapides, et il y a des décisions arbitraires.
Non, le git stash n’est pas nécessaire, ou en tous cas n’apporte pas de sécurité avant de faire un merge.
Git accepte de démarrer le merge seulement si les fichiers contenant des modifs non-commitées ne sont pas ceux concernés par le merge. L’option que tu cherches dans Git qui empêche de démarrer un merge en cas de danger existe déjà, elle est là par défaut et depuis toujours.
Ce que tu as raté, c’est l’option –merge de git reset (qui elle, n’existe pas depuis toujours). Conseiller “git reset –hard” à un débutant, par contre, je ne trouve pas que ce soit un bon conseil. C’est vraiment très, très rare que “git reset –hard” soit la bonne réponse à une question depuis qu’on a “reset –keep” et “reset –merge”.
“git stash” est nécessaire si les fichiers concernés par le merge sont modifiés localement, mais à ce moment là, Git aurait refusé de démarrer le merge de toutes façons (en suggérant “git stash”).
Pour les tags, je sais bien comment il marchent, mais je ne vois pas pourquoi un tag serait fait pour exister plus longtemps qu’une branche. Ce qui distingue la durée de vie, c’est plutôt le fait de faire un push ou pas dessus (i.e. garder privé les trucs qui n’intéressent pas les autres). Avec une branche, “git checkout ; git commit” va déplacer la branche, ce qui a peu de chances d’être ce que tu veux. Un tag est fait pour ne pas bouger (c’est ça la seule différence techniquement entre un lightweight tag et une branche avec Git: “git checkout ” te mets en detached HEAD), et c’est vraiment ce que tu veux ici.