Reputation: 1434
I want to perform a three-way diff between two git branches with a common merge base, and view it with kdiff3.
I've found lots of guidance on SO (and a few very similar questions (1, 2, 3) ) but I haven't found a direct answer. Notably, a comment on this answer implies that what I want is possible, but it didn't work for me. Hopefully that user might chime in here :)
For background, when I perform merges I use a "diff3" conflict style:
git config --global merge.conflictstyle diff3
And I have git mergetool
configured to use kdiff3.
When resolving merge conflicts this shows me four files:
However, git difftool
only will pull up the two branch tips. I want to see the base file, too. To be clear, I want to be able to perform this diff before merging, including on files without merge conflicts. (git mergetool
only shows the three-way diffs if there are conflicts).
Partial Solution #1:
With an individual file, I can export the three versions and manually call the 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 &
There are two problems with this:
Partial Solution #2:
Thanks to this answer and comment for the inspiration.
Create a custom merge driver that always returns "false", which creates a conflicted merge state without actually doing any auto-merging. Then perform the diff using git mergetool
. Then abort the merge when you're finished.
Add to .git/config
:
[merge "assert_conflict_states"]
name = assert_conflict_states
driver = false
Create (or append to) .git/info/attributes
to cause all merges to use the new driver:
* merge=assert_conflict_states
Perform the merge, which now doesn't do any automerging.
Do the diff. In my case: git mergetool
which brings up the kdiff3 three-way merge.
When done, abort the merge: git merge --abort
.
Undo step #2.
This would (sorta) work except that kdiff3 performs an automerge when called, so I still can't see the pre-merged diffs. I can fix this, though, by changing Git's stock kdiff3 driver file (.../git-core/mergetools/kdiff3
by removing the --auto
switch.
Even so, this has the following show-stopping problems:
attributes
before and after doing the diff.Information for the bounty:
According to answers given, this isn't possible with standard Git. So now I'm looking for a more out-of-the-box solution: How can I tweak Git to make this happen?
Here's one lead: Apparently, if only one of the three files has changed, this newer file is used in the merge result without actually calling the merge driver. This means that my custom "conflict-creating" merge driver is never called in this case. If it was, then my "Partial Solution #2" would actually function.
Could this behavior be changed by tweaking files or configurations? Or perhaps there's a way to create a custom diff driver? I'm not ready to start playing with the Git source code...
Any clever ideas?
Upvotes: 32
Views: 14401
Reputation: 12260
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:
git-diff3
somewhere on your pathmeld
or set GIT_DIFF3_TOOL
to your favourite three way diff programUsages:
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:
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)
Upvotes: 1
Reputation: 1002
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.
Upvotes: 1
Reputation: 60443
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
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
... 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
.
Upvotes: 13
Reputation: 76346
I don't think it is possible.
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.
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.
Upvotes: 3
Reputation: 38724
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.
Upvotes: 1