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


132

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

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

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


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.

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


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-объекта отличается, просто общий фрагмент.


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 обычно обнаруживает эту ситуацию автоматически. Сложная ситуация - когда перемещается только часть файла.
— Робин Грин
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.