【问题标题】:How to perform a three-way diff in Git without merging?如何在不合并的情况下在 Git 中执行三向差异?
【发布时间】:2017-01-23 09:37:19
【问题描述】:

我想在两个具有公共合并基础的 git 分支之间执行三向差异,并使用 kdiff3 进行查看。

我找到了很多关于 SO 的指导(以及一些非常相似的问题(123)),但我还没有找到直接的答案。值得注意的是,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 answercomment 的启发。

创建一个始终返回“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. 我必须在做差异之前和之后修改attributes
  4. 当然,我希望在不合并的情况下做到这一点 :)

赏金信息:

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

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

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

有什么聪明的想法吗?

【问题讨论】:

  • 也许 diffmerge 工具可以在stackoverflow.com/questions/18131515/… 中轻松配置。
  • @Marina-MSFT 谢谢!但是该方法仅在合并失败后才有效,并且在想要差异(不合并)时我无法弄清楚如何做到这一点。
  • @Marina-MSFT,没有“diffmerge”工具。有一个difftool 和一个mergetool。前者是双向的,后者仅用于合并。
  • @JanHudec,我的意思是应用程序sourcegear.com/diffmerge/index.html
  • 也许thisthis 有帮助?不幸的是,我无法通过 3 路合并运行它。我尝试添加cmd = 'C:/Program Files/KDiff3/kdiff3' $BASE $LOCAL $REMOTE -o $MERGED

标签: git version-control merge diff


【解决方案1】:

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

您只需要根据需要设置索引,您无需提交结果。准确设置所要求的内容的方法是直接 diffs-since-base 而没有合并准备,是

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

一个完整的手卷,其中 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 "$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. ...如果您使用 vimdiff,第 2 步将是 git mergetool。 vimdiff 从工作树中的内容开始,不进行自己的自动合并。看起来 kdiff3 想要忽略工作树。无论如何,将其设置为在没有 --auto 的情况下运行不会看起来太 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

【讨论】:

  • 非常好,jthill!这正是我一直在寻找的那种东西。今天晚些时候我会试一试。我有一些 linux 学习要做:)
  • 您好!我有一个关于你的脚本的问题,这个问题太长了,无法在 cmets 中提问。请在chat 上查看一下好吗?
  • @bitsmack 我知道已经晚了,但我可能遇到了同样的问题。这是我只使用 git 命令(更容易移植到 Windows)可以获得的不那么复杂:git-conservative-merge。令人惊讶的是,在 git 中还没有这样的合并策略。
【解决方案2】:

真的,git diff3 命令应该存在。 @FrédérirMarchal 的答案中显示的 meld 解决方案适用于一个文件,但我希望它适用于整个提交。所以我决定写一个脚本来做到这一点。这并不完美,但它是一个好的开始。

安装:

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

用法:

  • git diff3 branch1 branch2:在branch1branch1branch2 的合并基础和branch2 之间进行三向差异。
  • git diff3 commit1 commit2 commit3:在三个给定的提交之间做一个三向差异。
  • git diff3 HEAD^1 HEAD HEAD^2: 合并后,在 HEAD 和它的两个父节点之间做一个三向差异。

限制:

  • 我不处理重命名文件。重命名后的文件顺序是否一致,就看运气了。
  • git 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)

【讨论】:

    【解决方案3】:

    我使用以下粗略的 bash 脚本并融合来查看 合并两个分支后发生了什么变化:

    #!/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 如何合并文件,但它可以让您了解潜在冲突的位置。

    【讨论】:

      【解决方案4】:

      我认为这是不可能的。

      1. 合并逻辑实际上相当复杂。合并基数不一定是唯一的,合并代码会花很多时间来合理处理这种情况,但这在任何 diff 代码中都没有重复。

      2. Git 可以很容易地回到之前的状态。因此,如果您有任何更改,请隐藏您的更改,尝试合并,然后尝试 --abort it 或 reset,当您看起来足够并且不再需要结果时。

      【讨论】:

      • 谢谢扬!问题是,即使我合并,我也看不到三向差异,除非在存在未解决冲突的文件中。但我想看看这两个分支中的哪一个是任何给定更改集的来源。
      【解决方案5】:

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

      【讨论】:

        猜你喜欢
        • 2019-09-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多