git merge: zastosuj zmiany w kodzie, który został przeniesiony do innego pliku


137

Próbuję teraz dość mocnego manewru scalania git. Problem, który napotykam, polega na tym, że wprowadziłem pewne zmiany w kodzie w mojej gałęzi, ale mój kolega przeniósł ten kod do nowego pliku w swojej gałęzi. Więc kiedy to zrobiłem git merge my_branch his_branch, git nie zauważył, że kod w nowym pliku był taki sam jak stary, więc nie ma tam żadnych moich zmian.

Jaki jest najłatwiejszy sposób ponownego zastosowania zmian w kodzie w nowych plikach. Nie będę miał zbyt wielu problemów ze stwierdzeniem, które zmiany wymagają ponownego zastosowania (mogę po prostu użyć git log --stat). Ale o ile wiem, nie ma sposobu, aby git ponownie zastosował zmiany w nowych plikach. Najłatwiejszą rzeczą, jaką teraz widzę, jest ręczne ponowne zastosowanie zmian, co nie wygląda na dobry pomysł.

Wiem, że git rozpoznaje bloby, a nie pliki, więc z pewnością musi istnieć sposób, aby to powiedzieć „zastosuj tę dokładną zmianę kodu z tego zatwierdzenia, z wyjątkiem tego, gdzie to było, ale gdzie jest teraz w nowym pliku”.


2
Niezupełnie to samo, ale tutaj jest podobne pytanie z dobrymi odpowiedziami, które mogą mieć zastosowanie: stackoverflow.com/questions/2701790/…
— Mariano Desanze

4
Żadna z odpowiedzi nie opisuje, dlaczego git nie może automatycznie wykonywać takich połączeń . Myślałem, że to ma być wystarczająco inteligentne, aby wykryć zmiany nazw i automatycznie wykonać odpowiednie scalenie?
— Jan

Być może nie było to prawdą, gdy zadano pytanie, ale nowoczesne wersje gita (używam 1.9.5) mogą łączyć zmiany w zmienionych nazwach i przeniesionych plikach. Istnieje nawet --rename-thresholdopcja dostosowania wymaganego podobieństwa.
— Todd Owen

1
@ToddOwen, który zadziała, jeśli wykonujesz zwykłą rebase z gałęzi upstream, ale nadal będziesz mieć kłopoty, jeśli wybierasz lub przenosisz zakres zmian, które mogą nie obejmować zatwierdzenia zmieniającego nazwy plików .
— GuyPaddock

Czy to w ogóle problem, jeśli najpierw wykonasz rebase?
— hbogert

Odpowiedzi:


139

Miałem podobny problem i rozwiązałem go, dostosowując moją pracę do docelowej organizacji plików.

Powiedzmy, że zmodyfikowałeś original.txtswoją gałąź ( localgałąź), ale na gałęzi głównej, original.txtzostała skopiowana do innej, powiedzmy copy.txt. Ta kopia została wykonana w zatwierdzeniu, które nazywamy commit CP.

Chcesz zastosować wszystkie lokalne zmiany, zatwierdzenia Ai Bponiższe zmiany, które zostały wprowadzone w original.txtnowym pliku copy.txt.

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

Utwórz gałąź jednorazowego użytku movew punkcie początkowym zmian za pomocą git branch move X. To znaczy, umieść movegałąź w zatwierdzeniu X, tym przed zatwierdzeniami, które chcesz scalić; najprawdopodobniej jest to zatwierdzenie, z którego wyszedłeś, aby wprowadzić zmiany. Jak napisał poniżej użytkownik @digory doo , możesz git merge-base master localznaleźć X.

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

W tej gałęzi wydaj następujące polecenie zmiany nazwy:

git mv original.txt copy.txt

Spowoduje to zmianę nazwy pliku. Zauważ, że copy.txtw tym momencie jeszcze nie istniał w twoim drzewie.
Zatwierdź swoją zmianę (nazywamy to zatwierdzenie MV).

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

Możesz teraz oprzeć swoją pracę na move:

git rebase move local

Powinno to działać bez problemu, a zmiany zostaną zastosowane copy.txtw lokalnym oddziale.

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

