git merge: applica le modifiche al codice spostato in un altro file


132

Sto tentando una manovra di merge git piuttosto muscolosa in questo momento. Un problema che sto riscontrando è che ho apportato alcune modifiche ad alcuni codici nel mio ramo, ma il mio collega ha spostato quel codice in un nuovo file nel suo ramo. Quindi, quando l'ho fatto git merge my_branch his_branch, git non ha notato che il codice nel nuovo file era lo stesso del vecchio, e quindi nessuna delle mie modifiche è presente.

Qual è il modo più semplice per applicare nuovamente le mie modifiche al codice nei nuovi file. Non avrò troppi problemi per scoprire quali commit devono essere riapplicati (posso solo usare git log --stat). Ma per quanto ne so, non c'è modo di ottenere git per riapplicare le modifiche nei nuovi file. La cosa più semplice che sto vedendo in questo momento è riapplicare manualmente le modifiche, che non sembra una buona idea.

So che git riconosce i BLOB, non i file, quindi sicuramente deve esserci un modo per dirlo, "applica questo esatto cambio di codice da questo commit, tranne non dove fosse ma dove si trova ora in questo nuovo file".


2
Non è esattamente lo stesso, ma qui è una domanda simile con buone anwsers che potrebbero essere applicate: stackoverflow.com/questions/2701790/...
Mariano Desanze

4
Nessuna delle risposte descrive perché git non è in grado di eseguire automaticamente fusioni come questa. Ho pensato che avrebbe dovuto essere abbastanza intelligente da rilevare rinominazioni ed eseguire automaticamente l'unione appropriata?
Giovanni

Forse questo non era vero quando è stata posta la domanda, ma le versioni moderne di git (sto usando 1.9.5) possono unire le modifiche tra i file rinominati e spostati. C'è anche --rename-thresholdun'opzione per modificare la quantità di somiglianza richiesta.
Todd Owen,

1
@ToddOwen che funzionerà se stai eseguendo un rebase vanilla fuori da un ramo a monte, ma avrai comunque problemi se stai selezionando o esegui il back-porting di una serie di modifiche che potrebbero non includere il commit che rinomina i file .
GuyPaddock,

È / è stato un problema se si fa prima un rebase?
hbogert,

Risposte:


132

Ho riscontrato un problema simile e l'ho risolto riformulando il mio lavoro in modo che corrispondesse all'organizzazione del file di destinazione.

Supponiamo che tu abbia modificato original.txtil tuo ramo (il localramo), ma nel ramo principale original.txtsia stato copiato in un altro, diciamo copy.txt. Questa copia è stata eseguita in un commit che chiamiamo commit CP.

Si desidera applicare tutte le modifiche locali, le commit Ae le Bsuccessive, che sono state apportate original.txt, al nuovo file copy.txt.

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

Crea un ramo di lancio movenel punto iniziale delle modifiche con git branch move X. Vale a dire, metti il moveramo in commit X, quello prima dei commit che vuoi unire; molto probabilmente, questo è il commit da cui sei uscito per implementare le tue modifiche. Come l'utente @digory doo ha scritto di seguito, puoi farlo git merge-base master localper trovare X.

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

Su questo ramo, emettere il seguente comando di ridenominazione:

git mv original.txt copy.txt

Questo rinomina il file. Nota che copy.txtnon esisteva ancora nel tuo albero a questo punto.
Commetti la tua modifica (la chiamiamo commit MV).

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

Ora puoi modificare il lavoro su move:

git rebase move local

Questo dovrebbe funzionare senza problemi e le tue modifiche verranno applicate copy.txtnella tua filiale locale.

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

Ora, non è necessario o non è necessario eseguire il commit MVnella cronologia del ramo principale, poiché l'operazione di spostamento può portare a un conflitto con l'operazione di copia durante il commit CPnel ramo principale.

Devi solo rifare il lavoro di nuovo, scartando l'operazione di spostamento, come segue:

git rebase move local --onto CP

... dov'è CPil commit dove è copy.txtstato introdotto nell'altro ramo. Ciò ripristina tutte le modifiche copy.txtsopra il CPcommit. Ora, il tuo localramo è esattamente come se avessi sempre modificato copy.txte non original.txt, e puoi continuare a fonderti con gli altri.

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

È importante che le modifiche vengano applicate CPo copy.txtnon esistano e che le modifiche vengano applicate nuovamente original.txt.

Spero sia chiaro. Questa risposta arriva in ritardo, ma può essere utile a qualcun altro.


2
È un sacco di lavoro, ma penso che dovrebbe funzionare in linea di principio. Penso che avrai più fortuna con merge che rebase.
asmeurer,

