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
----
git-interpret-trailers - help add structured information into commit messages
git-interpret-trailers - add or parse structured information in commit messages
SYNOPSIS
--------
[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
-----------
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
message.
This command reads some patches or commit messages from either the
<file> arguments or the standard input if no <file> is specified. Then
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.
<file> arguments or the standard input if no <file> is specified. If
`--parse` is specified, the output consists of the parsed trailers.
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
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
'--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
-----------------------

View File

@ -205,7 +205,10 @@ endif::git-rev-list[]
- '%><(<N>)', '%><|(<N>)': similar to '% <(<N>)', '%<|(<N>)'
respectively, but padding both sides (i.e. the text is centered)
- %(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
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;
}
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 in_place = 0;
int trim_empty = 0;
struct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT;
LIST_HEAD(trailers);
struct option options[] = {
OPT_BOOL(0, "in-place", &in_place, N_("edit files in place")),
OPT_BOOL(0, "trim-empty", &trim_empty, N_("trim empty trailers")),
OPT_BOOL(0, "in-place", &opts.in_place, N_("edit files in place")),
OPT_BOOL(0, "trim-empty", &opts.trim_empty, N_("trim empty trailers")),
OPT_CALLBACK(0, "where", NULL, N_("action"),
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"),
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"),
N_("trailer(s) to add"), option_parse_trailer),
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,
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) {
int i;
for (i = 0; i < argc; i++)
process_trailers(argv[i], in_place, trim_empty, &trailers);
process_trailers(argv[i], &opts, &trailers);
} else {
if (in_place)
if (opts.in_place)
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);

View File

@ -871,16 +871,6 @@ const char *format_subject(struct strbuf *sb, const char *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)
{
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 char *msg = c->message;
struct commit_list *p;
const char *arg;
int ch;
/* these are independent of the commit */
@ -1292,9 +1283,18 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
return 1;
}
if (starts_with(placeholder, "(trailers)")) {
format_trailers(sb, msg + c->subject_off);
return strlen("(trailers)");
if (skip_prefix(placeholder, "(trailers", &arg)) {
struct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT;
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 */

View File

@ -539,25 +539,62 @@ cat >trailers <<EOF
Signed-off-by: A U Thor <author@example.com>
Acked-by: A U Thor <author@example.com>
[ v2 updated patch description ]
Signed-off-by: A U Thor <author@example.com>
Signed-off-by: A U Thor
<author@example.com>
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 &&
git add trailerfile &&
git commit -F - <<-EOF &&
git commit -F - <<-EOF
trailers: this commit message has trailers
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)
EOF
'
test_expect_success 'pretty format %(trailers) shows trailers' '
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
'

View File

@ -1341,4 +1341,80 @@ test_expect_success 'with cut line' '
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

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);
}
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 trailer_item *item;
list_for_each(pos, head) {
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);
}
}
@ -910,9 +912,37 @@ static int ends_with_blank_line(const char *buf, size_t len)
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,
const char *str,
struct list_head *head)
struct list_head *head,
const struct process_trailer_options *opts)
{
struct trailer_info info;
struct strbuf tok = STRBUF_INIT;
@ -922,9 +952,10 @@ static int process_input_file(FILE *outfile,
trailer_info_get(&info, str);
/* 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");
for (i = 0; i < info.trailer_nr; i++) {
@ -936,10 +967,12 @@ static int process_input_file(FILE *outfile,
if (separator_pos >= 1) {
parse_trailer(&tok, &val, NULL, trailer,
separator_pos);
if (opts->unfold)
unfold_value(&val);
add_trailer_item(head,
strbuf_detach(&tok, NULL),
strbuf_detach(&val, NULL));
} else {
} else if (!opts->only_trailers) {
strbuf_addstr(&val, trailer);
strbuf_strip_suffix(&val, "\n");
add_trailer_item(head,
@ -993,11 +1026,11 @@ static FILE *create_in_place_tempfile(const char *file)
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)
{
LIST_HEAD(head);
LIST_HEAD(arg_head);
struct strbuf sb = STRBUF_INIT;
int trailer_end;
FILE *outfile = stdout;
@ -1006,24 +1039,27 @@ void process_trailers(const char *file, int in_place, int trim_empty,
read_input_file(&sb, file);
if (in_place)
if (opts->in_place)
outfile = create_in_place_tempfile(file);
/* 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, trim_empty);
print_all(outfile, &head, opts);
free_all(&head);
/* 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))
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);
}
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;
};
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);
void trailer_info_get(struct trailer_info *info, const char *str);
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 */