git merge: применить изменения к коду, который переместился в другой файл

132

Я сейчас пытаюсь выполнить довольно мощный маневр слияния git. Одна из проблем, с которой я столкнулся, заключается в том, что я внес некоторые изменения в некоторый код в своей ветке, но мой коллега переместил этот код в новый файл в своей ветке. Когда я это сделал git merge my_branch his_branch, git не заметил, что код в новом файле был таким же, как и в старом, и поэтому никаких моих изменений там нет.

Какой самый простой способ снова применить мои изменения к коду в новых файлах? У меня не будет слишком много проблем с выяснением, какие коммиты нужно повторно применить (я могу просто использовать git log --stat). Но, насколько я могу судить, нет никакого способа заставить git повторно применить изменения к новым файлам. Самое простое, что я вижу прямо сейчас, - это повторно применить изменения вручную, что не похоже на хорошую идею.

Я знаю, что git распознает капли, а не файлы, поэтому наверняка должен быть способ сказать ему: «применить это точное изменение кода из этого коммита, за исключением того, где оно было, а где оно сейчас находится в этом новом файле».

asmeurer
источник
2
Не совсем то же самое, но вот аналогичный вопрос с хорошими ответами, которые могут применяться: stackoverflow.com/questions/2701790/…
Мариано Десанце
4
Ни один из ответов не описывает, почему git не может автоматически выполнять подобное слияние. Я думал, что он должен быть достаточно умным, чтобы обнаруживать переименования и автоматически выполнять соответствующее слияние?
Джон
Возможно, когда задавался вопрос, это было неправдой, но современные версии git (я использую 1.9.5) могут объединять изменения в переименованных и перемещенных файлах. Есть даже --rename-thresholdвозможность настроить необходимое количество сходства.
Тодд Оуэн,
1
@ToddOwen, который будет работать, если вы выполняете ванильную перебазировку из восходящей ветки, но у вас все равно возникнут проблемы, если вы выбираете вишню или выполняете обратный перенос ряда изменений, которые могут не включать фиксацию, которая переименовывает файлы ,
GuyPaddock
Было ли это проблемой, если вы сначала сделали ребаз?
hbogert

Ответы:

132

У меня была аналогичная проблема, и я решил ее, переместив свою работу в соответствие с организацией целевого файла.

Скажем, вы изменили original.txtсвою ветку ( localветку), но original.txt, скажем, основная ветка была скопирована в другую copy.txt. Эта копия была сделана в коммите, который мы называем коммитом CP.

Вы хотите применить к новому файлу все ваши локальные изменения, коммиты Aи Bниже, которые были сделаны .original.txtcopy.txt

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

Создайте одноразовую ветку moveв начальной точке ваших изменений с помощью git branch move X. Другими словами, поместите moveветку в точку фиксации X, ту, что перед коммитами, которые вы хотите объединить; скорее всего, это тот коммит, из которого вы вышли для реализации своих изменений. Как написал ниже пользователь @digory doo , вы можете сами git merge-base master localнайти X.

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

В этой ветке введите следующую команду переименования:

git mv original.txt copy.txt

Это переименовывает файл. Обратите внимание, что copy.txtна данный момент в вашем дереве еще не было.
Зафиксируйте свое изменение (мы называем это фиксацией MV).

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

Теперь вы можете перебазировать свою работу поверх move:

git rebase move local

Это должно работать без проблем, и ваши изменения будут применены copy.txtв вашем локальном филиале.

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

Теперь вы не обязательно хотите или должны иметь фиксацию MVв истории вашей основной ветки, потому что операция перемещения может привести к конфликту с операцией копирования при фиксации CPв основной ветке.

Вам нужно только снова переустановить свою работу, отказавшись от операции перемещения, как показано ниже:

git rebase move local --onto CP

... где CPкоммит, который copy.txtбыл введен в другой ветке. Это перебазирует все изменения copy.txtповерх CPкоммита. Теперь ваша localветка такая же, как если бы вы всегда изменяли, copy.txtа не изменяли original.txt, и вы можете продолжить слияние с другими.

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

Важно, чтобы изменения применялись CPили иначе copy.txtне существовало бы, и изменения применялись бы снова original.txt.

Надеюсь, это понятно. Этот ответ приходит с опозданием, но может быть полезен кому-то другому.

