diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt index e5733ccb2d1..aa2b5c11f20 100644 --- a/Documentation/diff-options.txt +++ b/Documentation/diff-options.txt @@ -700,6 +700,14 @@ matches a pattern if removing any number of the final pathname components matches the pattern. For example, the pattern "`foo*bar`" matches "`fooasdfbar`" and "`foo/bar/baz/asdf`" but not "`foobarx`". +--skip-to=:: +--rotate-to=:: + Discard the files before the named from the output + (i.e. 'skip to'), or move them to the end of the output + (i.e. 'rotate to'). These were invented primarily for use + of the `git difftool` command, and may not be very useful + otherwise. + ifndef::git-format-patch[] -R:: Swap two inputs; that is, show differences from index or diff --git a/Documentation/gitdiffcore.txt b/Documentation/gitdiffcore.txt index c970d9fe438..2bd1220477e 100644 --- a/Documentation/gitdiffcore.txt +++ b/Documentation/gitdiffcore.txt @@ -74,6 +74,7 @@ into another list. There are currently 5 such transformations: - diffcore-merge-broken - diffcore-pickaxe - diffcore-order +- diffcore-rotate These are applied in sequence. The set of filepairs 'git diff-{asterisk}' commands find are used as the input to diffcore-break, and @@ -276,6 +277,26 @@ Documentation t ------------------------------------------------ +diffcore-rotate: For Changing At Which Path Output Starts +--------------------------------------------------------- + +This transformation takes one pathname, and rotates the set of +filepairs so that the filepair for the given pathname comes first, +optionally discarding the paths that come before it. This is used +to implement the `--skip-to` and the `--rotate-to` options. It is +an error when the specified pathname is not in the set of filepairs, +but it is not useful to error out when used with "git log" family of +commands, because it is unreasonable to expect that a given path +would be modified by each and every commit shown by the "git log" +command. For this reason, when used with "git log", the filepair +that sorts the same as, or the first one that sorts after, the given +pathname is where the output starts. + +Use of this transformation combined with diffcore-order will produce +unexpected results, as the input to this transformation is likely +not sorted when diffcore-order is in effect. + + SEE ALSO -------- linkgit:git-diff[1], diff --git a/Makefile b/Makefile index 5a239cac20e..9b1bde2e0e6 100644 --- a/Makefile +++ b/Makefile @@ -863,6 +863,7 @@ LIB_OBJS += diffcore-delta.o LIB_OBJS += diffcore-order.o LIB_OBJS += diffcore-pickaxe.o LIB_OBJS += diffcore-rename.o +LIB_OBJS += diffcore-rotate.o LIB_OBJS += dir-iterator.o LIB_OBJS += dir.o LIB_OBJS += editor.o diff --git a/builtin/diff-files.c b/builtin/diff-files.c index bb852661020..70103c40952 100644 --- a/builtin/diff-files.c +++ b/builtin/diff-files.c @@ -54,6 +54,7 @@ int cmd_diff_files(int argc, const char **argv, const char *prefix) } if (!rev.diffopt.output_format) rev.diffopt.output_format = DIFF_FORMAT_RAW; + rev.diffopt.rotate_to_strict = 1; /* * Make sure there are NO revision (i.e. pending object) parameter, diff --git a/builtin/diff-index.c b/builtin/diff-index.c index c33d7af4785..176fe7ff2b4 100644 --- a/builtin/diff-index.c +++ b/builtin/diff-index.c @@ -41,6 +41,8 @@ int cmd_diff_index(int argc, const char **argv, const char *prefix) if (!rev.diffopt.output_format) rev.diffopt.output_format = DIFF_FORMAT_RAW; + rev.diffopt.rotate_to_strict = 1; + /* * Make sure there is one revision (i.e. pending object), * and there is no revision filtering parameters. diff --git a/builtin/diff-tree.c b/builtin/diff-tree.c index 178d12f07f9..f33d30d57bf 100644 --- a/builtin/diff-tree.c +++ b/builtin/diff-tree.c @@ -156,6 +156,8 @@ int cmd_diff_tree(int argc, const char **argv, const char *prefix) if (merge_base && opt->pending.nr != 2) die(_("--merge-base only works with two commits")); + opt->diffopt.rotate_to_strict = 1; + /* * NOTE! We expect "a..b" to expand to "^a b" but it is * 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) int saved_nrl = 0; int saved_dcctc = 0; + opt->diffopt.rotate_to_strict = 0; if (opt->diffopt.detect_rename) { if (!the_index.cache) repo_read_index(the_repository); diff --git a/builtin/diff.c b/builtin/diff.c index 0f4859abf75..617b9a4101d 100644 --- a/builtin/diff.c +++ b/builtin/diff.c @@ -491,6 +491,7 @@ int cmd_diff(int argc, const char **argv, const char *prefix) } rev.diffopt.flags.recursive = 1; + rev.diffopt.rotate_to_strict = 1; setup_diff_pager(&rev.diffopt); diff --git a/diff.c b/diff.c index bf2cbf15e77..6956f5e335c 100644 --- a/diff.c +++ b/diff.c @@ -5348,6 +5348,19 @@ static int diff_opt_word_diff_regex(const struct option *opt, return 0; } +static int diff_opt_rotate_to(const struct option *opt, const char *arg, int unset) +{ + struct diff_options *options = opt->value; + + BUG_ON_OPT_NEG(unset); + if (!strcmp(opt->long_name, "skip-to")) + options->skip_instead_of_rotate = 1; + else + options->skip_instead_of_rotate = 0; + options->rotate_to = arg; + return 0; +} + static void prep_parse_options(struct diff_options *options) { struct option parseopts[] = { @@ -5599,6 +5612,12 @@ static void prep_parse_options(struct diff_options *options) DIFF_PICKAXE_REGEX, PARSE_OPT_NONEG), OPT_FILENAME('O', NULL, &options->orderfile, N_("control the order in which files appear in the output")), + OPT_CALLBACK_F(0, "rotate-to", options, N_(""), + N_("show the change in the specified path first"), + PARSE_OPT_NONEG, diff_opt_rotate_to), + OPT_CALLBACK_F(0, "skip-to", options, N_(""), + N_("skip the output to the specified path"), + PARSE_OPT_NONEG, diff_opt_rotate_to), OPT_CALLBACK_F(0, "find-object", options, N_(""), N_("look for differences that change the number of occurrences of the specified object"), PARSE_OPT_NONEG, diff_opt_find_object), @@ -6693,6 +6712,8 @@ void diffcore_std(struct diff_options *options) diffcore_pickaxe(options); if (options->orderfile) diffcore_order(options->orderfile); + if (options->rotate_to) + diffcore_rotate(options); if (!options->found_follow) /* See try_to_follow_renames() in tree-diff.c */ diff_resolve_rename_copy(); diff --git a/diff.h b/diff.h index 527fb56d851..c8f3faea8aa 100644 --- a/diff.h +++ b/diff.h @@ -237,6 +237,27 @@ enum diff_submodule_format { struct diff_options { const char *orderfile; + /* + * "--rotate-to=" would start showing at and when + * the output reaches the end, wrap around by default. + * Setting skip_instead_of_rotate to true stops the output at the + * end, effectively discarding the earlier part of the output + * before 's diff (this is used to implement the + * "--skip-to=" option). + * + * When rotate_to_strict is set, it is an error if there is no + * in the diff. Otherwise, the output starts at the + * path that is the same as, or first path that sorts after, + * . Because it is unreasonable to require the exact + * match for "git log -p --rotate-to=" (i.e. not all + * commit would touch that single ), "git log" sets it + * to false. "git diff" sets it to true to detect an error + * in the command line option. + */ + const char *rotate_to; + int skip_instead_of_rotate; + int rotate_to_strict; + /** * A constant string (can and typically does contain newlines to look for * a block of text, not just a single line) to filter out the filepairs diff --git a/diffcore-rotate.c b/diffcore-rotate.c new file mode 100644 index 00000000000..445f060ab00 --- /dev/null +++ b/diffcore-rotate.c @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2021, Google LLC. + * Based on diffcore-order.c, which is Copyright (C) 2005, Junio C Hamano + */ +#include "cache.h" +#include "diff.h" +#include "diffcore.h" + +void diffcore_rotate(struct diff_options *opt) +{ + struct diff_queue_struct *q = &diff_queued_diff; + struct diff_queue_struct outq; + int rotate_to, i; + + if (!q->nr) + return; + + for (i = 0; i < q->nr; i++) { + int cmp = strcmp(opt->rotate_to, q->queue[i]->two->path); + if (!cmp) + break; /* exact match */ + if (!opt->rotate_to_strict && cmp < 0) + break; /* q->queue[i] is now past the target pathname */ + } + + if (q->nr <= i) { + /* we did not find the specified path */ + if (opt->rotate_to_strict) + die(_("No such path '%s' in the diff"), opt->rotate_to); + return; + } + + DIFF_QUEUE_CLEAR(&outq); + rotate_to = i; + + for (i = rotate_to; i < q->nr; i++) + diff_q(&outq, q->queue[i]); + for (i = 0; i < rotate_to; i++) { + if (opt->skip_instead_of_rotate) + diff_free_filepair(q->queue[i]); + else + diff_q(&outq, q->queue[i]); + } + free(q->queue); + *q = outq; +} diff --git a/diffcore.h b/diffcore.h index d2a63c5c71f..c1592bcd013 100644 --- a/diffcore.h +++ b/diffcore.h @@ -164,6 +164,7 @@ void diffcore_rename(struct diff_options *); void diffcore_merge_broken(void); void diffcore_pickaxe(struct diff_options *); void diffcore_order(const char *orderfile); +void diffcore_rotate(struct diff_options *); /* low-level interface to diffcore_order */ struct obj_order { diff --git a/t/t4056-diff-order.sh b/t/t4056-diff-order.sh index 63ea7144bb4..aec1d9d1b42 100755 --- a/t/t4056-diff-order.sh +++ b/t/t4056-diff-order.sh @@ -1,6 +1,6 @@ #!/bin/sh -test_description='diff order' +test_description='diff order & rotate' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME @@ -127,4 +127,74 @@ do ' done +### rotate and skip + +test_expect_success 'rotate and skip setup' ' + >sample1.t && + >sample2.t && + >sample3.t && + >sample4.t && + git add sample[1234].t && + git commit -m "added" sample[1234].t && + echo modified >>sample1.t && + echo modified >>sample2.t && + echo modified >>sample4.t && + git commit -m "updated" sample[1234].t +' + +test_expect_success 'diff --rotate-to' ' + git diff --rotate-to=sample2.t --name-only HEAD^ >actual && + test_write_lines sample2.t sample4.t sample1.t >expect && + test_cmp expect actual +' + +test_expect_success 'diff --skip-to' ' + git diff --skip-to=sample2.t --name-only HEAD^ >actual && + test_write_lines sample2.t sample4.t >expect && + test_cmp expect actual +' + +test_expect_success 'diff --rotate/skip-to error condition' ' + test_must_fail git diff --rotate-to=sample3.t HEAD^ && + test_must_fail git diff --skip-to=sample3.t HEAD^ +' + +test_expect_success 'log --rotate-to' ' + git log --rotate-to=sample3.t --raw HEAD~2.. >raw && + # just distill the commit header and paths + sed -n -e "s/^commit.*/commit/p" \ + -e "/^:/s/^.* //p" raw >actual && + + cat >expect <<-\EOF && + commit + sample4.t + sample1.t + sample2.t + commit + sample3.t + sample4.t + sample1.t + sample2.t + EOF + + test_cmp expect actual +' + +test_expect_success 'log --skip-to' ' + git log --skip-to=sample3.t --raw HEAD~2.. >raw && + # just distill the commit header and paths + sed -n -e "s/^commit.*/commit/p" \ + -e "/^:/s/^.* //p" raw >actual && + + cat >expect <<-\EOF && + commit + sample4.t + commit + sample3.t + sample4.t + EOF + + test_cmp expect actual +' + test_done