Git: путаница о алгоритме слияния, формате конфликта и взаимодействии с mergetools

Я не знаю подробностей, но насколько я понимаю процесс слияния и разрешения конфликтов, он выглядит следующим образом (предположим, что в репозитории имеется только один файл, модифицированный в двух ветвях):

  • Пользователь выдает команду git merge.
  • Git применяет некоторый git -специфический алгоритм, чтобы автоматически объединить два измененных файла. Для этой цели он создает BASE, LOCAL, OTHER и BACKUP версию файла.
  • Затем он записывает результат слияния в исходный отслеживаемый файл (назовите его MERGED).
  • Предположим, что существуют конфликты. Git использует некоторый формат для обозначения конфликтов (<<<<<<<, |||||||, =======, >>>>>>>). Затем он устанавливает свой статус "слияние" или аналогичный.
  • Если пользователь затем выдает git mergetool ..., открывается настроенный инструмент внешнего слияния с аргументами, указывающими на BASE, LOCAL, OTHER и, конечно, MERGED.

Есть несколько моментов, о которых я смущен:

  • Будет ли инструмент всегда понимать формат конфликта Git? Это стандартизировано? Как насчет опции diff3? Это также обычно понимается внешними инструментами?
  • Будет ли инструмент применять собственный (и, возможно, другой) алгоритм слияния и полностью удалить вывод Git?
  • Когда Git необходимо выполнить рекурсивное слияние (из-за нескольких оснований слияния), а промежуточное слияние создает конфликты - будет ли он рассматривать внутренние маркеры конфликта как обычный текст так же, как любой другой неконфликтный текст? Или рекурсивный формат конфликта?

Я не мог найти никакого объяснения, которое действительно расскажет историю целое.

Ответ 1

Полный ответ сложный. Эдвард Томсон покрывает большую часть этого. Здесь значительно более подробно.

Пусть начнется, тем не менее, с помощью этого: git mergetool run - я должен сказать, вы запустите - после завершения всего остального git merge. Ваши инструменты слияния даже не вводят изображение до тех пор, пока git merge не завершится (и не будет выполнено из-за конфликтов). Это меняет многое, как вы думаете об этом.

Как работает (рекурсивный и разрешающий) слияние

Пользователь выдает команду git merge.

Пока все хорошо.

Git применяет некоторый git -специфический алгоритм для автоматического слияния двух модифицированных файлов.

Упс, нет, мы уже свалились с рельсов, и поезд может отправиться со скалы.: -)

Первым шагом на этом этапе является выбор стратегии слияния. Позвольте выбрать стратегию по умолчанию (-s recursive). Если мы выберем какую-то другую стратегию, следующий шаг может быть другим (он совсем другой для -s ours и несколько отличается для -s octopus, но ни один из них не интересен сейчас).

Следующий шаг - найти все базы слияния. В любом случае есть только один. Мы вернемся к проблеме рекурсии позже. Однако не может быть базы слияния. Старые версии Git использовали пустое дерево в качестве поддельной базы слияния. Более новые - 2.9 или более поздние - требуют, чтобы вы добавили --allow-unrelated-histories здесь (и затем действуете точно так же). С пустым деревом каждый файл добавляется в обеих незанятых коммитах.

Если есть одна база слияния, она может быть такой же, как и кончик ответвления. Если это так, слияния не требуется. Однако есть и два подсектора. Не может быть ничего, что можно было бы слить, потому что базой слияния является другая фиксация, а другая фиксация - "позади" (является предком) текущей фиксации. В этом случае Git всегда ничего не делает. Или другой фиксатор может опережать (потомок) текущего фиксации. В этом случае Git обычно выполняет операцию быстрой перемотки вперед, если вы не укажете --no-ff. В обоих случаях (ускоренная перемотка вперед или --no-ff) фактическое слияние не происходит. Вместо этого происходит извлечение дополнительной атаки вперед. Он либо становится текущим фиксацией (ускоренное слияние: любая ветка, на которой вы находитесь, теперь указывает на дальнейшую фиксацию), либо Git делает новую фиксацию с использованием этого дерева фиксации, а новый commit становится текущей фиксацией.

Реальное слияние: объединение одной базы слияния с двумя коммитами