Teraz niekoniecznie chcesz lub musisz mieć zatwierdzenie MVw historii swojej głównej gałęzi, ponieważ operacja przenoszenia może prowadzić do konfliktu z operacją kopiowania przy zatwierdzaniu CPw głównej gałęzi.

Musisz tylko ponownie ustawić bazę swojej pracy, odrzucając operację przenoszenia w następujący sposób:

git rebase move local --onto CP

... gdzie CPjest zatwierdzenie, gdzie copy.txtzostało wprowadzone w innej gałęzi. Ten rebases wszystkie zmiany na copy.txtna szczycie CPpopełnić. Teraz twoja localgałąź jest dokładnie taka, jakbyś zawsze modyfikowała, copy.txta nie original.txt, i możesz kontynuować scalanie z innymi.

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

Ważne jest, aby zmiany zostały zastosowane CPlub w inny copy.txtsposób nie istniałyby, a zmiany zostałyby zastosowane ponownie original.txt.

Mam nadzieję, że to jest jasne. Ta odpowiedź jest spóźniona, ale może być przydatna dla kogoś innego.


2
To dużo pracy, ale myślę, że w zasadzie powinno działać. Myślę jednak, że będziesz miał więcej szczęścia z scalaniem niż rebase.
— asmeurer

8
To rozwiązanie obejmuje tylko podstawowe polecenia git, w przeciwieństwie do edycji łatek lub używania patch(co wiąże się również z potencjalnie dużym nakładem pracy), dlatego pomyślałem, że może być interesujące, aby to pokazać. Zwróć też uwagę, że napisanie odpowiedzi zajęło mi więcej czasu niż wprowadzenie zmian w moim przypadku. Na którym etapie poleciłbyś użycie scalania? i dlaczego? Jedyną różnicą, jaką widzę, jest to, że przez ponowne bazowanie wykonuję tymczasowe zatwierdzenie, które jest później odrzucane (zatwierdzenie MV), co nie jest możliwe w przypadku tylko scaleń.
— coredump

1
W przypadku rebase masz większe szanse na poradzenie sobie z konfliktami scalania, ponieważ zajmujesz się każdym zatwierdzeniem, podczas gdy w przypadku scalania radzisz sobie ze wszystkim naraz, co oznacza, że ​​niektóre zmiany, które mogły nastąpić przy ponownym ustawieniu bazy, nie istniałyby po scaleniu. To, co zrobiłbym, to scalić, a następnie ręcznie przenieść scalony plik. Może jednak źle zrozumiałem ideę twojej odpowiedzi.
— asmeurer

4
Dodałem kilka drzew ASCII, aby wyjaśnić podejście. Odnosząc się do merge vs. rebase w tym przypadku: to, co chcę zrobić, to wziąć wszystkie zmiany z „original.txt” w mojej gałęzi i zastosować je do „copy.txt” w gałęzi master, ponieważ z jakiegoś powodu „oryginalne”. txt ”został w pewnym momencie skopiowany (i nie przeniesiony) do„ copy.txt ”. Po tej kopii plik „original.txt” mógł również wyewoluować w gałęzi master. Gdybym scalił bezpośrednio, moje lokalne zmiany w pliku original.txt zostałyby zastosowane do zmodyfikowanego pliku original.txt w gałęzi master, co byłoby trudne do scalenia. Pozdrowienia.
— coredump

1
Tak czy inaczej, uważam, że to rozwiązanie zadziała (chociaż na szczęście nie mam teraz możliwości wypróbowania go), więc na razie oznaczę je jako odpowiedź.
— asmeurer

31

Zawsze możesz użyć git diff(lub git format-patch), aby wygenerować łatkę, a następnie ręcznie edytować nazwy plików w łatce i zastosować ją za pomocą git apply(lub git am).

Krótko mówiąc, jedynym sposobem, w jaki będzie działać automatycznie, jest to, że wykrywanie zmiany nazwy git może ustalić, że stare i nowe pliki to to samo - co wygląda na to, że tak naprawdę nie są w twoim przypadku, tylko ich fragment. To prawda, że ​​git używa obiektów blob, a nie plików, ale obiekt blob to po prostu zawartość całego pliku, bez dołączonej nazwy pliku i metadanych. Więc jeśli masz fragment kodu przeniesiony między dwoma plikami, nie są one w rzeczywistości tym samym obiektem blob - reszta zawartości obiektu BLOB jest inna, tylko wspólny fragment.


