Как выполнить трехсторонний diff в Git без слияния?

Я хочу выполнить трехсторонний разграничение между двумя ветвями git с общей базой слияния и просмотреть его с помощью kdiff3.

Я нашел много рекомендаций по SO (и несколько очень похожих вопросов (1, 2, 3)), но я не нашел прямого ответа. Примечательно, что комментарий этого ответа подразумевает, что я хочу, но это не сработало для меня. Надеюсь, этот пользователь может прослушивать здесь:)

Для фона, когда я выполняю слияния, я использую стиль конфликта "diff3":

git config --global merge.conflictstyle diff3

И у меня git mergetool настроено на использование kdiff3.

При разрешении конфликтов слияния это показывает мне четыре файла:

  • Текущий файл ветки ($ LOCAL)
  • Другой файл ветки ($ REMOTE)
  • Файл, являющийся общим предком двух ветвей ($ BASE)
  • Объединенный выходной файл ($ MERGED)

Однако git difftool только подтянет две подсказки ветки. Я тоже хочу увидеть базовый файл. Чтобы быть ясным, я хочу иметь возможность выполнить этот diff перед слиянием, в том числе в файлах без конфликтов слияния. (git mergetool показывает только трехсторонние различия, если есть конфликты).


Частичное решение № 1:

В отдельном файле я могу экспортировать три версии и вручную вызвать diff:

git show local_branch:filename > localfile
git show remote_branch:filename > remotefile
git show `git merge-base local_branch remote_branch`:filename > basefile

{kdiff3_path}/kdiff3.exe --L1 "Base" --L2 "Local" --L3 "Remote" -o "outputfile" basefile localfile remotefile &

Есть две проблемы с этим:

  • Я хочу, чтобы он работал для всего проекта, а не только для определенного файла.
  • Это некрасиво! Я могу script его, но я надеюсь, что там будет гораздо более чистый способ использования стандартных процессов git.

Частичное решение №2:

Благодаря этому ответу и comment для вдохновения.

Создайте пользовательский драйвер слияния, который всегда возвращает "false", что создает конфликтное состояние слияния без фактического автоматического слияния. Затем выполните diff с помощью git mergetool. Затем прервите слияние, когда закончите.

  • Добавить в .git/config:

    [merge "assert_conflict_states"]
        name = assert_conflict_states
        driver = false
    
  • Создайте (или добавьте) .git/info/attributes, чтобы заставить все слияния использовать новый драйвер:

    * merge=assert_conflict_states
    
  • Выполните слияние, которое теперь не выполняет никаких операций.

  • Сделайте разницу. В моем случае: git mergetool, который вызывает трехстороннее слияние kdiff3.

  • Когда закончите, отмените слияние: git merge --abort.

  • Отменить шаг № 2.

Это будет (sorta) работать, за исключением того, что kdiff3 выполняет автоарьер при вызове, поэтому я все еще не могу видеть предварительно сложенные различия. Однако я могу исправить это, изменив файл драйвера wd > wkk kdiff3 (.../git-core/mergetools/kdiff3, удалив переключатель --auto.

Тем не менее, это имеет следующие проблемы остановки:

  • Это работает только тогда, когда оба файла изменились! В случае, когда только один файл изменился, обновленный файл заменяет старый файл, и слияние никогда не вызывается.
  • Мне нужно изменить драйвер git kdiff3, который вообще не переносится.
  • Мне нужно изменить attributes до и после выполнения diff.
  • И, конечно же, я надеялся сделать это без слияния:)

Информация для награды:

В соответствии с полученными ответами это невозможно при использовании стандартного Git. Итак, теперь я ищу более готовое решение: как я могу настроить git, чтобы это произошло?

Здесь один вывод: по-видимому, если только один из трех файлов был изменен, этот новый файл используется в результате слияния без фактического вызова драйвера слияния. Это означает, что мой пользовательский драйвер слияния, создающий конфликт, никогда не вызывается в этом случае. Если бы это было так, то мое "Partial Solution № 2" действительно функционировало бы.

Можно ли изменить это поведение путем настройки файлов или конфигураций? Или, может быть, есть способ создать собственный драйвер diff? Я не готов начать играть с исходным кодом git...

Любые умные идеи?

Ответ 1

Я хочу иметь возможность выполнить этот diff перед слиянием, в том числе в файлах без конфликтов слияния.

Вам просто нужно настроить индекс, как вам нравится, вам никогда не придется фиксировать результаты. Способ настроить именно то, что было задано, прямой diff-from-base без предварительной слияния,

git merge -s ours --no-ff --no-commit $your_other_tip

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

git merge --no-ff --no-commit $your_other_tip

Выберите начальную точку, а затем

  • заставляет слияние для всех записей, которые показывают изменения в либо подсказка:

    #!/bin/sh
    
    git checkout -m .
    
    # identify paths that show changes in either tip but were automerged
    scratch=`mktemp -t`
    sort <<EOD | uniq -u >"$scratch"
    $(  # paths that show changes at either tip:
        (   git diff --name-only  ...MERGE_HEAD
            git diff --name-only  MERGE_HEAD...
        ) | sort -u )
    $(  # paths that already show a conflict:
        git ls-files -u | cut -f2- )
    EOD
    
    # un-automerge them: strip the resolved-content entry and explicitly 
    # add the base/ours/theirs content entries
    git update-index --force-remove --stdin <"$scratch"
    stage_paths_from () {
            xargs -a "$1" -d\\n git ls-tree -r $2 |
            sed "s/ [^ ]*//;s/\t/ $3\t/" |
            git update-index --index-info
    }
    stage_paths_from "$scratch" $(git merge-base @ MERGE_HEAD) 1 
    stage_paths_from "$scratch" @ 2
    stage_paths_from "$scratch" MERGE_HEAD 3
    
  • ... если вы используете vimdiff, шаг 2 будет просто git mergetool. vimdiff начинается с того, что в worktree и не делает собственного автосекретаря. Похоже, kdiff3 хочет игнорировать рабочую строку. Anyhoo, настроив его на запуск без --auto, не выглядит слишком хакивым:

    # one-time setup:
    wip=~/libexec/my-git-mergetools
    mkdir -p "$wip"
    cp -a "$(git --exec-path)/mergetools/kdiff3" "$wip"
    sed -si 's/--auto //g' "$wip"/kdiff3
    

    а затем вы можете

    MERGE_TOOLS_DIR=~/libexec/my-git-mergetools git mergetool
    

