Subject: Re: [PATCH v6 3/3] notes.c: introduce '--separator=<paragraph-break>' option

From: Junio C Hamano <[email-masked]>
Date: Thu, 23 Feb 2023 10:21:27 -0800

Teng Long <[email-masked]> writes:

> So this commit
> introduces a new '--separator' option for 'git-notes-add' and
> 'git-notes-append', for example when execute:

    Introduce a new '--separator' option for 'git notes add' and
    'git notes append'.

> We will check the option value and if the value doesn't contail
> a trailing '\n', will add it automatically,

contail?

	A newline is added to the value given to --separator if it
	does not end with one already.

> so execute
>       $ git notes add -m foo -m bar --separator="-"
> and
>       $ export LF="
>       "
>       $ git notes add -m foo -m bar --separator="-$LF"
>
> have the same behavour.

	Running A and B produces the same result.

> +	subcommand). If you specify multiple `-m` and `-F`, a blank
> +	line will be inserted between the messages. Use the `--separator`
> +	option to insert other delimiters. 

Concise and readable.  Nice.

> @@ -86,7 +88,13 @@ the command can read the input given to the `post-rewrite` hook.)
>  
>  append::
>  	Append to the notes of an existing object (defaults to HEAD).
> -	Creates a new notes object if needed.
> +	Creates a new notes object if needed. 
> +	The default delimiter is a blank line, use the `--separator`

Re-read the above three lines, pretending that it is already 6
months and you've forgotten about adding the feature yourself.

Notice something?

A reader with fresh mind would read and think along the description
but

 - OK, append command is used to append to the note to an existing
   object;

 - OK, if the object does not have a note yet, we will create one;

 - The default delimiter?  Delimiter for what?  I am puzzled.

at the third step, gets puzzled.  The command takes the existing
note's contents, adds a delimiter and then appends the new material
given by the user, but because that is not clear after reading the
first two lines, the sudden appearance of "delimiter" would confuse
readers.

> +	option to insert other delimiters. More specifically, if the
> +	note and the message are not empty, the delimiter will be
> +	inserted between them. If you specify multiple `-m` and `-F`

Again, the order of presentation is somewhat backwards and that is
why we need to say "More specifically" here.

> +	options, the delimiter will be inserted between the messages
> +	too.

	Append new message(s) given by `-m` or `-F` options to an
	existing note, or add them as a new note if one does not
	exist, for the object (defaults to HEAD).  When appending to
	an existing note, a blank line is added before each new
	message as an inter-paragraph separator.  The separator can
	be customized with the `--separator` option.

or something along that line, perhaps?

> +--separator <paragraph-break>::
> +	The '<paragraph-break>' inserted between paragraphs.
> +	A blank line by default.

Here is where you need to say about promoting incomplete line
separator more than the proposed log message.

	Specify a string used as a custom inter-paragraph separator
	(a newline is added at the end as needed).  Defaults to a
	blank line.

> diff --git a/builtin/notes.c b/builtin/notes.c
> index 553ae2bd..e0ada862 100644
> --- a/builtin/notes.c
> +++ b/builtin/notes.c
> @@ -24,11 +24,12 @@
> ...
> +static void insert_separator(struct strbuf *message, size_t pos)
> +{
> +	if (!separator)
> +		strbuf_insertstr(message, pos, "\n");
> +	else if (separator[strlen(separator) - 1] == '\n')
> +		strbuf_insertstr(message, pos, separator);
> +	else
> +		strbuf_insertf(message, pos, "%s%s", separator, "\n");
> +}

It looks like you are very fond of "insert", but aren't we always
appending with the latest control flow?  In other words, is it worth
carrying 'pos' around?

