如何在Git中进行三路比较而不合并?

30
我想在两个具有共同合并基础的git分支之间执行三向差异,并使用kdiff3查看它。
我在SO上找到了很多指导(以及一些非常相似的问题(1, 2, 3)),但我没有找到直接的答案。特别是,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:

感谢此答案评论的启发。

创建一个自定义合并驱动程序,始终返回“false”,这将创建一个有冲突的合并状态,而不会实际执行任何自动合并。然后使用git mergetool执行差异比较。完成后中止合并。

  1. Add to .git/config:

    [merge "assert_conflict_states"]
        name = assert_conflict_states
        driver = false
    
  2. Create (or append to) .git/info/attributes to cause all merges to use the new driver:

    * merge=assert_conflict_states
    
  3. Perform the merge, which now doesn't do any automerging.

  4. Do the diff. In my case: git mergetool which brings up the kdiff3 three-way merge.

  5. When done, abort the merge: git merge --abort.

  6. Undo step #2.

这个方法有点可行,但是当调用kdiff3时它会执行自动合并,所以我仍然无法看到合并前的差异。不过我可以通过修改Git默认的kdiff3驱动文件(.../git-core/mergetools/kdiff3)来解决这个问题,具体方法是删除--auto开关。
即便如此,这个方法还存在以下几个严重问题:
  1. 只有当两个文件都发生了变化时才能使用。如果只有一个文件发生了变化,那么更新后的文件将替换旧文件,而合并操作则永远不会被调用。
  2. 我必须修改Git的kdiff3驱动程序,这样做根本不可移植。
  3. 在进行差异比较之前和之后,我必须修改attributes
  4. 当然,我希望能够在不进行合并的情况下完成这个任务 :)

赏金相关信息:

根据提供的答案,标准的Git无法实现此功能。因此,我正在寻找更加开箱即用的解决方案:如何调整Git以实现此目的?

这里有一个线索:显然,如果只有三个文件中的一个发生了更改,则在合并结果中使用此新文件而不实际调用合并驱动程序。这意味着在这种情况下,我的自定义“冲突创建”合并驱动程序从未被调用。如果它被调用了,那么我的“部分解决方案#2”实际上就能够起作用。

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

有什么聪明的想法吗?


或许可以在https://dev59.com/fmMl5IYBdhLWcg3wyJYe 中轻松配置diffmerge工具。 - Marina Liu
@Marina-MSFT 谢谢!但是那种方法只适用于合并失败后,我还没有找到如何在想要进行差异比较时使用它(而不进行合并)。 - bitsmack
@Marina-MSFT,"diffmerge" 工具不存在。有一个 difftool 和一个 mergetool。前者是双向的,后者仅在合并时使用。 - Jan Hudec
@JanHudec,我的意思是这个应用程序http://www.sourcegear.com/diffmerge/index.html。 - Marina Liu
也许这个这个有所帮助?不幸的是,我无法使用三方合并运行它。我尝试添加cmd = 'C:/Program Files/KDiff3/kdiff3' $BASE $LOCAL $REMOTE -o $MERGED - Christoph
5个回答

12

我希望在合并之前能够执行这个差异比较,包括没有合并冲突的文件。

你只需要按照自己的喜好设置索引,无需提交结果。设置方法如下,直接进行基于原始版本的差异比较,不需要进行合并准备:

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

这是一种完整的手动合并方式,其中git只为您最终决定提交的父级设置,但最好使用普通合并来完成此操作,同时仍然可以进入并检查所有内容。

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

选择您的起点,然后...
  1. force a merge visit for all entries that show any changes in either 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
    
  2. ... if you were using vimdiff, step 2 would be just git mergetool. vimdiff starts from what's in the worktree and doesn't do its own automerge. It looks like kdiff3 wants to ignore the worktree. Anyhoo, setting it up to run without --auto doesn't look too 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
    

    and then you can

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

撤回这个操作只需要执行一般的git merge --abort或者git reset --hard命令即可。


非常好,jthill!这正是我在寻找的东西。我稍后会尝试一下。我还有一些Linux学习要做 :) - bitsmack
你好!我有一个关于你的脚本的问题,这个问题太长了,无法在评论区里提问。请你到聊天室里查看一下吗? - bitsmack
@bitsmack 我知道有点晚,但我可能遇到了同样的问题。这是我只使用 git 命令能够获得的最简单的方法(更容易移植到 Windows):git-conservative-merge。令人惊讶的是,git 中还没有这样的合并策略。 - dawid

2

我不认为这是可能的。

  1. 合并逻辑实际上相当复杂。合并基础不一定唯一,合并代码会尽力合理地处理这种情况,但这并没有在任何差异代码中重复。

  2. Git使返回先前状态变得容易。如果您有任何更改,请将其存储,尝试合并,然后在查看足够并且不再需要结果时使用--abortreset


1
谢谢Jan!问题在于,即使我合并了,除非有未解决的冲突,在文件中我看不到三方差异。但我想知道这两个分支中的哪一个是任何给定更改集的源头。 - bitsmack

1
据我所记,这是不可能的。您可以像您引用的答案中所描述的那样,将 diff mergebase 与 local_branch 和 diff mergebase 与 remote_branch 进行比较合并。但我认为目前还没有一种标准的 Git 命令可以实现您请求的三方合并功能。您可以在 Git 邮件列表上提出请求,以添加此功能。

1
我使用下面这个简单的 bash 脚本和 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")

我没有测试过那个脚本。它不会展示给你git如何合并文件,但它可以让你了解潜在冲突的位置。

1

实际上,应该存在git diff3命令。@FrédérirMarchal的回答中显示的meld解决方案适用于一个文件,但我希望它能够在整个提交中起作用。因此,我决定编写一个脚本来完成这个任务。它不是完美的,但它是一个很好的开始。

安装:

  • 将下面的脚本复制到您的路径中的git-diff3
  • 安装meld或将GIT_DIFF3_TOOL设置为您喜欢的三向差异程序

用法:

  • git diff3 branch1 branch2:在branch1branch2和它们的合并基础之间进行三向差异。
  • git diff3 commit1 commit2 commit3:在给定的三个提交之间进行三向差异。
  • git diff3 HEAD^1 HEAD HEAD^2:在执行合并后,在HEAD和其两个父级之间进行三向差异。

限制:

  • 我不处理文件重命名。如果重命名后的文件与原来的文件顺序相同或不同,那就是运气问题。
  • git diff不同,我的diff是全局的,覆盖所有更改过的文件;我将diff锚定在文件边界处。我的======== START $file ========... END ...标记给diff提供了几行匹配,但如果有大的更改,它可能仍然会混淆。

脚本:

#!/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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接