Merge branch 'jk/trailers-parse'

"git interpret-trailers" has been taught a "--parse" and a few
other options to make it easier for scripts to grab existing
trailer lines from a commit log message.

* jk/trailers-parse:
  doc/interpret-trailers: fix "the this" typo
  pretty: support normalization options for %(trailers)
  t4205: refactor %(trailers) tests
  pretty: move trailer formatting to trailer.c
  interpret-trailers: add --parse convenience option
  interpret-trailers: add an option to unfold values
  interpret-trailers: add an option to show only existing trailers
  interpret-trailers: add an option to show only the trailers
  trailer: put process_trailers() options into a struct
This commit is contained in:
Junio C Hamano 2017-08-26 22:55:04 -07:00
commit 06cf4f2d87
8 changed files with 314 additions and 52 deletions

View File

@ -3,24 +3,27 @@ git-interpret-trailers(1)
NAME NAME
---- ----
git-interpret-trailers - help add structured information into commit messages git-interpret-trailers - add or parse structured information in commit messages
SYNOPSIS SYNOPSIS
-------- --------
[verse] [verse]
'git interpret-trailers' [--in-place] [--trim-empty] [(--trailer <token>[(=|:)<value>])...] [<file>...] 'git interpret-trailers' [options] [(--trailer <token>[(=|:)<value>])...] [<file>...]
'git interpret-trailers' [options] [--parse] [<file>...]
DESCRIPTION DESCRIPTION
----------- -----------
Help adding 'trailers' lines, that look similar to RFC 822 e-mail Help parsing or adding 'trailers' lines, that look similar to RFC 822 e-mail
headers, at the end of the otherwise free-form part of a commit headers, at the end of the otherwise free-form part of a commit
message. message.
This command reads some patches or commit messages from either the This command reads some patches or commit messages from either the
<file> arguments or the standard input if no <file> is specified. Then <file> arguments or the standard input if no <file> is specified. If
this command applies the arguments passed using the `--trailer` `--parse` is specified, the output consists of the parsed trailers.
option, if any, to the commit message part of each input file. The
result is emitted on the standard output. Otherwise, this command applies the arguments passed using the
`--trailer` option, if any, to the commit message part of each input
file. The result is emitted on the standard output.
Some configuration variables control the way the `--trailer` arguments Some configuration variables control the way the `--trailer` arguments
are applied to each commit message and the way any existing trailer in are applied to each commit message and the way any existing trailer in
@ -103,6 +106,22 @@ OPTIONS
and applies to all '--trailer' options until the next occurrence of and applies to all '--trailer' options until the next occurrence of
'--if-missing' or '--no-if-missing'. '--if-missing' or '--no-if-missing'.
--only-trailers::
Output only the trailers, not any other parts of the input.
--only-input::
Output only trailers that exist in the input; do not add any
from the command-line or by following configured `trailer.*`
rules.
--unfold::
Remove any whitespace-continuation in trailers, so that each
trailer appears on a line by itself with its full content.
--parse::
A convenience alias for `--only-trailers --only-input
--unfold`.
CONFIGURATION VARIABLES CONFIGURATION VARIABLES
----------------------- -----------------------

View File

@ -205,7 +205,10 @@ endif::git-rev-list[]
- '%><(<N>)', '%><|(<N>)': similar to '% <(<N>)', '%<|(<N>)' - '%><(<N>)', '%><|(<N>)': similar to '% <(<N>)', '%<|(<N>)'
respectively, but padding both sides (i.e. the text is centered) respectively, but padding both sides (i.e. the text is centered)
- %(trailers): display the trailers of the body as interpreted by - %(trailers): display the trailers of the body as interpreted by
linkgit:git-interpret-trailers[1] linkgit:git-interpret-trailers[1]. If the `:only` option is given,
omit non-trailer lines from the trailer block. If the `:unfold`
option is given, behave as if interpret-trailer's `--unfold` option
was given. E.g., `%(trailers:only:unfold)` to do both.
NOTE: Some placeholders may depend on other options given to the NOTE: Some placeholders may depend on other options given to the
revision traversal engine. For example, the `%g*` reflog options will revision traversal engine. For example, the `%g*` reflog options will

View File