7
Questa soluzione coinvolge solo i comandi git di base, a differenza della modifica delle patch o dell'uso patch(che comporta anche potenzialmente molto lavoro), ed è per questo che ho pensato che potesse essere interessante mostrarlo. Inoltre, nota che ho impiegato più tempo per scrivere la risposta che per applicare effettivamente le modifiche, nel mio caso. A quale passaggio consiglieresti di utilizzare un'unione? e perché? L'unica differenza che vedo è che con il rebasing, eseguo un commit temporaneo che viene successivamente scartato (commit MV), il che non è possibile solo con le fusioni.
coredump

1
Con rebase, hai maggiori possibilità di gestire i conflitti di unione, perché gestisci ogni commit, mentre con una fusione gestisci tutto in una volta, il che significa che alcuni cambiamenti che potrebbero essere avvenuti con un rebase sarebbero inesistenti con una fusione. Quello che vorrei fare è unire, quindi spostare manualmente il file unito. Forse ho frainteso l'idea della tua risposta, però.
asmeurer

4
Ho aggiunto alcuni alberi ASCII per chiarire l'approccio. Regaring merge vs. rebase in quel caso: quello che voglio fare è prendere tutte le modifiche su 'original.txt' nel mio ramo e applicarle a 'copy.txt' nel ramo principale, perché per qualche ragione, 'originale. txt 'è stato copiato (e non spostato) in' copy.txt 'ad un certo punto. Dopo quella copia, 'original.txt' potrebbe anche essersi evoluto sul ramo principale. Se fossi unito direttamente, le mie modifiche locali su original.txt verrebbero applicate a original.txt modificato nel ramo master, che sarebbe difficile da unire. Saluti.
coredump

1
Ad ogni modo, comunque, credo che questa soluzione funzionerà (anche se per fortuna non ho una situazione per provarla in questo momento), quindi per ora la segnerò come risposta.
asmeurer

31

Puoi sempre usare git diff(o git format-patch) per generare la patch, quindi modificare manualmente i nomi dei file nella patch e applicarla con git apply(o git am).

A parte questo, l'unico modo in cui funzionerà automaticamente è se il rilevamento della ridenominazione di git può capire che i file vecchi e nuovi sono la stessa cosa - il che sembra che non siano davvero nel tuo caso, solo una parte di essi. È vero che git usa BLOB, non file, ma un BLOB è solo il contenuto di un intero file, senza il nome file e i metadati allegati. Pertanto, se si sposta un blocco di codice tra due file, questi non sono realmente lo stesso BLOB: il resto del contenuto del BLOB è diverso, solo il blocco in comune.


1
Bene, è la risposta migliore finora. Non riuscivo a capire come git format-patchlavorare per un commit. Se lo faccio git format-patch SHA1, genera un sacco di file patch per l'intera storia. Ma credo git show SHA1 > diff.patchche funzionerà altrettanto bene.
asmeurer

1
@asmeurer: utilizzare l' -1opzione. La normale modalità di funzionamento per il formato patch è un intervallo di revisione origin/master..master, quindi è possibile preparare facilmente una serie di patch.
Cascabel,

1
In realtà, un'altra nota. git applye git amsono troppo esigenti, perché vogliono gli stessi numeri di riga. Ma attualmente sto avendo successo con il patchcomando UNIX .
asmeurer

24

Ecco una soluzione di unione di incontrare un conflitto di unione con rinomina e modifica e risolverlo con mergetool riconoscendo i file di origine 3 merge corretti.

  • Dopo un'unione fallita a causa del 'file cancellato' che ritieni sia stato rinominato e modificato:

    1. Interrompi l'unione.
    2. Salva i file rinominati sul tuo ramo.
    3. E unisci di nuovo.

Procedura dettagliata:

Crea un file.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 un ramo in cui dovrai modificare in seguito:

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

Crea il nome e modifica sul 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

Scambia su filiale e modifica anche lì:

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

Tentativo di unire master:

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

Si noti che il conflitto è difficile da risolvere e che i file sono stati rinominati. Annulla, imita la ridenominazione:

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

Prova a unire di nuovo:

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

Grande! L'unione provoca un conflitto "normale" che può essere risolto 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

Interessante. Dovrò provare questo la prossima volta che incontrerò questo problema.
asmeurer

1
In quella soluzione, mi sembra che il primo passo, l'unione interrotta, sia utile solo per scoprire quali file sono stati rinominati e modificati in remoto. Se li conosci in anticipo. Potresti saltare questo passaggio e, in sostanza, la soluzione è semplicemente rinominare i file manualmente localmente e quindi unire e risolvere i conflitti come al solito.

1
Grazie. La mia situazione era che mi muovevo B.txt -> C.txte A.txt -> B.txtcon git mv, e git non riusciva a far corrispondere automaticamente i conflitti di unione correttamente (stava ottenendo conflitti di unione tra vecchio B.txte nuovo B.txt). Utilizzando questo metodo, i conflitti di unione sono ora tra i file corretti.
cib

Funziona solo quando l'intero file viene spostato, ma git dovrebbe generalmente rilevare automaticamente quella situazione. La situazione difficile è quando viene spostata solo una parte di un file.
Robin Green,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.