如何在不合并的情况下在 Git 中执行三向差异?
How to perform a three-way diff in Git without merging?
我想在具有共同合并基础的两个 git 分支之间执行三向差异,并使用 kdiff3 查看它。
我找到了很多关于 SO 的指导(以及一些非常相似的问题(1, 2, ) ) but I haven't found a direct answer. Notably, a comment on this answer 暗示我想要的是可能的,但它对我不起作用。希望用户可以插话这里 :)
对于背景,当我执行合并时,我使用 "diff3" 冲突样式:
git config --global merge.conflictstyle diff3
我 git mergetool
配置为使用 kdiff3。
解决合并冲突时显示四个文件:
- 当前分支的文件($LOCAL)
- 其他分支的文件($REMOTE)
- 作为两个分支的共同祖先的文件($BASE)
- 合并后的输出文件($MERGED)
然而,git difftool
只会拉起两个分支提示。我也想看看基本文件。 明确地说,我希望能够在合并 之前执行此差异,包括在没有合并冲突的文件上。 (git mergetool
仅在存在冲突时显示三向差异)。
部分解决方案#1:
对于单个文件,我可以导出三个版本并手动调用差异:
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 &
这有两个问题:
- 我希望它适用于整个项目,而不仅仅是特定文件。
- 这太丑了!我可以编写脚本,但我希望有一种使用标准 git 流程的更简洁的方法。
部分解决方案#2:
感谢 this answer and comment 的启发。
创建一个始终 returns "false" 的自定义合并驱动程序,它创建了一个冲突的合并状态,但实际上没有进行任何自动合并。然后使用 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。
除了 kdiff3 在调用时执行自动合并外,这会(有点)工作,所以我仍然无法看到预合并的差异。不过,我可以通过更改 Git 的常用 kdiff3 驱动程序文件(.../git-core/mergetools/kdiff3
通过删除 --auto
开关来解决此问题。
即便如此,这仍然存在以下问题:
- 这仅在两个文件都已更改时有效!在只有一个文件更改的情况下,更新的文件将替换旧文件并且永远不会调用合并。
- 我必须修改 Git kdiff3 驱动程序,它根本不可移植。
- 我必须在做 diff 之前和之后修改
attributes
。
- 当然,我希望在不合并的情况下执行此操作:)
悬赏信息:
根据给出的答案,这在标准 Git 中是不可能的。所以现在我正在寻找一个更开箱即用的解决方案:我如何调整 Git 来实现这一点?
这里有一个线索:显然,如果三个文件中只有一个发生了变化,这个较新的文件将用于合并结果,而不会实际调用合并驱动程序。这意味着在这种情况下永远不会调用我的自定义 "conflict-creating" 合并驱动程序。如果是,那么我的 "Partial Solution #2" 就可以正常工作了。
是否可以通过调整文件或配置来改变这种行为?或者也许有一种方法可以创建自定义差异驱动程序?我还没准备好开始玩 Git 源代码...
有什么妙招吗?
据我所知这是不可能的。您可以将 mergebase 与 local_branch
和 mergebase 与 remote_branch
进行区分,如您引用的答案中所述。但我认为还没有像您使用标准 git 命令请求的那样获得三向合并的工具。您可以在 Git 邮件列表中请求添加此功能。
我觉得不可能。
合并逻辑其实挺复杂的。合并基础不一定是唯一的,合并代码会花很多篇幅来合理地处理这种情况,但这在任何差异代码中都不会重复。
Git 可以轻松回到之前的状态。因此,如果您有任何更改,请隐藏您的更改,尝试合并,然后 --abort
它或 reset
当您已经看够了并且不再需要结果时。
I want to be able to perform this diff before merging, including on files without merge conflicts.
您只需根据需要设置索引,无需提交结果。完全按照要求设置的方法,直接 diffs-since-base 没有合并准备,是
git merge -s ours --no-ff --no-commit $your_other_tip
一个完整的 handroll,其中 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 "" -d\n git ls-tree -r |
sed "s/ [^ ]*//;s/\t/ \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 从工作树中的内容开始,不进行自己的自动合并。看起来 kdiff3 想要忽略工作树。 Anyhoo,在没有 --auto 的情况下将其设置为 运行 看起来 too 也不太hacky:
# 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
。
我使用以下原始 bash 脚本和 meld 来查看 在 合并两个分支后发生了什么变化:
#!/bin/bash
filename=""
if [ -z "$filename" ] ; then
echo "Usage: [=10=] 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=""
hash1=
hash2=
if [ -z "$filename" ] ; then
echo "Usage: [=11=] 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")
我还没有测试过那个脚本。它不会向您展示 git 如何合并文件,但它会让您了解潜在冲突的位置。
真的,git diff3
命令应该存在。 @FrédérirMarchal 的回答中显示的 meld
解决方案适用于一个文件,但我希望它适用于整个提交。所以我决定写一个脚本来做到这一点。它并不完美,但它是一个好的开始。
安装:
- 将下面的脚本复制到您路径中的
git-diff3
某处
- 安装
meld
或将 GIT_DIFF3_TOOL
设置为您最喜欢的三向差异程序
用法:
git diff3 branch1 branch2
:在 branch1
、branch1
和 branch2
的合并基础以及 branch2
.[=46= 之间进行三向差异]
git diff3 commit1 commit2 commit3
:在三个给定的提交之间进行三向差异。
git diff3 HEAD^1 HEAD HEAD^2
:合并之后,在 HEAD 和它的两个父节点之间做一个三向差异。
限制:
- 我不处理重命名文件。改名后的文件顺序与否,就看运气了
- 与
git diff
不同,我的差异对所有更改的文件都是全局的;我将差异锚定在文件边界。我的 ======== START $file ========
和 ... END ...
标记为 diff 提供了匹配的几行,但如果有很大的变化,它可能仍然会感到困惑。
脚本:
#!/bin/bash
GIT_DIFF3_TOOL=${GIT_DIFF3_TOOL:-meld}
if [[ $# == 2 ]]; then
c1=
c3=
c2=`git merge-base $c1 $c3`
elif [[ $# == 3 ]]; then
c1=
c2=
c3=
else
echo "Usages:
[=10=] branch1 branch2 -- compare two branches with their merge bases
[=10=] commit1 commit2 commit3 -- compare three commits
[=10=] 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=
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)
我想在具有共同合并基础的两个 git 分支之间执行三向差异,并使用 kdiff3 查看它。
我找到了很多关于 SO 的指导(以及一些非常相似的问题(1, 2,
对于背景,当我执行合并时,我使用 "diff3" 冲突样式:
git config --global merge.conflictstyle diff3
我 git mergetool
配置为使用 kdiff3。
解决合并冲突时显示四个文件:
- 当前分支的文件($LOCAL)
- 其他分支的文件($REMOTE)
- 作为两个分支的共同祖先的文件($BASE)
- 合并后的输出文件($MERGED)
然而,git difftool
只会拉起两个分支提示。我也想看看基本文件。 明确地说,我希望能够在合并 之前执行此差异,包括在没有合并冲突的文件上。 (git mergetool
仅在存在冲突时显示三向差异)。
部分解决方案#1:
对于单个文件,我可以导出三个版本并手动调用差异:
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 &
这有两个问题:
- 我希望它适用于整个项目,而不仅仅是特定文件。
- 这太丑了!我可以编写脚本,但我希望有一种使用标准 git 流程的更简洁的方法。
部分解决方案#2:
感谢 this answer and comment 的启发。
创建一个始终 returns "false" 的自定义合并驱动程序,它创建了一个冲突的合并状态,但实际上没有进行任何自动合并。然后使用 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。
除了 kdiff3 在调用时执行自动合并外,这会(有点)工作,所以我仍然无法看到预合并的差异。不过,我可以通过更改 Git 的常用 kdiff3 驱动程序文件(.../git-core/mergetools/kdiff3
通过删除 --auto
开关来解决此问题。
即便如此,这仍然存在以下问题:
- 这仅在两个文件都已更改时有效!在只有一个文件更改的情况下,更新的文件将替换旧文件并且永远不会调用合并。
- 我必须修改 Git kdiff3 驱动程序,它根本不可移植。
- 我必须在做 diff 之前和之后修改
attributes
。 - 当然,我希望在不合并的情况下执行此操作:)
悬赏信息:
根据给出的答案,这在标准 Git 中是不可能的。所以现在我正在寻找一个更开箱即用的解决方案:我如何调整 Git 来实现这一点?
这里有一个线索:显然,如果三个文件中只有一个发生了变化,这个较新的文件将用于合并结果,而不会实际调用合并驱动程序。这意味着在这种情况下永远不会调用我的自定义 "conflict-creating" 合并驱动程序。如果是,那么我的 "Partial Solution #2" 就可以正常工作了。
是否可以通过调整文件或配置来改变这种行为?或者也许有一种方法可以创建自定义差异驱动程序?我还没准备好开始玩 Git 源代码...
有什么妙招吗?
据我所知这是不可能的。您可以将 mergebase 与 local_branch
和 mergebase 与 remote_branch
进行区分,如您引用的答案中所述。但我认为还没有像您使用标准 git 命令请求的那样获得三向合并的工具。您可以在 Git 邮件列表中请求添加此功能。
我觉得不可能。
合并逻辑其实挺复杂的。合并基础不一定是唯一的,合并代码会花很多篇幅来合理地处理这种情况,但这在任何差异代码中都不会重复。
Git 可以轻松回到之前的状态。因此,如果您有任何更改,请隐藏您的更改,尝试合并,然后
--abort
它或reset
当您已经看够了并且不再需要结果时。
I want to be able to perform this diff before merging, including on files without merge conflicts.
您只需根据需要设置索引,无需提交结果。完全按照要求设置的方法,直接 diffs-since-base 没有合并准备,是
git merge -s ours --no-ff --no-commit $your_other_tip
一个完整的 handroll,其中 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 "" -d\n git ls-tree -r | sed "s/ [^ ]*//;s/\t/ \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 从工作树中的内容开始,不进行自己的自动合并。看起来 kdiff3 想要忽略工作树。 Anyhoo,在没有 --auto 的情况下将其设置为 运行 看起来 too 也不太hacky:# 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
。
我使用以下原始 bash 脚本和 meld 来查看 在 合并两个分支后发生了什么变化:
#!/bin/bash
filename=""
if [ -z "$filename" ] ; then
echo "Usage: [=10=] 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=""
hash1=
hash2=
if [ -z "$filename" ] ; then
echo "Usage: [=11=] 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")
我还没有测试过那个脚本。它不会向您展示 git 如何合并文件,但它会让您了解潜在冲突的位置。
真的,git diff3
命令应该存在。 @FrédérirMarchal 的回答中显示的 meld
解决方案适用于一个文件,但我希望它适用于整个提交。所以我决定写一个脚本来做到这一点。它并不完美,但它是一个好的开始。
安装:
- 将下面的脚本复制到您路径中的
git-diff3
某处 - 安装
meld
或将GIT_DIFF3_TOOL
设置为您最喜欢的三向差异程序
用法:
git diff3 branch1 branch2
:在branch1
、branch1
和branch2
的合并基础以及branch2
.[=46= 之间进行三向差异]git diff3 commit1 commit2 commit3
:在三个给定的提交之间进行三向差异。git diff3 HEAD^1 HEAD HEAD^2
:合并之后,在 HEAD 和它的两个父节点之间做一个三向差异。
限制:
- 我不处理重命名文件。改名后的文件顺序与否,就看运气了
- 与
git diff
不同,我的差异对所有更改的文件都是全局的;我将差异锚定在文件边界。我的======== START $file ========
和... END ...
标记为 diff 提供了匹配的几行,但如果有很大的变化,它可能仍然会感到困惑。
脚本:
#!/bin/bash
GIT_DIFF3_TOOL=${GIT_DIFF3_TOOL:-meld}
if [[ $# == 2 ]]; then
c1=
c3=
c2=`git merge-base $c1 $c3`
elif [[ $# == 3 ]]; then
c1=
c2=
c3=
else
echo "Usages:
[=10=] branch1 branch2 -- compare two branches with their merge bases
[=10=] commit1 commit2 commit3 -- compare three commits
[=10=] 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=
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)