CoreDump
источник
2
Это большая работа, но я думаю, что в принципе она должна работать. Я думаю, вам больше повезет со слиянием, чем с перебазированием.
asmeurer
7
Это решение включает только базовые команды git, в отличие от редактирования патчей или использования patch(что также потенциально требует большого объема работы), и поэтому я подумал, что было бы интересно показать его. Также обратите внимание, что в моем случае я потратил больше времени на то, чтобы написать ответ, чем на то, чтобы применить изменения. На каком этапе вы бы рекомендовали использовать слияние? и почему? Единственное различие, которое я вижу, заключается в том, что путем перебазирования я делаю временную фиксацию, которая позже отбрасывается (фиксация MV), что невозможно только при слияниях.
coredump 01
1
С перебазированием у вас больше шансов справиться с конфликтами слияния, потому что вы имеете дело с каждой фиксацией, тогда как при слиянии вы имеете дело со всем сразу, то есть некоторые изменения, которые могли произойти с перебазированием, не будут существовать при слиянии. Я бы сделал слияние, а затем вручную переместил объединенный файл. Возможно, я неправильно понял идею вашего ответа.
asmeurer 01
4
Я добавил несколько деревьев ASCII, чтобы прояснить подход. Что касается слияния и перебазирования в этом случае: то, что я хочу сделать, это взять все изменения в 'original.txt' в моей ветке и применить их к 'copy.txt' в основной ветке, потому что по какой-то причине 'original. txt 'был скопирован (а не перемещен) в' copy.txt 'в какой-то момент. После этой копии файл original.txt также мог развиться в ветке master. Если бы я выполнял слияние напрямую, мои локальные изменения в original.txt были бы применены к измененному original.txt в основной ветке, что было бы трудно объединить. С уважением.
coredump
1
В любом случае, я считаю, что это решение будет работать (хотя, к счастью, сейчас у меня нет ситуации, в которой можно было бы его попробовать), поэтому пока я отмечу его как ответ.
asmeurer 08
31

Вы всегда можете использовать git diff(или git format-patch) для создания патча, затем вручную отредактировать имена файлов в патче и применить его с помощью git apply(или git am).

Если не считать этого, единственный способ, которым это будет работать автоматически, - это если обнаружение переименования git сможет определить, что старый и новый файлы - это одно и то же - что, похоже, на самом деле не в вашем случае, а просто их кусок. Это правда, что git использует большие двоичные объекты, а не файлы, но большой двоичный объект - это просто содержимое всего файла без прикрепленных файлов и метаданных. Таким образом, если у вас есть фрагмент кода, перемещенный между двумя файлами, на самом деле это не один и тот же blob - остальное содержимое blob-объекта отличается, просто общий фрагмент.

Cascabel
источник
1
Что ж, это пока лучший ответ. Я не мог понять, как заставить git format-patchработать коммит. Если я это сделаю git format-patch SHA1, он создаст целую кучу файлов патчей для всей истории. Но, думаю, git show SHA1 > diff.patchтоже сработает.
asmeurer
1
@asmeurer: используйте -1опцию. Обычный режим работы для format-patch - это диапазон ревизий, например origin/master..master, так что вы можете легко подготовить серию исправлений.
Cascabel
1
Собственно, еще одно замечание. git applyи git amслишком разборчивы, потому что хотят одинаковые номера строк. Но в настоящее время я успешно использую patchкоманду UNIX .
asmeurer
24

Вот решение слияния при обнаружении конфликта слияния с переименованием и редактированием и его разрешение с помощью mergetool, распознающего правильные 3 исходных файла слияния.

  • После сбоя слияния из-за «удаленного файла», который, как вы понимаете, был переименован и отредактирован:

    1. Вы прерываете слияние.
    2. Зафиксируйте переименованные файлы в своей ветке.
    3. И снова сливаемся.

Прохождение:

Создайте файл .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

Создайте ветку, в которой вы будете редактировать позже:

$ git branch branch-with-edits
Branch branch-with-edits set up to track local branch 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

Переключитесь на ветку и отредактируйте там тоже:

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

Попытка слить мастер:

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

Обратите внимание, что конфликт трудно разрешить, и файлы были переименованы. Прервать, имитировать переименование:

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

Попробуйте слить еще раз:

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

Большой! Слияние приводит к «нормальному» конфликту, который можно разрешить с помощью 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
Винсент Шейб
источник
Интересный. Мне придется попробовать это в следующий раз, когда я столкнусь с этой проблемой.
asmeurer
1
В этом решении мне кажется, что первый шаг - прерванное слияние - полезен только для того, чтобы узнать, какие файлы были переименованы и отредактированы удаленно. Если вы их знаете заранее. Вы можете пропустить этот шаг, и, по сути, решение состоит в том, чтобы просто переименовать файлы вручную локально, а затем объединить и разрешить конфликты как обычно.
1
Спасибо. Моя ситуация в том , что я переехал B.txt -> C.txtи A.txt -> B.txtс мерзавца мв и мерзавцем не может автоматически соответствовать конфликты слияния правильно (получал конфликты слияния между старым B.txtи новым B.txt). При использовании этого метода конфликты слияния теперь возникают между правильными файлами.
cib
Это работает только при перемещении всего файла, но git обычно обнаруживает эту ситуацию автоматически. Сложная ситуация - когда перемещается только часть файла.
Робин Грин