diff mbox series

[4/4] cherry-pick: Add `--empty` for more robust redundant commit handling

Message ID 20240119060721.3734775-5-brianmlyles@gmail.com (mailing list archive)
State New, archived
Headers show
Series [1/4] sequencer: Do not require `allow_empty` for redundant commit options | expand

Commit Message

Brian Lyles Jan. 19, 2024, 5:59 a.m. UTC
From: Brian Lyles <brianmlyles@gmail.com>

As with `git-rebase` and `git-am`, `git-cherry-pick` can result in a
commit being made redundant if the content from the picked commit is
already present in the target history. However, `git-cherry-pick` does
not have the same options available that `git-rebase` and `git-am` have.

There are three things that can be done with these redundant commits:
drop them, keep them, or have the cherry-pick stop and wait for the user
to take an action. `git-rebase` has the `--empty` option added in commit
e98c4269c8 (rebase (interactive-backend): fix handling of commits that
become empty, 2020-02-15), which handles all three of these scenarios.
Similarly, `git-am` got its own `--empty` in 7c096b8d61 (am: support
--empty=<option> to handle empty patches, 2021-12-09).

`git-cherry-pick`, on the other hand, only supports two of the three
possiblities: Keep the redundant commits via `--keep-redundant-commits`,
or have the cherry-pick fail by not specifying that option. There is no
way to automatically drop redundant commits.

In order to bring `git-cherry-pick` more in-line with `git-rebase` and
`git-am`, this commit adds an `--empty` option to `git-cherry-pick`. It
has the same three options (keep, drop, and stop), and largely behaves
the same. The notable difference is that for `git-cherry-pick`, the
default will be `stop`, which maintains the current behavior when the
option is not specified.

The `--keep-redundant-commits` option will be documented as a deprecated
synonym of `--empty=keep`, and will be supported for backwards
compatibility for the time being.

Signed-off-by: Brian Lyles <brianmlyles@gmail.com>
---
 Documentation/git-cherry-pick.txt | 28 ++++++++++++++++++-------
 builtin/revert.c                  | 35 ++++++++++++++++++++++++++++++-
 sequencer.c                       |  6 ++++++
 t/t3505-cherry-pick-empty.sh      | 26 ++++++++++++++++++++++-
 4 files changed, 86 insertions(+), 9 deletions(-)

Comments

Kristoffer Haugsbakk Jan. 20, 2024, 8:24 p.m. UTC | #1
Hi

On Fri, Jan 19, 2024, at 06:59, brianmlyles@gmail.com wrote:
> From: Brian Lyles <brianmlyles@gmail.com>
> ---keep-redundant-commits::
> -	If a commit being cherry picked duplicates a commit already in the
> -	current history, it will become empty.  By default these
> -	redundant commits cause `cherry-pick` to stop so the user can
> -	examine the commit. This option overrides that behavior and
> -	creates an empty commit object. Note that use of this option only
> +--empty=(stop|drop|keep)::
> +	How to handle commits being cherry-picked that are redundant with
> +	changes already in the current history.
> ++
> +-- 

Trailing whitespace on this line.
Brian Lyles Jan. 21, 2024, 6:28 p.m. UTC | #2
On Sat, Jan 20, 2024 at 2:24 PM Kristoffer Haugsbakk
<code@khaugsbakk.name> wrote:

> On Fri, Jan 19, 2024, at 06:59, brianmlyles@gmail.com wrote:
> > From: Brian Lyles <brianmlyles@gmail.com>
> > ---keep-redundant-commits::
> > -     If a commit being cherry picked duplicates a commit already in the
> > -     current history, it will become empty.  By default these
> > -     redundant commits cause `cherry-pick` to stop so the user can
> > -     examine the commit. This option overrides that behavior and
> > -     creates an empty commit object. Note that use of this option only
> > +--empty=(stop|drop|keep)::
> > +     How to handle commits being cherry-picked that are redundant with
> > +     changes already in the current history.
> > ++
> > +--
>
> Trailing whitespace on this line.

Thank you -- This will be corrected with v2.

Is the sample pre-commit hook the ideal way to prevent this in the
future? Or is there some config I could set globally to enforce this
across repositories? I was having a little trouble finding a good way to
accomplish this globally.

Thanks,
Brian Lyles
Kristoffer Haugsbakk Jan. 21, 2024, 10:05 p.m. UTC | #3
On Sun, Jan 21, 2024, at 19:28, Brian Lyles wrote:
> Is the sample pre-commit hook the ideal way to prevent this in the
> future? Or is there some config I could set globally to enforce this
> across repositories? I was having a little trouble finding a good way to
> accomplish this globally.

I don’t know of any global config. So a pre-commit hook is probably the
safest bet. Personally I set all my editors to remove trailing space and
they very seldom mess it up. :)
Junio C Hamano Jan. 21, 2024, 10:41 p.m. UTC | #4
Brian Lyles <brianmlyles@gmail.com> writes:

>> Trailing whitespace on this line.
>
> Thank you -- This will be corrected with v2.
>
> Is the sample pre-commit hook the ideal way to prevent this in the
> future?

The example we ship in templates/hooks--pre-commit.sample in our
source has such a check at the end of it.
Phillip Wood Jan. 22, 2024, 10:40 a.m. UTC | #5
Hi Brian

