How to perform a three-way diff in Git without merging?

这一生的挚爱 提交于 2019-11-28 20:28:15

I want to be able to perform this diff before merging, including on files without merge conflicts.

You just have to set up the index as you like, you never have to commit results. The way to set up for exactly what was asked, straight diffs-since-base with no merge prep, is

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

a complete handroll in which git only sets up parents for whatever you eventually decide to commit as the a result, but it's probably better to do this with a normal merge while still being able to get in there and examine everything,

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

Pick your starting point, and then

  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
    

Backout from this is just the usual git merge --abort or git reset --hard.

I don't think it is possible.

  1. The merge logic is actually quite complex. The merge base is not necessarily unique and the merge code goes to great length to deal with such situation reasonably, but this is not duplicated in any diff code.

  2. Git makes it easy to go back to previous state. So stash your changes if you have any, try the merge and then --abort it or reset when you've looked enough and don't need the result any more.

As far as I remember this is not possible. You can diff mergebase against local_branch and mergebase against remote_branch like described in the answer you referenced. But I think there is no facility yet to get a 3-way merge like you requested with a standard git command. You might request on the Git mailing list that this feature gets added.

I use the following crude bash script and meld to see what was changed after merging two branches:

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

It can probably be hacked to see the differences between a file in the current directory and two branches:

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

I haven't tested that script. It won't show you how git would merge the file but it gives you an idea of where the potential conflicts are.

Really, the git diff3 command ought to exist. The meld solution shown in @FrédérirMarchal's answer is good for one file, but I want it to work over whole commits. So I decided to write a script to do just that. It's not perfect, but it's a good start.

Installation:

  • copy the script below in git-diff3 somewhere on your path
  • install meld or set GIT_DIFF3_TOOL to your favourite three way diff program

Usages:

  • git diff3 branch1 branch2: do a three way diff between branch1, the merge base of branch1 and branch2, and branch2.
  • git diff3 commit1 commit2 commit3: do a three way diff between the three given commits.
  • git diff3 HEAD^1 HEAD HEAD^2: after doing a merge, do a three way diff between HEAD and its two parents.

Limitations:

  • I don't handle renaming files. It'll be the luck of the draw if the renamed file is in the same order or not.
  • Unlike git diff, my diff is global over all the changed files; I'm anchoring the diff at file boundaries. My ======== START $file ======== and ... END ... markers give the diff a couple lines that will match, but if there are big changes it might still get confused.

The script:

#!/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)
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!