Merge branch 'jk/trailers-parse' into next
authorJunio C Hamano <gitster@pobox.com>
Sun, 20 Aug 2017 06:07:18 +0000 (23:07 -0700)
committerJunio C Hamano <gitster@pobox.com>
Sun, 20 Aug 2017 06:07:18 +0000 (23:07 -0700)
"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:
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

1  2 
Documentation/git-interpret-trailers.txt
Documentation/pretty-formats.txt
builtin/interpret-trailers.c
pretty.c
t/t7513-interpret-trailers.sh
trailer.c
trailer.h
index 0ef93204f14a3d7409696a045afe01537843ce38,1df8aabf51e4cc8c2fedd66b8c45a384dfa8d8a1..30386e887b3681df435fd980c69ca10b51d082a3
@@@ -3,24 -3,27 +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, the 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
@@@ -80,29 -83,22 +83,45 @@@ OPTION
        trailer to the input messages. See the description of this
        command.
  
 +--where <placement>::
 +--no-where::
 +      Specify where all new trailers will be added.  A setting
 +      provided with '--where' overrides all configuration variables
 +      and applies to all '--trailer' options until the next occurrence of
 +      '--where' or '--no-where'.
 +
 +--if-exists <action>::
 +--no-if-exists::
 +      Specify what action will be performed when there is already at
 +      least one trailer with the same <token> in the message.  A setting
 +      provided with '--if-exists' overrides all configuration variables
 +      and applies to all '--trailer' options until the next occurrence of
 +      '--if-exists' or '--no-if-exists'.
 +
 +--if-missing <action>::
 +--no-if-missing::
 +      Specify what action will be performed when there is no other
 +      trailer with the same <token> in the message.  A setting
 +      provided with '--if-missing' overrides all configuration variables
 +      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
  -----------------------
  
@@@ -193,8 -189,8 +212,8 @@@ trailer.<token>.where:
        configuration variable and it overrides what is specified by
        that option for trailers with the specified <token>.
  
 -trailer.<token>.ifexist::
 -      This option takes the same values as the 'trailer.ifexist'
 +trailer.<token>.ifexists::
 +      This option takes the same values as the 'trailer.ifexists'
        configuration variable and it overrides what is specified by
        that option for trailers with the specified <token>.
  
index 973d19606b63c314786de4965160b2c285987947,efa67a716e5013e3855d33eb1d6d1dcc494a00f6..d433d50f8104dd5c86e49c844047208fe455b6a0
@@@ -173,17 -173,13 +173,17 @@@ endif::git-rev-list[
  - '%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];
 -  adding `auto,` at the beginning (e.g. `%C(auto,red)`) will emit
 -  color only when colors are 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). `auto` alone (i.e.
 -  `%C(auto)`) will turn on auto coloring on the next placeholders
 -  until the color is switched again.
 +  "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 '%'
  - '%><(<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
