Справочная информация: идентификатор файла
Все это на самом деле сводится к тому, что я называю идентификатор файла , что является сложной проблемой - не только в Git, но и сложным в целом: см. статья в Википедии по философской проблеме .Git, однако, делает это особенно сложно, потому что:
Когда это происходит, my-control.js
имеет всю историю, а my-control.html
имеет всю историю плюс 1 или 2 коммита от мастера.
У Git нет истории файлов.У Git есть только история коммитов.Точнее, коммиты являются , история и файлы к этому не имеют отношения.Коммиты содержат файлов, но это никак не контролирует историю: коммиты - это история.
У меня есть подробности об этом, например, мой ответ на Отсутствует удалениестрок в истории файлов (git) .Если вы попросите Git --follow
файл переименовать, Git использует его упрощение истории, чтобы показать только коммиты, которые касаются именованного файла - и когда одно из этих «прикосновений» - «Git обнаруживает переименование», Git начинает искать новое имя в этот момент и перестает искать старое.(Или, поскольку Git движется назад, лучше сказать, что он начинает искать старое имя и перестает искать новое.)
Этот метод, очевидно, может потерпеть неудачу при слияниях, поскольку одна ногаиз слияния может иметь «неправильное» имя.Тем не менее, в любом случае упрощение истории обычно сводится только к одному этапу слияния!
Если вы не используете --follow
, но используете git log -- <em>path(s)</em>
или эквивалентный, Git просто не обнаруживает переименование: онопросто упрощает историю, используя заданный путь или пути.
Немного замученная аналогия
Что я делаю не так?
Ничего или, может быть, все,Проблема в том, что Git иногда может, а иногда и не может определить, что файл с именем Bob в одном месте и файл с именем Robert в другом месте относятся к одному и тому же парню.Он может или не может правильно идентифицировать пару файлов.Боб и Роберт - один и тот же парень или нет?
Почему это иногда случается, а иногда работает?
Это, по крайней мере, дает твердый ответ: Git может идентифицировать два файла, если они достаточно похожи, и другие условия также выполняются.То есть, вы показываете Git два снимка с некоторыми файлами («людьми») в них и позволяете угадать, кто есть кто, а кто перемещался.Если есть только один файл с надписью «Bob» на более раннем рисунке, и один файл с надписью «Robert» на более позднем рисунке, Git может быть в состоянии обнаружить, что это один и тот же парень,до тех пор, пока он не потерял конечность или не получил дополнительную голову или что-то подобное.Однако, если на обеих фотографиях есть парни с именами «Боб» и «Роберт», Git будет предполагать, что два «Боба» - один и тот же парень, а два «Роберта» - один и тот же, и что ранееБоб никогда не будет последним Робертом, и даже наоборот.
Технический: git merge
, граф фиксации и git diff --find-renames
Давайте посмотрим, как на самом деле работает git merge
.Чтобы попасть туда, нам нужно начать с двух вещей: граф фиксации и git diff --find-renames
.
График фиксации является важнейшим ключом к слиянию.Каждый коммит записывает необработанный хэш-идентификатор своего родительского коммита, если это обычный коммит, или, если это коммит слияния , оба (или все 1 )его родителей.Как правило, есть только два родителя слияния.Давайте нарисуем немного графика коммитов в качестве примера и выберем несколько конкретных коммитов для обсуждения.Вместо того, чтобы использовать полные большие уродливые хеш-идентификаторы, давайте использовать заглавную букву для указания конкретных коммитов (и круглые точки для менее интересных).У нас будут ветви branch
и main
, которые разделяются при коммите B
, но были объединены хотя бы один раз в прошлом:
o--o---D--o--o--E <-- branch
/ \
...--o--B--o----C--M--o--o--F <-- main
Когда мы произвели слияние при коммите M
(для слияния), слияния branch
в main
, база слияния была ясной и очевидной: последний общий коммит был B
.Коммит B
был и есть в обеих ветвях.Таким образом, способ, которым Git произвел слияние, был следующим:
git diff --find-renames <hash-of-B> <hash-of-C> # what we did, on main
git diff --find-renames <hash-of-B> <hash-of-D> # what they did, on branch
Затем Git объединил два набора изменений, применил объединенные изменения к снимку, сохраненному в B
, и сделалрезультирующий коммит слияния M
.
Поскольку M
является коммитом слияния , он запоминает и C
и D
,Когда Git просматривает историю, которая, помните, состоит из коммитов, он должен посещать обоих родителей, когда он движется назад от M
.
Теперь мы собираемсязапустить git checkout main; git merge branch
.То есть мы выберем коммит F
в качестве нашего текущего коммита и попросим Git объединить коммит E
в F
.Git теперь должен найти базу слияния: последний коммит, который был на обеих ветвях.
Можете ли вы угадать, какой коммит является базой слияния?Это не B
на этот раз!
Поиск базы слияния - это всего лишь достижимость , и я передам более полное обсуждение Think Like (a) Git , но ответ здесь заключается в том, что, пройдя назад от F
до M
, мы можем достичь D
, а пройдя назад от E
, мы можем достичь D
прямо вдоль верхней линии.Таким образом, D
является базой для слияния на этот раз.Git еще раз запускает две команды git diff
:
git diff --find-renames <hash-of-D> <hash-of-F> # what we did on main
git diff --find-renames <hash-of-D> <hash-of-E> # what they did on branch
Каждый diff имеет левую сторону, коммит D
и правую сторону, коммит tip конкретной ветви.Git находит оба набора изменений, включая обнаружение переименований.Поэтому, если в базовом и подсказочном коммите есть какой-то файл с другим именем, и Git решил, что этот является тем же файлом под другим именем - например, Боб на левой фотографии стал помечен как Роберт вправильный - тогда Git объявит, что файл был переименован.
Git теперь объединит два набора изменений, используя базовый (D
) снимок в качестве основы, к которой применяются изменения.Если изменения включают «переименовать файл», Git также сделает переименование.Если файл помечен Бобом в базе и Робертом в обоих подсказках, то оба diff имеют одинаковое переименование, и все хорошо.Если только одно изменение переименовывает файл, то имя, которое вы получите, зависит от того, в какой ветке вы находитесь при слиянии: мы переименовали Боба в Роберта или они сделали это?
Куда все идет очень плохо, если Git не может обнаружить переименование.Что, если Боб потерял руку, и Гит не узнает, что парень, помеченный Робертом на одной из фотографий, - это тот же самый парень?
1 Объединяется с тремя или более родителяминазываются слияниями осьминога в Git.У Linux есть одно слияние в 66 направлений, из которых Линус Торвальдс заметил: это не осьминог, это слияние Ктулху .
Что вы можете сделать сэто: индекс сходства
Что я могу сделать, чтобы это исправить?
Самым простым, безусловно, является избегание переименования.Git сначала считает, что метки на файлах - имена путей.Если базовый коммит и оба подсказки имеют файлы с именем bob.txt
, почему, это должен быть тот же самый парень, Боб.Тут нечего путать.
Однако переименование уже произошло.Один из способов исправить это - организовать для всех будущих слияний использование нового имени: если файл должен называться robert
, убедитесь, что каждое future base merge base и в будущем branch-tip вызовите файл robert
, и путаницы не будет.
Если это невозможно, есть еще одна надежда на автоматизацию: Дайте Git больше (или другую) информацию.Фактически, сделайте Git умнее: скажите Git, что он должен совпадать с Бобом и Робертом, даже если он потерял всех своих конечностей.
FlКроме того, Git отличается от git diff
против git merge
, но оба используют одну и ту же идею - установить индекс сходства .Когда Git сравнивает два снимка (два коммита), если какой-то файл пропал слева, а новый файл появился справа, Git сравнивает содержимое этих файлов .
Используя git diff --find-renames
(или короче, git diff -M
), вы можете добавить порог индекса подобия : например,
git diff -M10
.Число после M
(или --find-renames=
) представляет собой минимальный требуемый индекс подобия для двух файлов, которые следует считать "одним и тем же" файлом, т. Е. Для Git решить, какой корабль (или был)Корабль Тесея или тот, кто носит имя Боба, тот же самый, что и парень, носящий имя Роберта.
Внутреннее вычисление Git схожести двух файлов не меняется, но порог в Git, точка, в которой он объявляет, что эти два разных файла действительно один и тот же файл , делает.Снижение порога делает Git очень счастливым для идентификации файлов с разными именами.Повышение этого делает Git более неохотным.
Порог сходства по умолчанию составляет 50%, -M50
.Точно одинаковые байтовые файлы идентичны на 100%.Другие менее похожи / более непохожи.Фактическая формула в моем ответе на Попытка понять `git diff` и` git mv` механизм обнаружения переименования , но в целом способ найти пригодное число - использовать git diff
на базе слияния идве подсказки ветви.Установите очень низкий порог, запустите git diff
, и Git скажет вам, какие файлы были сопоставлены, и каково их фактическое сходство.
(Чтобы найти базу слияния, запустите git merge-base --all <em>commit1 commit2</em>
, где два идентификатора фиксацииназовите ветку tip commit коммиты. Вы можете использовать здесь имена веток, или необработанные хеш-идентификаторы, или что-нибудь подходящее для Git в соответствии с документацией gitrevisions . После этого у вас будет хеш-код базы, которую выможно использовать в качестве одного из аргументов git diff
.)
Вы можете указать тот же порог для git merge
, используя -X find-renames=<em>number</em>
.Вы можете просто использовать очень низкое число, но это может найти слишком много переименований.Чтобы узнать, что Git будет думать, что переименовано, используйте git diff
.
Если все остальное терпит неудачу
Если ничего из вышеперечисленного недостаточно (что может ) случается), у вас нет полностью альтернативы:
Есть ли способ сказать Git "нет, эти изменения должны применяться к этому файлу"?
Существует полностью ручной способ слияния файлов:
Запустите слияние, используя --no-commit
, чтобы сказать Git, что Git не долженпредположим, что слияние прошло успешно.
Разрешите все, что можете, используя более простые методы.
Если в Git неправильно определены файлы, распакуйтебазовая версия файла слияния из некоторой известной или выбранной вручную фиксации базы слияния.Если нет, то он уже находится в index в слоте стадии 1, так что вы можете извлечь его оттуда.В любом случае, извлеките файл в рабочее дерево под именем, которое вы можете использовать.Например:
git show $hash:$basepath > file.base
Аналогично, извлеките «нашу» и «их» версию файла в рабочее дерево:
git show HEAD:file > file.ours
git show MERGE_HEAD:$theirpath > file.theirs
Теперь, когда у вас есть все три версиифайл, используйте git merge-file
, чтобы выполнить трехстороннее объединение файла.Как только у вас будет правильный результат слияния в вашем рабочем дереве, поместите его под правильным именем и используйте git add
, чтобы скопировать его в индекс, готовый для фиксации.Обязательно удалите из индекса любую неправильную (--theirs
) версию, которая осталась позади - git status
сообщит вам о таких файлах, если они существуют.
Когда слияние завершитсяиспользуйте git commit
(или в достаточно новых версиях Git, git merge --continue
), чтобы завершить слияние.
Это - мы вручную выбираем три файла и используем для них какую-то программу слияния - вот как мысделал это в старые добрые времена до Git.Добро пожаловать в 1990-е!: -)