notes.c: introduce '--separator=<paragraph-break>' option

When adding new notes or appending to an existing notes, we will
insert a blank line between the paragraphs, like:

     $ git notes add -m foo -m bar
     $ git notes show HEAD
     foo

     bar

The default behavour sometimes is not enough, the user may want
to use a custom delimiter between paragraphs, like when
specifying '-m', '-F', '-C', '-c' options. So this commit
introduce a new '--separator' option for 'git notes add' and
'git notes append', for example when executing:

    $ git notes add -m foo -m bar --separator="-"
    $ git notes show HEAD
    foo
    -
    bar

a newline is added to the value given to --separator if it
does not end with one already. So when executing:

      $ git notes add -m foo -m bar --separator="-"
and
      $ export LF="
      "
      $ git notes add -m foo -m bar --separator="-$LF"

Both the two exections produce the same result.

The reason we use a "strbuf" array to concat but not "string_list", is
that the binary file content may contain '\0' in the middle, this will
cause the corrupt result if using a string to save.

Signed-off-by: Teng Long <dyroneteng@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
maint
Teng Long 2023-05-27 15:57:51 +08:00 committed by Junio C Hamano
parent 59587049e2
commit 90bc19b3ae
3 changed files with 227 additions and 29 deletions

View File

@ -9,9 +9,9 @@ SYNOPSIS
-------- --------
[verse] [verse]
'git notes' [list [<object>]] 'git notes' [list [<object>]]
'git notes' add [-f] [--allow-empty] [-F <file> | -m <msg> | (-c | -C) <object>] [<object>] 'git notes' add [-f] [--allow-empty] [--separator=<paragraph-break>] [-F <file> | -m <msg> | (-c | -C) <object>] [<object>]
'git notes' copy [-f] ( --stdin | <from-object> [<to-object>] ) 'git notes' copy [-f] ( --stdin | <from-object> [<to-object>] )
'git notes' append [--allow-empty] [-F <file> | -m <msg> | (-c | -C) <object>] [<object>] 'git notes' append [--allow-empty] [--separator=<paragraph-break>] [-F <file> | -m <msg> | (-c | -C) <object>] [<object>]
'git notes' edit [--allow-empty] [<object>] 'git notes' edit [--allow-empty] [<object>]
'git notes' show [<object>] 'git notes' show [<object>]
'git notes' merge [-v | -q] [-s <strategy> ] <notes-ref> 'git notes' merge [-v | -q] [-s <strategy> ] <notes-ref>
@ -65,7 +65,9 @@ add::
However, if you're using `add` interactively (using an editor However, if you're using `add` interactively (using an editor
to supply the notes contents), then - instead of aborting - to supply the notes contents), then - instead of aborting -
the existing notes will be opened in the editor (like the `edit` the existing notes will be opened in the editor (like the `edit`
subcommand). 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.


copy:: copy::
Copy the notes for the first object onto the second object (defaults to Copy the notes for the first object onto the second object (defaults to
@ -85,8 +87,12 @@ corresponding <to-object>. (The optional `<rest>` is ignored so that
the command can read the input given to the `post-rewrite` hook.) the command can read the input given to the `post-rewrite` hook.)


append:: append::
Append to the notes of an existing object (defaults to HEAD). Append new message(s) given by `-m` or `-F` options to an
Creates a new notes object if needed. 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.


edit:: edit::
Edit the notes for a given object (defaults to HEAD). Edit the notes for a given object (defaults to HEAD).
@ -159,6 +165,11 @@ OPTIONS
Allow an empty note object to be stored. The default behavior is Allow an empty note object to be stored. The default behavior is
to automatically remove empty notes. to automatically remove empty notes.


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

--ref <ref>:: --ref <ref>::
Manipulate the notes tree in <ref>. This overrides Manipulate the notes tree in <ref>. This overrides
`GIT_NOTES_REF` and the "core.notesRef" configuration. The ref `GIT_NOTES_REF` and the "core.notesRef" configuration. The ref

View File

@ -8,6 +8,7 @@
*/ */


#include "cache.h" #include "cache.h"
#include "alloc.h"
#include "config.h" #include "config.h"
#include "builtin.h" #include "builtin.h"
#include "gettext.h" #include "gettext.h"
@ -27,11 +28,12 @@
#include "worktree.h" #include "worktree.h"
#include "write-or-die.h" #include "write-or-die.h"


static char *separator = "\n";
static const char * const git_notes_usage[] = { static const char * const git_notes_usage[] = {
N_("git notes [--ref <notes-ref>] [list [<object>]]"), N_("git notes [--ref <notes-ref>] [list [<object>]]"),
N_("git notes [--ref <notes-ref>] add [-f] [--allow-empty] [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]"), N_("git notes [--ref <notes-ref>] add [-f] [--allow-empty] [--separator=<paragraph-break>] [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]"),
N_("git notes [--ref <notes-ref>] copy [-f] <from-object> <to-object>"), N_("git notes [--ref <notes-ref>] copy [-f] <from-object> <to-object>"),
N_("git notes [--ref <notes-ref>] append [--allow-empty] [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]"), N_("git notes [--ref <notes-ref>] append [--allow-empty] [--separator=<paragraph-break>] [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]"),
N_("git notes [--ref <notes-ref>] edit [--allow-empty] [<object>]"), N_("git notes [--ref <notes-ref>] edit [--allow-empty] [<object>]"),
N_("git notes [--ref <notes-ref>] show [<object>]"), N_("git notes [--ref <notes-ref>] show [<object>]"),
N_("git notes [--ref <notes-ref>] merge [-v | -q] [-s <strategy>] <notes-ref>"), N_("git notes [--ref <notes-ref>] merge [-v | -q] [-s <strategy>] <notes-ref>"),
@ -99,11 +101,19 @@ static const char * const git_notes_get_ref_usage[] = {
static const char note_template[] = static const char note_template[] =
N_("Write/edit the notes for the following object:"); N_("Write/edit the notes for the following object:");


struct note_msg {
int stripspace;
struct strbuf buf;
};

struct note_data { struct note_data {
int given; int given;
int use_editor; int use_editor;
char *edit_path; char *edit_path;
struct strbuf buf; struct strbuf buf;
struct note_msg **messages;
size_t msg_nr;
size_t msg_alloc;
}; };


static void free_note_data(struct note_data *d) static void free_note_data(struct note_data *d)
@ -113,6 +123,12 @@ static void free_note_data(struct note_data *d)
free(d->edit_path); free(d->edit_path);
} }
strbuf_release(&d->buf); strbuf_release(&d->buf);

while (d->msg_nr--) {
strbuf_release(&d->messages[d->msg_nr]->buf);
free(d->messages[d->msg_nr]);
}
free(d->messages);
} }


static int list_each_note(const struct object_id *object_oid, static int list_each_note(const struct object_id *object_oid,
@ -213,65 +229,97 @@ static void write_note_data(struct note_data *d, struct object_id *oid)
} }
} }


