git merge: appliquer les modifications au code qui a été déplacé vers un autre fichier


132

Je tente une manœuvre de fusion git assez costaud en ce moment. Un problème que je rencontre est que j'ai apporté des modifications à du code dans ma branche, mais mon collègue a déplacé ce code dans un nouveau fichier dans sa branche. Donc quand je l'ai fait git merge my_branch his_branch, git n'a pas remarqué que le code dans le nouveau fichier était le même que l'ancien, et donc aucune de mes modifications n'est là.

Quelle est la manière la plus simple d'appliquer à nouveau mes modifications au code dans les nouveaux fichiers. Je n'aurai pas trop de problèmes pour savoir quels commits doivent être réappliqués (je peux simplement utiliser git log --stat). Mais pour autant que je sache, il n'y a aucun moyen de demander à git de réappliquer les modifications dans les nouveaux fichiers. La chose la plus simple que je vois en ce moment est de réappliquer manuellement les modifications, ce qui ne semble pas être une bonne idée.

Je sais que git reconnaît les blobs, pas les fichiers, donc il doit sûrement y avoir un moyen de le dire, "appliquer ce changement de code exact à partir de ce commit, sauf pas où il était mais où il se trouve maintenant dans ce nouveau fichier".


2
Pas exactement la même chose, mais voici une question similaire avec de bonnes réponses qui pourraient s'appliquer: stackoverflow.com/questions/2701790/…
Mariano Desanze

4
Aucune des réponses ne décrit pourquoi git n'est pas capable d'effectuer automatiquement des fusions comme celle-ci. Je pensais que c'était censé être assez intelligent pour détecter les renommés et effectuer automatiquement la fusion appropriée?
John

Ce n'était peut-être pas vrai lorsque la question a été posée, mais les versions modernes de git (j'utilise la version 1.9.5) peuvent fusionner les modifications entre les fichiers renommés et déplacés. Il existe même une --rename-thresholdoption pour modifier le degré de similitude requis.
Todd Owen

1
@ToddOwen qui fonctionnera si vous effectuez un rebase vanille à partir d'une branche en amont, mais vous rencontrerez toujours des problèmes si vous sélectionnez ou rétroportez une gamme de modifications qui peuvent ne pas inclure le commit qui renomme les fichiers .
GuyPaddock

Est / était-ce un problème si vous faites d'abord un rebase?
hbogert le

Réponses:


132

J'ai eu un problème similaire et je l'ai résolu en rebasant mon travail pour qu'il corresponde à l'organisation de fichiers cible.

Disons que vous avez modifié original.txtsur votre branche (la localbranche), mais sur la branche maître, original.txta été copiée dans une autre, par exemple copy.txt. Cette copie a été faite dans un commit que nous nommons commit CP.

Vous voulez appliquer toutes vos modifications locales, commits Aet Bci - dessous, qui ont été apportées original.txt, au nouveau fichier copy.txt.

 ---- X -----CP------ (master)
       \ 
        \--A---B--- (local)

Créez une branche jetable moveau point de départ de vos modifications avec git branch move X. C'est-à-dire, mettez la movebranche à commit X, celle qui précède les commits que vous souhaitez fusionner; il s'agit probablement du commit à partir duquel vous vous êtes diversifié pour implémenter vos modifications. Comme l'a écrit l' utilisateur @digory doo ci-dessous, vous pouvez le faire git merge-base master localpour trouver X.

 ---- X (move)-----CP----- (master)
       \ 
        \--A---B--- (local)

Sur cette branche, exécutez la commande de changement de nom suivante:

git mv original.txt copy.txt

Cela renomme le fichier. Notez que cela copy.txtn'existait pas encore dans votre arbre à ce stade.
Validez votre modification (nous appelons ce commit MV).

        /--MV (move)
       /
 ---- X -----CP----- (master)
       \ 
        \--A---B--- (local)

Vous pouvez maintenant rebaser votre travail en plus de move:

git rebase move local

Cela devrait fonctionner sans problème et vos modifications sont appliquées copy.txtdans votre succursale locale.

        /--MV (move)---A'---B'--- (local)
       /
 ---- X -----CP----- (master)

Maintenant, vous ne voulez pas ou n'avez pas nécessairement besoin d'avoir une validation MVdans l'historique de votre branche principale, car l'opération de déplacement peut conduire à un conflit avec l'opération de copie lors de la validation CPdans la branche principale.