On 21/01/2024 18:28, Brian Lyles wrote:
> Is the sample pre-commit hook the ideal way to prevent this in the
> future? Or is there some config I could set globally to enforce this
> across repositories? I was having a little trouble finding a good way to
> accomplish this globally.

If you want to run the same hooks in all your repositories then you can 
run 'git config --global core.hooksPath <my-hooks-path>' and git will 
look for hooks in 'my-hooks-path' instead of '.git/hooks'. It makes it 
tricky to run different linters in different projects though.

I'll try and take a proper look at these patches in the next couple of days.

Best Wishes

Phillip
Kristoffer Haugsbakk Jan. 22, 2024, 8:55 p.m. UTC | #6
On Sun, Jan 21, 2024, at 19:28, Brian Lyles wrote:
> Thank you -- This will be corrected with v2.
>
> Is the sample pre-commit hook the ideal way to prevent this in the
> future? Or is there some config I could set globally to enforce this
> across repositories? I was having a little trouble finding a good way to
> accomplish this globally.
>
> Thanks,
> Brian Lyles

Oh, and this thread reminded me https://lore.kernel.org/git/xmqqle8hrtcs.fsf@gitster.g/T/#t

that editorconfig[1] has this option:

```
trim_trailing_whitespace = true
```

So I guess that should be enough for all editors that respect this
config (although I haven’t tested it).


Brian Lyles Jan. 23, 2024, 5:23 a.m. UTC | #7
On Sun, Jan 21, 2024 at 4:05 PM Kristoffer Haugsbakk
<code@khaugsbakk.name> wrote:

> I don’t know of any global config. So a pre-commit hook is probably the
> safest bet. Personally I set all my editors to remove trailing space and
> they very seldom mess it up. :)

Fair point -- Apparently this was a gap in my nvim config.
Easily added.

On Mon, Jan 22, 2024 at 2:55 PM Kristoffer Haugsbakk
<code@khaugsbakk.name> wrote:

> Oh, and this thread reminded me https://lore.kernel.org/git/xmqqle8hrtcs.fsf@gitster.g/T/#t
>
> that editorconfig[1] has this option:
>
> ```
> trim_trailing_whitespace = true
> ```
>
> So I guess that should be enough for all editors that respect this
> config (although I haven’t tested it).
>
> 
Kristoffer Haugsbakk Jan. 23, 2024, 7:11 a.m. UTC | #8
On Tue, Jan 23, 2024, at 06:23, Brian Lyles wrote:
>> Oh, and this thread reminded me https://lore.kernel.org/git/xmqqle8hrtcs.fsf@gitster.g/T/#t
>>
>> that editorconfig[1] has this option:
>>
>> ```
>> trim_trailing_whitespace = true
>> ```
>> […]
>
> Is there a good reason that this should not just be added to the
> `.editorconfig` in this repository? Would a patch for this be welcome?

I was thinking the same thing when I saw that other thread. It doesn’t
hurt to try.

I was also curious about some subtleties like: does it dictate enforcing
this for all the lines of a touched files or only the modified ones?
Because the latter is much more useful.
Phillip Wood Jan. 23, 2024, 2:25 p.m. UTC | #9
Hi Brian


On 19/01/2024 05:59, brianmlyles@gmail.com wrote:
> From: Brian Lyles <brianmlyles@gmail.com>
> 
> As with `git-rebase` and `git-am`, `git-cherry-pick` can result in a
> commit being made redundant if the content from the picked commit is
> already present in the target history. However, `git-cherry-pick` does
> not have the same options available that `git-rebase` and `git-am` have.
> 
> There are three things that can be done with these redundant commits:
> drop them, keep them, or have the cherry-pick stop and wait for the user
> to take an action. `git-rebase` has the `--empty` option added in commit
> e98c4269c8 (rebase (interactive-backend): fix handling of commits that
> become empty, 2020-02-15), which handles all three of these scenarios.
> Similarly, `git-am` got its own `--empty` in 7c096b8d61 (am: support
> --empty=<option> to handle empty patches, 2021-12-09).
> 
> `git-cherry-pick`, on the other hand, only supports two of the three
> possiblities: Keep the redundant commits via `--keep-redundant-commits`,
> or have the cherry-pick fail by not specifying that option. There is no
> way to automatically drop redundant commits.
> 
> In order to bring `git-cherry-pick` more in-line with `git-rebase` and
> `git-am`, this commit adds an `--empty` option to `git-cherry-pick`. It
> has the same three options (keep, drop, and stop), and largely behaves
> the same. The notable difference is that for `git-cherry-pick`, the
> default will be `stop`, which maintains the current behavior when the
> option is not specified.

Thanks for the well explained commit message

> The `--keep-redundant-commits` option will be documented as a deprecated
> synonym of `--empty=keep`, and will be supported for backwards
> compatibility for the time being.

I'm not sure if we need to deprecate it as in "it will be removed in the 
future" or just reduce it prominence in favor of --empty

