如何在不合并的情况下在 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。

解决合并冲突时显示四个文件:

  1. 当前分支的文件($LOCAL)
  2. 其他分支的文件($REMOTE)
  3. 作为两个分支的共同祖先的文件($BASE)
  4. 合并后的输出文件($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 &

这有两个问题:

  1. 我希望它适用于整个项目,而不仅仅是特定文件。
  2. 这太丑了!我可以编写脚本,但我希望有一种使用标准 git 流程的更简洁的方法。

部分解决方案#2:

感谢 this answer and comment 的启发。

创建一个始终 returns "false" 的自定义合并驱动程序,它创建了一个冲突的合并状态,但实际上没有进行任何自动合并。然后使用 git mergetool 执行差异。然后在完成后中止合并。

  1. 添加到.git/config:

    [merge "assert_conflict_states"]
        name = assert_conflict_states
        driver = false
    
  2. 创建(或附加到).git/info/attributes 以使所有合并使用新驱动程序:

    * merge=assert_conflict_states
    
  3. 执行合并,现在不执行任何自动合并。

  4. 做差异。在我的例子中:git mergetool 会启动 kdiff3 三向合并。

  5. 完成后,中止合并:git merge --abort.

  6. 撤消步骤 #2。

除了 kdiff3 在调用时执行自动合并外,这会(有点)工作,所以我仍然无法看到预合并的差异。不过,我可以通过更改 Git 的常用 kdiff3 驱动程序文件(.../git-core/mergetools/kdiff3 通过删除 --auto 开关来解决此问题。

即便如此,这仍然存在以下问题:

  1. 这仅在两个文件都已更改时有效!在只有一个文件更改的情况下,更新的文件将替换旧文件并且永远不会调用合并。
  2. 我必须修改 Git kdiff3 驱动程序,它根本不可移植。
  3. 我必须在做 diff 之前和之后修改 attributes
  4. 当然,我希望在不合并的情况下执行此操作:)

悬赏信息:

根据给出的答案,这在标准 Git 中是不可能的。所以现在我正在寻找一个更开箱即用的解决方案:我如何调整 Git 来实现这一点?

这里有一个线索:显然,如果三个文件中只有一个发生了变化,这个较新的文件将用于合并结果,而不会实际调用合并驱动程序。这意味着在这种情况下永远不会调用我的自定义 "conflict-creating" 合并驱动程序。如果是,那么我的 "Partial Solution #2" 就可以正常工作了。

是否可以通过调整文件或配置来改变这种行为?或者也许有一种方法可以创建自定义差异驱动程序?我还没准备好开始玩 Git 源代码...

有什么妙招吗?

据我所知这是不可能的。您可以将 mergebase 与 local_branch 和 mergebase 与 remote_branch 进行区分,如您引用的答案中所述。但我认为还没有像您使用标准 git 命令请求的那样获得三向合并的工具。您可以在 Git 邮件列表中请求添加此功能。

我觉得不可能。

  1. 合并逻辑其实挺复杂的。合并基础不一定是唯一的,合并代码会花很多篇幅来合理地处理这种情况,但这在任何差异代码中都不会重复。

  2. 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

选择起点,然后

  1. 对显示任何更改的所有条目强制进行合并访问 任一提示:

    #!/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
    
  2. ...如果您使用的是 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 --abortgit 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:在 branch1branch1branch2 的合并基础以及 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)