> +static void parse_messages(struct string_list *messages, struct note_data *d)
> +{
> +	size_t i;
> +	for (i = 0; i < messages->nr; i++) {
> +		if (d->buf.len)
> +			insert_separator(&d->buf, d->buf.len);
> +		strbuf_insertstr(&d->buf, d->buf.len,
> +				 messages->items[i].string);
> +		strbuf_stripspace(&d->buf, 0);

This is not a new problem, but if we get three 100-byte messages, we

 - add the first 100-byte message to d->buf and then run
   stripspace() over that 100-byte.

 - add separator and then the second 100-byte message to d->buf, and
   then run stripspace() over that 200-plus-byte.

 - add separator and then the third 100-byte message to d->buf, and
   then run stripspace() over that 300-plus-byte.

Shouldn't we be doing better?

> +		d->given = 1;

Do we understand what d->given flag represents?  My understanding is
that it becomes true only when any of the -m/-F/-c/-C options are given
to tell the command what message to use, so that we can automatically
open the editor to ask for the message when nothing is given.

So, I suspect that

	d->given = !!messages->nr;

at the beginning of the function, or

	d->given = !!d->buf.len;

may be equivalent[*], instead of setting it once every iteration?

	Side note: The latter is slightly more strict, as giving a
	run of empty strings with the default separator would result
	in an empty d->buf and d->given will become false.

> +	}
> +}

This helper is not parsing, but just processing after the whole
thing was parsed.  "parse_messages" -> "concatenate_messages" or
something, perhaps?

Now d->given is set in parse_reuse_arg() and parse_reedit_arg(),
because -c/-C is another source of a paragraph.  Shouldn't the
paragraph taken by these options go in the message list to be
concatenated together with other messages, or are -c/-C incompatible
with -m/-F and we are OK with two separate and distinct behaviour?

>  static int parse_msg_arg(const struct option *opt, const char *arg, int unset)
>  {
> -	struct note_data *d = opt->value;
> +	struct string_list *msg = opt->value;
>  
>  	BUG_ON_OPT_NEG(unset);
>  
> -	if (d->buf.len)
> -		strbuf_addch(&d->buf, '\n');
> -	strbuf_addstr(&d->buf, arg);
> -	strbuf_stripspace(&d->buf, 0);
> -
> -	d->given = 1;
> +	string_list_append(msg, arg);
>  	return 0;
>  }

OK, this one now does not concatenate; just accumulates to the "msg"
string list.

>  static int parse_file_arg(const struct option *opt, const char *arg, int unset)
>  {
> -	struct note_data *d = opt->value;
> +	struct string_list *msg = opt->value;
> +	struct strbuf buf = STRBUF_INIT;
>  
>  	BUG_ON_OPT_NEG(unset);
>  
> -	if (d->buf.len)
> -		strbuf_addch(&d->buf, '\n');
>  	if (!strcmp(arg, "-")) {
> -		if (strbuf_read(&d->buf, 0, 1024) < 0)
> +		if (strbuf_read(&buf, 0, 1024) < 0)
>  			die_errno(_("cannot read '%s'"), arg);
> -	} else if (strbuf_read_file(&d->buf, arg, 1024) < 0)
> +	} else if (strbuf_read_file(&buf, arg, 1024) < 0)
>  		die_errno(_("could not open or read '%s'"), arg);
> -	strbuf_stripspace(&d->buf, 0);
>  
> -	d->given = 1;
> +	string_list_append(msg, buf.buf);
> +	strbuf_release(&buf);
>  	return 0;
>  }

Ditto.

> @@ -418,6 +438,8 @@ static int add(int argc, const char **argv, const char *prefix)
>  		OPT_BOOL(0, "allow-empty", &allow_empty,
>  			N_("allow storing empty note")),
>  		OPT__FORCE(&force, N_("replace existing notes"), PARSE_OPT_NOCOMPLETE),
> +		OPT_STRING(0, "separator", &separator, N_("separator"),
> +			N_("insert <paragraph-break> between paragraphs")),
>  		OPT_END()
>  	};
>  
> @@ -429,6 +451,7 @@ static int add(int argc, const char **argv, const char *prefix)
>  		usage_with_options(git_notes_add_usage, options);
>  	}
>  
> +	parse_messages(&messages, &d);

Yup, this one is "concatenate_messages".  We do not read the
existing one, we accumulated everything the user gave us in
messages, and concatenate them using the separator.  The result
is stored in d->buf.

I wonder why separator is not a parameter into the helper function?

In short, I think handling of -m/-F got vastly better from the
previous rounds in this version, but I am not sure about the only
other remaining "strbuf_addch(&d->buf, '\n')" in parse_reuse_arg().
I am inclined to say that that codepath should behave the same way,
but I didn't think it as deeply as you did, so...?

Thanks.