git merge: aplica cambios al código que se movió a un archivo diferente

132

Estoy intentando una maniobra de fusión git bastante robusta en este momento. Un problema con el que me encuentro es que hice algunos cambios en algún código en mi sucursal, pero mi colega movió ese código a un nuevo archivo en su sucursal. Entonces, cuando lo hice git merge my_branch his_branch, git no notó que el código en el nuevo archivo era el mismo que el anterior, por lo que ninguno de mis cambios está allí.

¿Cuál es la forma más fácil de aplicar mis cambios nuevamente al código en los nuevos archivos? No tendré demasiados problemas para descubrir qué commits deben volver a aplicarse (solo puedo usar git log --stat). Pero por lo que puedo decir, no hay forma de que git vuelva a aplicar los cambios en los nuevos archivos. Lo más fácil que estoy viendo en este momento es volver a aplicar manualmente los cambios, lo que no parece una buena idea.

Sé que git reconoce blobs, no archivos, por lo que seguramente debe haber una manera de decirlo, "aplique este cambio de código exacto desde este commit, excepto no dónde estaba sino dónde está ahora en este nuevo archivo".

asmeurer
fuente
2
No es exactamente lo mismo, pero aquí hay una pregunta similar con buenas respuestas que podrían aplicarse: stackoverflow.com/questions/2701790/…
Mariano Desanze
44
Ninguna de las respuestas describe por qué git no puede realizar fusiones como esta automáticamente. ¿Pensé que se suponía que era lo suficientemente inteligente como para detectar cambios de nombre y realizar la fusión adecuada automáticamente?
John
Quizás esto no era cierto cuando se hizo la pregunta, pero las versiones modernas de git (estoy usando 1.9.5) pueden combinar cambios en archivos renombrados y movidos. Incluso hay una --rename-thresholdopción para ajustar la cantidad de similitud que se requiere.
Todd Owen
1
@ToddOwen que funcionará si está haciendo un rebase de vainilla fuera de una rama aguas arriba, pero aún se encontrará con problemas si está eligiendo o haciendo una copia de seguridad de una serie de cambios que pueden no incluir la confirmación que renombra los archivos .
GuyPaddock
¿Es / fue este un problema en absoluto si primero realiza un rebase?
hbogert

Respuestas:

132

Tuve un problema similar y lo resolví modificando mi trabajo para que coincida con la organización del archivo de destino.

Digamos que modificó original.txten su rama (la localrama), pero en la rama maestra, original.txtse ha copiado a otra, por ejemplo copy.txt. Esta copia se ha realizado en un commit que llamamos commit CP.

Desea aplicar todos los cambios locales, confirmaciones Ay a Bcontinuación, que se realizaron en original.txtel nuevo archivo copy.txt.

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

Cree una rama desechable moveen el punto de partida de sus cambios con git branch move X. Es decir, ponga la moverama en commit X, la anterior a las commits que desea fusionar; Lo más probable es que este sea el compromiso desde el que se ramificó para implementar sus cambios. Como el usuario @digory doo escribió a continuación, puede hacer git merge-base master localpara encontrar X.

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

En esta rama, emita el siguiente comando de cambio de nombre:

git mv original.txt copy.txt

Esto cambia el nombre del archivo. Tenga en cuenta que copy.txttodavía no existía en su árbol en este momento.
Compromete tu cambio (llamamos a este compromiso MV).

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

Ahora puede cambiar su trabajo por encima de move:

git rebase move local

Esto debería funcionar sin problemas y sus cambios se aplicarán copy.txten su sucursal local.

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

Ahora, no necesariamente desea o necesita comprometerse MVen el historial de su sucursal principal, porque la operación de mover puede generar un conflicto con la operación de copia al confirmar CPen la sucursal principal.

Solo tiene que volver a basar su trabajo nuevamente, descartando la operación de movimiento, de la siguiente manera:

git rebase move local --onto CP

... dónde CPestá el commit donde copy.txtse introdujo en la otra rama. Esto reajusta todos los cambios copy.txten la parte superior de la CPconfirmación. Ahora, su localrama es exactamente como si siempre la modificara copy.txty no original.txt, y puede continuar fusionándose con otros.

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

Es importante que los cambios se apliquen CPo de lo contrario copy.txtno existirían y los cambios se volverían a aplicar original.txt.

Espero que esto esté claro. Esta respuesta llega tarde, pero puede ser útil para otra persona.