Теперь мы находимся в фазе, где у нас есть одно фиксированное значение слияния B, а две коммиты L (локальная или левая сторона, --ours) и R (удаленная или правая сторона, --theirs). Теперь две нормальные (-s recursive и -s resolve) стратегии выполняют пару операций git diff --name-status с включенным обнаружением переименования, чтобы увидеть, есть ли файлы в изменении B-to-L, которые изменяют их имена, и если являются файлами в B-to-R, которые меняют имена. Это также выясняет, есть ли недавно добавленные файлы в L или R, и если файлы удаляются в L или R. Вся эта информация объединяется для создания идентификаторов файлов, так что Git знает, какие наборы изменений объединяются, Здесь могут быть конфликты: файл, путь которого был P B в базе, но теперь он как P L, так и P R, имеет переименовать/переименовать конфликт, например.

Любые конфликты на этом этапе - я называю их конфликтами высокого уровня - лежат вне домена слияния на уровне файлов: они сделают Git завершение этого процесса слияния конфликтом независимо от того, что еще происходит. Тем временем, однако, мы закончили с "идентифицированными файлами", как я сказал выше, без его определения. Понятно, что это означает, что только потому, что какой-то путь P был изменен, это не значит, что это новый файл. Если в базовом commit B был файл base, и он теперь называется renamed в L, но все еще называется base в R, Git будет использовать новое имя, но сравнить B: base с L: переименован и B: база с R: базой, когда Git переходит к объединению изменений на уровне файла.

Другими словами, идентификатор файла, который мы вычисляем на этом этапе, сообщает нам (и Git), что файлы в B соответствуют файлам в L и/или R. Это имя не обязательно по имени пути. Обычно это случай, когда все три пути совпадают.

Есть несколько небольших настроек, которые вы можете вставить во время этой первой фазы diff:

  • Renormalization (merge.renormalize): вы можете сделать Git применить текстовые преобразования из настроек .gitattributes и/или core.eol. Настройки .gitattributes включают в себя фильтр ident и любые смазочные и чистые фильтры (хотя здесь применяется только направление размытия).

    (Я предположил, что Git сделал это раньше, так как это может повлиять на обнаружение переименования. Однако я не проверял это, и я просто просмотрел источник Git и, похоже, не использовал его на данном этапе. Поэтому, возможно, merge.renormalize здесь не применяется, хотя фильтр smudge может радикально переписать файл. Рассмотрим пару фильтров, которая, например, шифрует и расшифровывает. Вероятно, это ошибка, хотя и небольшая. К счастью, для EOL-преобразования нет эффект вообще на значения индекса подобия.)

  • Вы можете установить индекс подобия, когда Git рассмотрит файлы, которые будут переименованы, или полностью отключить обнаружение переименования. Это опция расширенной стратегии -X find-renames=n, ранее называемая порогом переименования. Это то же самое, что и параметр git diff -M или --find-renames.

  • Git в настоящее время не имеет возможности установить порог "break" a la git diff -B. Это также влияет на вычисление идентификатора файла, но если вы не можете его установить, это не имеет большого значения. (Вероятно, вы должны иметь возможность установить его: еще один миггер.)

Объединение отдельных файлов

Теперь, когда мы определили наши файлы и решили, какие из них совпадают с другими, мы, наконец, переходим к уровню слияния файлов. Обратите внимание, что здесь, если вы используете встроенный драйвер слияния, остальные устанавливаемые параметры diff начнут действовать.

Позвольте мне процитировать этот бит снова, поскольку это имеет значение:

Git применяется некоторый... алгоритм автоматического слияния двух модифицированных файлов. Для этого он создает BASE, LOCAL, OTHER и BACKUP версию файла.

В этот момент задействованы три (не четыре) файла, но Git не создает ни одного из них. Это файлы из B, L и R. Эти три файла существуют как объекты blob в репозитории. (Если Git является перенормировкой файлов, ему необходимо создать перенормированные объекты как объекты blob в этой точке, но затем они живут в репозитории, а Git просто вид притворяются, что они были в первоначальных коммитах.)