@ -73,15 +73,24 @@ static int option_parse_trailer(const struct option *opt,
return 0; return 0;
} }
static int parse_opt_parse(const struct option *opt, const char *arg,
int unset)
{
struct process_trailer_options *v = opt->value;
v->only_trailers = 1;
v->only_input = 1;
v->unfold = 1;
return 0;
}
int cmd_interpret_trailers(int argc, const char **argv, const char *prefix) int cmd_interpret_trailers(int argc, const char **argv, const char *prefix)
{ {
int in_place = 0; struct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT;
int trim_empty = 0;
LIST_HEAD(trailers); LIST_HEAD(trailers);
struct option options[] = { struct option options[] = {
OPT_BOOL(0, "in-place", &in_place, N_("edit files in place")), OPT_BOOL(0, "in-place", &opts.in_place, N_("edit files in place")),
OPT_BOOL(0, "trim-empty", &trim_empty, N_("trim empty trailers")), OPT_BOOL(0, "trim-empty", &opts.trim_empty, N_("trim empty trailers")),
OPT_CALLBACK(0, "where", NULL, N_("action"), OPT_CALLBACK(0, "where", NULL, N_("action"),
N_("where to place the new trailer"), option_parse_where), N_("where to place the new trailer"), option_parse_where),
@ -90,6 +99,11 @@ int cmd_interpret_trailers(int argc, const char **argv, const char *prefix)
OPT_CALLBACK(0, "if-missing", NULL, N_("action"), OPT_CALLBACK(0, "if-missing", NULL, N_("action"),
N_("action if trailer is missing"), option_parse_if_missing), N_("action if trailer is missing"), option_parse_if_missing),
OPT_BOOL(0, "only-trailers", &opts.only_trailers, N_("output only the trailers")),
OPT_BOOL(0, "only-input", &opts.only_input, N_("do not apply config rules")),
OPT_BOOL(0, "unfold", &opts.unfold, N_("join whitespace-continued values")),
{ OPTION_CALLBACK, 0, "parse", &opts, NULL, N_("set parsing options"),
PARSE_OPT_NOARG | PARSE_OPT_NONEG, parse_opt_parse },
OPT_CALLBACK(0, "trailer", &trailers, N_("trailer"), OPT_CALLBACK(0, "trailer", &trailers, N_("trailer"),
N_("trailer(s) to add"), option_parse_trailer), N_("trailer(s) to add"), option_parse_trailer),
OPT_END() OPT_END()
@ -98,14 +112,20 @@ int cmd_interpret_trailers(int argc, const char **argv, const char *prefix)
argc = parse_options(argc, argv, prefix, options, argc = parse_options(argc, argv, prefix, options,
git_interpret_trailers_usage, 0); git_interpret_trailers_usage, 0);
if (opts.only_input && !list_empty(&trailers))
usage_msg_opt(
_("--trailer with --only-input does not make sense"),
git_interpret_trailers_usage,
options);
if (argc) { if (argc) {
int i; int i;
for (i = 0; i < argc; i++) for (i = 0; i < argc; i++)
process_trailers(argv[i], in_place, trim_empty, &trailers); process_trailers(argv[i], &opts, &trailers);
} else { } else {
if (in_place) if (opts.in_place)
die(_("no input file given for in-place editing")); die(_("no input file given for in-place editing"));
process_trailers(NULL, in_place, trim_empty, &trailers); process_trailers(NULL, &opts, &trailers);
} }
new_trailers_clear(&trailers); new_trailers_clear(&trailers);

View File

@ -871,16 +871,6 @@ const char *format_subject(struct strbuf *sb, const char *msg,
return msg; return msg;
} }
static void format_trailers(struct strbuf *sb, const char *msg)
{
struct trailer_info info;
trailer_info_get(&info, msg);
strbuf_add(sb, info.trailer_start,
info.trailer_end - info.trailer_start);
trailer_info_release(&info);
}
static void parse_commit_message(struct format_commit_context *c) static void parse_commit_message(struct format_commit_context *c)
{ {
const char *msg = c->message + c->message_off; const char *msg = c->message + c->message_off;
@ -1074,6 +1064,7 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
const struct commit *commit = c->commit; const struct commit *commit = c->commit;
const char *msg = c->message; const char *msg = c->message;
struct commit_list *p; struct commit_list *p;
const char *arg;
int ch; int ch;
/* these are independent of the commit */ /* these are independent of the commit */
@ -1292,9 +1283,18 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
return 1; return 1;
} }
if (starts_with(placeholder, "(trailers)")) { if (skip_prefix(placeholder, "(trailers", &arg)) {
format_trailers(sb, msg + c->subject_off); struct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT;
return strlen("(trailers)"); while (*arg == ':') {
if (skip_prefix(arg, ":only", &arg))
opts.only_trailers = 1;
else if (skip_prefix(arg, ":unfold", &arg))
opts.unfold = 1;
}
if (*arg == ')') {
format_trailers_from_commit(sb, msg + c->subject_off, &opts);
return arg - placeholder + 1;
}
} }
return 0; /* unknown placeholder */ return 0; /* unknown placeholder */

View File

@ -539,25 +539,62 @@ cat >trailers <<EOF
Signed-off-by: A U Thor <author@example.com> Signed-off-by: A U Thor <author@example.com>
Acked-by: A U Thor <author@example.com> Acked-by: A U Thor <author@example.com>
[ v2 updated patch description ] [ v2 updated patch description ]
Signed-off-by: A U Thor <author@example.com> Signed-off-by: A U Thor
<author@example.com>
EOF EOF
test_expect_success 'pretty format %(trailers) shows trailers' ' unfold () {
perl -0pe 's/\n\s+/ /'
}
test_expect_success 'set up trailer tests' '
echo "Some contents" >trailerfile && echo "Some contents" >trailerfile &&
git add trailerfile && git add trailerfile &&
git commit -F - <<-EOF && git commit -F - <<-EOF
trailers: this commit message has trailers trailers: this commit message has trailers
This commit is a test commit with trailers at the end. We parse this This commit is a test commit with trailers at the end. We parse this
message and display the trailers using %bT message and display the trailers using %(trailers).
$(cat trailers) $(cat trailers)
EOF EOF
'
test_expect_success 'pretty format %(trailers) shows trailers' '
git log --no-walk --pretty="%(trailers)" >actual && git log --no-walk --pretty="%(trailers)" >actual &&
cat >expect <<-EOF && {
$(cat trailers) cat trailers &&
echo
} >expect &&
test_cmp expect actual
'
EOF test_expect_success '%(trailers:only) shows only "key: value" trailers' '
git log --no-walk --pretty="%(trailers:only)" >actual &&
{
grep -v patch.description <trailers &&
echo
} >expect &&
test_cmp expect actual
'
test_expect_success '%(trailers:unfold) unfolds trailers' '
git log --no-walk --pretty="%(trailers:unfold)" >actual &&
{
unfold <trailers &&
echo
} >expect &&
test_cmp expect actual
'
test_expect_success ':only and :unfold work together' '
git log --no-walk --pretty="%(trailers:only:unfold)" >actual &&
git log --no-walk --pretty="%(trailers:unfold:only)" >reverse &&
test_cmp actual reverse &&
{
grep -v patch.description <trailers | unfold &&
echo
} >expect &&
test_cmp expect actual test_cmp expect actual
' '

View File

@ -1341,4 +1341,80 @@ test_expect_success 'with cut line' '
test_cmp expected actual test_cmp expected actual
' '
test_expect_success 'only trailers' '
git config trailer.sign.command "echo config-value" &&
cat >expected <<-\EOF &&
existing: existing-value
sign: config-value
added: added-value
EOF
git interpret-trailers \
--trailer added:added-value \
--only-trailers >actual <<-\EOF &&
my subject
my body
existing: existing-value
EOF
test_cmp expected actual
'
test_expect_success 'only-trailers omits non-trailer in middle of block' '
git config trailer.sign.command "echo config-value" &&
cat >expected <<-\EOF &&
Signed-off-by: nobody <nobody@nowhere>
Signed-off-by: somebody <somebody@somewhere>
sign: config-value
EOF
git interpret-trailers --only-trailers >actual <<-\EOF &&
subject
it is important that the trailers below are signed-off-by
so that they meet the "25% trailers Git knows about" heuristic
Signed-off-by: nobody <nobody@nowhere>
this is not a trailer
Signed-off-by: somebody <somebody@somewhere>
EOF
test_cmp expected actual
'
test_expect_success 'only input' '
git config trailer.sign.command "echo config-value" &&
cat >expected <<-\EOF &&
existing: existing-value
EOF
git interpret-trailers \
--only-trailers --only-input >actual <<-\EOF &&
my subject
my body
existing: existing-value
EOF
test_cmp expected actual
'
test_expect_success 'unfold' '
cat >expected <<-\EOF &&
foo: continued across several lines
EOF
# pass through tr to make leading and trailing whitespace more obvious
tr _ " " <<-\EOF |
my subject
my body
foo:_
__continued
___across
____several
_____lines
___
EOF
git interpret-trailers --only-trailers --only-input --unfold >actual &&
test_cmp expected actual
'
test_done test_done

114
trailer.c
View File

@ -159,13 +159,15 @@ static void print_tok_val(FILE *outfile, const char *tok, const char *val)
fprintf(outfile, "%s%c %s\n", tok, separators[0], val); fprintf(outfile, "%s%c %s\n", tok, separators[0], val);
} }
static void print_all(FILE *outfile, struct list_head *head, int trim_empty) static void print_all(FILE *outfile, struct list_head *head,
const struct process_trailer_options *opts)
{ {
struct list_head *pos; struct list_head *pos;
struct trailer_item *item; struct trailer_item *item;
list_for_each(pos, head) { list_for_each(pos, head) {
item = list_entry(pos, struct trailer_item, list); item = list_entry(pos, struct trailer_item, list);
if (!trim_empty || strlen(item->value) > 0) if ((!opts->trim_empty || strlen(item->value) > 0) &&
(!opts->only_trailers || item->token))
print_tok_val(outfile, item->token, item->value); print_tok_val(outfile, item->token, item->value);
} }
} }
@ -910,9 +912,37 @@ static int ends_with_blank_line(const char *buf, size_t len)
return is_blank_line(buf + ll); return is_blank_line(buf + ll);
} }
static void unfold_value(struct strbuf *val)
{
struct strbuf out = STRBUF_INIT;
size_t i;
strbuf_grow(&out, val->len);
i = 0;
while (i < val->len) {
char c = val->buf[i++];
if (c == '\n') {
/* Collapse continuation down to a single space. */
while (i < val->len && isspace(val->buf[i]))
i++;
strbuf_addch(&out, ' ');
} else {
strbuf_addch(&out, c);
}
}
/* Empty lines may have left us with whitespace cruft at the edges */
strbuf_trim(&out);
/* output goes back to val as if we modified it in-place */
strbuf_swap(&out, val);
strbuf_release(&out);
}
static int process_input_file(FILE *outfile, static int process_input_file(FILE *outfile,
const char *str, const char *str,
struct list_head *head) struct list_head *head,
const struct process_trailer_options *opts)
{ {
struct trailer_info info; struct trailer_info info;
struct strbuf tok = STRBUF_INIT; struct strbuf tok = STRBUF_INIT;
@ -922,9 +952,10 @@ static int process_input_file(FILE *outfile,
trailer_info_get(&info, str); trailer_info_get(&info, str);
/* Print lines before the trailers as is */ /* Print lines before the trailers as is */
fwrite(str, 1, info.trailer_start - str, outfile); if (!opts->only_trailers)
fwrite(str, 1, info.trailer_start - str, outfile);
if (!info.blank_line_before_trailer) if (!opts->only_trailers && !info.blank_line_before_trailer)
fprintf(outfile, "\n"); fprintf(outfile, "\n");
for (i = 0; i < info.trailer_nr; i++) { for (i = 0; i < info.trailer_nr; i++) {
@ -936,10 +967,12 @@ static int process_input_file(FILE *outfile,
if (separator_pos >= 1) { if (separator_pos >= 1) {
parse_trailer(&tok, &val, NULL, trailer, parse_trailer(&tok, &val, NULL, trailer,
separator_pos); separator_pos);
if (opts->unfold)
unfold_value(&val);
add_trailer_item(head, add_trailer_item(head,
strbuf_detach(&tok, NULL), strbuf_detach(&tok, NULL),
strbuf_detach(&val, NULL)); strbuf_detach(&val, NULL));
} else { } else if (!opts->only_trailers) {
strbuf_addstr(&val, trailer); strbuf_addstr(&val, trailer);
strbuf_strip_suffix(&val, "\n"); strbuf_strip_suffix(&val, "\n");
add_trailer_item(head, add_trailer_item(head,
@ -993,11 +1026,11 @@ static FILE *create_in_place_tempfile(const char *file)
return outfile; return outfile;
} }
void process_trailers(const char *file, int in_place, int trim_empty, void process_trailers(const char *file,
const struct process_trailer_options *opts,
struct list_head *new_trailer_head) struct list_head *new_trailer_head)
{ {
LIST_HEAD(head); LIST_HEAD(head);
LIST_HEAD(arg_head);
struct strbuf sb = STRBUF_INIT; struct strbuf sb = STRBUF_INIT;
int trailer_end; int trailer_end;
FILE *outfile = stdout; FILE *outfile = stdout;
@ -1006,24 +1039,27 @@ void process_trailers(const char *file, int in_place, int trim_empty,
read_input_file(&sb, file); read_input_file(&sb, file);
if (in_place) if (opts->in_place)
outfile = create_in_place_tempfile(file); outfile = create_in_place_tempfile(file);
/* Print the lines before the trailers */ /* Print the lines before the trailers */
trailer_end = process_input_file(outfile, sb.buf, &head); trailer_end = process_input_file(outfile, sb.buf, &head, opts);
process_command_line_args(&arg_head, new_trailer_head); if (!opts->only_input) {
LIST_HEAD(arg_head);
process_command_line_args(&arg_head, new_trailer_head);
process_trailers_lists(&head, &arg_head);
}
process_trailers_lists(&head, &arg_head); print_all(outfile, &head, opts);
print_all(outfile, &head, trim_empty);
free_all(&head); free_all(&head);
/* Print the lines after the trailers as is */ /* Print the lines after the trailers as is */
fwrite(sb.buf + trailer_end, 1, sb.len - trailer_end, outfile); if (!opts->only_trailers)
fwrite(sb.buf + trailer_end, 1, sb.len - trailer_end, outfile);
if (in_place) if (opts->in_place)
if (rename_tempfile(&trailers_tempfile, file)) if (rename_tempfile(&trailers_tempfile, file))
die_errno(_("could not rename temporary file to %s"), file); die_errno(_("could not rename temporary file to %s"), file);
@ -1080,3 +1116,49 @@ void trailer_info_release(struct trailer_info *info)
free(info->trailers[i]); free(info->trailers[i]);
free(info->trailers); free(info->trailers);
} }
static void format_trailer_info(struct strbuf *out,
const struct trailer_info *info,
const struct process_trailer_options *opts)
{
int i;
/* If we want the whole block untouched, we can take the fast path. */
if (!opts->only_trailers && !opts->unfold) {
strbuf_add(out, info->trailer_start,
info->trailer_end - info->trailer_start);
return;
}
for (i = 0; i < info->trailer_nr; i++) {
char *trailer = info->trailers[i];
int separator_pos = find_separator(trailer, separators);
if (separator_pos >= 1) {
struct strbuf tok = STRBUF_INIT;
struct strbuf val = STRBUF_INIT;
parse_trailer(&tok, &val, NULL, trailer, separator_pos);
if (opts->unfold)
unfold_value(&val);
strbuf_addf(out, "%s: %s\n", tok.buf, val.buf);
strbuf_release(&tok);
strbuf_release(&val);
} else if (!opts->only_trailers) {
strbuf_addstr(out, trailer);
}
}
}
void format_trailers_from_commit(struct strbuf *out, const char *msg,
const struct process_trailer_options *opts)
{
struct trailer_info info;
trailer_info_get(&info, msg);
format_trailer_info(out, &info, opts);
trailer_info_release(&info);
}

View File

@ -63,11 +63,36 @@ struct new_trailer_item {
enum trailer_if_missing if_missing; enum trailer_if_missing if_missing;
}; };
void process_trailers(const char *file, int in_place, int trim_empty, struct process_trailer_options {
int in_place;
int trim_empty;
int only_trailers;
int only_input;
int unfold;
};
#define PROCESS_TRAILER_OPTIONS_INIT {0}
void process_trailers(const char *file,
const struct process_trailer_options *opts,
struct list_head *new_trailer_head); struct list_head *new_trailer_head);
void trailer_info_get(struct trailer_info *info, const char *str); void trailer_info_get(struct trailer_info *info, const char *str);
void trailer_info_release(struct trailer_info *info); void trailer_info_release(struct trailer_info *info);
/*
* Format the trailers from the commit msg "msg" into the strbuf "out".
* Note two caveats about "opts":
*
* - this is primarily a helper for pretty.c, and not
* all of the flags are supported.
*
* - this differs from process_trailers slightly in that we always format
* only the trailer block itself, even if the "only_trailers" option is not
* set.
*/
void format_trailers_from_commit(struct strbuf *out, const char *msg,
const struct process_trailer_options *opts);
#endif /* TRAILER_H */ #endif /* TRAILER_H */