Как выполнить трехсторонний 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)