Как прокомментировал Evert , в идеале вам не нужно ничего делать.
На практике, насколько хорошо это работает, несколько варьируется. Смотрите подробное обсуждение ниже. Обратите внимание, что за любые параметры конфигурации, такие как diff.renameLimit
, отвечает лицо, выполняющее операцию сравнения или слияния позже. Если это вы , вы можете установить свои настройки сейчас, но если это кто-то другой, они должны установить свои настройки (когда захотят).
Подробности
Важно знать, что Git не хранит изменения в файлы. Вместо этого Git хранит снимков файлов. Каждый коммит содержит полный снимок каждого файла.
Каждый коммит также содержит некоторые метаданные - информацию о самом коммите - включая имя и адрес электронной почты того, кто например, сделал коммит, но также включает необработанный идентификатор ha sh его предыдущего коммита, который Git вызывает родительского коммита . Эти строки фиксируются вместе, хотя и в обратном направлении.
Каждый коммит имеет свой уникальный идентификатор ha sh. Этот идентификатор ha sh на самом деле представляет собой криптографическую c контрольную сумму содержимого фиксации как из его основных данных - его снимка - так и из его метаданных. Это означает, что после выполнения коммита его нельзя изменить ни на один бит: изменение любого отдельного бита или любой группы бит просто делает новый коммит с новым уникальным идентификатором ha sh, в то время как существующий коммит все еще там.
Git в целом работает в обратном направлении. имя ветви содержит необработанный идентификатор ha sh последнего коммита . Оттуда Git находит второй-последний коммит, который находит третий-последний и так далее:
... <-F <-G <-H <--branch
Имя branch
содержит фактический идентификатор ha sh о каком-то коммите мы просто звоним H
. Когда Git читает H
из своей большой базы данных, используя этот идентификатор ha sh, H
содержит идентификатор ha sh предыдущего коммита G
. Git теперь может читать G
из своей Git -objects-database, которая получает идентификатор ha sh предыдущего коммита F
. Это позволяет git log
и другим командам работать через коммиты в обратном направлении.
Опять же, каждый коммит содержит снимок всех своих файлов. Но Git показывает вам изменения. Это работает так, что когда вы Git показываете какой-то коммит, Git ищет родителя коммита - его обратную ссылку на его предыдущий коммит - и извлекает его во временную рабочую область ( в памяти) оба фиксирует. Git может сравнивать файлы в каждом коммите.
Если коммит G
README.md
и коммит H
README.md
идентичны, Git даже не скажет вам что H
имеет README.md
. Однако, если они отличаются, Git сравнит два README.md
файла содержимого и покажет, что изменило . Вот как вы видите изменения.
Вы можете сравнить любой двух коммитов, не только родитель и потомок, но сравнение родителей и потомков чрезвычайно распространено: многие команды Git просто сделай это автоматически. Некоторые, такие как git diff
, позволяют выбрать два коммита, а некоторые, например git merge
, выбирают коммит самостоятельно, как мы увидим через мгновение.
Обнаружение переименования
Если вы переименовываете некоторые файлы, то на самом деле происходит следующее: ранний коммит G
имеет файл, скажем, README.txt
, а позже коммит H
имеет файл с именем README.md
. Git замечает, что G
не имеет README.md
и H
не имеет README.txt
, и догадывается, что может быть, просто возможно, вы переименованный эти два файла в этих двух коммитах.
Если вы переименуете целую коллекцию файлов - в глазах Git нет каталогов; файлы просто имеют длинные имена: a/b/c.ext
- это имя файла, это не папка с именем a
, содержащая папку с именем b
и т. д., это просто длинное имя с косой чертой - Git попытается сопоставить каждая файловая пара, которая может. (Совсем недавно были предприняты некоторые попытки улучшить сопоставление имен, чтобы принять во внимание типичное «изложение папок». Несколько раз оно ошибалось, но я думаю, что оно вернулось сейчас.)
Это Обнаружение переименования - опционально во внутреннем механизме различия Git. При запуске git diff
он включен по умолчанию в современном Git, но выключен по умолчанию в очень старых версиях Git. Вы можете включить его с помощью git diff --find-renames
(для краткости git diff -M
) или установить diff.renames
в true
в вашей конфигурации.
Обнаружение слияния и переименования
При запуске git checkout somebranch; git merge otherbranch
, Git опирается на граф фиксации , чтобы найти базу слияния . Я собираюсь опустить все детали об этом здесь; подробнее см. другие ответы.
Рассмотрим граф фиксации, который выглядит следующим образом:
I--J <-- somebranch (HEAD)
/
...--G--H
\
K--L <-- otherbranch
То есть names somebranch
и otherbranch
выбирают коммиты J
и L
соответственно. Коммиты через H
находятся в обеих ветвях , в то время как коммиты I-J
находятся только в somebranch
, а в данный момент коммиты K-L
находятся только в otherbranch
. Вы запустили git checkout somebranch
, на что указывает HEAD
, связанный с именем somebranch
, и git merge otherbranch
теперь запускается.
Git теперь найдет базовый коммит слияния H
на свой. Найдя базу слияния, Git теперь необходимо преобразовать снимки в H
, J
и L
в , что вы изменили на somebranch
и , на что они изменились otherbranch
соответственно.
Поскольку git diff
может находить переименования, слияние выполняется только git diff
с опцией find-renames, которая наверняка включена:
git diff --find-renames <hash-of-H> <hash-of-J> # what we changed
git diff --find-renames <hash-of-H> <hash-of-L> # what they changed
В объединить там, где не было переименований, детектор переименования ничего не находит, а Git просто объединяет, например, README.md
в H
на основе изменений, найденных при сравнении H
README.md
с J
README.md
и затем снова на основе L
README.md
. Однако, когда переименовывает , Git должен соединить каждую пару файлов. Например, если:
README.txt
в H
равно README.md
в J
, а README.txt
в H
равно README.txt
в L
затем вы переименовали файл, а они не сделали. Комбинацией этих операций является переименование файла .
Итак, в вашем случае вы собираетесь сделать коммит, в котором имена файлов теперь subfolder/a/file.ext
и так далее, когда они были просто a/file.ext
и так далее. Если повезет, Git будет правильно сопоставлять базу слияния a/file.ext
с a/file.ext
другого парня, в этом дифференциале и правильно совпадут a/file.ext
с вашим subfolder/a/file.ext
. Разница покажет, что одна сторона переименовала файл , а другая - нет, а комбинация этих двух изменений включает в себя «переименование файла».
Когда это происходит go не так?
Git Детектор переименования, управляемый его механизмом сравнения, зависит от трех вещей:
Файл с его старым именем должен быть там -на-слева и не-там-справа, а под новым именем он должен быть там-справа-а не-там-слева.
То есть предположим, что у нас было path/to/file.ext
, а теперь new/path/to/file.ext
. Мы поместим старый коммит слева, а новый справа. Но что, если мы создали новое и отличное path/to/file.ext
справа?
Git даже не попытаются сравнить path/to/file.ext
слева с new/path/to/file.ext
справа, потому что он сопоставит левую сторону path/to/file.ext
с новым, но не связанным path/to/file.ext
справа. Git просто вызовет new/path/to/file.ext
справа новый файл .
Следовательно, первоначальное сравнение left-vs-right должно показать некоторые «удаленные» файлы слева и некоторые "новые" файлы справа. Детектор переименования попытается сопоставить левые удаленные файлы с правыми добавленными файлами и преобразовать такие пары файлов в (обнаруженные) переименования.
Даже если у вас есть такая пара, Git не будет вызывать файл , переименованный , если содержимое не похоже на . То есть, предположим, вы не только переименовали файл, но и что-то изменили. Git сделает быстрый тест на сходство , выраженное в процентах. Если левый и правый файлы по крайней мере «похожи на 50%», Git сочтет это кандидатом на переименование.
Git должен сделать это для каждые слева и непарный файл с правой стороны. То есть для каждой пары файлов Git должен вычислять индекс сходства. left/file1.a
похоже на right/file2.b
? Насколько похоже left/file1.a
на right/file3.c
? Повторите для каждого файла с обеих сторон.
Чтобы сделать это go быстрее, Git может легко найти 100% идентичные файлы. Поэтому вы можете сначала зафиксировать операцию переименования, затем зафиксировать изменения в переименованных файлах и улучшить положение при выполнении фиксации за коммитом, как это делает git log
.
Это менее полезно во время слияний, поскольку git merge
никогда не идет коммит за коммитом. (Я думаю, у него должна быть возможность сделать это, просто чтобы найти переименования, но это не так.)
Порог сходства по умолчанию, равный 50%, является только значением по умолчанию. При запуске git diff
вы можете повысить или понизить минимальное требуемое сходство, используя параметр -M
или --find-renames
с числом, как в -M30
, чтобы уменьшить его до 30% или -M75
, чтобы повысить его до 75%. 1 При использовании git merge
вы можете установить его с помощью -X find-renames=<number>
, чтобы выбрать предел. (В старых версиях Git вместо этого вам нужно -X rename-threshold=<number>
: посмотрите документацию вашей конкретной версии Git, например, git help merge
.)
Last, Git накладывает пределы переименования из-за сложности поиска похожих файлов.
Предел по умолчанию в современном Git (Git 2.26) составляет 400, т. е. может быть обнаружено 400 переименований. Если вы переименовали 402 файла, 400 из них будут обнаружены, а два - нет. Вы можете повысить или понизить этот лимит, используя diff.renameLimit
. Установка его в ноль говорит Git, что искусственно не ограничивать обнаружение переименования.
Команда git merge
и имеет свои собственные отдельные ручки настройки, но не имеет значения по умолчанию для включения обнаружения переименования даже в старых версиях Git, он будет подчиняться вашему diff.renameLimit
, если вы не установите отдельное merge.renameLimit
.
Я установил diff.renameLimit
на ноль, чтобы отменить ограничение переименования. Это приводит к тому, что некоторые команды git diff
и git merge
иногда запускаются очень медленно, но мне не нужно беспокоиться об этом (и я знаю, что при необходимости включите его).
1 Осторожно, --find-renames=4
означает 40%, а не 4%. Вы можете добавить символ %
, --find-renames=4%
или просто написать его как --find-renames=04
. Вероятно, не стоит снижать порог переименования слишком далеко, так как Git начнет искать переименования, которые не имеют смысла.