1
Jak dotąd to najlepsza odpowiedź. Nie mogłem wymyślić, jak zrobić git format-patchpracę dla zatwierdzenia. Jeśli to zrobię git format-patch SHA1, wygeneruje całą masę plików łatek dla całej historii. Ale myślę, że git show SHA1 > diff.patchzadziała równie dobrze.
— asmeurer

1
@asmeurer: użyj -1opcji. Normalnym trybem działania dla formatu-patch jest zakres wersji, na przykład origin/master..mastertak, aby można było łatwo przygotować serię poprawek.
— Cascabel

1
Właściwie kolejna uwaga. git applyi git amsą zbyt wybredni, ponieważ chcą mieć te same numery linii. Ale obecnie odnoszę sukcesy z patchpoleceniem UNIX .
— asmeurer

25

Oto rozwiązanie scalania polegające na napotkaniu konfliktu scalania przy zmianie nazwy i edycji oraz rozwiązaniu go za pomocą narzędzia Mergetool rozpoznającego prawidłowe 3 pliki źródłowe scalania.

  • Jeśli scalanie nie powiedzie się z powodu „usuniętego pliku”, o którym zdajesz sobie sprawę, że została zmieniona nazwa i została poddana edycji:

    1. Przerwałeś scalanie.
    2. Zatwierdź pliki o zmienionych nazwach w swoim oddziale.
    3. I ponownie połącz.

Przejście:

Utwórz plik.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

Utwórz oddział, w którym będziesz edytować później:

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

Utwórz zmianę nazwy i edytuj na wzorcu:

$ 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

Zamień na gałąź i tam też edytuj:

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

Spróbuj połączyć wzorzec:

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

Zauważ, że konflikt jest trudny do rozwiązania - i że nazwy plików zostały zmienione. Przerwij, naśladuj zmianę nazwy:

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

Spróbuj ponownie scalić:

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

Świetny! Scalanie powoduje `` normalny '' konflikt, który można rozwiązać za pomocą narzędzia 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

Ciekawy. Będę musiał spróbować, gdy następnym razem napotkam ten problem.
— asmeurer

1
W tym rozwiązaniu wydaje mi się, że pierwszy krok, czyli operacja scalania, która została przerwana, jest przydatna tylko w celu ustalenia, które pliki zostały zmienione i edytowane zdalnie. Jeśli znasz je z góry. Możesz pominąć ten krok i, w zasadzie, rozwiązaniem jest po prostu ręczna lokalna zmiana nazw plików, a następnie scalenie i rozwiązanie konfliktów w zwykły sposób.

1
Dziękuję Ci. Moja sytuacja była taka, że przeniósł się B.txt -> C.txti A.txt -> B.txtze git mv i git nie mógł automatycznie dopasować konflikty scalania poprawnie (był już konflikty scalania między stare B.txti nowe B.txt). Korzystając z tej metody, konflikty scalania są teraz między poprawnymi plikami.
— cib

Działa to tylko wtedy, gdy przenoszony jest cały plik, ale git powinien generalnie wykryć tę sytuację automatycznie. Sytuacja jest trudna, gdy przenoszona jest tylko część pliku.
— Robin Green,

1

Moje szybkie rozwiązanie tego problemu (w moim przypadku nie był to pojedynczy plik, ale cała struktura katalogów) to:

  • przenieś plik (i) z „my_branch” do lokalizacji, w której się znajdują w „his_branch” (git rm / git add)
  • git commit -m "moved to original location for merging"
  • git merge his_branch (tym razem bez konfliktów!)
  • przenieś plik (i) do żądanej lokalizacji (git rm / git add)
  • git commit -m "moved back to final location after merge"

W historii będziesz mieć dwa dodatkowe zatwierdzenia.

Ale ponieważ git ścieżek ruchomy z plików git blame, git logetc będzie nadal działać na tych plikach, ponieważ zobowiązuje które poruszały pliki ich nie zmieni. Więc nie widzę żadnej wady tej metody i jest to bardzo proste do zrozumienia.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.