Il vous suffit de rebaser à nouveau votre travail en annulant l'opération de déplacement, comme suit:

git rebase move local --onto CP

... où CPest le commit où a copy.txtété introduit dans l'autre branche. Cela rebase tous les changements copy.txten plus du CPcommit. Maintenant, votre localbranche est exactement comme si vous aviez toujours modifié copy.txtet non original.txt, et vous pouvez continuer à fusionner avec d'autres.

                /--A''---B''-- (local)
               /
 -----X-------CP----- (master)

Il est important que les modifications soient appliquées CPou copy.txtn'existeraient pas et que les modifications seraient appliquées de nouveau original.txt.

J'espère que c'est clair. Cette réponse arrive tardivement, mais cela peut être utile à quelqu'un d'autre.


2
C'est beaucoup de travail, mais je pense que cela devrait fonctionner en principe. Je pense que vous aurez plus de chance avec la fusion qu'avec le rebase.
asmeurer

7
Cette solution n'implique que des commandes git de base, contrairement à l'édition de correctifs ou à l'utilisation patch(ce qui implique également potentiellement beaucoup de travail), et c'est pourquoi j'ai pensé qu'il pourrait être intéressant de le montrer. Notez également que j'ai pris plus de temps pour rédiger la réponse que pour appliquer réellement les changements, dans mon cas. À quelle étape recommanderiez-vous d'utiliser une fusion? et pourquoi? La seule différence que je vois est qu'en rebasant, je crée un commit temporaire qui est ensuite rejeté (commit MV), ce qui n'est pas possible avec les fusions uniquement.
coredump

1
Avec rebase, vous avez plus de chances de gérer les conflits de fusion, car vous traitez chaque commit, alors qu'avec une fusion, vous gérez tout en même temps, ce qui signifie que certains changements qui auraient pu se produire avec un rebase seraient inexistants avec une fusion. Ce que je ferais, c'est fusionner, puis déplacer manuellement le fichier fusionné. J'ai peut-être mal compris l'idée de votre réponse.
asmeurer

4
J'ai ajouté quelques arbres ASCII pour clarifier l'approche. Regaring merge vs rebase dans ce cas: ce que je veux faire est de prendre toutes les modifications sur 'original.txt' dans ma branche et de les appliquer à 'copy.txt' dans la branche principale, car pour une raison quelconque, 'original. txt 'a été copié (et non déplacé) dans' copy.txt 'à un moment donné. Après cette copie, 'original.txt' peut également avoir évolué sur la branche master. Si je fusionnais directement, mes modifications locales sur original.txt seraient appliquées à l'original.txt modifié dans la branche principale, ce qui serait difficile à fusionner. Cordialement.
coredump

1
Quoi qu'il en soit, cependant, je crois que cette solution fonctionnera (bien que je n'ai heureusement pas de situation pour l'essayer pour le moment), donc pour l'instant, je vais la marquer comme la réponse.
asmeurer

31

Vous pouvez toujours utiliser git diff(ou git format-patch) pour générer le patch, puis aller éditer manuellement les noms de fichiers dans le patch, et l'appliquer avec git apply(ou git am).

À part cela, la seule façon dont cela fonctionnera automatiquement est si la détection de changement de nom de git peut comprendre que l'ancien et le nouveau fichier sont la même chose - ce qui semble ne pas être vraiment dans votre cas, juste une partie d'entre eux. Il est vrai que git utilise des blobs, pas des fichiers, mais un blob est juste le contenu d'un fichier entier, sans le nom de fichier et les métadonnées attachés. Donc, si vous avez un morceau de code déplacé entre deux fichiers, ils ne sont pas vraiment le même blob - le reste du contenu du blob est différent, juste le morceau en commun.


1
Eh bien, c'est la meilleure réponse à ce jour. Je ne savais pas comment faire git format-patchfonctionner un commit. Si je le fais git format-patch SHA1, cela génère tout un tas de fichiers de correctifs pour toute l'histoire. Mais je suppose que git show SHA1 > diff.patchcela fonctionnera aussi bien.
asmeurer

1
@asmeurer: utilisez l' -1option. Le mode de fonctionnement normal de format-patch est une plage de révision, par exemple origin/master..master, afin que vous puissiez facilement préparer une série de patchs.
Cascabel

1
En fait, une autre note. git applyet git amsont trop pointilleux, car ils veulent les mêmes numéros de ligne. Mais je réussis actuellement avec la patchcommande UNIX .
asmeurer

