Git fork

range-diff: optionally include merge commits' diffs in the analysis

The `git log` command already offers support for including diffs for
merges, via the `--diff-merges=<format>` option.

Let's add corresponding support for `git range-diff`, too. This makes it
more convenient to spot differences between commit ranges that contain
merges.

This is especially true in scenarios with non-trivial merges, i.e.
merges introducing changes other than, or in addition to, what merge ORT
would have produced. Merging a topic branch that changes a function
signature into a branch that added a caller of that function, for
example, would require the merge commit itself to adjust that caller to
the modified signature.

In my code reviews, I found the `--diff-merges=remerge` option
particularly useful.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Signed-off-by: Junio C Hamano <gitster@pobox.com>

authored by

Johannes Schindelin and committed by
Junio C Hamano
f8043236 eb8374c6

+50 -5
+12 -1
Documentation/git-range-diff.txt
··· 10 10 [verse] 11 11 'git range-diff' [--color=[<when>]] [--no-color] [<diff-options>] 12 12 [--no-dual-color] [--creation-factor=<factor>] 13 - [--left-only | --right-only] 13 + [--left-only | --right-only] [--diff-merges=<format>] 14 14 ( <range1> <range2> | <rev1>...<rev2> | <base> <rev1> <rev2> ) 15 15 [[--] <path>...] 16 16 ··· 80 80 --right-only:: 81 81 Suppress commits that are missing from the second specified range 82 82 (or the "right range" when using the `<rev1>...<rev2>` format). 83 + 84 + --diff-merges=<format>:: 85 + Instead of ignoring merge commits, generate diffs for them using the 86 + corresponding `--diff-merges=<format>` option of linkgit:git-log[1], 87 + and include them in the comparison. 88 + + 89 + Note: In the common case, the `remerge` mode will be the most natural one 90 + to use, as it shows only the diff on top of what Git's merge machinery would 91 + have produced. In other words, if a merge commit is the result of a 92 + non-conflicting `git merge`, the `remerge` mode will represent it with an empty 93 + diff. 83 94 84 95 --[no-]notes[=<ref>]:: 85 96 This flag is passed to the `git log` program
+10
builtin/range-diff.c
··· 21 21 { 22 22 struct diff_options diffopt = { NULL }; 23 23 struct strvec other_arg = STRVEC_INIT; 24 + struct strvec diff_merges_arg = STRVEC_INIT; 24 25 struct range_diff_options range_diff_opts = { 25 26 .creation_factor = RANGE_DIFF_CREATION_FACTOR_DEFAULT, 26 27 .diffopt = &diffopt, ··· 36 37 OPT_PASSTHRU_ARGV(0, "notes", &other_arg, 37 38 N_("notes"), N_("passed to 'git log'"), 38 39 PARSE_OPT_OPTARG), 40 + OPT_PASSTHRU_ARGV(0, "diff-merges", &diff_merges_arg, 41 + N_("style"), N_("passed to 'git log'"), 0), 39 42 OPT_BOOL(0, "left-only", &left_only, 40 43 N_("only emit output related to the first range")), 41 44 OPT_BOOL(0, "right-only", &right_only, ··· 61 64 /* force color when --dual-color was used */ 62 65 if (!simple_color) 63 66 diffopt.use_color = 1; 67 + 68 + /* If `--diff-merges` was specified, imply `--merges` */ 69 + if (diff_merges_arg.nr) { 70 + range_diff_opts.include_merges = 1; 71 + strvec_pushv(&other_arg, diff_merges_arg.v); 72 + } 64 73 65 74 for (i = 0; i < argc; i++) 66 75 if (!strcmp(argv[i], "--")) { ··· 155 164 res = show_range_diff(range1.buf, range2.buf, &range_diff_opts); 156 165 157 166 strvec_clear(&other_arg); 167 + strvec_clear(&diff_merges_arg); 158 168 strbuf_release(&range1); 159 169 strbuf_release(&range2); 160 170
+11 -4
range-diff.c
··· 38 38 * as struct object_id (will need to be free()d). 39 39 */ 40 40 static int read_patches(const char *range, struct string_list *list, 41 - const struct strvec *other_arg) 41 + const struct strvec *other_arg, 42 + unsigned int include_merges) 42 43 { 43 44 struct child_process cp = CHILD_PROCESS_INIT; 44 45 struct strbuf buf = STRBUF_INIT, contents = STRBUF_INIT; ··· 49 50 size_t size; 50 51 int ret = -1; 51 52 52 - strvec_pushl(&cp.args, "log", "--no-color", "-p", "--no-merges", 53 + strvec_pushl(&cp.args, "log", "--no-color", "-p", 53 54 "--reverse", "--date-order", "--decorate=no", 54 55 "--no-prefix", "--submodule=short", 55 56 /* ··· 64 65 "--pretty=medium", 65 66 "--show-notes-by-default", 66 67 NULL); 68 + if (!include_merges) 69 + strvec_push(&cp.args, "--no-merges"); 67 70 strvec_push(&cp.args, range); 68 71 if (other_arg) 69 72 strvec_pushv(&cp.args, other_arg->v); ··· 96 99 } 97 100 98 101 if (skip_prefix(line, "commit ", &p)) { 102 + char *q; 99 103 if (util) { 100 104 string_list_append(list, buf.buf)->util = util; 101 105 strbuf_reset(&buf); 102 106 } 103 107 CALLOC_ARRAY(util, 1); 108 + if (include_merges && (q = strstr(p, " (from "))) 109 + *q = '\0'; 104 110 if (repo_get_oid(the_repository, p, &util->oid)) { 105 111 error(_("could not parse commit '%s'"), p); 106 112 FREE_AND_NULL(util); ··· 571 577 572 578 struct string_list branch1 = STRING_LIST_INIT_DUP; 573 579 struct string_list branch2 = STRING_LIST_INIT_DUP; 580 + unsigned int include_merges = range_diff_opts->include_merges; 574 581 575 582 if (range_diff_opts->left_only && range_diff_opts->right_only) 576 583 res = error(_("options '%s' and '%s' cannot be used together"), "--left-only", "--right-only"); 577 584 578 - if (!res && read_patches(range1, &branch1, range_diff_opts->other_arg)) 585 + if (!res && read_patches(range1, &branch1, range_diff_opts->other_arg, include_merges)) 579 586 res = error(_("could not parse log for '%s'"), range1); 580 - if (!res && read_patches(range2, &branch2, range_diff_opts->other_arg)) 587 + if (!res && read_patches(range2, &branch2, range_diff_opts->other_arg, include_merges)) 581 588 res = error(_("could not parse log for '%s'"), range2); 582 589 583 590 if (!res) {
+1
range-diff.h
··· 16 16 int creation_factor; 17 17 unsigned dual_color:1; 18 18 unsigned left_only:1, right_only:1; 19 + unsigned include_merges:1; 19 20 const struct diff_options *diffopt; /* may be NULL */ 20 21 const struct strvec *other_arg; /* may be NULL */ 21 22 };
+16
t/t3206-range-diff.sh
··· 909 909 test_cmp expect actual 910 910 ' 911 911 912 + test_expect_success '--diff-merges' ' 913 + renamed_oid=$(git rev-parse --short renamed-file) && 914 + tree=$(git merge-tree unmodified renamed-file) && 915 + clean=$(git commit-tree -m merge -p unmodified -p renamed-file $tree) && 916 + clean_oid=$(git rev-parse --short $clean) && 917 + conflict=$(git commit-tree -m merge -p unmodified -p renamed-file^ $tree) && 918 + conflict_oid=$(git rev-parse --short $conflict) && 919 + 920 + git range-diff --diff-merges=1 $clean...$conflict >actual && 921 + cat >expect <<-EOF && 922 + 1: $renamed_oid < -: ------- s/12/B/ 923 + 2: $clean_oid = 1: $conflict_oid merge 924 + EOF 925 + test_cmp expect actual 926 + ' 927 + 912 928 test_done