diff mbox series

reflog: fix expire --single-worktree

Message ID 63eade0e-bf2c-4906-8b4c-689797cff737@web.de (mailing list archive)
State New, archived
Headers show
Series reflog: fix expire --single-worktree | expand

Commit Message

René Scharfe Oct. 28, 2023, 11:58 a.m. UTC
33d7bdd645 (builtin/reflog.c: use parse-options api for expire, delete
subcommands, 2022-01-06) broke the option --single-worktree of git
reflog expire and added a non-printable short flag for it, presumably by
accident.  While before it set the variable "all_worktrees" to 0, now it
sets it to 1, its default value.  --no-single-worktree is required now
to set it to 0.

Fix it by replacing the variable with one that has the opposite meaning,
to avoid the negation and its potential for confusion.  The new variable
"single_worktree" directly captures whether --single-worktree was given.

Also remove the unprintable short flag SOH (start of heading) because it
is undocumented, hard to use and is likely to have been added by mistake
in connection with the negation bug above.

Signed-off-by: René Scharfe <l.s.r@web.de>
---
 builtin/reflog.c  |  6 +++---
 t/t1410-reflog.sh | 23 +++++++++++++++++++++++
 2 files changed, 26 insertions(+), 3 deletions(-)

--
2.42.0

Comments

Junio C Hamano Oct. 29, 2023, 10:31 p.m. UTC | #1
René Scharfe <l.s.r@web.de> writes:

> ... and added a non-printable short flag for it, presumably by
> accident.

Very well spotted.

FWIW, with the following patch on top of this patch, all tests pass
(and without your fix, of course this notices the "\001" and breaks
numerous tests that use "git reflog").  So you seem to have found
the only one broken instance (among those that are tested, anyway).

 parse-options.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git i/parse-options.c w/parse-options.c