static void insert_separator(struct strbuf *message, size_t pos)
{
size_t sep_len = strlen(separator);
if (sep_len && separator[sep_len - 1] == '\n')
strbuf_insertstr(message, pos, separator);
else
strbuf_insertf(message, pos, "%s%s", separator, "\n");
}

static void concat_messages(struct note_data *d)
{
struct strbuf msg = STRBUF_INIT;

size_t i;
for (i = 0; i < d->msg_nr ; i++) {
if (d->buf.len)
insert_separator(&d->buf, d->buf.len);
strbuf_add(&msg, d->messages[i]->buf.buf, d->messages[i]->buf.len);
strbuf_addbuf(&d->buf, &msg);
if (d->messages[i]->stripspace)
strbuf_stripspace(&d->buf, 0);
strbuf_reset(&msg);
}
strbuf_release(&msg);
}

static int parse_msg_arg(const struct option *opt, const char *arg, int unset) static int parse_msg_arg(const struct option *opt, const char *arg, int unset)
{ {
struct note_data *d = opt->value; struct note_data *d = opt->value;
struct note_msg *msg = xmalloc(sizeof(*msg));


BUG_ON_OPT_NEG(unset); BUG_ON_OPT_NEG(unset);


if (d->buf.len) strbuf_init(&msg->buf, strlen(arg));
strbuf_addch(&d->buf, '\n'); strbuf_addstr(&msg->buf, arg);
strbuf_addstr(&d->buf, arg); ALLOC_GROW_BY(d->messages, d->msg_nr, 1, d->msg_alloc);
strbuf_stripspace(&d->buf, 0); d->messages[d->msg_nr - 1] = msg;

msg->stripspace = 1;
d->given = 1;
return 0; return 0;
} }