> Signed-off-by: Brian Lyles <brianmlyles@gmail.com>
> ---
>   Documentation/git-cherry-pick.txt | 28 ++++++++++++++++++-------
>   builtin/revert.c                  | 35 ++++++++++++++++++++++++++++++-
>   sequencer.c                       |  6 ++++++
>   t/t3505-cherry-pick-empty.sh      | 26 ++++++++++++++++++++++-
>   4 files changed, 86 insertions(+), 9 deletions(-)
> 
> diff --git a/Documentation/git-cherry-pick.txt b/Documentation/git-cherry-pick.txt
> index 806295a730..8c20a10d4b 100644
> --- a/Documentation/git-cherry-pick.txt
> +++ b/Documentation/git-cherry-pick.txt
> @@ -132,23 +132,37 @@ effect to your index in a row.
>   	keeps commits that were initially empty (i.e. the commit recorded the
>   	same tree as its parent).  Commits which are made empty due to a
>   	previous commit will cause the cherry-pick to fail.  To force the
> -	inclusion of those commits use `--keep-redundant-commits`.
> +	inclusion of those commits use `--empty=keep`.
>   
>   --allow-empty-message::
>   	By default, cherry-picking a commit with an empty message will fail.
>   	This option overrides that behavior, allowing commits with empty
>   	messages to be cherry picked.
>   
> ---keep-redundant-commits::
> -	If a commit being cherry picked duplicates a commit already in the
> -	current history, it will become empty.  By default these
> -	redundant commits cause `cherry-pick` to stop so the user can
> -	examine the commit. This option overrides that behavior and
> -	creates an empty commit object. Note that use of this option only
> +--empty=(stop|drop|keep)::
> +	How to handle commits being cherry-picked that are redundant with
> +	changes already in the current history.
> ++
> +--
> +`stop`;;

I'm still on the fence about "stop" vs "ask". I see in your tests you've 
accidentally used "ask" which makes me wonder if that is the more 
familiar term for users who probably use "git rebase" more often than 
"git am".

