git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
From: Johannes Schindelin <Johannes.Schindelin@gmx.de>
To: Junio C Hamano <gitster@pobox.com>
Cc: git@vger.kernel.org, ZheNing Hu <adlternative@gmail.com>
Subject: Re: [PATCH] diff: --{rotate,skip}-to=<path>
Date: Fri, 12 Feb 2021 17:15:09 +0100 (CET)	[thread overview]
Message-ID: <nycvar.QRO.7.76.6.2102121713390.29765@tvgsbejvaqbjf.bet> (raw)
In-Reply-To: <xmqqo8gqwasu.fsf@gitster.c.googlers.com>

Hi,

On Thu, 11 Feb 2021, Junio C Hamano wrote:

> 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.

This is more a drive-by comment, as I have no stake in this, but don't we
already have a more generic solution via `-O<orderfile>`? See
https://git-scm.com/docs/git-diff#Documentation/git-diff.txt--Oltorderfilegt
for details.

Thanks,
Dscho

>
> Signed-off-by: Junio C Hamano <gitster@pobox.com>
> ---
>
>  * Please do not take the presense of --skip-to and --rotate-to as
>    encouraging "git difftool" to gain the same pair of options.  I
>    think "--skip-to" would suffice for the originally presented use
>    case (i.e. eeek, my difftool session got interrupted when I was
>    looking at path X, and I only have just a few more paths to go,
>    X, Y and Z) and if that is the case, there is no need to expose
>    "--rotate-to" to the end users.  Of course, if it is easy to quit
>    the "difftool" session in the middle, then giving the users only
>    "--rotate-to" without exposing "--skip-to" is also OK.
>
>    I decided to diagnose a pathname that is not in the diff-queue as
>    an error when running the "diff" family, while allowing the
>    command to continue when running the "log" family, so it may turn
>    out to be that "git difftool --skip-to" do not have to implement
>    its own error handling after all.
>
>    It has been quite a long time for me to write any real patch with
>    a non-trivial amount of code change myself, so I may be getting
>    rusty, but it is refreshing to write code every so often ;-)
>
>  Documentation/diff-options.txt |  8 ++++
>  Documentation/gitdiffcore.txt  | 21 ++++++++++
>  Makefile                       |  1 +
>  builtin/diff-files.c           |  1 +
>  builtin/diff-index.c           |  2 +
>  builtin/diff-tree.c            |  3 ++
>  builtin/diff.c                 |  1 +
>  diff.c                         | 21 ++++++++++
>  diff.h                         | 21 ++++++++++
>  diffcore-rotate.c              | 46 ++++++++++++++++++++++
>  diffcore.h                     |  1 +
>  t/t4056-diff-order.sh          | 72 +++++++++++++++++++++++++++++++++-
>  12 files changed, 197 insertions(+), 1 deletion(-)
>  create mode 100644 diffcore-rotate.c
>
> diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt
> index e5733ccb2d..7c5b3cf42b 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=<file>::
> +--rotate-to=<file::
> +	Discard the files before the named <file> 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 c970d9fe43..2bd1220477 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 5a239cac20..9b1bde2e0e 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 4742a4559b..e037efb07e 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 7f5281c461..06635e8fb2 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 9fc95e959f..b6a9a9328e 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 5cfe1717e8..f1b88c7389 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 69e3bc00ed..71e4738548 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_("<path>"),
> +			       N_("show the change in the specified path first"),
> +			       PARSE_OPT_NONEG, diff_opt_rotate_to),
> +		OPT_CALLBACK_F(0, "skip-to", options, N_("<path>"),
> +			       N_("skip the output to the specified path"),
> +			       PARSE_OPT_NONEG, diff_opt_rotate_to),
>  		OPT_CALLBACK_F(0, "find-object", options, N_("<object-id>"),
>  			       N_("look for differences that change the number of occurrences of the specified object"),
>  			       PARSE_OPT_NONEG, diff_opt_find_object),
> @@ -6669,6 +6688,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 2ff2b1c7f2..45300e3597 100644
> --- a/diff.h
> +++ b/diff.h
> @@ -227,6 +227,27 @@ enum diff_submodule_format {
>  struct diff_options {
>  	const char *orderfile;
>
> +	/*
> +	 * "--rotate-to=<file>" would start showing at <file> 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 <file>'s diff (this is used to implement the
> +	 * "--skip-to=<file>" option).
> +	 *
> +	 * When rotate_to_strict is set, it is an error if there is no
> +	 * <file> in the diff.  Otherwise, the output starts at the
> +	 * path that is the same as, or first path that sorts after,
> +	 * <file>.  Because it is unreasonable to require the exact
> +	 * match for "git log -p --rotate-to=<file>" (i.e. not all
> +	 * commit would touch that single <file>), "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 0000000000..445f060ab0
> --- /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 d2a63c5c71..c1592bcd01 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 63ea7144bb..aec1d9d1b4 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
> --
> 2.30.1-633-gb004a83696
>
>

  reply	other threads:[~2021-02-12 16:18 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-02-11 21:32 [PATCH] diff: --{rotate,skip}-to=<path> Junio C Hamano
2021-02-12 16:15 ` Johannes Schindelin [this message]
2021-02-12 17:54   ` Junio C Hamano

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: http://vger.kernel.org/majordomo-info.html

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=nycvar.QRO.7.76.6.2102121713390.29765@tvgsbejvaqbjf.bet \
    --to=johannes.schindelin@gmx.de \
    --cc=adlternative@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=gitster@pobox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).