static int parse_file_arg(const struct option *opt, const char *arg, int unset) static int parse_file_arg(const struct option *opt, const char *arg, int unset)
{ {
struct note_data *d = opt->value; struct note_data *d = opt->value;
struct note_msg *msg = xmalloc(sizeof(*msg));


BUG_ON_OPT_NEG(unset); BUG_ON_OPT_NEG(unset);


if (d->buf.len) strbuf_init(&msg->buf , 0);
strbuf_addch(&d->buf, '\n');
if (!strcmp(arg, "-")) { if (!strcmp(arg, "-")) {
if (strbuf_read(&d->buf, 0, 1024) < 0) if (strbuf_read(&msg->buf, 0, 1024) < 0)
die_errno(_("cannot read '%s'"), arg); die_errno(_("cannot read '%s'"), arg);
} else if (strbuf_read_file(&d->buf, arg, 1024) < 0) } else if (strbuf_read_file(&msg->buf, arg, 1024) < 0)
die_errno(_("could not open or read '%s'"), arg); die_errno(_("could not open or read '%s'"), arg);
strbuf_stripspace(&d->buf, 0);


d->given = 1; ALLOC_GROW_BY(d->messages, d->msg_nr, 1, d->msg_alloc);
d->messages[d->msg_nr - 1] = msg;
msg->stripspace = 1;
return 0; return 0;
} }


static int parse_reuse_arg(const struct option *opt, const char *arg, int unset) static int parse_reuse_arg(const struct option *opt, const char *arg, int unset)
{ {
struct note_data *d = opt->value; struct note_data *d = opt->value;
char *buf; struct note_msg *msg = xmalloc(sizeof(*msg));
char *value;
struct object_id object; struct object_id object;
enum object_type type; enum object_type type;
unsigned long len; unsigned long len;


BUG_ON_OPT_NEG(unset); BUG_ON_OPT_NEG(unset);


if (d->buf.len) strbuf_init(&msg->buf, 0);
strbuf_addch(&d->buf, '\n');

if (repo_get_oid(the_repository, arg, &object)) if (repo_get_oid(the_repository, arg, &object))
die(_("failed to resolve '%s' as a valid ref."), arg); die(_("failed to resolve '%s' as a valid ref."), arg);
if (!(buf = repo_read_object_file(the_repository, &object, &type, &len))) if (!(value = repo_read_object_file(the_repository, &object, &type, &len)))
die(_("failed to read object '%s'."), arg); die(_("failed to read object '%s'."), arg);
if (type != OBJ_BLOB) { if (type != OBJ_BLOB) {
free(buf); strbuf_release(&msg->buf);
free(value);
free(msg);
die(_("cannot read note data from non-blob object '%s'."), arg); die(_("cannot read note data from non-blob object '%s'."), arg);
} }
strbuf_add(&d->buf, buf, len);
free(buf);


d->given = 1; strbuf_add(&msg->buf, value, len);
free(value);

msg->buf.len = len;
ALLOC_GROW_BY(d->messages, d->msg_nr, 1, d->msg_alloc);
d->messages[d->msg_nr - 1] = msg;
msg->stripspace = 0;
return 0; return 0;
} }


