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

asmeurer
źródło
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.

coredump
źródło
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.

Cascabel
źródło
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
Vincent Scheib
źródło
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.

Ernesto Baschny
źródło