Следующий шаг очень важен, и именно там индекс входит в картину. Идентификаторы хэшей этих трех объектов блоба - это H B, H L и H R. Git готово помещать эти три хэша в индекс, в слоты 1, 2 и 3 соответственно, но теперь использует правила, описанные в git read-tree в разделе 3-Way Merge:

  • Если все три хэша равны, файл уже объединен и ничего не происходит: хеш переходит в нулевой номер. Даже если только второй и третий хеши равны, файл все еще уже объединен: оба L и R делают одно и то же изменение относительно B. Новый хеш переходит в нулевой слот и слияние файлов завершено.
  • Если H B= H L и H B ≠ H R, правая сторона (удаленная/другой/ --theirs) файл должен быть результатом. Этот хеш переходит в нулевой слот и слияние файлов завершено.
  • Если H B ≠ H L и H B= H R, левая часть (локальная/ --ours) должен быть результатом. Этот хеш переходит в нулевой слот и слияние файлов завершено.
  • Это оставляет только случай, когда все три хэша различаются. Теперь файлы действительно нужно объединить. Git помещает все три хэша в три слота индекса.

Есть несколько особых случаев, которые могут применяться в данный момент, все связанные с конфликтами более высокого уровня. Возможно, один или два индексных слота оставлены пустыми для некоторых имен путей, потому что индекс тщательно управляется таким образом, чтобы он синхронизировался с рабочим деревом (чтобы он мог играть свою роль кеша, который ускоряет Git много). Но в принципе, особенно когда речь идет о драйверах слияния, мы можем думать об этом как о "всех трех слотах" - они могут быть только тремя слотами, распределенными по нескольким именам, в случае переименованных файлов.

Вызов драйверов слияния (.gitattributes)

В этот момент у нас есть фактическое слияние уровня файла для выполнения. У нас есть три входных файла. Их фактическое содержимое хранится в репозитории, как объекты blob. Их хэш-идентификаторы хранятся в индексе в слотах с 1 по 3 (как правило, с одной записью индекса, но в случае переименований, возможно, с использованием более чем одной записи индекса). Мы можем теперь:

  • Используйте Git встроенный слияние файлов (который также доступен как внешняя команда, git merge-file).

    Встроенный слияние файлов работает непосредственно из индекса (хотя, если мы хотим запустить его через git merge-file, мы должны извлечь капли в файловую систему). Он извлекает файлы, выполняет ли их дело, чтобы объединить их, и, необязательно, в зависимости от расширенных стратегий-опций -X ours или -X theirs -writes маркеры конфликтов. Он возвращает свой окончательный результат в дерево работы под любым именем пути Git, выбранным как конечное имя пути, и завершено.

  • Используйте драйвер слияния (через .gitattributes). Драйвер слияния запускается с аргументами. Однако эти аргументы создаются с помощью Git извлечения трех объектов blob в три временных файла.

    Аргументы расширяются из того, что мы ставим как %O, %A, %B, %L и %P. Эти буквы аргументов не совсем соответствуют используемому: %O - это имя базового файла, %A - это имя версии слева/локально/--ours, %B - это имя версии правого/другого/удаленного/--theirs, %L - это параметр conflict-marker-size (по умолчанию 7), а %P - это путь, который Git хочет использовать для сохранения конечного результата в работа дерево.

    Обратите внимание, что %O, %A и %B - все имена временных файлов, созданных Git (для хранения содержимого blob). Ни один из них не соответствует %P. Git ожидает, что драйвер слияния оставит результат слияния в пути %A (который Git затем переименует в %P сам по себе).

Во всех случаях объединенный файл переходит к рабочему дереву на данный момент. Если слияние прошло хорошо, слоты с более высоким номером в индексе очищаются: Git, по сути, запускает git add в дереве дерева, записывая данные в репозиторий в виде объекта blob и получая Идентификатор хэша, который переходит в нулевой слот. Если слияние не удалось с конфликтами, слоты с более высоким номером остаются на месте; слот 0 остается пустым.

Конечным результатом всего этого является то, что рабочее дерево содержит объединенные файлы, возможно, с маркерами конфликтов, а индекс содержит результат слияния, возможно, с конфликтами, которые должны быть разрешены.

Использование git mergetool