24

Voici une solution de fusion pour rencontrer un conflit de fusion avec renommer et modifier et le résoudre avec mergetool reconnaissant les 3 fichiers source de fusion corrects.

  • Après l'échec d'une fusion en raison d'un `` fichier supprimé '' dont vous vous rendez compte qu'il a été renommé et modifié:

    1. Vous abandonnez la fusion.
    2. Validez les fichiers renommés sur votre branche.
    3. Et fusionner à nouveau.

Procédure pas à pas:

Créez un fichier.txt:

$ git init
Initialized empty Git repository in /tmp/git-rename-and-modify-test/.git/

$ echo "A file." > file.txt
$ git add file.txt
$ git commit -am "file.txt added."
[master (root-commit) 401b10d] file.txt added.
 1 file changed, 1 insertion(+)
 create mode 100644 file.txt

Créez une branche dans laquelle vous modifierez plus tard:

$ git branch branch-with-edits
Branch branch-with-edits set up to track local branch master.

Créez le changement de nom et modifiez sur le maître:

$ git mv file.txt renamed-and-edited.txt
$ echo "edits on master" >> renamed-and-edited.txt 
$ git commit -am "file.txt + edits -> renamed-and-edited.txt."
[master def790f] file.txt + edits -> renamed-and-edited.txt.
 2 files changed, 2 insertions(+), 1 deletion(-)
 delete mode 100644 file.txt
 create mode 100644 renamed-and-edited.txt

Passez à la branche et modifiez-y aussi:

$ git checkout branch-with-edits 
Switched to branch 'branch-with-edits'
Your branch is behind 'master' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)
$ 
$ echo "edits on branch" >> file.txt 
$ git commit -am "file.txt edited on branch."
[branch-with-edits 2c4760e] file.txt edited on branch.
 1 file changed, 1 insertion(+)

Tentative de fusion du maître:

$ git merge master
CONFLICT (modify/delete): file.txt deleted in master and modified in HEAD. Version HEAD of file.txt left in tree.
Automatic merge failed; fix conflicts and then commit the result.

Notez que le conflit est difficile à résoudre et que les fichiers ont été renommés. Abandonner, imiter le changement de nom:

$ git merge --abort
$ git mv file.txt renamed-and-edited.txt
$ git commit -am "Preparing for merge; Human noticed renames files were edited."
[branch-with-edits ca506da] Preparing for merge; Human noticed renames files were edited.
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename file.txt => renamed-and-edited.txt (100%)

Essayez à nouveau de fusionner:

$ git merge master
Auto-merging renamed-and-edited.txt
CONFLICT (add/add): Merge conflict in renamed-and-edited.txt
Recorded preimage for 'renamed-and-edited.txt'
Automatic merge failed; fix conflicts and then commit the result.

Génial! La fusion entraîne un conflit `` normal '' qui peut être résolu avec mergetool:

$ git mergetool
Merging:
renamed-and-edited.txt

Normal merge conflict for 'renamed-and-edited.txt':
  {local}: created file
  {remote}: created file
$ git commit 
Recorded resolution for 'renamed-and-edited.txt'.
[branch-with-edits 2264483] Merge branch 'master' into branch-with-edits

Intéressant. Je vais devoir essayer ceci la prochaine fois que je rencontre ce problème.
asmeurer

1
Dans cette solution, il me semble que la première étape, la fusion qui est interrompue, n'est utile que pour savoir quels fichiers ont été renommés et modifiés à distance. Si vous les connaissez à l'avance. Vous pouvez sauter cette étape et, fondamentalement, la solution consiste simplement à renommer les fichiers manuellement localement, puis à fusionner et à résoudre les conflits comme d'habitude.

1
Je vous remercie. Ma situation était que j'ai déménagé B.txt -> C.txtet A.txt -> B.txtavec git mv, et git ne pouvait pas automatiquement faire correspondre correctement les conflits de fusion (obtenait des conflits de fusion entre l'ancien B.txtet le nouveau B.txt). En utilisant cette méthode, les conflits de fusion sont désormais entre les fichiers corrects.
cib

Cela ne fonctionne que lorsque le fichier entier est déplacé, mais git devrait généralement détecter cette situation automatiquement. La situation délicate est lorsque seule une partie d'un fichier est déplacée.
Robin Green
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.