Git: вручную объединить изменения в необнаруженном переименовании

В этом вопросе используется Git 2.7.0.windows.1, поэтому некоторые команды git могут быть устаревшими.

Если моя команда git merge не обнаруживает переименованный файл, как я могу указать git вручную объединить изменения в двух файлах, которые должны быть одним файлом, не запуская слияние заново и используя более низкий порог переименования?

Этапы воспроизведения:

git init
echo "//Hello world!" > hw.h
git add . && git commit -m "Initial commit"
git checkout -b someBranch
mv hw.h hw.hpp
echo "//Foobar" > hw.hpp
git add . && git commit -m "Change hw to HPP & change content"
git checkout master
echo "//Boofar" > hw.h
git add . && git commit -m "Change content of hw"
git merge -X rename-threshold=100% someBranch

Вы получите конфликты слияния, но не конфликтующие фрагменты. То есть единственный конфликт/ошибка, которую вы должны получить, это:

CONFLICT (modify/delete): hw.h deleted in branchB and modified in HEAD. Version HEAD of hw.h left in tree.
Automatic merge failed; fix conflicts and then commit the result.

И git status --porcelain покажет:

UD hw.h
A  hw.hpp

Обычно при слиянии идеально установить порог обнаружения переименований, достаточно низкий, чтобы переименования обнаруживались. Некоторые люди рекомендуют 5%, например . В моем случае я выполняю массовое слияние (> 1000 файлов и около 9 млн LOC), и мне пришлось поднять порог переименования достаточно высоко, чтобы избежать любых «ложных срабатываний»; буквально на один процент ниже, и я получил огромное количество ложно обнаруженных переименований (я знаю, дублированный код — отстой). При значении, которое я использовал, я получаю лишь небольшое количество пропущенных переименований, что кажется лучшим вариантом.

TL;DR снижение порога переименования для меня не вариант; как я могу, не начиная слияние, сказать git считать hw.h и hw.hpp одним файлом (с конфликтами), а не двумя файлами, как показано выше?


person Nick Giampietro    schedule 30.03.2017    source источник
comment
Кстати, я чувствую твою боль. Однажды я уже пытался сделать такое слияние. В итоге мы использовали другую стратегию переноса. :-)   -  person torek    schedule 30.03.2017


Ответы (1)


Инструменты для этого немного неуклюжи, но они есть.

Вы должны быть уверены, что само слияние остановится перед фиксацией. В вашем случае это происходит автоматически. Для более сложных слияний, когда Git думает, что делает это правильно, но это не так, вы должны добавить --no-commit, но это повлияет на следующие несколько шагов. Мы пока проигнорируем эту проблему.

Далее вам нужно получить все три версии рассматриваемого файла. Поскольку Git остановился из-за конфликта, мы в хорошей форме: все три версии доступны через индекс. Помните, что нас интересуют три версии: merge base, --ours и --theirs.

Если Git обнаружил правильное переименование, все три версии будут в индексе под одним именем. Раз этого не было, то их нет: нам нужно два имени. (В случае «Git считает, что слияние выполнено правильно», базовая версия слияния файла вообще отсутствует в индексе, и нам нужно получить ее каким-то другим способом.) Два имени в вашем случае здесь: hw.h и hw.hpp, так что теперь мы делаем это:

$ git show :1:hw.h > hw.h.base    # extract base version
$ git show :2:hw.h > hw.h         # extract ours
$ mv hw.hpp hw.h.theirs           # move theirs into place

(Переименование не является строго необходимым, оно просто поможет сохранить все правильно и красиво проиллюстрировано.)

Теперь мы хотим объединить один файл с git merge-file:

$ git merge-file hw.h hw.h.base hw.h.theirs

Это использует ваш настроенный merge.conflictStyle, так что то, что находится в объединенном файле, выглядит так, как вы ожидаете, за исключением того, что метки на конфликтующих строках немного отличаются. У меня установлено diff3, поэтому я получаю:

$ cat hw.h
<<<<<<< hw.h
//Boofar
||||||| hw.h.base
//Hello world!
=======
//Foobar
>>>>>>> hw.h.theirs

Теперь вы можете разрешить это как обычно: rm дополнительные файлы .base и .theirs, git add окончательный результат, git rm --cached hw.hpp и git commit. (Это зависит от вас, когда git rm --cached hw.hpp: это безопасно сделать в любой момент времени до фиксации, но после этого вы больше не сможете получить «их» из индекса; см. ниже.)

Обратите внимание, что «наша» и «их» версии также доступны через git show HEAD:path и git show MERGE_HEAD:path. Чтобы получить базовую версию без индекса, нам нужно будет запустить git merge-base HEAD MERGE_HEAD, чтобы найти его хэш-идентификатор (а затем предположить, что существует также одна база слияния1) и git show <hash>:path. Это то, что мы должны сделать, если Git считает, что выполнил слияние правильно.

Также обратите внимание, что если вы действительно хотите — я полагаю, что это будет верно только в том случае, если вы хотите использовать какие-то другие инструменты, которые у вас есть, которые требуют этого — вы можете использовать git update-index для перетасовки записей в указателе, перемещая hw.hpp в slot-3 hw.h, чтобы он действительно отображался как "их" и отображался таким образом в git status. Для этого конкретного примера:

 $ printf '100644 bbda177a6ecfe285153467ff8fd332de5ecfb2f8 3\thw.h' |
     git update-index --index-info

Хэш здесь получен от git ls-files --stage и является хешем для hw.hpp. (Вам нужен второй шаг, чтобы удалить запись индекса hw.hpp.)


1Используйте git merge-base --all, чтобы найти все базы слияния. Если их несколько, вы можете выбрать одну из них произвольно (это делает -s resolve) или попытаться объединить все базы слияния в виртуальную базу слияния. Чтобы объединить две базы слияния, вы находите их собственную базу слияния и объединяете две базы, как если бы они были ответвлениями, используя эту базу слияния. Рекурсивно и повторяйте по мере необходимости — это то, что Git делает со стратегией -s recursive по умолчанию — до тех пор, пока у вас не будет единой базовой версии файла слияния.

person torek    schedule 30.03.2017
comment
Может быть, безопаснее сказать $ cp hw.hpp hw.h.theirs # move theirs into place, чтобы hw.hpp не исчез? Или не рискованно использовать mv, потому что их hw.hpp можно восстановить через git? - person Nick Giampietro; 31.03.2017
comment
Еще один дополнительный вопрос: если я захочу зафиксировать переименование в hw.hpp, как изменятся первые три команды? - person Nick Giampietro; 31.03.2017
comment
@NickGiampietro: Да, пока файлы находятся в индексе, вы можете использовать git show или git checkout-index для их извлечения (и использовать git ls-files --stage для перечисления того, что находится в индексе, включая хэш-идентификаторы и номера этапов). Чтобы произошло переименование, вы должны git update-index блобы с их номерами этапов или, может быть, даже просто использовать git mv, хотя я не пробовал этого и не уверен, что происходит с различными этапами (я знаком только с git mv разрешенного, только нулевой этап, файл). - person torek; 31.03.2017
comment
Еще одно дополнение: после этого я вижу в своем целевом файле все конфликты, как вы сказали. Однако, когда я запускаю git mergetool <file>, я получаю сообщение об ошибке, говорящее, что файлы не нуждаются в слиянии. Это нормально? - person Nick Giampietro; 01.04.2017
comment
Я никогда не использую git mergetool, но я думаю, что он рассматривает три этапа, что может быть причиной использования git update-index для управления записями этапов. - person torek; 01.04.2017