Je veux partager des recettes pour résoudre quelques problèmes qui surviennent parfois lorsque vous travaillez avec git, et qui ne sont pas "directement assez évidents".
Au début, je pensais accumuler davantage de recettes de ce type, mais tout a son temps. Je pense que s'il y a un avantage, alors c'est possible et petit à petit ...
Alors ...
Fusionner les vieilles branches avec un minimum de douleur
Préambule. Il existe une branche principale ( master
), qui valide activement les nouvelles fonctionnalités et corrections; il y a une branche de feature
parallèle, dans laquelle les développeurs ont navigué pendant un certain temps dans leur propre nirvana, puis ont soudainement découvert qu'ils n'avaient pas été avec le maître depuis un mois maintenant, et la fusion "sur le front" (tête à tête) n'était déjà pas anodine.
(Oui, il ne s'agit pas d'un monde idéal où tout est correct, le crime est absent, les enfants sont toujours obéissants et traversent même strictement la main avec leur mère, en regardant attentivement autour).
But: être en colère. En même temps, de sorte qu'il s'agissait d'une fusion «pure», sans fonctionnalités. C'est-à-dire de sorte que dans le référentiel public du graphe de branche, deux threads sont connectés en un seul point avec le message "branche fusionnée" maître "en fonction". Et tout le mal de tête «celui-ci» sur le temps et les efforts nécessaires, le nombre de conflits résolus et la quantité de cheveux laissés inutilement tachés.
L'intrigue. Le fait que dans le git vous pouvez éditer le dernier commit avec la clé --amend
sait. L'astuce est que ce "dernier commit" peut être localisé n'importe où et contenir n'importe quoi. Par exemple, il peut s'agir non seulement du «dernier commit sur la branche linéaire», où ils ont oublié de corriger la faute de frappe, mais aussi du commit de fusion de la fusion habituelle ou «octopus». --amend
que survoler les modifications proposées et "incorporera" le commit modifié dans l'arborescence, comme s'il était réellement le résultat d'une fusion honnête et de la résolution des conflits. En substance, git merge
et git commit --amend
permettent de séparer complètement le "lieu d'implantation" ("ce commit dans l'arborescence sera ICI") et le contenu du commit lui-même.
L'idée de base d'un commit de fusion complexe avec un historique propre est simple: d'abord nous «faisons une place» en créant un commit de fusion propre (quel que soit le contenu), puis nous le réécrivons avec --amend
, ce qui rend le contenu «correct».
"Pilule un endroit." Cela est facile à faire en définissant une stratégie de fusion qui ne posera pas de questions inutiles sur la résolution des conflits.
git checkout feature git merge master -s ours
Ah oui. Avant la fusion, il était nécessaire de créer une branche "sauvegarde" à partir de la tête de la fonctionnalité. Après tout, rien n'est vraiment fusionné ... Mais que ce soit le 2ème paragraphe, et non le 0ème. En général, nous passons à la fonctionnalité non fusionnée et fusionnons maintenant honnêtement. En aucune façon possible, malgré tous les "hacks sales". Ma façon personnelle est de regarder la branche principale à partir du moment de la dernière fusion et d'évaluer les commits de problèmes possibles (par exemple: corriger une faute de frappe à un endroit - pas un problème. Massivement (de nombreux fichiers), ils ont renommé une entité - un problème, etc.). À partir des commits de problèmes, nous créons de nouvelles branches (je le fais ingénieusement - master1, master2, master3, etc.). Et puis nous fusionnons branche par branche, en passant de l'ancien au nouveau et en corrigeant les conflits (qui sont généralement évidents avec cette approche). Suggérer d'autres méthodes (je ne suis pas un magicien; j'apprends juste; je serai heureux de faire des commentaires constructifs!). En fin de compte, passant (peut-être) quelques heures sur des opérations purement routinières (qui peuvent être confiées au junior, car il n'y a tout simplement pas de conflits compliqués avec cette approche), nous obtenons l'état final du code: toutes les innovations / corrections de l'assistant ont été portées avec succès vers la branche fonctionnalité, tous les tests pertinents marché sur ce code, etc. Un code réussi doit être validé.
Réécriture de la «success story». En étant sur le commit, où "tout est fait", exécutez ce qui suit:
git tag mp git checkout mp git reset feature git checkout feature git tag -d mp
(Je déchiffre: en utilisant la balise (mp - merge point), nous passons à l'état HEAD détaché, à partir de là, le réinitialisons à la tête de notre branche, où au tout début une «implantation» a été faite par un commit de fusion frauduleux. La balise n'est plus nécessaire, nous la supprimons donc). Nous sommes maintenant sur le commit de fusion «pur» d'origine; en même temps, dans la copie de travail, nous avons les fichiers "corrects", où tout ce dont vous avez besoin se trouve. Maintenant, vous devez ajouter tous les fichiers modifiés à l'index, et en particulier regarder attentivement les non-staged (il y aura tous les nouveaux fichiers qui sont apparus dans la branche principale). Nous y ajoutons également tout ce dont nous avons besoin.
Enfin, lorsque tout est prêt, nous entrons notre bon commit dans la place réservée:
git commit --amend
Hourra! Tout a fonctionné! Vous pouvez pousser une branche avec désinvolture dans un référentiel public, et personne ne saura que vous avez réellement passé une demi-journée de travail sur cette fusion.
Upd: manière plus concise
Trois mois après cette publication, l'article " Comment et pourquoi voler des arbres en git " par capslocky
Sur la base de ses motivations, il est possible d'atteindre exactement le même objectif de manière plus courte et sans mécanismes auxiliaires: pas besoin de «poster de l'espace», de considérer les fichiers non organisés après la réinitialisation et de les modifier; Vous pouvez créer une validation de fusion directe avec le contenu souhaité en une seule étape.
Nous commençons immédiatement avec la fusion en utilisant toutes les méthodes disponibles (comme au paragraphe 2 ci-dessus). L'histoire intermédiaire décousue et les hacks ne sont toujours pas pertinents. Et puis, au lieu de la revendication 3, avec la substitution du commit de fusion, nous faisons une fusion artificielle , comme dans l'article:
git tag mp git checkout feature git merge --ff $(git commit-tree mp^{tree} -m "merged branch 'master' into 'feature'" -p feature -p master) git tag -d mp
Toute la magie ici se fait en une seule étape par la troisième commande (git commit-tree).
Sélectionnez une partie du fichier, en gardant l'historique
Préambule: le fichier a été encodé-encodé, et finalement encodé de sorte que même le studio visuel a commencé à ralentir, à le digérer (sans parler de JetBrains). (Oui, nous sommes de retour dans le monde "imparfait". Comme toujours).
Les cerveaux intelligents ont pensé, pensé et distingué plusieurs entités qui peuvent être rendues dans un fichier séparé. Mais! Si vous le prenez, copiez et collez un morceau du fichier et collez-le dans un autre - ce sera un fichier complètement nouveau du point de vue de git. En cas de problème, une recherche dans l'historique n'indiquera sans ambiguïté que «où est cette personne handicapée?» Qui a partagé le fichier. Et il peut être nécessaire de trouver la source d'origine non pas du tout «pour les répressions», mais purement constructive - pour savoir pourquoi cette ligne a été modifiée; quel bug il a corrigé (ou pas corrigé). Je veux que le fichier soit nouveau, mais en même temps, toute l'histoire des changements demeure!
L'intrigue. Avec certains effets de bord légèrement ennuyeux, cela peut être fait. Pour être précis, il y a un fichier file.txt
, à partir duquel je veux mettre en évidence une partie dans file2.txt
. (et gardez toujours l'histoire, oui). Exécutez cet extrait:
f=file.txt; f1=file1.txt; f2=file2.txt cp $f $f2 git add $f2 git mv $f $f1 git commit -m"split $f step 1, converted to $f1 and $f2"
Par conséquent, nous obtenons les fichiers file1.txt
et file2.txt
. Ils ont tous deux exactement la même histoire (réelle; comme le fichier d'origine). Oui, le file.txt
origine a dû être renommé; c'est l'effet de bord "légèrement gênant". Malheureusement, je n'ai pas trouvé de moyen de sauvegarder l'histoire, mais afin de NE PAS renommer le fichier source (si quelqu'un le pouvait, dites-le moi!). Cependant, le git supportera tout; maintenant, personne ne se soucie de renommer le fichier dans un commit séparé:
git mv $f1 $f git commit -m"split finish, rename $f1 to $f"
Maintenant file2.txt
dorure affichera le même historique de ligne que le fichier d'origine. L'essentiel est de ne pas fusionner ces deux commits ensemble (sinon toute la magie disparaîtra; je l'ai essayé!). Mais en même temps, personne ne se soucie d'éditer des fichiers directement dans le processus de séparation; il n'est pas nécessaire de le faire plus tard avec des validations distinctes. Et oui, vous pouvez sélectionner plusieurs fichiers à la fois!
Le point clé de la recette: renommer le fichier source en un autre dans le même commit, où il en est fait (et éventuellement édité) une copie (copies). Et laissez ce commit vivre à l'avenir (il ne fera jamais d'erreur avec le renommage inverse).
Upd: une paire de recettes de Lissov
Séparez la partie du référentiel historique
Vous êtes sur la dernière version du référentiel initial. La tâche consiste à séparer un dossier. (J'ai vu des options pour plusieurs dossiers, mais il est plus facile et plus compréhensible de tout d'abord plier tout en un, ou de répéter les opérations suivantes plusieurs fois.)
Important! Tous les mouvements doivent être effectués avec la git mv
, sinon le git peut perdre l'historique.
Nous réalisons:
git filter-branch --prune-empty --subdirectory-filter "{directory}" [branch]
{répertoire} est le dossier à séparer. Par conséquent, nous obtenons le dossier avec l'historique complet des validations uniquement, c'est-à-dire que dans chaque validation, seuls les fichiers de ce dossier sont affichés. Naturellement, une partie des validations sera vide, --prune-empty les supprime.
Maintenant changez l'origine:
git remote set-url origin {another_repository_url}` git checkout move_from_Repo_1
Si le second référentiel est propre, vous pouvez aller directement au master. Eh bien, poussez:
git push -u move_from_Repo_1
Extrait entier (pour un copier-coller facile):
directory="directory_to_extract"; newurl="another_repository_url" git filter-branch --prune-empty --subdirectory-filter "$directory" git remote set-url origin "$newurl" git checkout move_from_Repo_1 git push -u move_from_Repo_1
Fusionner deux référentiels ensemble
Supposons que vous ayez fait quelque chose 2 fois plus haut et obtenu des brunchs move_from_Repo_1
et move_from_Repo_2
, et dans chacun vous avez transféré des fichiers en utilisant git mv
à l'endroit où ils devraient être après la fusion. Reste maintenant à contrôler:
br1="move_from_Repo_1"; br2="move_from_Repo_2" git checkout master git merge origin/$br1 --allow-unrelated-histories git merge origin/$br2 --allow-unrelated-histories git push
L'astuce est dans les histoires --allow-unrelated-history. En conséquence, nous obtenons un référentiel avec un historique complet de toutes les modifications.