@ -406,6 +454,7 @@ static int add(int argc, const char **argv, const char *prefix)
struct object_id object, new_note; struct object_id object, new_note;
const struct object_id *note; const struct object_id *note;
struct note_data d = { .buf = STRBUF_INIT }; struct note_data d = { .buf = STRBUF_INIT };

struct option options[] = { struct option options[] = {
OPT_CALLBACK_F('m', "message", &d, N_("message"), OPT_CALLBACK_F('m', "message", &d, N_("message"),
N_("note contents as a string"), PARSE_OPT_NONEG, N_("note contents as a string"), PARSE_OPT_NONEG,
@ -422,6 +471,8 @@ static int add(int argc, const char **argv, const char *prefix)
OPT_BOOL(0, "allow-empty", &allow_empty, OPT_BOOL(0, "allow-empty", &allow_empty,
N_("allow storing empty note")), N_("allow storing empty note")),
OPT__FORCE(&force, N_("replace existing notes"), PARSE_OPT_NOCOMPLETE), 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() OPT_END()
}; };


@ -433,6 +484,10 @@ static int add(int argc, const char **argv, const char *prefix)
usage_with_options(git_notes_add_usage, options); usage_with_options(git_notes_add_usage, options);
} }


if (d.msg_nr)
concat_messages(&d);
d.given = !!d.buf.len;

object_ref = argc > 1 ? argv[1] : "HEAD"; object_ref = argc > 1 ? argv[1] : "HEAD";


if (repo_get_oid(the_repository, object_ref, &object)) if (repo_get_oid(the_repository, object_ref, &object))
@ -587,6 +642,8 @@ static int append_edit(int argc, const char **argv, const char *prefix)
parse_reuse_arg), parse_reuse_arg),
OPT_BOOL(0, "allow-empty", &allow_empty, OPT_BOOL(0, "allow-empty", &allow_empty,
N_("allow storing empty note")), N_("allow storing empty note")),
OPT_STRING(0, "separator", &separator, N_("separator"),
N_("insert <paragraph-break> between paragraphs")),
OPT_END() OPT_END()
}; };
int edit = !strcmp(argv[0], "edit"); int edit = !strcmp(argv[0], "edit");
@ -600,6 +657,10 @@ static int append_edit(int argc, const char **argv, const char *prefix)
usage_with_options(usage, options); usage_with_options(usage, options);
} }


if (d.msg_nr)
concat_messages(&d);
d.given = !!d.buf.len;

