TL; DR
Поиграть с аргументом опции стратегии для обнаружения переименования. В зависимости от вашего винтажного Git, это либо -X find-renames=<em>threshold</em>
, либо -X rename-threshold=<em>threshold</em>
. Используйте git diff
для определения подходящего порогового значения; в git diff
это параметр -M
или --find-renames
.
Помните, что вишневый отбор реализован как слияние, при этом база слияния является родителем коммита, выбранного вишней, коммит --ours
является коммитом HEAD
(как обычно), а коммит --theirs
будучи коммитом, ты собираешь вишню.
Long
Git never записывает что-либо как операцию переименования. Если вы переименуете файл и сделаете коммит, Git просто запишет новый снимок.
Рассмотрим, например, типичную головоломку Найди отличия . Вам дают две фотографии и просят выяснить, в чем дело. Если на левой стороне изображение «до», а на правой стороне «после» и стул пропадает, вы можете сказать «стул снят». Если в другом месте появляется другой стул, вы можете сказать: «Один стул удален, а другой добавлен». Но что, если два стула выглядят одинаково ? 1025 *
Вы могли бы сказать: стул A удален, а стул B добавлен, как вы делаете, когда два стула выглядят очень по-разному. Или можно сказать, что стул A переместился на в положение B! (Но так ли это на самом деле? Может быть, стул A был удален, и был добавлен другой стул B, и вы просто можете ' Разница не в этом. Здесь есть несколько более глубоких философских вопросов, которые мы увидим.)
Во всяком случае, снимки Git похожи на картинки. Они никогда не содержат движения , никогда! Это зависит от того, кто сравнивает снимки, даже если это кто-то из Git. Вы говорите Git: сравните, для меня, снимок A и снимок B. Git сообщит о файле как перемещенный , если он пропал без вести из одного имени в A, и точно такое же содержимое имеет появляется под другим именем в B, и вы сказали Git: «проверьте вещи, чтобы увидеть, не переместились ли они тоже».
Это ваш базовый git diff <commit-L> <commit-R>
, где поиск переименования включается с помощью опции -M
или --find-renames
. (L здесь означает левую сторону, а R - правую.) Git найдет такие переименования, если файлы на 100% идентичны. Но что, если это не так, а что если стул сдвинулся, но получил несколько царапин на этом пути?
Git будет считать "перемещенный файл" тем же файлом , что и исходный файл, если он соответствует критерию наилучшее соответствие . По сути, Git сначала находит все файлы, которые, по-видимому, исчезли из коммита L, и все новые файлы, которые, кажется, были созданы в коммите R. Он помещает все эти имена в очередь кандидатов на переименование .
Затем для каждого такого файла Git сравнивает все L-файлы со всеми R-файлами. (Как вы можете догадаться, это довольно трудоемкий процесс. Здесь есть несколько внутренних оптимизаций, в том числе сначала быстрая проверка на 100% -ная идентичность, что очень просто по причинам, связанным с внутренним Git.) Git вычисляет индекс сходства для каждой пары. Если индекс сходства превышает пороговое значение, которое вы выбрали - или 50%, если вы его не выбрали - Git считает это соединение подходящим. Git выбирает best такую пару, которая имеет наивысшую оценку сходства.
Найдя наилучшее соединение, два файла удаляются из очереди кандидатов на переименование. Эти два файла теперь идентифицированы как один и тот же файл , или, по аналогии с нашим стулом, как "одно и то же кресло" на изображениях слева и справа, только что перемещенные и, возможно, немного поцарапанные в процессе.
Я называю это процессом определения идентичности файла. С философской точки зрения, это ответ Git на проблему Корабль Тесея , или, более неформально, парадокс Дедушка Топор . «Это топор моего дедушки. Мой отец заменил ручку, а я заменил голову, но это все тот же топор!» Два файла - это один и тот же файл, как только они были идентифицированы как таковые.
Ради скорости Git по умолчанию объединяет любые два файла в коммитах L и R как "одинаковые", если они имеют одинаковое имя. С git diff
у вас есть возможность прервать это соединение, в случае, если оно неверно; это помещает больше имен файлов в очередь обнаружения переименования, делая это дольше.
Это все о git diff
; а как насчет git merge
? (И почему git merge
когда я собираю вишню!)
Мы скоро выясним, почему, но давайте поговорим о git merge
сейчас. Когда мы используем Git, мы используем от git merge
до объединения изменений, которые были сделаны в двух разных направлениях разработки - обычно в двух разных ветвях - часто двумя разными людьми. Чтобы объединить эти изменения, Git должен сначала найти точку , в которой работа расходится. Эта точка является базой слияния , и, поскольку Git полностью посвящен коммитам, это равносильно нахождению общий коммит между двумя строками.
Все это имеет большой смысл, когда мы рисуем это как коммит. Каждый коммит запоминает свой родительский коммит - коммит, который приходит непосредственно перед этим конкретным коммитом, поэтому мы можем рисовать коммиты слева направо, со старыми коммитами слева и более новыми справа, например:
... <-o <-o <-o ...
Предположим, что Алиса и Боб начинаются с общего исходного репозитория - например, оба выполняли git clone
в одном и том же репозитории Git - так что у них есть несколько коммитов, заканчивающихся последним коммитом на master
...--F--G--H <-- master
Имя master
содержит фактический хеш-идентификатор некоторого коммита H
, который Git вызывает наконечник ветви .
Теперь Алиса делает некоторую работу и делает новый или два коммита. Ее коммиты получают новые уникальные хэш-идентификаторы, которые никогда больше никому не будут использоваться:
I--J <-- master (Alice's)
/
...--F--G--H <-- origin/master
Тем временем Боб выполняет некоторую работу и делает один или два новых коммита, и его коммиты получают новые уникальные хэш-идентификаторы, которые никогда больше никому не будут использоваться:
I--J <-- [Alice's master]
/
...--F--G--H <-- origin/master
\
K--L <-- master (Bob's)
Как только мы каким-то образом получаем все коммитов вместе в общий репозиторий, у нас есть две ветви , мастер Алисы и мастер Боба, с общим начальным коммитом, оригинал master
:
I--J <-- alice/master
/
...--F--G--H
\
K--L <-- bob/master
Мы можем сделать это независимо от того, являемся ли мы Алисой, Бобом или кем-то третьим от имени Кэрол, если у нас есть коммит . Коммиты имеют значение! имена - здесь я использую alice/master
и bob/master
для определения местоположения коммитов J
и L
- просто здесь, чтобы помочь нам найти коммиты.
Теперь совершенно очевидно, что Алиса и Боб оба начали с коммита H
, так что теперь стало легко видеть как Git объединит работу Алисы с работой Боба: Git просто нужно сравнить - то есть, git diff
- отправьте H
против J
, чтобы увидеть, что сделала Алиса, и сравните H
с L
, чтобы увидеть, что сделал Боб. Итак, Git делает это:
git diff --find-renames <hash-of-H> <hash-of-J> # what Alice changed
git diff --find-renames <hash-of-H> <hash-of-L> # what Bob changed
Обратите внимание на опцию --find-renames
, которая использует метрику «50% аналог» по умолчанию, чтобы найти любые файлы, которые были переименованы, пока Алиса или Боб работали. (Стоит задуматься: почему Git не нужно смотреть ни на один из промежуточных коммитов? Это особенно важно, потому что в некоторых случаях это может помочь с обнаружением переименования. Git этого не делает Впрочем.)
В любом случае, Gтеперь объединяет два набора изменений, применяя объединенный набор изменений к снимку из базы слияния. Результат, если все идет хорошо, фиксируется как новый коммит слияния , который идет после нашего текущего коммита - к какому из этих двух ветвей прикреплено HEAD
. 1
Когда вы запускаете git merge
, вы можете дать Git аргумент -X rename-threshold
, точно так же, как вы можете git diff
дать такой аргумент. Merge просто передает это же число в diff, чтобы контролировать, насколько строгим или нет должен быть детектор переименования, при определении идентичности файла.
1 Мы не рисовали HEAD
in, поэтому мы добавляем alice/master
или bob/master
? Пока Git не отправится на коммит, это не имеет значения! Ну, это не совсем так. Имеет значение в случае конфликтов переименования: если Алиса и Боб переименовали какой-то конкретный файл, какое имя следует использовать Git? Он будет использовать любое имя в коммите HEAD
по умолчанию. Это также влияет на то, как размечается файл рабочего дерева, в случае более типичного конфликта слияния.
Cherry-pick (наконец-то!)
Когда вы используете git cherry-pick
, Git считает это забавным видом слияния. Давайте снова нарисуем несколько цепочек коммитов и посмотрим, как это работает:
...--o--*--o--P--C--o--o <-- branch-X
\
o--o--L <-- branch-Y (HEAD)
Имя HEAD
здесь прикреплено к branch-Y
, чтобы указать, что L
- это коммит, который мы извлекли прямо сейчас. Этот коммит является --ours
коммитом. Коммит C
выше - это тот, который мы хотим выбрать cherry (C для Cherry), а P
- его родитель. (Я знаю, что P
может означать Pick, но мне нужно было письмо для Parent, поэтому P - для Parent, а C - для Cherry.) Большинство других коммитов неинтересны - нам никогда не нужны их хэш-идентификаторы, поэтому мы просто показываем их как o
. Я пометил одну *
как очевидную базу слияния, но на самом деле Git тоже не собирается ее использовать!
Теперь Git будет выполнять слияние, как если бы мы запустили git merge
, за исключением того, что вместо находим базу слияния, которая будет коммитом *
, а Git просто использует родителя P
в качестве базы слияния. Git теперь работает:
git diff --find-renames <hash-of-P> <hash-of-L>
чтобы увидеть, что мы изменили - Git попытается сохранить эти изменения! - а затем:
git diff --find-renames <hash-of-P> <hash-of-C>
чтобы увидеть, что они изменили, в одном их коммите мы собираем вишню.
Git теперь будет объединять эти изменения, как это всегда происходит при любом слиянии, с возможностью возникновения конфликтов слияния. Как вы уже видели, --find-renames
зависит от значений индекса сходства значений файлов, хранящихся в коммитах P
, C
и L
. Git должен обнаруживать переименования между P
и L
, чтобы идентифицировать определенные файлы как тот же файл , иначе он не будет знать, как объединить изменения в этом файле .