36

Suppose that I just rebased the branch foo on master, with conflicts. I want to make sure that I did not accidentally damage the content of foo during conflict resolution by introducing extra changes or losing changes (other than that which is appropriate for the conflict resolution). I have done this via:

diff -u <(git diff `git merge-base master foo@{1}` foo@{1}) \
        <(git diff `git merge-base master foo    ` foo    )

(update: or the equivalent ... syntax for git-diff which I have just been reminded of:)

diff -u <(git diff master...foo@{1}) <(git diff master...foo) | mate

This shows me all the changes that have occurred to master..foo considered as a patch, which is exactly what I want to check for being minimal. However, the invocation is complex and the output is not entirely straightforward to interpret.

Is there a better way to accomplish this task — to provide the same information, but with a better method or format — or should I just take the above and wrap it up in a script?

Kevin Reid
  • 21,218
  • 9
  • 61
  • 82
  • Isn't this essentially equivalent to `git diff foo@{1} foo`? – twalberg Sep 17 '12 at 18:41
  • 2
    @twalberg No; `git diff foo@{1} foo` shows both my conflict resolutions and the changes made to master that I rebased onto, rather than just the conflict resolutions. – Kevin Reid Sep 17 '12 at 18:44
  • Ah, right... I was only considering my own most common rebase case, in which the merge-base of the source and destination stay the same (rewriting/squashing a branch before pushing). – twalberg Sep 17 '12 at 18:46
  • Related: https://stackoverflow.com/questions/16304574/how-to-list-branches-that-contain-an-equivalent-commit – Ioannis Filippidis Sep 09 '17 at 04:57
  • The question title is relevant also to comparing by only the subject of commits, so that "nearly-equivalent" commits show up (that wouldn't even with `git cherry`). `git log` with a diff viewer is an option in that case: https://stackoverflow.com/a/46127413/1959808 – Ioannis Filippidis Sep 09 '17 at 05:33
  • 1
    With Git 2.19 (Q3 2018), don't forget `git range-diff @{u} @{1} @`. See [my answer below](https://stackoverflow.com/a/51956712/6309). – VonC Aug 21 '18 at 21:07

6 Answers6

7

What we really want to show is the conflict combined diff that is generated as if we had done a merge to get from (a) to (b) where (a) was previously based on (upstream-old) and (b) is now based on (upstream-new).

We don't want just a straight diff.

Instead, we can essentially do the merge but force the resulting tree to be $b^{tree} which we already know is the correct "end" point of what we want.

More or less, let's assume that we have

newcommit -> the new version of the series
oldcommit -> the old version of the series
upstream -> the (new) version of the base of the series

We can generate the merge via

git commit-tree newcommit^{tree} -p oldcommit -p upstream -m "message"

and then show the result with "git show" and this will generate a combined diff format that shows all the necessary bits as a conflict resolution which automatically ignores changes that weren't actually part of resolving conflicts.

This even works for simply ammending a change as well, and you can do a bit more work to ensure that the generated merge commit has exact author and commit timestamps so that it's consistent over multiple calls (since we are storing a loose ref into the object database).

Unfortunately I have not been able to figure out how to just get "git diff" to diff the same way without actually generating a tree yet. I'm not sure what arguments we'd have to pass to get to that point.

Jake
  • 159
  • 1
  • 5
  • Clever idea! I'll have to see how useful the results are the next time I have this problem. – Kevin Reid Oct 21 '16 at 23:46
  • 1
    I just had an opportunity to try this out. I found a caveat that needs to be noted: `upstream` _deleted_ a file that had changes (so as to split it into several other files), and the diff produced by this technique does not mention that file at all, so would not show the loss of those changes. – Kevin Reid Nov 27 '16 at 04:37
6

Even better than interdiff, now with Git 2.19 (Q3 2018) you have git range-diff.
See "Git diff - two disjoint revision ranges"

The git range-diff documentation includes the following example:

When a rebase required merge conflicts to be resolved, compare the changes introduced by the rebase directly afterwards using:

$ git range-diff @{u} @{1} @

A typical output of git range-diff would look like this:

------------
-:  ------- > 1:  0ddba11 Prepare for the inevitable!
1:  c0debee = 2:  cab005e Add a helpful message at the start
2:  f00dbal ! 3:  decafe1 Describe a bug
    @@ -1,3 +1,3 @@
     Author: A U Thor <author@example.com>

    -TODO: Describe a bug
    +Describe a bug
    @@ -324,5 +324,6
      This is expected.

    -+What is unexpected is that it will also crash.
    ++Unexpectedly, it also crashes. This is a bug, and the jury is
    ++still out there how to fix it best. See ticket #314 for details.

    Contact
3:  bedead < -:  ------- TO-UNDO
------------

In this example, there are 3 old and 3 new commits, where the developer:

  • removed the 3rd,
  • added a new one before the first two, and
  • modified the commit message of the 2nd commit as well its diff.

When the output goes to a terminal, it is color-coded by default, just like regular git diff's output. In addition, the first line (adding a commit) is green, the last line (deleting a commit) is red, the second line (with a perfect match) is yellow like the commit header of git show's output, and the third line colors the old commit red, the new one green and the rest like git show's commit header.


With Git 2.20, colors are better supported for the new kind of (range) diff

See commit 2543a64, commit 8d5ccb5, commit 7648b79 (17 Aug 2018), and commit 4441067, commit f103a6f, commit 29ef759, commit 017ac45, commit 9d1e16b, commit 84120cc, commit c5e64ca, commit 991eb4f (14 Aug 2018) by Stefan Beller (stefanbeller).
(Merged by Junio C Hamano -- gitster -- in commit 30035d1, 17 Sep 2018)

range-diff: indent special lines as context

The range-diff coloring is a bit fuzzy when it comes to special lines of a diff, such as indicating new and old files with +++ and ---, as it would pickup the first character and interpret it for its coloring, which seems annoying as in regular diffs, these lines are colored bold via DIFF_METAINFO.

By indenting these lines by a white space, they will be treated as context which is much more useful, an example on the range diff series itself:

git range-diff pr-1/dscho/branch-diff-v3...pr-1/dscho/branch-diff-v4

(from repository github.com/gitgitgadget/git)

[...]
    + diff --git a/Documentation/git-range-diff.txt b/Documentation/git-range-diff.txt
    + new file mode 100644
    + --- /dev/null
    + +++ b/Documentation/git-range-diff.txt
    +@@
    ++git-range-diff(1)
[...]
    +
      diff --git a/Makefile b/Makefile
      --- a/Makefile
      +++ b/Makefile
[...]

The first lines that introduce the new file for the man page will have the '+' sign colored and the rest of the line will be bold.

The later lines that indicate a change to the Makefile will be treated as context both in the outer and inner diff, such that those lines stay regular color.

VonC
  • 1,042,979
  • 435
  • 3,649
  • 4,283
3

git-cherry searches for commits on one branch that aren't on another. You'd use it as:

git cherry -v OLDTIP TIP UPSTREAM

i.e. look for commits that were on OLDTIP but which aren't on UPSTREAM..TIP. It looks at the patch signature to see whether a patch is included, so if a patch got added, dropped or modified during the rebase then it will show up in the list. Things that got applied without changes won't appear.

A similar effect can be achieved by going back to OLDTIP and doing git rebase -i TIP, since that uses the same logic to populate the list of commits.

Thomas Leonard
  • 6,728
  • 2
  • 30
  • 37
  • This doesn't seem to help with examining intra-file changes. I'm interested in hunk-level changes lost or damaged due to bad conflict resolutions, not whole commits. – Kevin Reid Sep 26 '12 at 16:33
  • The idea here is to reduce the number of commits you need to compare (ideally to zero). It's not useful if you have a small number of large, conflicting commits, but it might help with a large number of small commits. – Thomas Leonard Sep 27 '12 at 14:13
  • Ah, I would guess, then, that by this standard I have a small number of large commits, because the follow-up question that came to mind is “Once I have the commits to compare, how do you propose I compare them, other than performing the diff-diff I started with?” – Kevin Reid Sep 27 '12 at 14:17
2

Differences between master and rebased foo:

git diff master..foo

vs.

Differences of pre-rebase foo after branching off master (note three dots):

git diff master...foo@{1}

?

user1338062
  • 9,351
  • 3
  • 54
  • 56
  • Thanks, using `...` shortens the command, and I've actually used that before so I should have thought of it. However, I'm still left with explicitly comparing those two diffs themselves. Do you have a suggestion for that? – Kevin Reid Sep 20 '12 at 16:52
2

Since it's s rebase and not a merge, you might want to compare foo with itself but before the merge. If I recall correctly foo@{1} will yield the parent of the current commit for foo, and maybe this is not what you are looking for.

I think you can do something like the following (assuming you haven't done a git gc):

On the branch foo after the rebase:

$ git reflog

This will show how your branch's HEAD has been moved. You should see some records like this (depending if you rebase interactively or not):

64d3c9e HEAD@{15}: rebase -i (finish): returning to refs/heads/master
64d3c9e HEAD@{16}: rebase -i (fixup): Fixes external dependencies
049169e HEAD@{17}: rebase -i (fixup): updating HEAD
d4c2e69 HEAD@{18}: rebase -i (pick): Fixes external dependencies
049169e HEAD@{19}: rebase -i (fixup): Imports futures
e490bed HEAD@{20}: rebase -i (fixup): updating HEAD
...
etc
...

Look forward to the last of commit of foo before the merge. Depending on what you have done this may be difficult or not. That's why I have not provided a do-it script.

Pick up the commit id that have the last commit for foo before the rebase and then compare to that commit id. Say that commit id is: XXXX:

 git diff XXXX...foo

Maybe that's what you want.

manu
  • 2,940
  • 4
  • 25
  • 47
  • 1
    As I responded to twalberg, this will not exclude the changes to master which I rebased onto, and is therefore very noisy. (By the way, digging through HEAD's reflog is not necessary; the *branch*'s reflog will contain only the before and after states, and `foo@{1}` is exactly the commit you propose manually searching for. `@` looks into the reflog, not ancestry.) – Kevin Reid Sep 24 '12 at 22:17
  • @KevinReid You're right, I confused `@` with `~`. I don't use them often. Sorry. – manu Sep 24 '12 at 23:23
1

A slightly different approach: Git has good support for showing this in the case of merges. i.e. "what changed relative to parent-1 which can't be explained by parent-2?"

There are a couple of ways you could leverage that support for your rebase:

Option 1:

  1. Do a throw-away merge first (instead of a rebase). Then the normal merge display will show what the merge changed. Check this is what you wanted.

  2. Go back and rebase, and compare the rebased tip against the merge result - they should be identical.

Option 2 (if rebasing is easier than merging):

  1. Do the rebase.
  2. Use a graft to make it look like a merge locally/temporarily.

To do that, create a .git/info/grafts with a single line:

TIP UPSTREAM OLDTIP

Where TIP is the commit ID of your rebased branch and the others are the two desired parents (i.e. the two things you would have merged, had you been doing a merge). Then examine it as if it were a real merge.

I'm not sure about automating it; I tend to do this stuff with gitk open, because it makes comparing the right revisions easy (using the right-click menu).

If you really want to compare two diffs then you might want to look at interdiff(1), but I'm not sure how well it will cope with the base files being different.

Thomas Leonard
  • 6,728
  • 2
  • 30
  • 37
  • Regarding option 1, in the cases where I want to make this check, a merge is highly likely to have *different conflicts* than a rebase, and in any case I am looking to verify the rebase that actually happened. Regarding option 2, I tried it and found that the view is cluttered with the changes introduced on master, which is exactly what I want to hide. I don't have `interdiff`, but its man page says “The diffs must both be relative to the same files.” – Kevin Reid Sep 26 '12 at 16:41
  • I have a similar answer for this that works for a series and is basically "just use commit-tree to generate the merge post-rebase by assuming the merge result should be whatever the tree of the finished rebase is This just lets us essentially do a merge from upstream into v1 and force the result to have the tree of v2. This then can be passed to git-show and it will produce the lovely combined format, without having to rely on grafts, and thus it can be automated. – Jake Oct 21 '16 at 18:49