volcado de memoria
fuente
2
Eso es mucho trabajo, pero creo que debería funcionar en principio. Sin embargo, creo que tendrás mejor suerte con la fusión que con el rebase.
asmeurer
77
Esta solución solo involucra comandos básicos de git, a diferencia de la edición de parches o el uso patch(que también implica mucho trabajo), y es por eso que pensé que podría ser interesante mostrarlo. Además, tenga en cuenta que me tomó más tiempo escribir la respuesta que aplicar los cambios, en mi caso. ¿En qué paso recomendarías usar una fusión? ¿y por qué? La única diferencia que veo es que al hacer un rebase, hago una confirmación temporal que luego se descarta (commit MV), lo que no es posible solo con fusiones.
coredump
1
Con rebase, tiene una mayor probabilidad de lidiar con conflictos de fusión, porque trata con cada commit, mientras que con una fusión trata con todo a la vez, lo que significa que algunos cambios que podrían haber ocurrido con una rebase serían inexistentes con una fusión. Lo que haría es fusionar, luego mover manualmente el archivo fusionado. Sin embargo, tal vez no entendí bien la idea de tu respuesta.
asmeurer 01 de
44
Agregué algunos árboles ASCII para aclarar el enfoque. Regeging merge vs. txt 'se copió (y no se movió) en' copy.txt 'en algún momento. Después de esa copia, 'original.txt' también puede haber evolucionado en la rama maestra. Si me fusionara directamente, mis cambios locales en original.txt se aplicarían al original.txt modificado en la rama maestra, lo que sería difícil de fusionar. Saludos.
coredump
1
Sin embargo, de cualquier manera, creo que esta solución funcionará (aunque afortunadamente no tengo una situación para probarla en este momento), por lo que por ahora lo marcaré como la respuesta.
Asmeurer
31

Siempre puede usar git diff(o git format-patch) para generar el parche, luego editar manualmente los nombres de archivo en el parche y aplicarlo con git apply(o git am).

Aparte de esto, la única forma en que funcionará automáticamente es si la detección de cambio de nombre de git puede descubrir que los archivos antiguos y nuevos son lo mismo, lo que parece que no son realmente en su caso, solo una parte de ellos. Es cierto que git usa blobs, no archivos, pero un blob es solo el contenido de un archivo completo, sin el nombre de archivo y los metadatos adjuntos. Entonces, si tiene un fragmento de código movido entre dos archivos, no son realmente el mismo blob: el resto del contenido del blob es diferente, solo el fragmento en común.

Cascabel
fuente
1
Bueno, es la mejor respuesta hasta ahora. No pude encontrar la manera de hacer que el git format-patchtrabajo para un compromiso. Si lo hago git format-patch SHA1, genera un montón de archivos de parche para toda la historia. Pero supongo git show SHA1 > diff.patchque funcionará igual de bien.
asmeurer
1
@asmeurer: usa la -1opción. El modo normal de operación para el parche de formato es un rango de revisión origin/master..master, por lo que puede preparar fácilmente una serie de parches.
Cascabel
1
En realidad, otra nota. git applyy git amson demasiado exigentes, porque quieren los mismos números de línea. Pero actualmente estoy teniendo éxito con el patchcomando UNIX .
asmeurer
24

Aquí hay una solución de fusión de encontrar un conflicto de fusión con renombrar y editar y resolverlo con mergetool reconociendo los 3 archivos de origen de fusión correctos.

  • Después de que una fusión falla debido al 'archivo eliminado' que se da cuenta de que fue renombrado y editado:

    1. Abortas la fusión.
    2. Confirma archivos renombrados en tu sucursal.
    3. Y fusionarse de nuevo.

Recorrido:

Crea un archivo.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

Crea una rama donde editarás más tarde:

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

Crea el cambio de nombre y edita en master:

$ 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

Cambia a rama y edita allí también:

$ 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(+)

Intento de fusionar maestro:

$ 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.

Observe que el conflicto es difícil de resolver y que los archivos fueron renombrados. Abortar, imitar el cambio de nombre:

$ 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%)

Intenta fusionar de nuevo:

$ 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.

¡Excelente! La fusión da como resultado un conflicto 'normal' que se puede resolver con 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
Vincent Scheib
fuente
Interesante. Tendré que intentarlo la próxima vez que me encuentre con este problema.
Asmeurer
1
En esa solución, me parece que el primer paso, la fusión que se cancela, solo es útil para averiguar qué archivos se renombraron y editaron de forma remota. Si los conoces de antemano. Puede omitir ese paso y, básicamente, la solución es simplemente cambiar el nombre de los archivos de forma local y luego fusionar y resolver los conflictos como de costumbre.
1
Gracias. Mi situación era que me mudé B.txt -> C.txty A.txt -> B.txtcon git mv, y git no podía hacer coincidir automáticamente los conflictos de fusión correctamente (estaba obteniendo conflictos de fusión entre lo antiguo B.txty lo nuevo B.txt). Al usar este método, los conflictos de fusión ahora están entre los archivos correctos.
cib
Esto solo funciona cuando se mueve todo el archivo, pero git generalmente debería detectar esa situación automáticamente. La situación difícil es cuando solo se mueve una parte de un archivo.
Robin Green el