if (d.given && edit) if (d.given && edit)
fprintf(stderr, _("The -m/-F/-c/-C options have been deprecated " fprintf(stderr, _("The -m/-F/-c/-C options have been deprecated "
"for the 'edit' subcommand.\n" "for the 'edit' subcommand.\n"
@ -623,7 +684,7 @@ static int append_edit(int argc, const char **argv, const char *prefix)
&type, &size); &type, &size);


if (d.buf.len && prev_buf && size) if (d.buf.len && prev_buf && size)
strbuf_insertstr(&d.buf, 0, "\n"); insert_separator(&d.buf, 0);
if (prev_buf && size) if (prev_buf && size)
strbuf_insert(&d.buf, 0, prev_buf, size); strbuf_insert(&d.buf, 0, prev_buf, size);
free(prev_buf); free(prev_buf);

View File

@ -362,6 +362,7 @@ test_expect_success 'do not create empty note with -m ""' '
' '


test_expect_success 'create note with combination of -m and -F' ' test_expect_success 'create note with combination of -m and -F' '
test_when_finished git notes remove HEAD &&
cat >expect-combine_m_and_F <<-EOF && cat >expect-combine_m_and_F <<-EOF &&
foo foo


@ -380,6 +381,25 @@ test_expect_success 'create note with combination of -m and -F' '
test_cmp expect-combine_m_and_F actual test_cmp expect-combine_m_and_F actual
' '


test_expect_success 'create note with combination of -m and -F and --separator' '
cat >expect-combine_m_and_F <<-\EOF &&
foo
-------
xyzzy
-------
bar
-------
zyxxy
-------
baz
EOF
echo "xyzzy" >note_a &&
echo "zyxxy" >note_b &&
git notes add -m "foo" -F note_a -m "bar" -F note_b -m "baz" --separator "-------" &&
git notes show >actual &&
test_cmp expect-combine_m_and_F actual
'

test_expect_success 'remove note with "git notes remove"' ' test_expect_success 'remove note with "git notes remove"' '
git notes remove HEAD^ && git notes remove HEAD^ &&
git notes remove && git notes remove &&
@ -521,6 +541,85 @@ test_expect_success 'listing non-existing notes fails' '
test_must_be_empty actual test_must_be_empty actual
' '


test_expect_success 'append: specify an empty separator' '
test_when_finished git notes remove HEAD &&
cat >expect <<-\EOF &&
notes-1

notes-2
EOF

git notes add -m "notes-1" &&
git notes append --separator="" -m "notes-2" &&
git notes show >actual &&
test_cmp expect actual
'

test_expect_success 'append: specify separator with line break' '
test_when_finished git notes remove HEAD &&
cat >expect <<-\EOF &&
notes-1
-------
notes-2
EOF

git notes add -m "notes-1" &&
git notes append --separator="-------$LF" -m "notes-2" &&
git notes show >actual &&
test_cmp expect actual
'

test_expect_success 'append: specify separator without line break' '
test_when_finished git notes remove HEAD &&
cat >expect <<-\EOF &&
notes-1
-------
notes-2
EOF

git notes add -m "notes-1" &&
git notes append --separator="-------" -m "notes-2" &&
git notes show >actual &&
test_cmp expect actual
'

test_expect_success 'append: specify separator with multiple messages' '
test_when_finished git notes remove HEAD &&
cat >expect <<-\EOF &&
notes-1
-------
notes-2
-------
notes-3
EOF

git notes add -m "notes-1" &&
git notes append --separator="-------" -m "notes-2" -m "notes-3" &&
git notes show >actual &&
test_cmp expect actual
'

test_expect_success 'append note with combination of -m and -F and --separator' '
test_when_finished git notes remove HEAD &&
cat >expect-combine_m_and_F <<-\EOF &&
m-notes-1
-------
f-notes-1
-------
m-notes-2
-------
f-notes-2
-------
m-notes-3
EOF

echo "f-notes-1" >note_a &&
echo "f-notes-2" >note_b &&
git notes append -m "m-notes-1" -F note_a -m "m-notes-2" -F note_b -m "m-notes-3" --separator "-------" &&
git notes show >actual &&
test_cmp expect-combine_m_and_F actual
'

test_expect_success 'append to existing note with "git notes append"' ' test_expect_success 'append to existing note with "git notes append"' '
cat >expect <<-EOF && cat >expect <<-EOF &&
Initial set of notes Initial set of notes
@ -818,6 +917,33 @@ test_expect_success 'create note from blob with "git notes add -C" reuses blob i
test_cmp blob actual test_cmp blob actual
' '


test_expect_success 'create note from blob with "-C", also specify "-m", "-F" and "--separator"' '
# 8th will be reuseed in following tests, so rollback when the test is done
test_when_finished "git notes remove && git notes add -C $(cat blob)" &&
commit=$(git rev-parse HEAD) &&
cat >expect <<-EOF &&
commit $commit
Author: A U Thor <author@example.com>
Date: Thu Apr 7 15:20:13 2005 -0700

${indent}8th

Notes:
${indent}This is a blob object
${indent}-------
${indent}This is created by -m
${indent}-------
${indent}This is created by -F
EOF

git notes remove &&
echo "This is a blob object" | git hash-object -w --stdin >blob &&
echo "This is created by -F" >note_a &&
git notes add -C $(cat blob) -m "This is created by -m" -F note_a --separator="-------" &&
git log -1 >actual &&
test_cmp expect actual
'

test_expect_success 'create note from other note with "git notes add -c"' ' test_expect_success 'create note from other note with "git notes add -c"' '
test_commit 9th && test_commit 9th &&
commit=$(git rev-parse HEAD) && commit=$(git rev-parse HEAD) &&