Commit 42977bf5 authored by Junio C Hamano's avatar Junio C Hamano

Merge branch 'aw/pretty-trailers'

The %(trailers) formatter in "git log --format=..."  now allows to
optionally pick trailers selectively by keyword, show only values,
etc.

* aw/pretty-trailers:
  pretty: add support for separator option in %(trailers)
  strbuf: separate callback for strbuf_expand:ing literals
  pretty: add support for "valueonly" option in %(trailers)
  pretty: allow showing specific trailers
  pretty: single return path in %(trailers) handling
  pretty: allow %(trailers) options with explicit value
  doc: group pretty-format.txt placeholders descriptions
parents 54b469b9 0b691d86
This diff is collapsed.
......@@ -1057,13 +1057,26 @@ static size_t parse_padding_placeholder(struct strbuf *sb,
return 0;
}
static int match_placeholder_arg(const char *to_parse, const char *candidate,
const char **end)
static int match_placeholder_arg_value(const char *to_parse, const char *candidate,
const char **end, const char **valuestart,
size_t *valuelen)
{
const char *p;
if (!(skip_prefix(to_parse, candidate, &p)))
return 0;
if (valuestart) {
if (*p == '=') {
*valuestart = p + 1;
*valuelen = strcspn(*valuestart, ",)");
p = *valuestart + *valuelen;
} else {
if (*p != ',' && *p != ')')
return 0;
*valuestart = NULL;
*valuelen = 0;
}
}
if (*p == ',') {
*end = p + 1;
return 1;
......@@ -1075,6 +1088,47 @@ static int match_placeholder_arg(const char *to_parse, const char *candidate,
return 0;
}
static int match_placeholder_bool_arg(const char *to_parse, const char *candidate,
const char **end, int *val)
{
const char *argval;
char *strval;
size_t arglen;
int v;
if (!match_placeholder_arg_value(to_parse, candidate, end, &argval, &arglen))
return 0;
if (!argval) {
*val = 1;
return 1;
}
strval = xstrndup(argval, arglen);
v = git_parse_maybe_bool(strval);
free(strval);
if (v == -1)
return 0;
*val = v;
return 1;
}
static int format_trailer_match_cb(const struct strbuf *key, void *ud)
{
const struct string_list *list = ud;
const struct string_list_item *item;
for_each_string_list_item (item, list) {
if (key->len == (uintptr_t)item->util &&
!strncasecmp(item->string, key->buf, key->len))
return 1;
}
return 0;
}
static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
const char *placeholder,
void *context)
......@@ -1084,10 +1138,14 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
const char *msg = c->message;
struct commit_list *p;
const char *arg;
int ch;
size_t res;
char **slot;
/* these are independent of the commit */
res = strbuf_expand_literal_cb(sb, placeholder, NULL);
if (res)
return res;
switch (placeholder[0]) {
case 'C':
if (starts_with(placeholder + 1, "(auto)")) {
......@@ -1106,16 +1164,6 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
*/
return ret;
}
case 'n': /* newline */
strbuf_addch(sb, '\n');
return 1;
case 'x':
/* %x00 == NUL, %x0a == LF, etc. */
ch = hex2chr(placeholder + 1);
if (ch < 0)
return 0;
strbuf_addch(sb, ch);
return 3;
case 'w':
if (placeholder[1] == '(') {
unsigned long width = 0, indent1 = 0, indent2 = 0;
......@@ -1322,24 +1370,53 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
if (skip_prefix(placeholder, "(trailers", &arg)) {
struct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT;
struct string_list filter_list = STRING_LIST_INIT_NODUP;
struct strbuf sepbuf = STRBUF_INIT;
size_t ret = 0;
opts.no_divider = 1;
if (*arg == ':') {
arg++;
for (;;) {
if (match_placeholder_arg(arg, "only", &arg))
const char *argval;
size_t arglen;
if (match_placeholder_arg_value(arg, "key", &arg, &argval, &arglen)) {
uintptr_t len = arglen;
if (!argval)
goto trailer_out;
if (len && argval[len - 1] == ':')
len--;
string_list_append(&filter_list, argval)->util = (char *)len;
opts.filter = format_trailer_match_cb;
opts.filter_data = &filter_list;
opts.only_trailers = 1;
else if (match_placeholder_arg(arg, "unfold", &arg))
opts.unfold = 1;
else
} else if (match_placeholder_arg_value(arg, "separator", &arg, &argval, &arglen)) {
char *fmt;
strbuf_reset(&sepbuf);
fmt = xstrndup(argval, arglen);
strbuf_expand(&sepbuf, fmt, strbuf_expand_literal_cb, NULL);
free(fmt);
opts.separator = &sepbuf;
} else if (!match_placeholder_bool_arg(arg, "only", &arg, &opts.only_trailers) &&
!match_placeholder_bool_arg(arg, "unfold", &arg, &opts.unfold) &&
!match_placeholder_bool_arg(arg, "valueonly", &arg, &opts.value_only))
break;
}
}
if (*arg == ')') {
format_trailers_from_commit(sb, msg + c->subject_off, &opts);
return arg - placeholder + 1;
ret = arg - placeholder + 1;
}
trailer_out:
string_list_clear(&filter_list, 0);
strbuf_release(&sepbuf);
return ret;
}
return 0; /* unknown placeholder */
......
......@@ -380,6 +380,27 @@ void strbuf_expand(struct strbuf *sb, const char *format, expand_fn_t fn,
}
}
size_t strbuf_expand_literal_cb(struct strbuf *sb,
const char *placeholder,
void *context)
{
int ch;
switch (placeholder[0]) {
case 'n': /* newline */
strbuf_addch(sb, '\n');
return 1;
case 'x':
/* %x00 == NUL, %x0a == LF, etc. */
ch = hex2chr(placeholder + 1);
if (ch < 0)
return 0;
strbuf_addch(sb, ch);
return 3;
}
return 0;
}
size_t strbuf_expand_dict_cb(struct strbuf *sb, const char *placeholder,
void *context)
{
......
......@@ -320,6 +320,14 @@ void strbuf_expand(struct strbuf *sb,
expand_fn_t fn,
void *context);
/**
* Used as callback for `strbuf_expand` to only expand literals
* (i.e. %n and %xNN). The context argument is ignored.
*/
size_t strbuf_expand_literal_cb(struct strbuf *sb,
const char *placeholder,
void *context);
/**
* Used as callback for `strbuf_expand()`, expects an array of
* struct strbuf_expand_dict_entry as context, i.e. pairs of
......
......@@ -578,6 +578,24 @@ test_expect_success '%(trailers:only) shows only "key: value" trailers' '
test_cmp expect actual
'
test_expect_success '%(trailers:only=yes) shows only "key: value" trailers' '
git log --no-walk --pretty=format:"%(trailers:only=yes)" >actual &&
grep -v patch.description <trailers >expect &&
test_cmp expect actual
'
test_expect_success '%(trailers:only=no) shows all trailers' '
git log --no-walk --pretty=format:"%(trailers:only=no)" >actual &&
cat trailers >expect &&
test_cmp expect actual
'
test_expect_success '%(trailers:only=no,only=true) shows only "key: value" trailers' '
git log --no-walk --pretty=format:"%(trailers:only=yes)" >actual &&
grep -v patch.description <trailers >expect &&
test_cmp expect actual
'
test_expect_success '%(trailers:unfold) unfolds trailers' '
git log --no-walk --pretty="%(trailers:unfold)" >actual &&
{
......@@ -598,6 +616,105 @@ test_expect_success ':only and :unfold work together' '
test_cmp expect actual
'
test_expect_success 'pretty format %(trailers:key=foo) shows that trailer' '
git log --no-walk --pretty="format:%(trailers:key=Acked-by)" >actual &&
echo "Acked-by: A U Thor <author@example.com>" >expect &&
test_cmp expect actual
'
test_expect_success 'pretty format %(trailers:key=foo) is case insensitive' '
git log --no-walk --pretty="format:%(trailers:key=AcKed-bY)" >actual &&
echo "Acked-by: A U Thor <author@example.com>" >expect &&
test_cmp expect actual
'
test_expect_success 'pretty format %(trailers:key=foo:) trailing colon also works' '
git log --no-walk --pretty="format:%(trailers:key=Acked-by:)" >actual &&
echo "Acked-by: A U Thor <author@example.com>" >expect &&
test_cmp expect actual
'
test_expect_success 'pretty format %(trailers:key=foo) multiple keys' '
git log --no-walk --pretty="format:%(trailers:key=Acked-by:,key=Signed-off-By)" >actual &&
grep -v patch.description <trailers >expect &&
test_cmp expect actual
'
test_expect_success '%(trailers:key=nonexistant) becomes empty' '
git log --no-walk --pretty="x%(trailers:key=Nacked-by)x" >actual &&
echo "xx" >expect &&
test_cmp expect actual
'
test_expect_success '%(trailers:key=foo) handles multiple lines even if folded' '
git log --no-walk --pretty="format:%(trailers:key=Signed-Off-by)" >actual &&
grep -v patch.description <trailers | grep -v Acked-by >expect &&
test_cmp expect actual
'
test_expect_success '%(trailers:key=foo,unfold) properly unfolds' '
git log --no-walk --pretty="format:%(trailers:key=Signed-Off-by,unfold)" >actual &&
unfold <trailers | grep Signed-off-by >expect &&
test_cmp expect actual
'
test_expect_success 'pretty format %(trailers:key=foo,only=no) also includes nontrailer lines' '
git log --no-walk --pretty="format:%(trailers:key=Acked-by,only=no)" >actual &&
{
echo "Acked-by: A U Thor <author@example.com>" &&
grep patch.description <trailers
} >expect &&
test_cmp expect actual
'
test_expect_success '%(trailers:key) without value is error' '
git log --no-walk --pretty="tformat:%(trailers:key)" >actual &&
echo "%(trailers:key)" >expect &&
test_cmp expect actual
'
test_expect_success '%(trailers:key=foo,valueonly) shows only value' '
git log --no-walk --pretty="format:%(trailers:key=Acked-by,valueonly)" >actual &&
echo "A U Thor <author@example.com>" >expect &&
test_cmp expect actual
'
test_expect_success 'pretty format %(trailers:separator) changes separator' '
git log --no-walk --pretty=format:"X%(trailers:separator=%x00,unfold)X" >actual &&
printf "XSigned-off-by: A U Thor <author@example.com>\0Acked-by: A U Thor <author@example.com>\0[ v2 updated patch description ]\0Signed-off-by: A U Thor <author@example.com>X" >expect &&
test_cmp expect actual
'
test_expect_success 'pretty format %(trailers) combining separator/key/valueonly' '
git commit --allow-empty -F - <<-\EOF &&
Important fix
The fix is explained here
Closes: #1234
EOF
git commit --allow-empty -F - <<-\EOF &&
Another fix
The fix is explained here
Closes: #567
Closes: #890
EOF
git commit --allow-empty -F - <<-\EOF &&
Does not close any tickets
EOF
git log --pretty="%s% (trailers:separator=%x2c%x20,key=Closes,valueonly)" HEAD~3.. >actual &&
test_write_lines \
"Does not close any tickets" \
"Another fix #567, #890" \
"Important fix #1234" >expect &&
test_cmp expect actual
'
test_expect_success 'trailer parsing not fooled by --- line' '
git commit --allow-empty -F - <<-\EOF &&
this is the subject
......
......@@ -1129,10 +1129,11 @@ static void format_trailer_info(struct strbuf *out,
const struct trailer_info *info,
const struct process_trailer_options *opts)
{
size_t origlen = out->len;
size_t i;
/* If we want the whole block untouched, we can take the fast path. */
if (!opts->only_trailers && !opts->unfold) {
if (!opts->only_trailers && !opts->unfold && !opts->filter && !opts->separator) {
strbuf_add(out, info->trailer_start,
info->trailer_end - info->trailer_start);
return;
......@@ -1147,15 +1148,29 @@ static void format_trailer_info(struct strbuf *out,
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);
if (!opts->filter || opts->filter(&tok, opts->filter_data)) {
if (opts->unfold)
unfold_value(&val);
if (opts->separator && out->len != origlen)
strbuf_addbuf(out, opts->separator);
if (!opts->value_only)
strbuf_addf(out, "%s: ", tok.buf);
strbuf_addbuf(out, &val);
if (!opts->separator)
strbuf_addch(out, '\n');
}
strbuf_release(&tok);
strbuf_release(&val);
} else if (!opts->only_trailers) {
if (opts->separator && out->len != origlen) {
strbuf_addbuf(out, opts->separator);
}
strbuf_addstr(out, trailer);
if (opts->separator) {
strbuf_rtrim(out);
}
}
}
......
......@@ -72,6 +72,10 @@ struct process_trailer_options {
int only_input;
int unfold;
int no_divider;
int value_only;
const struct strbuf *separator;
int (*filter)(const struct strbuf *, void *);
void *filter_data;
};
#define PROCESS_TRAILER_OPTIONS_INIT {0}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment