如何在没有合并的情况下在Git中执行三向差异?

时间:2017-01-23 09:37:20

标签: git version-control merge diff

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

我已经找到了很多关于SO的指导(以及一些非常相似的问题(123))但我还没找到直接回答。值得注意的是,对this answer的评论意味着我想要的是可能的,但它对我没有用。希望用户可能会在这里发出声音:)

对于背景,当我执行合并时,我使用" diff3"冲突风格:

git config --global merge.conflictstyle diff3

我已将git mergetool配置为使用kdiff3。

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

  1. 当前分支的文件($ LOCAL)
  2. 另一个分支的文件($ REMOTE)
  3. 该文件是两个分支的共同祖先($ BASE)
  4. 合并后的输出文件($ MERGED)
  5. 但是,git difftool只会提取两个分支提示。我也希望看到基本文件。 要清楚,我希望能够在合并之前执行此差异,包括没有合并冲突的文件。 (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 &
    

    这有两个问题:

    1. 我希望它适用于整个项目,而不仅仅是一个特定的文件。
    2. 这太丑了!我可以编写脚本,但我希望使用标准的git进程可以更清晰。
    3. 部分解决方案#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。

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

        即便如此,这还有以下停止显示的问题:

        1. 这仅适用于两个文件都已更改的情况!如果只更改了一个文件,则更新的文件将替换旧文件,并且永远不会调用合并。
        2. 我必须修改Git kdiff3驱动程序,它根本不可移植。
        3. 我必须在做差异之前和之后修改attributes
        4. 当然,我希望在不合并的情况下这样做:)
        5. 赏金信息:

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

          这里有一个主角:显然,如果三个文件中只有一个发生了变化,则在合并结果中使用这个较新的文件而不实际调用合并驱动程序。这意味着我的定制"冲突创造"在这种情况下,从不调用合并驱动程序。如果是,那么我的"部分解决方案#2"实际上会起作用。

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

          任何聪明的想法?

5 个答案:

答案 0 :(得分:8)

  

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

您只需根据需要设置索引,就不必提交结果。设置确切问题的方法是直接的差异 - 基础,没有合并准备,是

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从工作区中的内容开始,并没有自己的automerge。看起来kdiff3想要忽略工作树。 Anyhoo,将其设置为在没有--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
    
  3. 退出此通常是git merge --abortgit reset --hard

答案 1 :(得分:2)

我认为不可能。

  1. 合并逻辑实际上非常复杂。合并基础不一定是唯一的,合并代码可以合理地处理这种情况,但在任何差异代码中都不会重复。

  2. Git让您可以轻松回到以前的状态。如果您有任何更改,请隐藏您的更改,尝试合并,然后--abortreset,当您看上去不够并且不再需要结果时。

答案 2 :(得分:1)

据我记得,这是不可能的。您可以将mergebase与local_branch区分开,并将mergebase与remote_branch区分开,如您引用的答案中所述。但我认为没有任何设施可以像您使用标准git命令那样获得3路合并。您可以在Git邮件列表中请求添加此功能。

答案 3 :(得分:1)

我使用以下原始bash脚本并混合看看在合并两个分支之后发生了什么变化:

<div class="my_class" (click)="clickEvent()"  
    [ngClass]="status ? 'success' : 'danger'">                
     Some content
</div>

status: boolean = false;
clickEvent(){
    this.status = !this.status;       
}

可能会被黑客攻击以查看当前目录中的文件与两个分支之间的差异:

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

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

答案 4 :(得分:1)

确实,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不同,我的差异是所有已更改文件的全局差异;我将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)