Это работает так же, как и драйвер слияния. Помимо запуска только после завершения слияния с результатами в индексе и дереве работы, основные отличия заключаются в следующем:

  • git mergetool создаст дополнительные копии файлов (файлы .orig).
  • Он точно знает, как запускать каждый известный инструмент, т.е. какие аргументы передаются, чтобы сделать этот инструмент полезным. Например, нет эквивалента заполнительу %O драйвера.
  • Он может запускать команды во всех незагруженных файлах в некотором каталоге.

Фактически, git mergetool представляет собой большую оболочку script: она использует git ls-files -u, чтобы найти незакрепленные записи индекса, и git checkout-index, чтобы извлечь каждый этап из индекса. У него даже есть особые случаи для конфликтов более высокого уровня, таких как добавление/добавление или переименование/удаление.

В известном инструменте есть дополнительный фрагмент драйвера - script: посмотрите

$ ls $(git --exec-path)/mergetools

чтобы увидеть все отдельные драйверы. Им передается флаг $base_present для обработки конфликтов добавления/добавления. (Они получены, т.е. Работают с . "$MERGE_TOOLS_DIR/$tool", так что они могут переопределять функции оболочки, определенные в script.)

Для неизвестных инструментов вы используете имена переменных оболочки $BASE, $LOCAL и $REMOTE, чтобы узнать, где script установил три файла, извлеченные из индекса, и вы напишете свой результат на $MERGED (на самом деле это имя рабочего дерева для файла). script делает это:

setup_user_tool () {
        merge_tool_cmd=$(get_merge_tool_cmd "$tool")
        test -n "$merge_tool_cmd" || return 1

        diff_cmd () {
                ( eval $merge_tool_cmd )
        }

        merge_cmd () {
                ( eval $merge_tool_cmd )
        }
}

i.e., eval ваша команда инструмента в под-оболочке, так что вы не можете переопределять все, как могут быть известны известные инструменты.

Рекурсивное слияние

Когда Git необходимо выполнить рекурсивное слияние...

В большинстве случаев этот вопрос довольно спорный. Инструмент слияния никогда не видит эту ситуацию вообще, потому что git mergetool вызывается после того, как Git завершил рекурсивное слияние и оставил результат в индексе и дереве. Тем не менее, драйверы слияния действительно говорят здесь.

Когда стратегия слияния -s recursive объединяет базы слияния для создания нового "виртуального коммита", он вызывает другой git merge -well, точнее, просто вызывает себя рекурсивно - на основе суммы слияния фиксируется (но см. ниже). Этот внутренний git merge знает, что он вызывается рекурсивно, поэтому, когда он собирается применить драйвер слияния .gitattributes, он проверяет там параметр recursive =. Это определяет, будет ли снова использоваться драйвер слияния, или какой-либо другой драйвер слияния используется для внутреннего слияния. Для встроенного драйвера слияния Git отключает расширенные параметры стратегии, т.е. Не действует ни -X ours, ни -X theirs.

Когда внутреннее объединение завершается, его результат - все файлы, которые будут оставлены в дереве работы, были не внутренним, рекурсивным слиянием, а фактически сохранены как реальная фиксация. Это справедливо даже в случае неразрешенных конфликтов. Эти неразрешенные конфликты могут даже содержать маркеры конфликтов. Тем не менее, это новая битва "виртуального слияния", и это истинное совершение; у него просто нет внешнего имени, с помощью которого вы можете найти хеш фиксации.

Если на этом конкретном уровне есть три или более базы слияния, а не только две базы слияния, эта новая база виртуального слияния теперь объединяется со следующей оставшейся базой слияния, итеративно. Логически, Git может использовать стратегию разделения и побед здесь: если первоначально было 32 базы слияния, они могли объединить их по два за один раз, чтобы произвести 16 коммитов, объединить эти два за раз, чтобы создать 8, и так далее, Помимо того, что ceil (log2 (N)) сливается вместо слияния N-1, однако, не ясно, что это будет покупать много: уже довольно редко приходится иметь N > 1.

Ответ 2

Инструменты объединения не анализируют файл в рабочем каталоге с помощью маркеров конфликта. Они читают файлы-предки, наши и их файлы, которые git mergetool создает из индекса и места на диске для них.

Они будут использовать свою собственную логику для получения результата слияния и перезапишут файл, созданный с помощью Git.