Merge branch 'aw/pretty-trailers'
authorJunio C Hamano <gitster@pobox.com>
Thu, 7 Mar 2019 00:59:52 +0000 (09:59 +0900)
committerJunio C Hamano <gitster@pobox.com>
Thu, 7 Mar 2019 00:59:52 +0000 (09:59 +0900)
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

1  2 
Documentation/pretty-formats.txt
pretty.c
t/t4205-log-pretty-formats.sh
index 7bfffae7652815596dcc29962e67e22efcf3075b,4cfea4c9d69f23ea58db0406499e94c19cf38396..079598307a3897d913de030a4737ce2e43c1810d
@@@ -102,120 -102,158 +102,160 @@@ The title was >>t4119: test autocomputi
  +
  The placeholders are:
  
- - '%H': commit hash
- - '%h': abbreviated commit hash
- - '%T': tree hash
- - '%t': abbreviated tree hash
- - '%P': parent hashes
- - '%p': abbreviated parent hashes
- - '%an': author name
- - '%aN': author name (respecting .mailmap, see linkgit:git-shortlog[1]
-   or linkgit:git-blame[1])
- - '%ae': author email
- - '%aE': author email (respecting .mailmap, see
-   linkgit:git-shortlog[1] or linkgit:git-blame[1])
- - '%ad': author date (format respects --date= option)
- - '%aD': author date, RFC2822 style
- - '%ar': author date, relative
- - '%at': author date, UNIX timestamp
- - '%ai': author date, ISO 8601-like format
- - '%aI': author date, strict ISO 8601 format
- - '%cn': committer name
- - '%cN': committer name (respecting .mailmap, see
-   linkgit:git-shortlog[1] or linkgit:git-blame[1])
- - '%ce': committer email
- - '%cE': committer email (respecting .mailmap, see
-   linkgit:git-shortlog[1] or linkgit:git-blame[1])
- - '%cd': committer date (format respects --date= option)
- - '%cD': committer date, RFC2822 style
- - '%cr': committer date, relative
- - '%ct': committer date, UNIX timestamp
- - '%ci': committer date, ISO 8601-like format
- - '%cI': committer date, strict ISO 8601 format
- - '%d': ref names, like the --decorate option of linkgit:git-log[1]
- - '%D': ref names without the " (", ")" wrapping.
- - '%S': ref name given on the command line by which the commit was reached
-   (like `git log --source`), only works with `git log`
- - '%e': encoding
- - '%s': subject
- - '%f': sanitized subject line, suitable for a filename
- - '%b': body
- - '%B': raw body (unwrapped subject and body)
+ - Placeholders that expand to a single literal character:
+ '%n':: newline
+ '%%':: a raw '%'
+ '%x00':: print a byte from a hex code
+ - Placeholders that affect formatting of later placeholders:
+ '%Cred':: switch color to red
+ '%Cgreen':: switch color to green
+ '%Cblue':: switch color to blue
+ '%Creset':: reset color
+ '%C(...)':: color specification, as described under Values in the
+           "CONFIGURATION FILE" section of linkgit:git-config[1].  By
+           default, colors are shown only when enabled for log output
+           (by `color.diff`, `color.ui`, or `--color`, and respecting
+           the `auto` settings of the former if we are going to a
+           terminal). `%C(auto,...)` is accepted as a historical
+           synonym for the default (e.g., `%C(auto,red)`). Specifying
 -          `%C(always,...) will show the colors even when color is
++          `%C(always,...)` will show the colors even when color is
+           not otherwise enabled (though consider just using
+           `--color=always` to enable color for the whole output,
+           including this format and anything else git might color).
+           `auto` alone (i.e. `%C(auto)`) will turn on auto coloring
+           on the next placeholders until the color is switched
+           again.
+ '%m':: left (`<`), right (`>`) or boundary (`-`) mark
+ '%w([<w>[,<i1>[,<i2>]]])':: switch line wrapping, like the -w option of
+                           linkgit:git-shortlog[1].
+ '%<(<N>[,trunc|ltrunc|mtrunc])':: make the next placeholder take at
+                                 least N columns, padding spaces on
+                                 the right if necessary.  Optionally
+                                 truncate at the beginning (ltrunc),
+                                 the middle (mtrunc) or the end
+                                 (trunc) if the output is longer than
+                                 N columns.  Note that truncating
+                                 only works correctly with N >= 2.
+ '%<|(<N>)':: make the next placeholder take at least until Nth
+            columns, padding spaces on the right if necessary
+ '%>(<N>)', '%>|(<N>)':: similar to '%<(<N>)', '%<|(<N>)' respectively,
+                       but padding spaces on the left
+ '%>>(<N>)', '%>>|(<N>)':: similar to '%>(<N>)', '%>|(<N>)'
+                         respectively, except that if the next
+                         placeholder takes more spaces than given and
+                         there are spaces on its left, use those
+                         spaces
+ '%><(<N>)', '%><|(<N>)':: similar to '%<(<N>)', '%<|(<N>)'
+                         respectively, but padding both sides
+                         (i.e. the text is centered)
+ - Placeholders that expand to information extracted from the commit:
+ '%H':: commit hash
+ '%h':: abbreviated commit hash
+ '%T':: tree hash
+ '%t':: abbreviated tree hash
+ '%P':: parent hashes
+ '%p':: abbreviated parent hashes
+ '%an':: author name
+ '%aN':: author name (respecting .mailmap, see linkgit:git-shortlog[1]
+       or linkgit:git-blame[1])
+ '%ae':: author email
+ '%aE':: author email (respecting .mailmap, see linkgit:git-shortlog[1]
+       or linkgit:git-blame[1])
+ '%ad':: author date (format respects --date= option)
+ '%aD':: author date, RFC2822 style
+ '%ar':: author date, relative
+ '%at':: author date, UNIX timestamp
+ '%ai':: author date, ISO 8601-like format
+ '%aI':: author date, strict ISO 8601 format
+ '%cn':: committer name
+ '%cN':: committer name (respecting .mailmap, see
+       linkgit:git-shortlog[1] or linkgit:git-blame[1])
+ '%ce':: committer email
+ '%cE':: committer email (respecting .mailmap, see
+       linkgit:git-shortlog[1] or linkgit:git-blame[1])
+ '%cd':: committer date (format respects --date= option)
+ '%cD':: committer date, RFC2822 style
+ '%cr':: committer date, relative
+ '%ct':: committer date, UNIX timestamp
+ '%ci':: committer date, ISO 8601-like format
+ '%cI':: committer date, strict ISO 8601 format
+ '%d':: ref names, like the --decorate option of linkgit:git-log[1]
+ '%D':: ref names without the " (", ")" wrapping.
++'%S':: ref name given on the command line by which the commit was reached
++       (like `git log --source`), only works with `git log`
+ '%e':: encoding
+ '%s':: subject
+ '%f':: sanitized subject line, suitable for a filename
+ '%b':: body
+ '%B':: raw body (unwrapped subject and body)
  ifndef::git-rev-list[]
- '%N': commit notes
'%N':: commit notes
  endif::git-rev-list[]
- - '%GG': raw verification message from GPG for a signed commit
- - '%G?': show "G" for a good (valid) signature,
-   "B" for a bad signature,
-   "U" for a good signature with unknown validity,
-   "X" for a good signature that has expired,
-   "Y" for a good signature made by an expired key,
-   "R" for a good signature made by a revoked key,
-   "E" if the signature cannot be checked (e.g. missing key)
-   and "N" for no signature
- - '%GS': show the name of the signer for a signed commit
- - '%GK': show the key used to sign a signed commit
- - '%GF': show the fingerprint of the key used to sign a signed commit
- - '%GP': show the fingerprint of the primary key whose subkey was used
-   to sign a signed commit
- - '%gD': reflog selector, e.g., `refs/stash@{1}` or
-   `refs/stash@{2 minutes ago`}; the format follows the rules described
-   for the `-g` option. The portion before the `@` is the refname as
-   given on the command line (so `git log -g refs/heads/master` would
-   yield `refs/heads/master@{0}`).
- - '%gd': shortened reflog selector; same as `%gD`, but the refname
-   portion is shortened for human readability (so `refs/heads/master`
-   becomes just `master`).
- - '%gn': reflog identity name
- - '%gN': reflog identity name (respecting .mailmap, see
-   linkgit:git-shortlog[1] or linkgit:git-blame[1])
- - '%ge': reflog identity email
- - '%gE': reflog identity email (respecting .mailmap, see
-   linkgit:git-shortlog[1] or linkgit:git-blame[1])
- - '%gs': reflog subject
- - '%Cred': switch color to red
- - '%Cgreen': switch color to green
- - '%Cblue': switch color to blue
- - '%Creset': reset color
- - '%C(...)': color specification, as described under Values in the
-   "CONFIGURATION FILE" section of linkgit:git-config[1].
-   By default, colors are shown only when enabled for log output (by
-   `color.diff`, `color.ui`, or `--color`, and respecting the `auto`
-   settings of the former if we are going to a terminal). `%C(auto,...)`
-   is accepted as a historical synonym for the default (e.g.,
-   `%C(auto,red)`). Specifying `%C(always,...)` will show the colors
-   even when color is not otherwise enabled (though consider
-   just using `--color=always` to enable color for the whole output,
-   including this format and anything else git might color).  `auto`
-   alone (i.e. `%C(auto)`) will turn on auto coloring on the next
-   placeholders until the color is switched again.
- - '%m': left (`<`), right (`>`) or boundary (`-`) mark
- - '%n': newline
- - '%%': a raw '%'
- - '%x00': print a byte from a hex code
- - '%w([<w>[,<i1>[,<i2>]]])': switch line wrapping, like the -w option of
-   linkgit:git-shortlog[1].
- - '%<(<N>[,trunc|ltrunc|mtrunc])': make the next placeholder take at
-   least N columns, padding spaces on the right if necessary.
-   Optionally truncate at the beginning (ltrunc), the middle (mtrunc)
-   or the end (trunc) if the output is longer than N columns.
-   Note that truncating only works correctly with N >= 2.
- - '%<|(<N>)': make the next placeholder take at least until Nth
-   columns, padding spaces on the right if necessary
- - '%>(<N>)', '%>|(<N>)': similar to '%<(<N>)', '%<|(<N>)'
-   respectively, but padding spaces on the left
- - '%>>(<N>)', '%>>|(<N>)': similar to '%>(<N>)', '%>|(<N>)'
-   respectively, except that if the next placeholder takes more spaces
-   than given and there are spaces on its left, use those spaces
- - '%><(<N>)', '%><|(<N>)': similar to '%<(<N>)', '%<|(<N>)'
-   respectively, but padding both sides (i.e. the text is centered)
- - %(trailers[:options]): display the trailers of the body as interpreted
-   by linkgit:git-interpret-trailers[1]. The `trailers` string may be
-   followed by a colon and zero or more comma-separated options. 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.
+ '%GG':: raw verification message from GPG for a signed commit
+ '%G?':: show "G" for a good (valid) signature,
+       "B" for a bad signature,
+       "U" for a good signature with unknown validity,
+       "X" for a good signature that has expired,
+       "Y" for a good signature made by an expired key,
+       "R" for a good signature made by a revoked key,
+       "E" if the signature cannot be checked (e.g. missing key)
+       and "N" for no signature
+ '%GS':: show the name of the signer for a signed commit
+ '%GK':: show the key used to sign a signed commit
+ '%GF':: show the fingerprint of the key used to sign a signed commit
+ '%GP':: show the fingerprint of the primary key whose subkey was used
+       to sign a signed commit
+ '%gD':: reflog selector, e.g., `refs/stash@{1}` or `refs/stash@{2
+       minutes ago`}; the format follows the rules described for the
+       `-g` option. The portion before the `@` is the refname as
+       given on the command line (so `git log -g refs/heads/master`
+       would yield `refs/heads/master@{0}`).
+ '%gd':: shortened reflog selector; same as `%gD`, but the refname
+       portion is shortened for human readability (so
+       `refs/heads/master` becomes just `master`).
+ '%gn':: reflog identity name
+ '%gN':: reflog identity name (respecting .mailmap, see
+       linkgit:git-shortlog[1] or linkgit:git-blame[1])
+ '%ge':: reflog identity email
+ '%gE':: reflog identity email (respecting .mailmap, see
+       linkgit:git-shortlog[1] or linkgit:git-blame[1])
+ '%gs':: reflog subject
+ '%(trailers[:options])':: display the trailers of the body as
+                         interpreted by
+                         linkgit:git-interpret-trailers[1]. The
+                         `trailers` string may be followed by a colon
+                         and zero or more comma-separated options:
+ ** 'key=<K>': only show trailers with specified key. Matching is done
+    case-insensitively and trailing colon is optional. If option is
+    given multiple times trailer lines matching any of the keys are
+    shown. This option automatically enables the `only` option so that
+    non-trailer lines in the trailer block are hidden. If that is not
+    desired it can be disabled with `only=false`.  E.g.,
+    `%(trailers:key=Reviewed-by)` shows trailer lines with key
+    `Reviewed-by`.
+ ** 'only[=val]': select whether non-trailer lines from the trailer
+    block should be included. The `only` keyword may optionally be
+    followed by an equal sign and one of `true`, `on`, `yes` to omit or
+    `false`, `off`, `no` to show the non-trailer lines. If option is
+    given without value it is enabled. If given multiple times the last
+    value is used.
+ ** 'separator=<SEP>': specify a separator inserted between trailer
+    lines. When this option is not given each trailer line is
+    terminated with a line feed character. The string SEP may contain
+    the literal formatting codes described above. To use comma as
+    separator one must use `%x2C` as it would otherwise be parsed as
+    next option. If separator option is given multiple times only the
+    last one is used. E.g., `%(trailers:key=Ticket,separator=%x2C )`
+    shows all trailer lines whose key is "Ticket" separated by a comma
+    and a space.
+ ** 'unfold[=val]': make it behave as if interpret-trailer's `--unfold`
+    option was given. In same way as to for `only` it can be followed
+    by an equal sign and explicit value. E.g.,
+    `%(trailers:only,unfold=true)` unfolds and shows all trailer lines.
+ ** 'valueonly[=val]': skip over the key part of the trailer line and only
+    show the value part. Also this optionally allows explicit value.
  
  NOTE: Some placeholders may depend on other options given to the
  revision traversal engine. For example, the `%g*` reflog options will
diff --combined pretty.c
index 0ab45d10d702373e212fb36e4fa66feb26669ee7,6d58b21affd3b7d888aa9eae124df8d1bbea0a6c..d2995b13edf4c64965321882677f28847f47e21c
+++ b/pretty.c
@@@ -595,15 -595,14 +595,15 @@@ static char *replace_encoding_header(ch
        return strbuf_detach(&tmp, NULL);
  }
  
 -const char *logmsg_reencode(const struct commit *commit,
 -                          char **commit_encoding,
 -                          const char *output_encoding)
 +const char *repo_logmsg_reencode(struct repository *r,
 +                               const struct commit *commit,
 +                               char **commit_encoding,
 +                               const char *output_encoding)
  {
        static const char *utf8 = "UTF-8";
        const char *use_encoding;
        char *encoding;
 -      const char *msg = get_commit_buffer(commit, NULL);
 +      const char *msg = repo_get_commit_buffer(r, commit, NULL);
        char *out;
  
        if (!output_encoding || !*output_encoding) {
                 * the cached copy from get_commit_buffer, we need to duplicate it
                 * to avoid munging the cached copy.
                 */
 -              if (msg == get_cached_commit_buffer(the_repository, commit, NULL))
 +              if (msg == get_cached_commit_buffer(r, commit, NULL))
                        out = xstrdup(msg);
                else
                        out = (char *)msg;
                 */
                out = reencode_string(msg, output_encoding, use_encoding);
                if (out)
 -                      unuse_commit_buffer(commit, msg);
 +                      repo_unuse_commit_buffer(r, commit, msg);
        }
  
        /*
@@@ -1057,13 -1056,26 +1057,26 @@@ static size_t parse_padding_placeholder
        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;
        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)
        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)")) {
                         */
                        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;
                load_ref_decorations(NULL, DECORATE_SHORT_REFS);
                format_decorations_extended(sb, commit, c->auto_color, "", ", ", "");
                return 1;
 +      case 'S':               /* tag/branch like --source */
 +              if (!(c->pretty_ctx->rev && c->pretty_ctx->rev->sources))
 +                      return 0;
 +              slot = revision_sources_at(c->pretty_ctx->rev->sources, commit);
 +              if (!(slot && *slot))
 +                      return 0;
 +              strbuf_addstr(sb, *slot);
 +              return 1;
        case 'g':               /* reflog info */
                switch(placeholder[1]) {
                case 'd':       /* reflog selector */
  
        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 */
@@@ -1508,9 -1575,6 +1585,9 @@@ static size_t userformat_want_item(stru
        case 'N':
                w->notes = 1;
                break;
 +      case 'S':
 +              w->source = 1;
 +              break;
        }
        return 0;
  }
@@@ -1528,10 -1592,9 +1605,10 @@@ void userformat_find_requirements(cons
        strbuf_release(&dummy);
  }
  
 -void format_commit_message(const struct commit *commit,
 -                         const char *format, struct strbuf *sb,
 -                         const struct pretty_print_context *pretty_ctx)
 +void repo_format_commit_message(struct repository *r,
 +                              const struct commit *commit,
 +                              const char *format, struct strbuf *sb,
 +                              const struct pretty_print_context *pretty_ctx)
  {
        struct format_commit_context context;
        const char *output_enc = pretty_ctx->output_encoding;
         * convert a commit message to UTF-8 first
         * as far as 'format_commit_item' assumes it in UTF-8
         */
 -      context.message = logmsg_reencode(commit,
 -                                        &context.commit_encoding,
 -                                        utf8);
 +      context.message = repo_logmsg_reencode(r, commit,
 +                                             &context.commit_encoding,
 +                                             utf8);
  
        strbuf_expand(sb, format, format_commit_item, &context);
        rewrap_message_tail(sb, &context, 0, 0, 0);
        }
  
        free(context.commit_encoding);
 -      unuse_commit_buffer(commit, context.message);
 +      repo_unuse_commit_buffer(r, commit, context.message);
  }
  
  static void pp_header(struct pretty_print_context *pp,
index 7df8c3d4ec04ef86e12277e5debfeb8f8f825521,99f50fa401cbddc7df508d55b3f876728b97047f..f42a69faa2fde46d958577d10c50d2a6379ebf60
@@@ -578,6 -578,24 +578,24 @@@ test_expect_success '%(trailers:only) s
        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 +616,105 @@@ test_expect_success ':only and :unfold 
        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
        test_cmp expect actual
  '
  
 +test_expect_success 'set up %S tests' '
 +      git checkout --orphan source-a &&
 +      test_commit one &&
 +      test_commit two &&
 +      git checkout -b source-b HEAD^ &&
 +      test_commit three
 +'
 +
 +test_expect_success 'log --format=%S paints branch names' '
 +      cat >expect <<-\EOF &&
 +      source-b
 +      source-a
 +      source-b
 +      EOF
 +      git log --format=%S source-a source-b >actual &&
 +      test_cmp expect actual
 +'
 +
 +test_expect_success 'log --format=%S paints tag names' '
 +      git tag -m tagged source-tag &&
 +      cat >expect <<-\EOF &&
 +      source-tag
 +      source-a
 +      source-tag
 +      EOF
 +      git log --format=%S source-tag source-a >actual &&
 +      test_cmp expect actual
 +'
 +
 +test_expect_success 'log --format=%S paints symmetric ranges' '
 +      cat >expect <<-\EOF &&
 +      source-b
 +      source-a
 +      EOF
 +      git log --format=%S source-a...source-b >actual &&
 +      test_cmp expect actual
 +'
 +
 +test_expect_success '%S in git log --format works with other placeholders (part 1)' '
 +      git log --format="source-b %h" source-b >expect &&
 +      git log --format="%S %h" source-b >actual &&
 +      test_cmp expect actual
 +'
 +
 +test_expect_success '%S in git log --format works with other placeholders (part 2)' '
 +      git log --format="%h source-b" source-b >expect &&
 +      git log --format="%h %S" source-b >actual &&
 +      test_cmp expect actual
 +'
 +
  test_done