Отступ от этого только обычного git merge --abort или git reset --hard.

Ответ 2

Я не думаю, что это возможно.

  • Логика слияния на самом деле довольно сложна. База слияния необязательно уникальна, и код слияния имеет большую длину, чтобы разумно справиться с такой ситуацией, но это не дублируется в любом коде с расширением.

  • Git позволяет вернуться к предыдущему состоянию. Поэтому, если у вас есть какие-либо изменения, попробуйте слияние, а затем --abort it или reset, если вы достаточно посмотрели и больше не нуждаетесь в результатах.

Ответ 3

Насколько я помню, это невозможно. Вы можете отличить mergebase от local_branch и mergebase от remote_branch, как описано в ответе, на который вы ссылались. Но я думаю, что еще нет возможности получить трехстороннее слияние, как вы просили, с помощью стандартной команды git. Вы можете запросить в списке рассылки git, что эта функция добавлена.

Ответ 4

Я использую следующую грубую bash script и meld, чтобы увидеть, что было изменено после, слияние двух ветвей:

#!/bin/bash

filename="$1"

if [ -z "$filename" ] ; then
    echo "Usage: $0 filename"
    exit 1
fi

if [ ! -f "$filename" ] ; then
    echo "No file named \"$filename\""
    exit 1
fi

hashes=$(git log --merges -n1 --parents --format="%P")

hash1=${hashes% *}
hash2=${hashes#* }
if [ -z "$hash1" || -z "$hash2" ] ; then
    echo "Current commit isn't a merge of two branches"
    exit 1
fi

meld <(git show $hash1:"$filename") "$filename" <(git show $hash2:"$filename")

Вероятно, он может быть взломан, чтобы увидеть различия между файлом в текущем каталоге и двумя ветвями:

!/bin/bash

filename="$1"
hash1=$2
hash2=$3

if [ -z "$filename" ] ; then
    echo "Usage: $0 filename hash1 hash2"
    exit 1
fi

if [ ! -f "$filename" ] ; then
    echo "No file named \"$filename\""
    exit 1
fi

if [ -z "$hash1" || -z "$hash2" ] ; then
    echo "Missing hashes to compare"
    exit 1
fi

meld <(git show $hash1:"$filename") "$filename" <(git show $hash2:"$filename")

Я не тестировал это script. Он не покажет вам, как git будет объединять файл, но он дает вам представление о возможных конфликтах.

Ответ 5

Действительно, команда git diff3 должна существовать. meld решение, показанное в ответе @FrédérirMarchal, подходит для одного файла, но я хочу, чтобы оно работало над целыми коммитами. Поэтому я решил написать сценарий, чтобы сделать это. Это не идеально, но это хорошее начало.

Монтаж:

  • скопируйте скрипт ниже в git-diff3 где git-diff3 нибудь на вашем пути
  • установить meld или установить GIT_DIFF3_TOOL к вашей любимой трехходовым программе просмотра различий

Обычаи:

  • git diff3 branch1 branch2: выполните трехстороннее сравнение между branch1, базой слияния branch1 и branch2 и branch2.
  • git diff3 commit1 commit2 commit3: сделайте трехсторонний diff между тремя данными коммитами.
  • git diff3 HEAD^1 HEAD HEAD^2: после слияния выполните трехстороннее сравнение между HEAD и двумя его родителями.

Ограничения:

  • Я не занимаюсь переименованием файлов. Будет ничья, если переименованный файл будет в том же порядке или нет.
  • В отличие от git diff, мой diff является глобальным для всех измененных файлов; Я привязываю diff к границам файлов. Мои ======== START $file ======== и ... END... маркеры дают разности пару строк, которые будут совпадать, но если есть большие изменения, они все равно могут запутаться,

Сценарий:

#!/bin/bash

GIT_DIFF3_TOOL=${GIT_DIFF3_TOOL:-meld}

if [[ $# == 2 ]]; then
   c1=$1
   c3=$2
   c2='git merge-base $c1 $c3'
elif [[ $# == 3 ]]; then
   c1=$1
   c2=$2
   c3=$3
else
   echo "Usages:
   $0 branch1 branch2 -- compare two branches with their merge bases
   $0 commit1 commit2 commit3 -- compare three commits
   $0 HEAD^1 HEAD HEAD^2 -- compare a merge commit with its two parents" >&2
   exit 1
fi
echo "Comparing $c1 $c2 $c3" >&2


files=$( ( git diff --name-only $c1 $c2 ; git diff --name-only $c1 $c3 ) | sort -u )

show_files() {
   commit=$1
   for file in $files; do
      echo ======== START $file ========
      git show $commit:$file | cat
      echo ======== " END " $file ========
      echo
   done
}

$GIT_DIFF3_TOOL <(show_files $c1) <(show_files $c2) <(show_files $c3)