index 093eaf2db8..be8bedba29 100644
--- i/parse-options.c
+++ w/parse-options.c
@@ -469,7 +469,8 @@ static void parse_options_check(const struct option *opts)
 			optbug(opts, "uses incompatible flags "
 			       "LASTARG_DEFAULT and OPTARG");
 		if (opts->short_name) {
-			if (0x7F <= opts->short_name)
+			if (opts->short_name &&
+			    (opts->short_name < 0x21 || 0x7F <= opts->short_name))
 				optbug(opts, "invalid short name");
 			else if (short_opts[opts->short_name]++)
 				optbug(opts, "short name already used");
René Scharfe Oct. 30, 2023, 4:12 p.m. UTC | #2
Am 29.10.23 um 23:31 schrieb Junio C Hamano:
> René Scharfe <l.s.r@web.de> writes:
>
>> ... and added a non-printable short flag for it, presumably by
>> accident.
>
> Very well spotted.
>
> FWIW, with the following patch on top of this patch, all tests pass
> (and without your fix, of course this notices the "\001" and breaks
> numerous tests that use "git reflog").  So you seem to have found
> the only one broken instance (among those that are tested, anyway).
>
>  parse-options.c | 3 ++-
>  1 file changed, 2 insertions(+), 1 deletion(-)
>
> diff --git i/parse-options.c w/parse-options.c
> index 093eaf2db8..be8bedba29 100644
> --- i/parse-options.c
> +++ w/parse-options.c
> @@ -469,7 +469,8 @@ static void parse_options_check(const struct option *opts)
>  			optbug(opts, "uses incompatible flags "
>  			       "LASTARG_DEFAULT and OPTARG");
>  		if (opts->short_name) {
> -			if (0x7F <= opts->short_name)
> +			if (opts->short_name &&
> +			    (opts->short_name < 0x21 || 0x7F <= opts->short_name))

Good idea.  This is equivalent to !isprint(opts->short_name), which I
find to be more readable here.  Seeing why "char short_opts[128];" a
few lines up is big enough would become a bit harder, though.

>  				optbug(opts, "invalid short name");
>  			else if (short_opts[opts->short_name]++)
>  				optbug(opts, "short name already used");
Taylor Blau Oct. 30, 2023, 5:21 p.m. UTC | #3
On Mon, Oct 30, 2023 at 07:31:22AM +0900, Junio C Hamano wrote:
> René Scharfe <l.s.r@web.de> writes:
>
> > ... and added a non-printable short flag for it, presumably by
> > accident.
>
> Very well spotted.
>
> FWIW, with the following patch on top of this patch, all tests pass
> (and without your fix, of course this notices the "\001" and breaks
> numerous tests that use "git reflog").  So you seem to have found
> the only one broken instance (among those that are tested, anyway).

This makes sense to me, but obviously won't catch non-tested cases.  I
thought that a new Cocinelle rule might be appropriate here, but it is
frustratingly difficult to specify a constraint like:

    OPT_BOOL(e1, e2, e3, ...)

with

    !(e1 == 0 || (33 <= e1 && e1 <= 127))

I'll think on it a little bit, but this seems low priority enough that I
don't feel compelled to urgently deal with adding a new Coccinelle rule.

Thanks,
Taylor
Junio C Hamano Oct. 30, 2023, 11:11 p.m. UTC | #4
René Scharfe <l.s.r@web.de> writes:

> Am 29.10.23 um 23:31 schrieb Junio C Hamano:
>> René Scharfe <l.s.r@web.de> writes:
>>
>>> ... and added a non-printable short flag for it, presumably by
>>> accident.
>>
>> Very well spotted.
>>
>> FWIW, with the following patch on top of this patch, all tests pass
>> (and without your fix, of course this notices the "\001" and breaks
>> numerous tests that use "git reflog").  So you seem to have found
>> the only one broken instance (among those that are tested, anyway).
>>
>>  parse-options.c | 3 ++-
>>  1 file changed, 2 insertions(+), 1 deletion(-)
>>
>> diff --git i/parse-options.c w/parse-options.c
>> index 093eaf2db8..be8bedba29 100644
>> --- i/parse-options.c
>> +++ w/parse-options.c
>> @@ -469,7 +469,8 @@ static void parse_options_check(const struct option *opts)
>>  			optbug(opts, "uses incompatible flags "
>>  			       "LASTARG_DEFAULT and OPTARG");
>>  		if (opts->short_name) {
>> -			if (0x7F <= opts->short_name)
>> +			if (opts->short_name &&
>> +			    (opts->short_name < 0x21 || 0x7F <= opts->short_name))
>
> Good idea.  This is equivalent to !isprint(opts->short_name), which I
> find to be more readable here.

Thanks---I didn't think of using !isprint() but you are right.  It
is much shorter.

I am not absolutely certain if it is easier to read, though.  I get
always confused when asking myself if SP, HT, and LF are printables.
(in other words, I cannot immediately answer "does 'printable' mean
'can be sent to a teletype and have it do what is expected to be
done?"---the question I should be asking myself is "is 'printable'
synonym to 'when printed, some ink is consumed'?").

> Seeing why "char short_opts[128];" a
> few lines up is big enough would become a bit harder, though.

Sorry, but I do not quite follow.  We used to allow anything below
0x7e; now we clip that range further to reject anything below 0x21.
If [128] was big enough, it still is big enough, no?

Because the type of .short_name member is "int", we could have had
negative number in there and access to short_opts[] on the next line
would have been out of bounds.  By clipping the lower bound, we get
rid of that risk, no?

>>  				optbug(opts, "invalid short name");
>>  			else if (short_opts[opts->short_name]++)
>>  				optbug(opts, "short name already used");
Junio C Hamano Oct. 30, 2023, 11:24 p.m. UTC | #5
Taylor Blau <me@ttaylorr.com> writes:

> This makes sense to me, but obviously won't catch non-tested cases.

True, but I think we can practically ignore non-tested cases.

The parse_options_check() is used to validate the whole options[]
array that is passed to parse_options() family of API functions, and
its validation is not limited to the options that are given from the
command line in an invocation.  A non-tested case would happen when
a developer prepares and populates "struct option options[];" array
for a (possibly new) git subcommand *and* never uses that array to
call parse_options() in their implementation of that subcommand.
The compiler would catch the unused variable options[] in such a
case, and mark 1 eyeball would notice that none of the options
defined in that array are actually understood by the command, no?
René Scharfe Oct. 31, 2023, 6:16 a.m. UTC | #6
Am 31.10.23 um 00:11 schrieb Junio C Hamano:
> René Scharfe <l.s.r@web.de> writes:
>
>> Am 29.10.23 um 23:31 schrieb Junio C Hamano:
>>> René Scharfe <l.s.r@web.de> writes:
>>>
>>> diff --git i/parse-options.c w/parse-options.c
>>> index 093eaf2db8..be8bedba29 100644
>>> --- i/parse-options.c
>>> +++ w/parse-options.c
>>> @@ -469,7 +469,8 @@ static void parse_options_check(const struct option *opts)
>>>  			optbug(opts, "uses incompatible flags "
>>>  			       "LASTARG_DEFAULT and OPTARG");
>>>  		if (opts->short_name) {
>>> -			if (0x7F <= opts->short_name)
>>> +			if (opts->short_name &&
>>> +			    (opts->short_name < 0x21 || 0x7F <= opts->short_name))
>>
>> Good idea.  This is equivalent to !isprint(opts->short_name), which I
>> find to be more readable here.
>
> Thanks---I didn't think of using !isprint() but you are right.  It
> is much shorter.
>
> I am not absolutely certain if it is easier to read, though.  I get
> always confused when asking myself if SP, HT, and LF are printables.
> (in other words, I cannot immediately answer "does 'printable' mean
> 'can be sent to a teletype and have it do what is expected to be
> done?"---the question I should be asking myself is "is 'printable'
> synonym to 'when printed, some ink is consumed'?").

isprint() accepts SP, but not HT or LF.  Go figure.  And thus I made an
off-by-one error by suggesting this macro, because your version rejects
SP (0x20).  Am I unintentionally making a point here for using the
is-macros because I can't read numeric comparisons? O_o

isalnum() and ispunct() could be used instead.

>> Seeing why "char short_opts[128];" a
>> few lines up is big enough would become a bit harder, though.
>
> Sorry, but I do not quite follow.  We used to allow anything below
> 0x7e; now we clip that range further to reject anything below 0x21.
> If [128] was big enough, it still is big enough, no?
>
> Because the type of .short_name member is "int", we could have had
> negative number in there and access to short_opts[] on the next line
> would have been out of bounds.  By clipping the lower bound, we get
> rid of that risk, no?

Yes, but if the allowed range is hidden behind macro invocations then
the boundaries are no longer as obvious as in your version.

>>>  				optbug(opts, "invalid short name");
>>>  			else if (short_opts[opts->short_name]++)
>>>  				optbug(opts, "short name already used");
diff mbox series

Patch

diff --git a/builtin/reflog.c b/builtin/reflog.c
index df63a5892e..21337292f5 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -243,7 +243,7 @@  static int cmd_reflog_expire(int argc, const char **argv, const char *prefix)
 {
 	struct cmd_reflog_expire_cb cmd = { 0 };
 	timestamp_t now = time(NULL);
-	int i, status, do_all, all_worktrees = 1;
+	int i, status, do_all, single_worktree = 0;
 	unsigned int flags = 0;
 	int verbose = 0;
 	reflog_expiry_should_prune_fn *should_prune_fn = should_expire_reflog_ent;
@@ -268,7 +268,7 @@  static int cmd_reflog_expire(int argc, const char **argv, const char *prefix)
 		OPT_BOOL(0, "stale-fix", &cmd.stalefix,
 			 N_("prune any reflog entries that point to broken commits")),
 		OPT_BOOL(0, "all", &do_all, N_("process the reflogs of all references")),
-		OPT_BOOL(1, "single-worktree", &all_worktrees,
+		OPT_BOOL(0, "single-worktree", &single_worktree,
 			 N_("limits processing to reflogs from the current worktree only")),
 		OPT_END()
 	};
@@ -318,7 +318,7 @@  static int cmd_reflog_expire(int argc, const char **argv, const char *prefix)

 		worktrees = get_worktrees();
 		for (p = worktrees; *p; p++) {
-			if (!all_worktrees && !(*p)->is_current)
+			if (single_worktree && !(*p)->is_current)
 				continue;
 			collected.worktree = *p;
 			refs_for_each_reflog(get_worktree_ref_store(*p),
diff --git a/t/t1410-reflog.sh b/t/t1410-reflog.sh
index 6c45965b1e..09e7f3cdac 100755
--- a/t/t1410-reflog.sh
+++ b/t/t1410-reflog.sh
@@ -446,6 +446,29 @@  test_expect_success 'expire with multiple worktrees' '
 	)
 '

+test_expect_success 'expire one of multiple worktrees' '
+	git init main-wt2 &&
+	(
+		cd main-wt2 &&
+		test_tick &&
+		test_commit foo &&
+		git worktree add link-wt &&
+		test_tick &&
+		test_commit -C link-wt foobar &&
+		test_tick &&
+		test-tool ref-store worktree:link-wt for-each-reflog-ent HEAD \
+			>expect-link-wt &&
+		git reflog expire --verbose --all --expire=$test_tick \
+			--single-worktree &&
+		test-tool ref-store worktree:main for-each-reflog-ent HEAD \
+			>actual-main &&
+		test-tool ref-store worktree:link-wt for-each-reflog-ent HEAD \
+			>actual-link-wt &&
+		test_must_be_empty actual-main &&
+		test_cmp expect-link-wt actual-link-wt
+	)
+'
+
 test_expect_success REFFILES 'empty reflog' '
 	test_when_finished "rm -rf empty" &&
 	git init empty &&