index d7cbaf60f92b86d8634027d33f50574e19fcb21d,555111a078b1a912fd31f820ca2b7f983310f66d..b742539d4de20361bb43bcb4dc33cfc9165c42fb
@@@ -16,99 -16,54 +16,119 @@@ static const char * const git_interpret
        NULL
  };
  
 +static enum trailer_where where;
 +static enum trailer_if_exists if_exists;
 +static enum trailer_if_missing if_missing;
 +
 +static int option_parse_where(const struct option *opt,
 +                            const char *arg, int unset)
 +{
 +      return trailer_set_where(&where, arg);
 +}
 +
 +static int option_parse_if_exists(const struct option *opt,
 +                                const char *arg, int unset)
 +{
 +      return trailer_set_if_exists(&if_exists, arg);
 +}
 +
 +static int option_parse_if_missing(const struct option *opt,
 +                                 const char *arg, int unset)
 +{
 +      return trailer_set_if_missing(&if_missing, arg);
 +}
 +
 +static void new_trailers_clear(struct list_head *trailers)
 +{
 +      struct list_head *pos, *tmp;
 +      struct new_trailer_item *item;
 +
 +      list_for_each_safe(pos, tmp, trailers) {
 +              item = list_entry(pos, struct new_trailer_item, list);
 +              list_del(pos);
 +              free(item);
 +      }
 +}
 +
 +static int option_parse_trailer(const struct option *opt,
 +                                 const char *arg, int unset)
 +{
 +      struct list_head *trailers = opt->value;
 +      struct new_trailer_item *item;
 +
 +      if (unset) {
 +              new_trailers_clear(trailers);
 +              return 0;
 +      }
 +
 +      if (!arg)
 +              return -1;
 +
 +      item = xmalloc(sizeof(*item));
 +      item->text = arg;
 +      item->where = where;
 +      item->if_exists = if_exists;
 +      item->if_missing = if_missing;
 +      list_add_tail(&item->list, trailers);
 +      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;
 -      struct string_list trailers = STRING_LIST_INIT_NODUP;
 +      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),
 +              OPT_CALLBACK(0, "if-exists", NULL, N_("action"),
 +                           N_("action if trailer already exists"), option_parse_if_exists),
 +              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_STRING_LIST(0, "trailer", &trailers, N_("trailer"),
 -                              N_("trailer(s) to add")),
 +              OPT_CALLBACK(0, "trailer", &trailers, N_("trailer"),
 +                              N_("trailer(s) to add"), option_parse_trailer),
                OPT_END()
        };
  
        argc = parse_options(argc, argv, prefix, options,
                             git_interpret_trailers_usage, 0);
  
 -      if (opts.only_input && trailers.nr)
++      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);
        }
  
 -      string_list_clear(&trailers, 0);
 +      new_trailers_clear(&trailers);
  
        return 0;
  }
diff --combined pretty.c
index 39cad5112b603df9c8ed927e738b2f7917da7522,0e23fe3c0559318985a07a004c2d908cd4e261ba..94eab5c89ee1a1fa696b9fc491a5846008a2a255
+++ b/pretty.c
@@@ -1,5 -1,4 +1,5 @@@
  #include "cache.h"
 +#include "config.h"
  #include "commit.h"
  #include "utf8.h"
  #include "diff.h"
