diff: --{rotate,skip}-to=<path>

In the implementation of "git difftool", there is a case where the user wants to start viewing the diffs at a specific path and continue on to the rest, optionally wrapping around to the beginning. Since it is somewhat cumbersome to implement such a feature as a post-processing step of "git diff" output, let's support it internally with two new options. - "git diff --rotate-to=C", when the resulting patch would show paths A B C D E without the option, would "rotate" the paths to shows patch to C D E A B instead. It is an error when there is no patch for C is shown. - "git diff --skip-to=C" would instead "skip" the paths before C, and shows patch to C D E. Again, it is an error when there is no patch for C is shown. - "git log [-p]" also accepts these two options, but it is not an error if there is no change to the specified path. Instead, the set of output paths are rotated or skipped to the specified path or the first path that sorts after the specified path. Signed-off-by: Junio C Hamano <gitster@pobox.com>

Junio C Hamano committed Feb 11, 2021 at 11:57 UTC 1eb4136ac2a24764257567b930535fcece01719f
12 files changed +197 -1
Documentation/diff-options.txt
+8
@@ -700,6 +700,14 @@ matches a pattern if removing any number of the final pathname
700 components matches the pattern. For example, the pattern "`foo*bar`"
701 matches "`fooasdfbar`" and "`foo/bar/baz/asdf`" but not "`foobarx`".
702
703 +--skip-to=<file>::
704 +--rotate-to=<file>::
705 + Discard the files before the named <file> from the output
706 + (i.e. 'skip to'), or move them to the end of the output
707 + (i.e. 'rotate to'). These were invented primarily for use
708 + of the `git difftool` command, and may not be very useful
709 + otherwise.
710 +
711 ifndef::git-format-patch[]
712 -R::
713 Swap two inputs; that is, show differences from index or
Documentation/gitdiffcore.txt
+21
@@ -74,6 +74,7 @@ into another list. There are currently 5 such transformations:
74 - diffcore-merge-broken
75 - diffcore-pickaxe
76 - diffcore-order
77 +- diffcore-rotate
78
79 These are applied in sequence. The set of filepairs 'git diff-{asterisk}'
80 commands find are used as the input to diffcore-break, and
@@ -276,6 +277,26 @@ Documentation
277 t
278 ------------------------------------------------
279
280 +diffcore-rotate: For Changing At Which Path Output Starts
281 +---------------------------------------------------------
282 +
283 +This transformation takes one pathname, and rotates the set of
284 +filepairs so that the filepair for the given pathname comes first,
285 +optionally discarding the paths that come before it. This is used
286 +to implement the `--skip-to` and the `--rotate-to` options. It is
287 +an error when the specified pathname is not in the set of filepairs,
288 +but it is not useful to error out when used with "git log" family of
289 +commands, because it is unreasonable to expect that a given path
290 +would be modified by each and every commit shown by the "git log"
291 +command. For this reason, when used with "git log", the filepair
292 +that sorts the same as, or the first one that sorts after, the given
293 +pathname is where the output starts.
294 +
295 +Use of this transformation combined with diffcore-order will produce
296 +unexpected results, as the input to this transformation is likely
297 +not sorted when diffcore-order is in effect.
298 +
299 +
300 SEE ALSO
301 --------
302 linkgit:git-diff[1],
Makefile
+1
@@ -863,6 +863,7 @@ LIB_OBJS += diffcore-delta.o
863 LIB_OBJS += diffcore-order.o
864 LIB_OBJS += diffcore-pickaxe.o
865 LIB_OBJS += diffcore-rename.o
866 +LIB_OBJS += diffcore-rotate.o
867 LIB_OBJS += dir-iterator.o
868 LIB_OBJS += dir.o
869 LIB_OBJS += editor.o
builtin/diff-files.c
+1
@@ -54,6 +54,7 @@ int cmd_diff_files(int argc, const char **argv, const char *prefix)
54 }
55 if (!rev.diffopt.output_format)
56 rev.diffopt.output_format = DIFF_FORMAT_RAW;
57 + rev.diffopt.rotate_to_strict = 1;
58
59 /*
60 * Make sure there are NO revision (i.e. pending object) parameter,
builtin/diff-index.c
+2
@@ -41,6 +41,8 @@ int cmd_diff_index(int argc, const char **argv, const char *prefix)
41 if (!rev.diffopt.output_format)
42 rev.diffopt.output_format = DIFF_FORMAT_RAW;
43
44 + rev.diffopt.rotate_to_strict = 1;
45 +
46 /*
47 * Make sure there is one revision (i.e. pending object),
48 * and there is no revision filtering parameters.
builtin/diff-tree.c
+3
@@ -156,6 +156,8 @@ int cmd_diff_tree(int argc, const char **argv, const char *prefix)
156 if (merge_base && opt->pending.nr != 2)
157 die(_("--merge-base only works with two commits"));
158
159 + opt->diffopt.rotate_to_strict = 1;
160 +
161 /*
162 * NOTE! We expect "a..b" to expand to "^a b" but it is
163 * perfectly valid for revision range parser to yield "b ^a",
@@ -192,6 +194,7 @@ int cmd_diff_tree(int argc, const char **argv, const char *prefix)
194 int saved_nrl = 0;
195 int saved_dcctc = 0;
196
197 + opt->diffopt.rotate_to_strict = 0;
198 if (opt->diffopt.detect_rename) {
199 if (!the_index.cache)
200 repo_read_index(the_repository);
builtin/diff.c
+1
@@ -491,6 +491,7 @@ int cmd_diff(int argc, const char **argv, const char *prefix)
491 }
492
493 rev.diffopt.flags.recursive = 1;
494 + rev.diffopt.rotate_to_strict = 1;
495
496 setup_diff_pager(&rev.diffopt);
497
diff.c
+21
@@ -5348,6 +5348,19 @@ static int diff_opt_word_diff_regex(const struct option *opt,
5348 return 0;
5349 }
5350
5351 +static int diff_opt_rotate_to(const struct option *opt, const char *arg, int unset)
5352 +{
5353 + struct diff_options *options = opt->value;
5354 +
5355 + BUG_ON_OPT_NEG(unset);
5356 + if (!strcmp(opt->long_name, "skip-to"))
5357 + options->skip_instead_of_rotate = 1;
5358 + else
5359 + options->skip_instead_of_rotate = 0;
5360 + options->rotate_to = arg;
5361 + return 0;
5362 +}
5363 +
5364 static void prep_parse_options(struct diff_options *options)
5365 {
5366 struct option parseopts[] = {
@@ -5599,6 +5612,12 @@ static void prep_parse_options(struct diff_options *options)
5612 DIFF_PICKAXE_REGEX, PARSE_OPT_NONEG),
5613 OPT_FILENAME('O', NULL, &options->orderfile,
5614 N_("control the order in which files appear in the output")),
5615 + OPT_CALLBACK_F(0, "rotate-to", options, N_("<path>"),
5616 + N_("show the change in the specified path first"),
5617 + PARSE_OPT_NONEG, diff_opt_rotate_to),
5618 + OPT_CALLBACK_F(0, "skip-to", options, N_("<path>"),
5619 + N_("skip the output to the specified path"),
5620 + PARSE_OPT_NONEG, diff_opt_rotate_to),
5621 OPT_CALLBACK_F(0, "find-object", options, N_("<object-id>"),
5622 N_("look for differences that change the number of occurrences of the specified object"),
5623 PARSE_OPT_NONEG, diff_opt_find_object),
@@ -6669,6 +6688,8 @@ void diffcore_std(struct diff_options *options)
6688 diffcore_pickaxe(options);
6689 if (options->orderfile)
6690 diffcore_order(options->orderfile);
6691 + if (options->rotate_to)
6692 + diffcore_rotate(options);
6693 if (!options->found_follow)
6694 /* See try_to_follow_renames() in tree-diff.c */
6695 diff_resolve_rename_copy();
diff.h
+21
@@ -227,6 +227,27 @@ enum diff_submodule_format {
227 struct diff_options {
228 const char *orderfile;
229
230 + /*
231 + * "--rotate-to=<file>" would start showing at <file> and when
232 + * the output reaches the end, wrap around by default.
233 + * Setting skip_instead_of_rotate to true stops the output at the
234 + * end, effectively discarding the earlier part of the output
235 + * before <file>'s diff (this is used to implement the
236 + * "--skip-to=<file>" option).
237 + *
238 + * When rotate_to_strict is set, it is an error if there is no
239 + * <file> in the diff. Otherwise, the output starts at the
240 + * path that is the same as, or first path that sorts after,
241 + * <file>. Because it is unreasonable to require the exact
242 + * match for "git log -p --rotate-to=<file>" (i.e. not all
243 + * commit would touch that single <file>), "git log" sets it
244 + * to false. "git diff" sets it to true to detect an error
245 + * in the command line option.
246 + */
247 + const char *rotate_to;
248 + int skip_instead_of_rotate;
249 + int rotate_to_strict;
250 +
251 /**
252 * A constant string (can and typically does contain newlines to look for
253 * a block of text, not just a single line) to filter out the filepairs
diffcore-rotate.c new
+46
@@ -0,0 +1,46 @@
1 +/*
2 + * Copyright (C) 2021, Google LLC.
3 + * Based on diffcore-order.c, which is Copyright (C) 2005, Junio C Hamano
4 + */
5 +#include "cache.h"
6 +#include "diff.h"
7 +#include "diffcore.h"
8 +
9 +void diffcore_rotate(struct diff_options *opt)
10 +{
11 + struct diff_queue_struct *q = &diff_queued_diff;
12 + struct diff_queue_struct outq;
13 + int rotate_to, i;
14 +
15 + if (!q->nr)
16 + return;
17 +
18 + for (i = 0; i < q->nr; i++) {
19 + int cmp = strcmp(opt->rotate_to, q->queue[i]->two->path);
20 + if (!cmp)
21 + break; /* exact match */
22 + if (!opt->rotate_to_strict && cmp < 0)
23 + break; /* q->queue[i] is now past the target pathname */
24 + }
25 +
26 + if (q->nr <= i) {
27 + /* we did not find the specified path */
28 + if (opt->rotate_to_strict)
29 + die(_("No such path '%s' in the diff"), opt->rotate_to);
30 + return;
31 + }
32 +
33 + DIFF_QUEUE_CLEAR(&outq);
34 + rotate_to = i;
35 +
36 + for (i = rotate_to; i < q->nr; i++)
37 + diff_q(&outq, q->queue[i]);
38 + for (i = 0; i < rotate_to; i++) {
39 + if (opt->skip_instead_of_rotate)
40 + diff_free_filepair(q->queue[i]);
41 + else
42 + diff_q(&outq, q->queue[i]);
43 + }
44 + free(q->queue);
45 + *q = outq;
46 +}
diffcore.h
+1
@@ -164,6 +164,7 @@ void diffcore_rename(struct diff_options *);
164 void diffcore_merge_broken(void);
165 void diffcore_pickaxe(struct diff_options *);
166 void diffcore_order(const char *orderfile);
167 +void diffcore_rotate(struct diff_options *);
168
169 /* low-level interface to diffcore_order */
170 struct obj_order {
t/t4056-diff-order.sh
+71 -1
@@ -1,6 +1,6 @@
1 #!/bin/sh
2
3 -test_description='diff order'
3 +test_description='diff order & rotate'
4
5 GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
6 export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
@@ -127,4 +127,74 @@ do
127 '
128 done
129
130 +### rotate and skip
131 +
132 +test_expect_success 'rotate and skip setup' '
133 + >sample1.t &&
134 + >sample2.t &&
135 + >sample3.t &&
136 + >sample4.t &&
137 + git add sample[1234].t &&
138 + git commit -m "added" sample[1234].t &&
139 + echo modified >>sample1.t &&
140 + echo modified >>sample2.t &&
141 + echo modified >>sample4.t &&
142 + git commit -m "updated" sample[1234].t
143 +'
144 +
145 +test_expect_success 'diff --rotate-to' '
146 + git diff --rotate-to=sample2.t --name-only HEAD^ >actual &&
147 + test_write_lines sample2.t sample4.t sample1.t >expect &&
148 + test_cmp expect actual
149 +'
150 +
151 +test_expect_success 'diff --skip-to' '
152 + git diff --skip-to=sample2.t --name-only HEAD^ >actual &&
153 + test_write_lines sample2.t sample4.t >expect &&
154 + test_cmp expect actual
155 +'
156 +
157 +test_expect_success 'diff --rotate/skip-to error condition' '
158 + test_must_fail git diff --rotate-to=sample3.t HEAD^ &&
159 + test_must_fail git diff --skip-to=sample3.t HEAD^
160 +'
161 +
162 +test_expect_success 'log --rotate-to' '
163 + git log --rotate-to=sample3.t --raw HEAD~2.. >raw &&
164 + # just distill the commit header and paths
165 + sed -n -e "s/^commit.*/commit/p" \
166 + -e "/^:/s/^.* //p" raw >actual &&
167 +
168 + cat >expect <<-\EOF &&
169 + commit
170 + sample4.t
171 + sample1.t
172 + sample2.t
173 + commit
174 + sample3.t
175 + sample4.t
176 + sample1.t
177 + sample2.t
178 + EOF
179 +
180 + test_cmp expect actual
181 +'
182 +
183 +test_expect_success 'log --skip-to' '
184 + git log --skip-to=sample3.t --raw HEAD~2.. >raw &&
185 + # just distill the commit header and paths
186 + sed -n -e "s/^commit.*/commit/p" \
187 + -e "/^:/s/^.* //p" raw >actual &&
188 +
189 + cat >expect <<-\EOF &&
190 + commit
191 + sample4.t
192 + commit
193 + sample3.t
194 + sample4.t
195 + EOF
196 +
197 + test_cmp expect actual
198 +'
199 +
200 test_done