Search code examples
gitdifflocalradix

git difftool $BASE and $LOCAL are reversed


I merged a feature branch into master with --no-commit. When doing a git diff I see the correct changes: some lines have been added in the feature branch. When using git difftool I get a reversed result. The tool shows that the new lines were already present and are not there any more.

I checked git config and in fact $BASE refers to the changes and $LOCAL refers to the original version.

[difftool "p4merge"]
path = C:\\Program Files\\Perforce\\p4merge.exe
cmd = p4merge.exe $BASE $BASE

This shows actually all the new changes in the working dir on both sides.

[difftool "p4merge"]
path = C:\\Program Files\\Perforce\\p4merge.exe
cmd = p4merge.exe $LOCAL $LOCAL

This shows the original version on both sides.

[difftool "p4merge"]
path = C:\\Program Files\\Perforce\\p4merge.exe
cmd = p4merge.exe $BASE $LOCAL

This shows the result I was expectiong but it's the wrong order?

Eclipse shows the expected result not the one from the difftool. I also tried different tools: kdiff3 and p4merge.


Solution

  • According to the documentation:

    When git difftool is invoked with this tool (either through the -t or --tool option or the diff.tool configuration variable) the configured command line will be invoked with the following variables available: $LOCAL is set to the name of the temporary file containing the contents of the diff pre-image and $REMOTE is set to the name of the temporary file containing the contents of the diff post-image. $MERGED is the name of the file which is being compared. $BASE is provided for compatibility with custom merge tool commands and has the same value as $MERGED.

    To unpack that a bit:

    • git difftool needs, first, the hash IDs of two commits (or something else suitable; see below). Let's call these the "left" and "right" sides of the diff. When you use:

      git diff commit1 commi2
      

      or

      git difftool commit1 commit2
      

      the idea is to obtain instructions that would tell you how to change what's in commit1 into what's in commit2: how to change each file in the left side into each file in the right side.

    • The stuff inside a commit—the frozen files in the snapshot—is in a form that any program that isn't Git cannot use. So Git is going to extract the files from those commits into some temporary area. These files may have wild temporary names like /tmp/a9ZF3xq or some such.

    • $LOCAL has the wild / unpredictable temporary name of the left-side file.

    • $REMOTE has the wild / unpredictable temporary name of the right-side file.

    • $MERGED has the actual file name, such as src/sub/somefunc.py or whatever. If this file exists on disk, its contents may not be those of either the left or the right side file! $MERGED should be used only to show the name of the file, not to access any contents.

    • $BASE really should not be used, but if do you have something that uses it, it's just going to be identical to $MERGED.

    So, reading the contents of a file named $BASE or $MERGED is incorrect and will result in the wrong instructions sometimes. Do not use either one—use $LOCAL and $REMOTE instead, with $LOCAL holding the left-side contents and $REMOTE holding the right-side contents.

    Some git diff / git difftool commands use the work-tree

    I said above that git difftool—which is mostly the same as git diff—needs the hash IDs of two commits, but that's not quite true. What it really needs is two trees to compare.

    Every commit has a tree, in Git, because every commit contains a complete snapshot of all files as of their state at the time you (or whoever) made the commit. But there are two more trees you can use:

    • The index or staging area contains your proposed next commit. That is, it has a copy of every (to-be-committed) file. If you run git commit right now, the new commit will snapshot the files that are in the index / staging-area.

    • Your work-tree is a tree of files that you can, using git add, use to overwrite the files in your index / staging-area.

    Both git diff and git difftool can use the index and/or the work-tree as left and/or right side. If you use the index as one of the two sides, its files are in the same Git-only format as the ones in commits, so these files must be copied to temporary files that could have wild temporary names. But if and when you use the work-tree as one of the two sides, well, its files are all ordinary everyday files. Git can, and will, just direct the merge tool to look right at those files.

    That means that when you use the work-tree as one of the sides (and only in this case), $LOCAL or $REMOTE will have the name of a work-tree file in it, and that work-tree file has the contents that your tool is supposed to examine. In this case, $BASE and $MERGED will have the same name. This case is very common because, as with git diff:

    git difftool
    

    with no arguments means "compare the index contents to the work-tree contents". With one argument:

    git difftool HEAD
    

    Git compares the given commit to the work-tree. That is, in both of these cases, the right-side "commit" is whatever is in the work-tree. So here, $LOCAL is a temporary file name with what's in the HEAD commit or the index, but $REMOTE, $BASE, and $MERGE are all just the name of the work-tree file!

    Hence, of course:

    <command-to-view> $LOCAL
    

    will show you what's in the left side, because that's what $LOCAL is: a temporary file containing what's in the left side. Meanwhile:

    <command-to-view> $BASE
    

    will show you what's in the right side, because $BASE matches $REMOTE and $REMOTE is the name of a file containing what's in the right side. Using:

    <command-to-view/compare-both-of> $LOCAL $BASE
    

    will work for this case, but not in general. You must use:

    <command-to-view/compare-both-of> $LOCAL $REMOTE
    

    and if that command has the ability to say "this is a comparison of the contents that were stored in a file named ____" (fill in the blank), you can fill in the blank using $BASE.