range-diff: optionally accept pathspecs

The `git range-diff` command can be quite expensive, which is not a surprise given that the underlying algorithm to match up pairs of commits between the provided two commit ranges has a cubic runtime. Therefore it makes sense to restrict the commit ranges as much as possible, to reduce the amount of input to that O(N^3) algorithm. In chatty repositories with wide trees, this is not necessarily possible merely by choosing commit ranges wisely. Let's give users another option to restrict the commit ranges: by providing a pathspec. That helps in repositories with wide trees because it is likely that the user has a good idea which subset of the tree they are actually interested in. Example: git range-diff upstream/main upstream/seen HEAD -- range-diff.c This shows commits that are either in the local branch or in `seen`, but not in `main`, skipping all commits that do not touch `range-diff.c`. Note: Since we piggy-back the pathspecs onto the `other_arg` mechanism that was introduced to be able to pass through the `--notes` option to the revision machinery, we must now ensure that the `other_arg` array is appended at the end (the revision range must come before the pathspecs, if any). Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Johannes Schindelin committed Aug 26, 2022 at 09:39 UTC b75747829f4c277323c78b1c5973ad63ea038a2d
4 files changed +68 -17
Documentation/git-range-diff.txt
+4
@@ -12,6 +12,7 @@ SYNOPSIS
12 [--no-dual-color] [--creation-factor=<factor>]
13 [--left-only | --right-only]
14 ( <range1> <range2> | <rev1>...<rev2> | <base> <rev1> <rev2> )
15 + [[--] <path>...]
16
17 DESCRIPTION
18 -----------
@@ -19,6 +20,9 @@ DESCRIPTION
20 This command shows the differences between two versions of a patch
21 series, or more generally, two commit ranges (ignoring merge commits).
22
23 +In the presence of `<path>` arguments, these commit ranges are limited
24 +accordingly.
25 +
26 To that end, it first finds pairs of commits from both commit ranges
27 that correspond with each other. Two commits are said to correspond when
28 the diff between their patches (i.e. the author information, the commit
builtin/range-diff.c
+51 -15
@@ -38,9 +38,10 @@ int cmd_range_diff(int argc, const char **argv, const char *prefix)
38 OPT_END()
39 };
40 struct option *options;
41 - int res = 0;
41 + int i, dash_dash = -1, res = 0;
42 struct strbuf range1 = STRBUF_INIT, range2 = STRBUF_INIT;
43 struct object_id oid;
44 + const char *three_dots = NULL;
45
46 git_config(git_diff_ui_config, NULL);
47
@@ -48,7 +49,7 @@ int cmd_range_diff(int argc, const char **argv, const char *prefix)
49
50 options = parse_options_concat(range_diff_options, diffopt.parseopts);
51 argc = parse_options(argc, argv, prefix, options,
51 - builtin_range_diff_usage, 0);
52 + builtin_range_diff_usage, PARSE_OPT_KEEP_DASHDASH);
53
54 diff_setup_done(&diffopt);
55
@@ -56,8 +57,20 @@ int cmd_range_diff(int argc, const char **argv, const char *prefix)
57 if (!simple_color)
58 diffopt.use_color = 1;
59
59 - if (argc == 3) {
60 - if (get_oid_committish(argv[0], &oid))
60 + for (i = 0; i < argc; i++)
61 + if (!strcmp(argv[i], "--")) {
62 + dash_dash = i;
63 + break;
64 + }
65 +
66 + if (dash_dash == 3 ||
67 + (dash_dash < 0 && argc > 2 &&
68 + !get_oid_committish(argv[0], &oid) &&
69 + !get_oid_committish(argv[1], &oid) &&
70 + !get_oid_committish(argv[2], &oid))) {
71 + if (dash_dash < 0)
72 + ; /* already validated arguments */
73 + else if (get_oid_committish(argv[0], &oid))
74 usage_msg_optf(_("not a revision: '%s'"),
75 builtin_range_diff_usage, options,
76 argv[0]);
@@ -72,8 +85,16 @@ int cmd_range_diff(int argc, const char **argv, const char *prefix)
85
86 strbuf_addf(&range1, "%s..%s", argv[0], argv[1]);
87 strbuf_addf(&range2, "%s..%s", argv[0], argv[2]);
75 - } else if (argc == 2) {
76 - if (!is_range_diff_range(argv[0]))
88 +
89 + strvec_pushv(&other_arg, argv +
90 + (dash_dash < 0 ? 3 : dash_dash));
91 + } else if (dash_dash == 2 ||
92 + (dash_dash < 0 && argc > 1 &&
93 + is_range_diff_range(argv[0]) &&
94 + is_range_diff_range(argv[1]))) {
95 + if (dash_dash < 0)
96 + ; /* already validated arguments */
97 + else if (!is_range_diff_range(argv[0]))
98 usage_msg_optf(_("not a commit range: '%s'"),
99 builtin_range_diff_usage, options,
100 argv[0]);
@@ -84,25 +105,40 @@ int cmd_range_diff(int argc, const char **argv, const char *prefix)
105
106 strbuf_addstr(&range1, argv[0]);
107 strbuf_addstr(&range2, argv[1]);
87 - } else if (argc == 1) {
88 - const char *b = strstr(argv[0], "..."), *a = argv[0];
108 +
109 + strvec_pushv(&other_arg, argv +
110 + (dash_dash < 0 ? 2 : dash_dash));
111 + } else if (dash_dash == 1 ||
112 + (dash_dash < 0 && argc > 0 &&
113 + (three_dots = strstr(argv[0], "...")))) {
114 + const char *a, *b;
115 int a_len;
116
91 - if (!b)
117 + if (dash_dash < 0)
118 + ; /* already validated arguments */
119 + else if (!(three_dots = strstr(argv[0], "...")))
120 usage_msg_optf(_("not a symmetric range: '%s'"),
93 - builtin_range_diff_usage, options,
94 - argv[0]);
121 + builtin_range_diff_usage, options,
122 + argv[0]);
123
96 - a_len = (int)(b - a);
97 - if (!a_len) {
124 + if (three_dots == argv[0]) {
125 a = "HEAD";
126 a_len = strlen(a);
127 + } else {
128 + a = argv[0];
129 + a_len = (int)(three_dots - a);
130 }
101 - b += 3;
102 - if (!*b)
131 +
132 + if (three_dots[3])
133 + b = three_dots + 3;
134 + else
135 b = "HEAD";
136 +
137 strbuf_addf(&range1, "%s..%.*s", b, a_len, a);
138 strbuf_addf(&range2, "%.*s..%s", a_len, a, b);
139 +
140 + strvec_pushv(&other_arg, argv +
141 + (dash_dash < 0 ? 1 : dash_dash));
142 } else
143 usage_msg_opt(_("need two commit ranges"),
144 builtin_range_diff_usage, options);
range-diff.c
+1 -1
@@ -57,9 +57,9 @@ static int read_patches(const char *range, struct string_list *list,
57 "--pretty=medium",
58 "--notes",
59 NULL);
60 + strvec_push(&cp.args, range);
61 if (other_arg)
62 strvec_pushv(&cp.args, other_arg->v);
62 - strvec_push(&cp.args, range);
63 cp.out = -1;
64 cp.no_stdin = 1;
65 cp.git_cmd = 1;
t/t3206-range-diff.sh
+12 -1
@@ -162,7 +162,7 @@ test_expect_success 'A^! and A^-<n> (unmodified)' '
162 '
163
164 test_expect_success 'A^{/..} is not mistaken for a range' '
165 - test_must_fail git range-diff topic^.. topic^{/..} 2>error &&
165 + test_must_fail git range-diff topic^.. topic^{/..} -- 2>error &&
166 test_i18ngrep "not a commit range" error
167 '
168
@@ -772,6 +772,17 @@ test_expect_success '--left-only/--right-only' '
772 test_cmp expect actual
773 '
774
775 +test_expect_success 'ranges with pathspecs' '
776 + git range-diff topic...mode-only-change -- other-file >actual &&
777 + test_line_count = 2 actual &&
778 + topic_oid=$(git rev-parse --short topic) &&
779 + mode_change_oid=$(git rev-parse --short mode-only-change^) &&
780 + file_change_oid=$(git rev-parse --short mode-only-change) &&
781 + grep "$mode_change_oid" actual &&
782 + ! grep "$file_change_oid" actual &&
783 + ! grep "$topic_oid" actual
784 +'
785 +
786 test_expect_success 'submodule changes are shown irrespective of diff.submodule' '
787 git init sub-repo &&
788 test_commit -C sub-repo sub-first &&