@@@ -406,11 -405,11 +406,11 @@@ static void add_rfc2047(struct strbuf *
  const char *show_ident_date(const struct ident_split *ident,
                            const struct date_mode *mode)
  {
 -      unsigned long date = 0;
 +      timestamp_t date = 0;
        long tz = 0;
  
        if (ident->date_begin && ident->date_end)
 -              date = strtoul(ident->date_begin, NULL, 10);
 +              date = parse_timestamp(ident->date_begin, NULL, 10);
        if (date_overflows(date))
                date = 0;
        else {
@@@ -871,16 -870,6 +871,6 @@@ const char *format_subject(struct strbu
        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;
@@@ -947,7 -936,6 +937,7 @@@ static size_t parse_color(struct strbu
                          struct format_commit_context *c)
  {
        const char *rest = placeholder;
 +      const char *basic_color = NULL;
  
        if (placeholder[1] == '(') {
                const char *begin = placeholder + 2;
  
                if (!end)
                        return 0;
 +
                if (skip_prefix(begin, "auto,", &begin)) {
                        if (!want_color(c->pretty_ctx->color))
                                return end - placeholder + 1;
 +              } else if (skip_prefix(begin, "always,", &begin)) {
 +                      /* nothing to do; we do not respect want_color at all */
 +              } else {
 +                      /* the default is the same as "auto" */
 +                      if (!want_color(c->pretty_ctx->color))
 +                              return end - placeholder + 1;
                }
 +
                if (color_parse_mem(begin, end - begin, color) < 0)
                        die(_("unable to parse --pretty format"));
                strbuf_addstr(sb, color);
                return end - placeholder + 1;
        }
 +
 +      /*
 +       * We handle things like "%C(red)" above; for historical reasons, there
 +       * are a few colors that can be specified without parentheses (and
 +       * they cannot support things like "auto" or "always" at all).
 +       */
        if (skip_prefix(placeholder + 1, "red", &rest))
 -              strbuf_addstr(sb, GIT_COLOR_RED);
 +              basic_color = GIT_COLOR_RED;
        else if (skip_prefix(placeholder + 1, "green", &rest))
 -              strbuf_addstr(sb, GIT_COLOR_GREEN);
 +              basic_color = GIT_COLOR_GREEN;
        else if (skip_prefix(placeholder + 1, "blue", &rest))
 -              strbuf_addstr(sb, GIT_COLOR_BLUE);
 +              basic_color = GIT_COLOR_BLUE;
        else if (skip_prefix(placeholder + 1, "reset", &rest))
 -              strbuf_addstr(sb, GIT_COLOR_RESET);
 +              basic_color = GIT_COLOR_RESET;
 +
 +      if (basic_color && want_color(c->pretty_ctx->color))
 +              strbuf_addstr(sb, basic_color);
 +
        return rest - placeholder;
  }
  
@@@ -1074,6 -1044,7 +1064,7 @@@ static size_t format_commit_one(struct 
        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 */
  
        /* these depend on the commit */
        if (!commit->object.parsed)
 -              parse_object(commit->object.oid.hash);
 +              parse_object(&commit->object.oid);
  
        switch (placeholder[0]) {
        case 'H':               /* commit hash */
                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 */
index adbdf54f8dca744c6626131830db8e12b4c1f998,baf2feba9858de5591bfd1f7c3ba44bd356dd8ea..164719d1c9d3e76a08bbbeb968aaf535370db7d1
@@@ -681,36 -681,6 +681,36 @@@ test_expect_success 'using "where = bef
        test_cmp expected actual
  '
  
 +test_expect_success 'overriding configuration with "--where after"' '
 +      git config trailer.ack.where "before" &&
 +      cat complex_message_body >expected &&
 +      sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
 +              Fixes: Z
 +              Acked-by= Z
 +              Acked-by= Peff
 +              Reviewed-by: Z
 +              Signed-off-by: Z
 +      EOF
 +      git interpret-trailers --where after --trailer "ack: Peff" \
 +              complex_message >actual &&
 +      test_cmp expected actual
 +'
 +
 +test_expect_success 'using "where = before" with "--no-where"' '
 +      cat complex_message_body >expected &&
 +      sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
 +              Bug #42
 +              Fixes: Z
 +              Acked-by= Peff
 +              Acked-by= Z
 +              Reviewed-by: Z
 +              Signed-off-by: Z
 +      EOF
 +      git interpret-trailers --where after --no-where --trailer "ack: Peff" \
 +              --trailer "bug: 42" complex_message >actual &&
 +      test_cmp expected actual
 +'
 +
  test_expect_success 'using "where = after"' '
        git config trailer.ack.where "after" &&
        cat complex_message_body >expected &&
@@@ -977,23 -947,6 +977,23 @@@ test_expect_success 'using "ifExists = 
        test_cmp expected actual
  '
  
 +test_expect_success 'overriding configuration with "--if-exists replace"' '
 +      git config trailer.fix.key "Fixes: " &&
 +      git config trailer.fix.ifExists "add" &&
 +      cat complex_message_body >expected &&
 +      sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
 +              Bug #42
 +              Acked-by= Z
 +              Reviewed-by:
 +              Signed-off-by: Z
 +              Fixes: 22
 +      EOF
 +      git interpret-trailers --if-exists replace --trailer "review:" \
 +              --trailer "fix=53" --trailer "fix=22" --trailer "bug: 42" \
 +              <complex_message >actual &&
 +      test_cmp expected actual
 +'
 +
  test_expect_success 'using "ifExists = replace"' '
        git config trailer.fix.key "Fixes: " &&
        git config trailer.fix.ifExists "replace" &&
@@@ -1073,25 -1026,6 +1073,25 @@@ test_expect_success 'the default is "if
        test_cmp expected actual
  '
  
 +test_expect_success 'overriding configuration with "--if-missing doNothing"' '
 +      git config trailer.ifmissing "add" &&
 +      cat complex_message_body >expected &&
 +      sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
 +              Fixes: Z
 +              Acked-by= Z
 +              Acked-by= Junio
 +              Acked-by= Peff
 +              Reviewed-by:
 +              Signed-off-by: Z
 +      EOF
 +      git interpret-trailers --if-missing doNothing \
 +              --trailer "review:" --trailer "fix=53" \
 +              --trailer "cc=Linus" --trailer "ack: Junio" \
 +              --trailer "fix=22" --trailer "bug: 42" --trailer "ack: Peff" \
 +              <complex_message >actual &&
 +      test_cmp expected actual
 +'
 +
  test_expect_success 'when default "ifMissing" is "doNothing"' '
        git config trailer.ifmissing "doNothing" &&
        cat complex_message_body >expected &&
@@@ -1341,4 -1275,80 +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
diff --combined trailer.c
index d441cd9ac6f6778629bedb6e5e6f0296f0ec0584,6ec5505dc4c05e6874df92c84c8d34609821ac20..c30e3a0c0415db110bfce8d29514a8258647d7f7
+++ b/trailer.c
@@@ -1,5 -1,4 +1,5 @@@
  #include "cache.h"
 +#include "config.h"
  #include "string-list.h"
  #include "run-command.h"
  #include "commit.h"
   * Copyright (c) 2013, 2014 Christian Couder <chriscool@tuxfamily.org>
   */
  
 -enum action_where { WHERE_END, WHERE_AFTER, WHERE_BEFORE, WHERE_START };
 -enum action_if_exists { EXISTS_ADD_IF_DIFFERENT_NEIGHBOR, EXISTS_ADD_IF_DIFFERENT,
 -                      EXISTS_ADD, EXISTS_REPLACE, EXISTS_DO_NOTHING };
 -enum action_if_missing { MISSING_ADD, MISSING_DO_NOTHING };
 -
  struct conf_info {
        char *name;
        char *key;
        char *command;
 -      enum action_where where;
 -      enum action_if_exists if_exists;
 -      enum action_if_missing if_missing;
 +      enum trailer_where where;
 +      enum trailer_if_exists if_exists;
 +      enum trailer_if_missing if_missing;
  };
  
  static struct conf_info default_conf_info;
@@@ -58,7 -62,7 +58,7 @@@ static const char *git_generated_prefix
                pos != (head); \
                pos = is_reverse ? pos->prev : pos->next)
  
 -static int after_or_end(enum action_where where)
 +static int after_or_end(enum trailer_where where)
  {
        return (where == WHERE_AFTER) || (where == WHERE_END);
  }
@@@ -159,13 -163,15 +159,15 @@@ static void print_tok_val(FILE *outfile
                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);
        }
  }
@@@ -196,7 -202,7 +198,7 @@@ static int check_if_different(struct tr
                              int check_all,
                              struct list_head *head)
  {
 -      enum action_where where = arg_tok->conf.where;
 +      enum trailer_where where = arg_tok->conf.where;
        struct list_head *next_head;
        do {
                if (same_trailer(in_tok, arg_tok))
@@@ -295,16 -301,13 +297,16 @@@ static void apply_arg_if_exists(struct 
                else
                        free_arg_item(arg_tok);
                break;
 +      default:
 +              die("BUG: trailer.c: unhandled value %d",
 +                  arg_tok->conf.if_exists);
        }
  }
  
  static void apply_arg_if_missing(struct list_head *head,
                                 struct arg_item *arg_tok)
  {
 -      enum action_where where;
 +      enum trailer_where where;
        struct trailer_item *to_add;
  
        switch (arg_tok->conf.if_missing) {
                        list_add_tail(&to_add->list, head);
                else
                        list_add(&to_add->list, head);
 +              break;
 +      default:
 +              die("BUG: trailer.c: unhandled value %d",
 +                  arg_tok->conf.if_missing);
        }
  }
  
@@@ -333,7 -332,7 +335,7 @@@ static int find_same_and_apply_arg(stru
        struct trailer_item *in_tok;
        struct trailer_item *on_tok;
  
 -      enum action_where where = arg_tok->conf.where;
 +      enum trailer_where where = arg_tok->conf.where;
        int middle = (where == WHERE_AFTER) || (where == WHERE_BEFORE);
        int backwards = after_or_end(where);
        struct trailer_item *start_tok;
@@@ -375,50 -374,44 +377,50 @@@ static void process_trailers_lists(stru
        }
  }
  
 -static int set_where(struct conf_info *item, const char *value)
 +int trailer_set_where(enum trailer_where *item, const char *value)
  {
 -      if (!strcasecmp("after", value))
 -              item->where = WHERE_AFTER;
 +      if (!value)
 +              *item = WHERE_DEFAULT;
 +      else if (!strcasecmp("after", value))
 +              *item = WHERE_AFTER;
        else if (!strcasecmp("before", value))
 -              item->where = WHERE_BEFORE;
 +              *item = WHERE_BEFORE;
        else if (!strcasecmp("end", value))
 -              item->where = WHERE_END;
 +              *item = WHERE_END;
        else if (!strcasecmp("start", value))
 -              item->where = WHERE_START;
 +              *item = WHERE_START;
        else
                return -1;
        return 0;
  }
  
 -static int set_if_exists(struct conf_info *item, const char *value)
 +int trailer_set_if_exists(enum trailer_if_exists *item, const char *value)
  {
 -      if (!strcasecmp("addIfDifferent", value))
 -              item->if_exists = EXISTS_ADD_IF_DIFFERENT;
 +      if (!value)
 +              *item = EXISTS_DEFAULT;
 +      else if (!strcasecmp("addIfDifferent", value))
 +              *item = EXISTS_ADD_IF_DIFFERENT;
        else if (!strcasecmp("addIfDifferentNeighbor", value))
 -              item->if_exists = EXISTS_ADD_IF_DIFFERENT_NEIGHBOR;
 +              *item = EXISTS_ADD_IF_DIFFERENT_NEIGHBOR;
        else if (!strcasecmp("add", value))
 -              item->if_exists = EXISTS_ADD;
 +              *item = EXISTS_ADD;
        else if (!strcasecmp("replace", value))
 -              item->if_exists = EXISTS_REPLACE;
 +              *item = EXISTS_REPLACE;
        else if (!strcasecmp("doNothing", value))
 -              item->if_exists = EXISTS_DO_NOTHING;
 +              *item = EXISTS_DO_NOTHING;
        else
                return -1;
        return 0;
  }
  
 -static int set_if_missing(struct conf_info *item, const char *value)
 +int trailer_set_if_missing(enum trailer_if_missing *item, const char *value)
  {
 -      if (!strcasecmp("doNothing", value))
 -              item->if_missing = MISSING_DO_NOTHING;
 +      if (!value)
 +              *item = MISSING_DEFAULT;
 +      else if (!strcasecmp("doNothing", value))
 +              *item = MISSING_DO_NOTHING;
        else if (!strcasecmp("add", value))
 -              item->if_missing = MISSING_ADD;
 +              *item = MISSING_ADD;
        else
                return -1;
        return 0;
@@@ -478,18 -471,15 +480,18 @@@ static int git_trailer_default_config(c
        variable_name = strrchr(trailer_item, '.');
        if (!variable_name) {
                if (!strcmp(trailer_item, "where")) {
 -                      if (set_where(&default_conf_info, value) < 0)
 +                      if (trailer_set_where(&default_conf_info.where,
 +                                            value) < 0)
                                warning(_("unknown value '%s' for key '%s'"),
                                        value, conf_key);
                } else if (!strcmp(trailer_item, "ifexists")) {
 -                      if (set_if_exists(&default_conf_info, value) < 0)
 +                      if (trailer_set_if_exists(&default_conf_info.if_exists,
 +                                                value) < 0)
                                warning(_("unknown value '%s' for key '%s'"),
                                        value, conf_key);
                } else if (!strcmp(trailer_item, "ifmissing")) {
 -                      if (set_if_missing(&default_conf_info, value) < 0)
 +                      if (trailer_set_if_missing(&default_conf_info.if_missing,
 +                                                 value) < 0)
                                warning(_("unknown value '%s' for key '%s'"),
                                        value, conf_key);
                } else if (!strcmp(trailer_item, "separators")) {
@@@ -543,15 -533,15 +545,15 @@@ static int git_trailer_config(const cha
                conf->command = xstrdup(value);
                break;
        case TRAILER_WHERE:
 -              if (set_where(conf, value))
 +              if (trailer_set_where(&conf->where, value))
                        warning(_("unknown value '%s' for key '%s'"), value, conf_key);
                break;
        case TRAILER_IF_EXISTS:
 -              if (set_if_exists(conf, value))
 +              if (trailer_set_if_exists(&conf->if_exists, value))
                        warning(_("unknown value '%s' for key '%s'"), value, conf_key);
                break;
        case TRAILER_IF_MISSING:
 -              if (set_if_missing(conf, value))
 +              if (trailer_set_if_missing(&conf->if_missing, value))
                        warning(_("unknown value '%s' for key '%s'"), value, conf_key);
                break;
        default:
@@@ -566,9 -556,6 +568,9 @@@ static void ensure_configured(void
                return;
  
        /* Default config must be setup first */
 +      default_conf_info.where = WHERE_END;
 +      default_conf_info.if_exists = EXISTS_ADD_IF_DIFFERENT_NEIGHBOR;
 +      default_conf_info.if_missing = MISSING_ADD;
        git_config(git_trailer_default_config, NULL);
        git_config(git_trailer_config, NULL);
        configured = 1;
@@@ -672,27 -659,19 +674,27 @@@ static struct trailer_item *add_trailer
  }
  
  static void add_arg_item(struct list_head *arg_head, char *tok, char *val,
 -                       const struct conf_info *conf)
 +                       const struct conf_info *conf,
 +                       const struct new_trailer_item *new_trailer_item)
  {
        struct arg_item *new = xcalloc(sizeof(*new), 1);
        new->token = tok;
        new->value = val;
        duplicate_conf(&new->conf, conf);
 +      if (new_trailer_item) {
 +              if (new_trailer_item->where != WHERE_DEFAULT)
 +                      new->conf.where = new_trailer_item->where;
 +              if (new_trailer_item->if_exists != EXISTS_DEFAULT)
 +                      new->conf.if_exists = new_trailer_item->if_exists;
 +              if (new_trailer_item->if_missing != MISSING_DEFAULT)
 +                      new->conf.if_missing = new_trailer_item->if_missing;
 +      }
        list_add_tail(&new->list, arg_head);
  }
  
  static void process_command_line_args(struct list_head *arg_head,
 -                                    struct string_list *trailers)
 +                                    struct list_head *new_trailer_head)
  {
 -      struct string_list_item *tr;
        struct arg_item *item;
        struct strbuf tok = STRBUF_INIT;
        struct strbuf val = STRBUF_INIT;
                        add_arg_item(arg_head,
                                     xstrdup(token_from_item(item, NULL)),
                                     xstrdup(""),
 -                                   &item->conf);
 +                                   &item->conf, NULL);
        }
  
        /* Add an arg item for each trailer on the command line */
 -      for_each_string_list_item(tr, trailers) {
 -              int separator_pos = find_separator(tr->string, cl_separators);
 +      list_for_each(pos, new_trailer_head) {
 +              struct new_trailer_item *tr =
 +                      list_entry(pos, struct new_trailer_item, list);
 +              int separator_pos = find_separator(tr->text, cl_separators);
 +
                if (separator_pos == 0) {
                        struct strbuf sb = STRBUF_INIT;
 -                      strbuf_addstr(&sb, tr->string);
 +                      strbuf_addstr(&sb, tr->text);
                        strbuf_trim(&sb);
                        error(_("empty trailer token in trailer '%.*s'"),
                              (int) sb.len, sb.buf);
                        strbuf_release(&sb);
                } else {
 -                      parse_trailer(&tok, &val, &conf, tr->string,
 +                      parse_trailer(&tok, &val, &conf, tr->text,
                                      separator_pos);
                        add_arg_item(arg_head,
                                     strbuf_detach(&tok, NULL),
                                     strbuf_detach(&val, NULL),
 -                                   conf);
 +                                   conf, tr);
                }
        }
  
@@@ -910,9 -886,37 +912,37 @@@ static int ends_with_blank_line(const c
        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;
        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++) {
                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 -1000,11 +1026,11 @@@ static FILE *create_in_place_tempfile(c
        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 string_list *trailers)
 +                    struct list_head *new_trailer_head)
  {
        LIST_HEAD(head);
-       LIST_HEAD(arg_head);
        struct strbuf sb = STRBUF_INIT;
        int trailer_end;
        FILE *outfile = stdout;
  
        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);
-       process_command_line_args(&arg_head, new_trailer_head);
+       trailer_end = process_input_file(outfile, sb.buf, &head, opts);
  
-       process_trailers_lists(&head, &arg_head);
+       if (!opts->only_input) {
+               LIST_HEAD(arg_head);
 -              process_command_line_args(&arg_head, trailers);
++              process_command_line_args(&arg_head, new_trailer_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 -1090,49 +1116,49 @@@ void trailer_info_release(struct traile
                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);
+ }
diff --combined trailer.h
index 973b533a1ac8dd0b8b159225b6a80daed51389ab,a172811022f1498bb155246ff8608176275fe889..6d7f8c2a52305d3d937b69a877f1ae0b7af94363
+++ b/trailer.h
@@@ -1,33 -1,6 +1,33 @@@
  #ifndef TRAILER_H
  #define TRAILER_H
  
 +#include "list.h"
 +
 +enum trailer_where {
 +      WHERE_DEFAULT,
 +      WHERE_END,
 +      WHERE_AFTER,
 +      WHERE_BEFORE,
 +      WHERE_START
 +};
 +enum trailer_if_exists {
 +      EXISTS_DEFAULT,
 +      EXISTS_ADD_IF_DIFFERENT_NEIGHBOR,
 +      EXISTS_ADD_IF_DIFFERENT,
 +      EXISTS_ADD,
 +      EXISTS_REPLACE,
 +      EXISTS_DO_NOTHING
 +};
 +enum trailer_if_missing {
 +      MISSING_DEFAULT,
 +      MISSING_ADD,
 +      MISSING_DO_NOTHING
 +};
 +
 +int trailer_set_where(enum trailer_where *item, const char *value);
 +int trailer_set_if_exists(enum trailer_if_exists *item, const char *value);
 +int trailer_set_if_missing(enum trailer_if_missing *item, const char *value);
 +
  struct trailer_info {
        /*
         * True if there is a blank line before the location pointed to by
        size_t trailer_nr;
  };
  
- void process_trailers(const char *file, int in_place, int trim_empty,
 +/*
 + * A list that represents newly-added trailers, such as those provided
 + * with the --trailer command line option of git-interpret-trailers.
 + */
 +struct new_trailer_item {
 +      struct list_head list;
 +
 +      const char *text;
 +
 +      enum trailer_where where;
 +      enum trailer_if_exists if_exists;
 +      enum trailer_if_missing if_missing;
 +};
 +
 -                    struct string_list *trailers);
+ 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 */