Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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:

  1. The current branch's file ($LOCAL)
  2. The other branch's file ($REMOTE)
  3. The file which is the common ancestor of the two branches ($BASE)
  4. The merged output file ($MERGED)

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:

  1. I want it to work for the whole project, not just a specific file.
  2. This is ugly! I can script it, but I hope there's a much cleaner way using standard git processes.

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.

  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.

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:

  1. This only works when both files have changed! In the case where only one file changed, the updated file replaces the older file and the merge is never called.
  2. I have to modify the Git kdiff3 driver, which isn't portable at all.
  3. I have to modify attributes before and after doing the diff.
  4. And, of course, I was hoping to do this without merging :)

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?

like image 616
bitsmack Avatar asked Jan 23 '17 09:01

bitsmack


People also ask

What is an alternative to merging in git?

If you would prefer a clean, linear history free of unnecessary merge commits, you should reach for git rebase instead of git merge when integrating changes from another branch.

How do I turn off auto merge in git?

Use git-reset or git merge --abort to cancel a merge that had conflicts. Please note that all the changes will be reset, and this operation cannot be reverted, so make sure to commit or git-stash all your changes before you start a merge.

How do you prevent needs to merge?

In order to avoid a merge conflict, all changes must be on different lines, or in different files, which makes the merge simple for computers to resolve. In other words, if a change introduces any ambiguity even at a single line of code an automatic merging is canceled and the whole process must be finished manually.


2 Answers

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.

like image 166
jthill Avatar answered Oct 03 '22 20:10

jthill


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.

like image 34
Jan Hudec Avatar answered Oct 03 '22 19:10

Jan Hudec