> +	The cherry-pick will stop when the empty commit is applied, allowing
> +	you to examine the commit. This is the default behavior.
> +`drop`;;
> +	The empty commit will be dropped.
> +`keep`;;
> +	The empty commit will be kept. Note that use of this option only
>   	results in an empty commit when the commit was not initially empty,
>   	but rather became empty due to a previous commit. Commits that were
>   	initially empty will cause the cherry-pick to fail. To force the
>   	inclusion of those commits use `--allow-empty`.
> +--
> ++
> +Note that commits which start empty will cause the cherry-pick to fail (unless
> +`--allow-empty` is specified).
> ++
> +
> +--keep-redundant-commits::
> +	Deprecated synonym for `--empty=keep`.
>   
>   --strategy=<strategy>::
>   	Use the given merge strategy.  Should only be used once.
> diff --git a/builtin/revert.c b/builtin/revert.c
> index b2cfde7a87..1491c45e26 100644
> --- a/builtin/revert.c
> +++ b/builtin/revert.c
> @@ -45,6 +45,30 @@ static const char * const *revert_or_cherry_pick_usage(struct replay_opts *opts)
>   	return opts->action == REPLAY_REVERT ? revert_usage : cherry_pick_usage;
>   }
>   
> +enum empty_action {
> +	STOP_ON_EMPTY_COMMIT = 0,  /* output errors and stop in the middle of a cherry-pick */
> +	DROP_EMPTY_COMMIT,         /* skip with a notice message */
> +	KEEP_EMPTY_COMMIT,         /* keep recording as empty commits */
> +};
> +
> +static int parse_opt_empty(const struct option *opt, const char *arg, int unset)
> +{
> +	int *opt_value = opt->value;
> +
> +	BUG_ON_OPT_NEG(unset);
> +
> +	if (!strcmp(arg, "stop"))
> +		*opt_value = STOP_ON_EMPTY_COMMIT;
> +	else if (!strcmp(arg, "drop"))
> +		*opt_value = DROP_EMPTY_COMMIT;
> +	else if (!strcmp(arg, "keep"))
> +		*opt_value = KEEP_EMPTY_COMMIT;
> +	else
> +		return error(_("invalid value for '%s': '%s'"), "--empty", arg);
> +
> +	return 0;
> +}
> +
>   static int option_parse_m(const struct option *opt,
>   			  const char *arg, int unset)
>   {
> @@ -87,6 +111,7 @@ static int run_sequencer(int argc, const char **argv, const char *prefix,
>   	const char * const * usage_str = revert_or_cherry_pick_usage(opts);
>   	const char *me = action_name(opts);
>   	const char *cleanup_arg = NULL;
> +	enum empty_action empty_opt;
>   	int cmd = 0;
>   	struct option base_options[] = {
>   		OPT_CMDMODE(0, "quit", &cmd, N_("end revert or cherry-pick sequence"), 'q'),
> @@ -116,7 +141,10 @@ static int run_sequencer(int argc, const char **argv, const char *prefix,
>   			OPT_BOOL(0, "ff", &opts->allow_ff, N_("allow fast-forward")),
>   			OPT_BOOL(0, "allow-empty", &opts->allow_empty, N_("preserve initially empty commits")),
>   			OPT_BOOL(0, "allow-empty-message", &opts->allow_empty_message, N_("allow commits with empty messages")),
> -			OPT_BOOL(0, "keep-redundant-commits", &opts->keep_redundant_commits, N_("keep redundant, empty commits")),
> +			OPT_BOOL(0, "keep-redundant-commits", &opts->keep_redundant_commits, N_("deprecated: use --empty=keep instead")),
> +			OPT_CALLBACK_F(0, "empty", &empty_opt, "(stop|drop|keep)",
> +				       N_("how to handle commits that become empty"),
> +				       PARSE_OPT_NONEG, parse_opt_empty),
>   			OPT_END(),
>   		};
>   		options = parse_options_concat(options, cp_extra);
> @@ -136,6 +164,11 @@ static int run_sequencer(int argc, const char **argv, const char *prefix,
>   	prepare_repo_settings(the_repository);
>   	the_repository->settings.command_requires_full_index = 0;
>   
> +	if (opts->action == REPLAY_PICK) {
> +		opts->drop_redundant_commits = (empty_opt == DROP_EMPTY_COMMIT);
> +		opts->keep_redundant_commits = opts->keep_redundant_commits || (empty_opt == KEEP_EMPTY_COMMIT);
> +	}

The code changes look good but I think we want to update 
verify_opt_compatible() to check for "--empty" being combined with 
"--continue" etc. as well.

>   	if (cleanup_arg) {
>   		opts->default_msg_cleanup = get_cleanup_mode(cleanup_arg, 1);
>   		opts->explicit_cleanup = 1;
> diff --git a/sequencer.c b/sequencer.c
> index 582bde8d46..c49c27c795 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -2934,6 +2934,9 @@ static int populate_opts_cb(const char *key, const char *value,
>   	else if (!strcmp(key, "options.allow-empty-message"))
>   		opts->allow_empty_message =
>   			git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
> +	else if (!strcmp(key, "options.drop-redundant-commits"))
> +		opts->drop_redundant_commits =
> +			git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
>   	else if (!strcmp(key, "options.keep-redundant-commits"))
>   		opts->keep_redundant_commits =
>   			git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
> @@ -3478,6 +3481,9 @@ static int save_opts(struct replay_opts *opts)
>   	if (opts->allow_empty_message)
>   		res |= git_config_set_in_file_gently(opts_file,
>   				"options.allow-empty-message", "true");
> +	if (opts->drop_redundant_commits)
> +		res |= git_config_set_in_file_gently(opts_file,
> +				"options.drop-redundant-commits", "true");

It is good that we're saving the option - it would be good to add a test 
to check that we remember --empty after stopping for a conflict resolution.

>   	if (opts->keep_redundant_commits)
>   		res |= git_config_set_in_file_gently(opts_file,
>   				"options.keep-redundant-commits", "true");
> diff --git a/t/t3505-cherry-pick-empty.sh b/t/t3505-cherry-pick-empty.sh
> index 6adfd25351..ae0cf7886a 100755
> --- a/t/t3505-cherry-pick-empty.sh
> +++ b/t/t3505-cherry-pick-empty.sh
> @@ -89,7 +89,7 @@ test_expect_success 'cherry-pick a commit that becomes no-op (prep)' '
>   	git commit -m "add file2 on the side"
>   '
>   
> -test_expect_success 'cherry-pick a no-op without --keep-redundant' '
> +test_expect_success 'cherry-pick a no-op with neither --keep-redundant nor --empty' '
>   	git reset --hard &&
>   	git checkout fork^0 &&
>   	test_must_fail git cherry-pick main
> @@ -104,4 +104,28 @@ test_expect_success 'cherry-pick a no-op with --keep-redundant' '
>   	test_cmp expect actual
>   '
>   
> +test_expect_success 'cherry-pick a no-op with --empty=ask' '
> +	git reset --hard &&
> +	git checkout fork^0 &&
> +	test_must_fail git cherry-pick --empty=ask main

This is an example of why it is a good idea to check the error message 
when using "test_must_fail" as here the test will fail due to a bad 
value passed to "--empty" rather than for the reason we want the test to 
check. It would be good to add a separate test to check that we reject 
invalid "--empty" values.

> +'
> +
> +test_expect_success 'cherry-pick a no-op with --empty=drop' '
> +	git reset --hard &&
> +	git checkout fork^0 &&
> +	git cherry-pick --empty=drop main &&
> +	git show -s --format=%s >actual &&
> +	echo "add file2 on the side" >expect &&
> +	test_cmp expect actual

I think you could simplify this by using test_commit_message

Best Wishes

Phillip
Junio C Hamano Jan. 23, 2024, 5:32 p.m. UTC | #10
Brian Lyles <brianmlyles@gmail.com> writes:

> I do see that there are ~130 files with trailing whitespace in maint
> today, though I suspect that most of those are not intentional.

I got curious and took a look at files that has hits with "lines
that end with SP or HT":

    $ git grep -l -e '[  ]$'

There are some that can be cleaned up, but many of them are
intentional.

For example, CODE_OF_CONDUCT.md has these two (shown with $$$)
I think can be removed without breaking markdown:

    Community Impact Guidelines were inspired by $$$
    [Mozilla's code of conduct enforcement ladder][Mozilla CoC].

    For answers to common questions about this code of conduct, see the FAQ at
    [https://www.contributor-covenant.org/faq][FAQ]. Translations are available $$$
    at [https://www.contributor-covenant.org/translations][translations].

The one in Documentation/user-manual.txt is on borderline.  They
appear in a sample output from a command the user is typing (again,
$$$ shows where the SP at the end of line is):

    diff --git a/init-db.c b/init-db.c
    index 65898fa..b002dc6 100644
    --- a/init-db.c
    +++ b/init-db.c
    @@ -7,7 +7,7 @@
     $$$
     int main(int argc, char **argv)
    ...

As its purpose is to show human readers what they see in their
terminal should _look_ like, we _could_ do without the single space
on these otherwise empty lines, which denotes an empty line that
hasn't changed in the diff output.  But it would no longer match
byte-for-byte with what we are trying to illustrate.

There are many hits from the above grep in t/t[0-9][0-9][0-9][0-9]/
directories; these are canonical/expected output used in tests and
the spaces at the end of these lines are expected.
Junio C Hamano Jan. 23, 2024, 6:01 p.m. UTC | #11
Phillip Wood <phillip.wood123@gmail.com> writes:

> Thanks for the well explained commit message

;-)

>> The `--keep-redundant-commits` option will be documented as a deprecated
>> synonym of `--empty=keep`, and will be supported for backwards
>> compatibility for the time being.
>
> I'm not sure if we need to deprecate it as in "it will be removed in
> the future" or just reduce it prominence in favor of --empty

True, especially since --empty=keep is much less descriptive and the
part after "note that ..." below will take a long time before
sticking in readers' brain.

>> +--empty=(stop|drop|keep)::
>> +	How to handle commits being cherry-picked that are redundant with
>> +	changes already in the current history.

It might make it easier to understand if we moved the description in
'keep' that says something about --allow-empty here, as it should
apply to other two choices if I understand correctly.  Let me try:

    This option specifies how a commit that is not originally empty
    but becomes a no-op when cherry-picked due to earlier changes
    already applied or already in the current history.  Regardless
    of this this option, `cherry-pick` will fail on a commit that is
    empty in the original history---see `--allow-empty` to pass them
    intact.

or something.  Then the description of `keep` can become just as
short as other two, e.g. a single sentence "The commit that becomes
empty will be kept".

>> ...
>> +	The cherry-pick will stop when the empty commit is applied, allowing
>> +	you to examine the commit. This is the default behavior.
>> +`drop`;;
>> +	The empty commit will be dropped.
>> +`keep`;;
>> +	The empty commit will be kept. Note that use of this option only
>>   	results in an empty commit when the commit was not initially empty,
>>   	but rather became empty due to a previous commit. Commits that were
>>   	initially empty will cause the cherry-pick to fail. To force the
>>   	inclusion of those commits use `--allow-empty`.
>> +--
>> ++
>> +Note that commits which start empty will cause the cherry-pick to fail (unless
>> +`--allow-empty` is specified).
Junio C Hamano Jan. 23, 2024, 6:49 p.m. UTC | #12
Junio C Hamano <gitster@pobox.com> writes:

> I got curious and took a look at files that has hits with "lines
> that end with SP or HT":
>
>     $ git grep -l -e '[  ]$'

Another more interest way to check is to do this:

    $ git diff --check $(git hash-object --stdin -t tree </dev/null)

which will not just catch trailing whitespaces but other whitespace
errors.  Good thing is that the projects can even customize the rules
used for various paths using the attributes mechanism.

Here is a sample patch based on what the above command line found.

------ >8 ----------- >8 ----------- >8 ----------- >8 ------
Subject: [PATCH] docs: a few whitespace fixes

Some documentation files have 8-space indented lines where other
indented lines in the same list use a single HT to indent.  As
Documentation/.gitattributes says *.txt files use the default
whitespace rules, let's fix some of them as a practice.

Note that git-add documentation has other instances of space
indented lines, but they are samples of manually aligned output
of the "git add -i" command and it would be better to keep them
as-is.  Which in turn may mean we may want to loosen the whitespace
rules for some parts of the documentation files, but we currently do
not have such a feature.  The attribute based whitespace rules apply
to the whole file.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/diff-format.txt | 8 ++++----
 Documentation/git-add.txt     | 2 +-
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git c/Documentation/diff-format.txt w/Documentation/diff-format.txt
index a3ae8747a2..bcaf9ca608 100644
--- c/Documentation/diff-format.txt
+++ w/Documentation/diff-format.txt
@@ -8,16 +8,16 @@ These commands all compare two sets of things; what is
 compared differs:
 
 git-diff-index <tree-ish>::
-        compares the <tree-ish> and the files on the filesystem.
+	compares the <tree-ish> and the files on the filesystem.
 
 git-diff-index --cached <tree-ish>::
-        compares the <tree-ish> and the index.
+	compares the <tree-ish> and the index.
 
 git-diff-tree [-r] <tree-ish-1> <tree-ish-2> [<pattern>...]::
-        compares the trees named by the two arguments.
+	compares the trees named by the two arguments.
 
 git-diff-files [<pattern>...]::
-        compares the index and the files on the filesystem.
+	compares the index and the files on the filesystem.
 
 The "git-diff-tree" command begins its output by printing the hash of
 what is being compared. After that, all the commands print one output
diff --git c/Documentation/git-add.txt w/Documentation/git-add.txt
index ed44c1cb31..e1a5c27acd 100644
--- c/Documentation/git-add.txt
+++ w/Documentation/git-add.txt
@@ -73,7 +73,7 @@ in linkgit:gitglossary[7].
 
 -v::
 --verbose::
-        Be verbose.
+	Be verbose.
 
 -f::
 --force::
Brian Lyles Jan. 27, 2024, 11:56 p.m. UTC | #13
[+cc Junio]

On Tue, Jan 23, 2024 at 8:25 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> Hi Brian
>
>
> On 19/01/2024 05:59, brianmlyles@gmail.com wrote:
>
> > The `--keep-redundant-commits` option will be documented as a deprecated
> > synonym of `--empty=keep`, and will be supported for backwards
> > compatibility for the time being.
>
> I'm not sure if we need to deprecate it as in "it will be removed in the
> future" or just reduce it prominence in favor of --empty

This is also related to Junio's comment:

> On Tue, Jan 23, 2024 at 12:01 PM Junio C Hamano <gitster@pobox.com>
wrote:
>
> True, especially since --empty=keep is much less descriptive and the
> part after "note that ..." below will take a long time before
> sticking in readers' brain.

My primary motivation here was simply for consistency with `--empty` for
both git-rebase(1) and git-am(1). In theory, I am not opposed to
updating this patch to instead simply add a `--drop-redundant-commits`
option if we feel that provides better usability. However, I think that
the consistency would be better.

I will happily defer to the group consensus here, with the options I see
being:

1. No deprecation: just make `--keep-redundant-commits` a synonym of
  `--empty=keep`
2. Soft deprecation: Give a warning when `--keep-redundant-commits` is
  used
3. Add `--drop-redundant-commits` instead of `--empty`

My preference would be 2, followed by 1 and then 3.

> I'm still on the fence about "stop" vs "ask". I see in your tests you've
> accidentally used "ask" which makes me wonder if that is the more
> familiar term for users who probably use "git rebase" more often than
> "git am".

Oh, thank you for catching that. The cause here was actually because I
had initially written these tests prior to adding the "ask -> stop"
change rather than familiarity. I simply failed to update the tests
after moving things around.

> The code changes look good but I think we want to update
> verify_opt_compatible() to check for "--empty" being combined with
> "--continue" etc. as well.

It looks like `--keep-redundant-commits` was not being included in these
checks previously. I suspect that to be an oversight though.

I can add this for v2.

>
> >       if (cleanup_arg) {
> >               opts->default_msg_cleanup = get_cleanup_mode(cleanup_arg, 1);
> >               opts->explicit_cleanup = 1;
> > diff --git a/sequencer.c b/sequencer.c
> > index 582bde8d46..c49c27c795 100644
> > --- a/sequencer.c
> > +++ b/sequencer.c
> > @@ -2934,6 +2934,9 @@ static int populate_opts_cb(const char *key, const char *value,
> >       else if (!strcmp(key, "options.allow-empty-message"))
> >               opts->allow_empty_message =
> >                       git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
> > +     else if (!strcmp(key, "options.drop-redundant-commits"))
> > +             opts->drop_redundant_commits =
> > +                     git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
> >       else if (!strcmp(key, "options.keep-redundant-commits"))
> >               opts->keep_redundant_commits =
> >                       git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
> > @@ -3478,6 +3481,9 @@ static int save_opts(struct replay_opts *opts)
> >       if (opts->allow_empty_message)
> >               res |= git_config_set_in_file_gently(opts_file,
> >                               "options.allow-empty-message", "true");
> > +     if (opts->drop_redundant_commits)
> > +             res |= git_config_set_in_file_gently(opts_file,
> > +                             "options.drop-redundant-commits", "true");
>
> It is good that we're saving the option - it would be good to add a test
> to check that we remember --empty after stopping for a conflict resolution.

I can add a test for this in v2

> >       if (opts->keep_redundant_commits)
> >               res |= git_config_set_in_file_gently(opts_file,
> >                               "options.keep-redundant-commits", "true");
> > diff --git a/t/t3505-cherry-pick-empty.sh b/t/t3505-cherry-pick-empty.sh
> > index 6adfd25351..ae0cf7886a 100755
> > --- a/t/t3505-cherry-pick-empty.sh
> > +++ b/t/t3505-cherry-pick-empty.sh
> > @@ -89,7 +89,7 @@ test_expect_success 'cherry-pick a commit that becomes no-op (prep)' '
> >       git commit -m "add file2 on the side"
> >   '
> >
> > -test_expect_success 'cherry-pick a no-op without --keep-redundant' '
> > +test_expect_success 'cherry-pick a no-op with neither --keep-redundant nor --empty' '
> >       git reset --hard &&
> >       git checkout fork^0 &&
> >       test_must_fail git cherry-pick main
> > @@ -104,4 +104,28 @@ test_expect_success 'cherry-pick a no-op with --keep-redundant' '
> >       test_cmp expect actual
> >   '
> >
> > +test_expect_success 'cherry-pick a no-op with --empty=ask' '
> > +     git reset --hard &&
> > +     git checkout fork^0 &&
> > +     test_must_fail git cherry-pick --empty=ask main
>
> This is an example of why it is a good idea to check the error message
> when using "test_must_fail" as here the test will fail due to a bad
> value passed to "--empty" rather than for the reason we want the test to
> check. It would be good to add a separate test to check that we reject
> invalid "--empty" values.

An excellent catch, thank you. Will be addressed in v2

> > +'
> > +
> > +test_expect_success 'cherry-pick a no-op with --empty=drop' '
> > +     git reset --hard &&
> > +     git checkout fork^0 &&
> > +     git cherry-pick --empty=drop main &&
> > +     git show -s --format=%s >actual &&
> > +     echo "add file2 on the side" >expect &&
> > +     test_cmp expect actual
>
> I think you could simplify this by using test_commit_message

Thanks for pointing that function out -- you're absolutely right. Will
be addressed in v2.


Thanks for the review,
Brian Lyles
Brian Lyles Jan. 28, 2024, 12:07 a.m. UTC | #14
Hi Junio

On Tue, Jan 23, 2024 at 12:01 PM Junio C Hamano <gitster@pobox.com> wrote:
> Phillip Wood <phillip.wood123@gmail.com> writes:
>
> > Thanks for the well explained commit message
>
> ;-)
>
> >> The `--keep-redundant-commits` option will be documented as a deprecated
> >> synonym of `--empty=keep`, and will be supported for backwards
> >> compatibility for the time being.
> >
> > I'm not sure if we need to deprecate it as in "it will be removed in
> > the future" or just reduce it prominence in favor of --empty
>
> True, especially since --empty=keep is much less descriptive and the
> part after "note that ..." below will take a long time before
> sticking in readers' brain.

I responded to this in my reply to Phillip, and CC'd you there.

> >> +--empty=(stop|drop|keep)::
> >> +    How to handle commits being cherry-picked that are redundant with
> >> +    changes already in the current history.
>
> It might make it easier to understand if we moved the description in
> 'keep' that says something about --allow-empty here, as it should
> apply to other two choices if I understand correctly.  Let me try:
>
>     This option specifies how a commit that is not originally empty
>     but becomes a no-op when cherry-picked due to earlier changes
>     already applied or already in the current history.  Regardless
>     of this this option, `cherry-pick` will fail on a commit that is
>     empty in the original history---see `--allow-empty` to pass them
>     intact.
>
> or something.  Then the description of `keep` can become just as
> short as other two, e.g. a single sentence "The commit that becomes
> empty will be kept".

Thank you for this suggestion. You are correct that the difference
between `--empty` and `allow-empty` is relevant regardless of the option
selected by the user.

In fact, this entire tidbit is somewhat duplicative with the note I
already added after the options:

> Note that commits which start empty will cause the cherry-pick to fail
> (unless `--allow-empty` is specified).

I'll clean this up in v2. Here's what I am thinking currently:

> --empty=(stop|drop|keep)::
>     How to handle commits being cherry-picked that are redundant with
>     changes already in the current history.
> +
> --
> `stop`;;
>     The cherry-pick will stop when the commit is applied, allowing
>     you to examine the commit. This is the default behavior.
> `drop`;;
>     The commit will be dropped.
> `keep`;;
>     The commit will be kept.
> --
> +
> Note that this option species how to handle a commit that was not initially
> empty, but rather became empty due to a previous commit. Commits that were
> initially empty will cause the cherry-pick to fail. To force the inclusion of
> those commits, use `--allow-empty`.
> +

Thank you,
Brian Lyles
diff mbox series

Patch

diff --git a/Documentation/git-cherry-pick.txt b/Documentation/git-cherry-pick.txt
index 806295a730..8c20a10d4b 100644
--- a/Documentation/git-cherry-pick.txt
+++ b/Documentation/git-cherry-pick.txt
@@ -132,23 +132,37 @@  effect to your index in a row.
 	keeps commits that were initially empty (i.e. the commit recorded the
 	same tree as its parent).  Commits which are made empty due to a
 	previous commit will cause the cherry-pick to fail.  To force the
-	inclusion of those commits use `--keep-redundant-commits`.
+	inclusion of those commits use `--empty=keep`.
 
 --allow-empty-message::
 	By default, cherry-picking a commit with an empty message will fail.
 	This option overrides that behavior, allowing commits with empty
 	messages to be cherry picked.
 
---keep-redundant-commits::
-	If a commit being cherry picked duplicates a commit already in the
-	current history, it will become empty.  By default these
-	redundant commits cause `cherry-pick` to stop so the user can
-	examine the commit. This option overrides that behavior and
-	creates an empty commit object. Note that use of this option only
+--empty=(stop|drop|keep)::
+	How to handle commits being cherry-picked that are redundant with
+	changes already in the current history.
++
+-- 
+`stop`;;
+	The cherry-pick will stop when the empty commit is applied, allowing
+	you to examine the commit. This is the default behavior.
+`drop`;;
+	The empty commit will be dropped.
+`keep`;;
+	The empty commit will be kept. Note that use of this option only
 	results in an empty commit when the commit was not initially empty,
 	but rather became empty due to a previous commit. Commits that were
 	initially empty will cause the cherry-pick to fail. To force the
 	inclusion of those commits use `--allow-empty`.
+--
++
+Note that commits which start empty will cause the cherry-pick to fail (unless
+`--allow-empty` is specified).
++
+
+--keep-redundant-commits::
+	Deprecated synonym for `--empty=keep`.
 
 --strategy=<strategy>::
 	Use the given merge strategy.  Should only be used once.
diff --git a/builtin/revert.c b/builtin/revert.c
index b2cfde7a87..1491c45e26 100644
--- a/builtin/revert.c
+++ b/builtin/revert.c
@@ -45,6 +45,30 @@  static const char * const *revert_or_cherry_pick_usage(struct replay_opts *opts)
 	return opts->action == REPLAY_REVERT ? revert_usage : cherry_pick_usage;
 }
 
+enum empty_action {
+	STOP_ON_EMPTY_COMMIT = 0,  /* output errors and stop in the middle of a cherry-pick */
+	DROP_EMPTY_COMMIT,         /* skip with a notice message */
+	KEEP_EMPTY_COMMIT,         /* keep recording as empty commits */
+};
+
+static int parse_opt_empty(const struct option *opt, const char *arg, int unset)
+{
+	int *opt_value = opt->value;
+
+	BUG_ON_OPT_NEG(unset);
+
+	if (!strcmp(arg, "stop"))
+		*opt_value = STOP_ON_EMPTY_COMMIT;
+	else if (!strcmp(arg, "drop"))
+		*opt_value = DROP_EMPTY_COMMIT;
+	else if (!strcmp(arg, "keep"))
+		*opt_value = KEEP_EMPTY_COMMIT;
+	else
+		return error(_("invalid value for '%s': '%s'"), "--empty", arg);
+
+	return 0;
+}
+
 static int option_parse_m(const struct option *opt,
 			  const char *arg, int unset)
 {
@@ -87,6 +111,7 @@  static int run_sequencer(int argc, const char **argv, const char *prefix,
 	const char * const * usage_str = revert_or_cherry_pick_usage(opts);
 	const char *me = action_name(opts);
 	const char *cleanup_arg = NULL;
+	enum empty_action empty_opt;
 	int cmd = 0;
 	struct option base_options[] = {
 		OPT_CMDMODE(0, "quit", &cmd, N_("end revert or cherry-pick sequence"), 'q'),
@@ -116,7 +141,10 @@  static int run_sequencer(int argc, const char **argv, const char *prefix,
 			OPT_BOOL(0, "ff", &opts->allow_ff, N_("allow fast-forward")),
 			OPT_BOOL(0, "allow-empty", &opts->allow_empty, N_("preserve initially empty commits")),
 			OPT_BOOL(0, "allow-empty-message", &opts->allow_empty_message, N_("allow commits with empty messages")),
-			OPT_BOOL(0, "keep-redundant-commits", &opts->keep_redundant_commits, N_("keep redundant, empty commits")),
+			OPT_BOOL(0, "keep-redundant-commits", &opts->keep_redundant_commits, N_("deprecated: use --empty=keep instead")),
+			OPT_CALLBACK_F(0, "empty", &empty_opt, "(stop|drop|keep)",
+				       N_("how to handle commits that become empty"),
+				       PARSE_OPT_NONEG, parse_opt_empty),
 			OPT_END(),
 		};
 		options = parse_options_concat(options, cp_extra);
@@ -136,6 +164,11 @@  static int run_sequencer(int argc, const char **argv, const char *prefix,
 	prepare_repo_settings(the_repository);
 	the_repository->settings.command_requires_full_index = 0;
 
+	if (opts->action == REPLAY_PICK) {
+		opts->drop_redundant_commits = (empty_opt == DROP_EMPTY_COMMIT);
+		opts->keep_redundant_commits = opts->keep_redundant_commits || (empty_opt == KEEP_EMPTY_COMMIT);
+	}
+
 	if (cleanup_arg) {
 		opts->default_msg_cleanup = get_cleanup_mode(cleanup_arg, 1);
 		opts->explicit_cleanup = 1;
diff --git a/sequencer.c b/sequencer.c
index 582bde8d46..c49c27c795 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2934,6 +2934,9 @@  static int populate_opts_cb(const char *key, const char *value,
 	else if (!strcmp(key, "options.allow-empty-message"))
 		opts->allow_empty_message =
 			git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
+	else if (!strcmp(key, "options.drop-redundant-commits"))
+		opts->drop_redundant_commits =
+			git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
 	else if (!strcmp(key, "options.keep-redundant-commits"))
 		opts->keep_redundant_commits =
 			git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
@@ -3478,6 +3481,9 @@  static int save_opts(struct replay_opts *opts)
 	if (opts->allow_empty_message)
 		res |= git_config_set_in_file_gently(opts_file,
 				"options.allow-empty-message", "true");
+	if (opts->drop_redundant_commits)
+		res |= git_config_set_in_file_gently(opts_file,
+				"options.drop-redundant-commits", "true");
 	if (opts->keep_redundant_commits)
 		res |= git_config_set_in_file_gently(opts_file,
 				"options.keep-redundant-commits", "true");
diff --git a/t/t3505-cherry-pick-empty.sh b/t/t3505-cherry-pick-empty.sh
index 6adfd25351..ae0cf7886a 100755
--- a/t/t3505-cherry-pick-empty.sh
+++ b/t/t3505-cherry-pick-empty.sh
@@ -89,7 +89,7 @@  test_expect_success 'cherry-pick a commit that becomes no-op (prep)' '
 	git commit -m "add file2 on the side"
 '
 
-test_expect_success 'cherry-pick a no-op without --keep-redundant' '
+test_expect_success 'cherry-pick a no-op with neither --keep-redundant nor --empty' '
 	git reset --hard &&
 	git checkout fork^0 &&
 	test_must_fail git cherry-pick main
@@ -104,4 +104,28 @@  test_expect_success 'cherry-pick a no-op with --keep-redundant' '
 	test_cmp expect actual
 '
 
+test_expect_success 'cherry-pick a no-op with --empty=ask' '
+	git reset --hard &&
+	git checkout fork^0 &&
+	test_must_fail git cherry-pick --empty=ask main
+'
+
+test_expect_success 'cherry-pick a no-op with --empty=drop' '
+	git reset --hard &&
+	git checkout fork^0 &&
+	git cherry-pick --empty=drop main &&
+	git show -s --format=%s >actual &&
+	echo "add file2 on the side" >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'cherry-pick a no-op with --empty=keep' '
+	git reset --hard &&
+	git checkout fork^0 &&
+	git cherry-pick --empty=keep main &&
+	git show -s --format=%s >actual &&
+	echo "add file2 on main" >expect &&
+	test_cmp expect actual
+'
+
 test_done