{"thread":{"id":"37601","subject":"[PATCH v15 00/11] Add interpret-trailers builtin","startedAt":"2014-09-20T13:45:03Z","lastAt":"2014-09-22T21:23:29Z","messageCount":13,"participants":["Christian Couder","Junio C Hamano"],"isPatch":true,"patchVersion":15,"patchTotal":11},"messages":[{"id":"249684","messageId":"20140920134048.18999.79434.chriscool@tuxfamily.org","threadId":"37601","inReplyTo":null,"subject":"[PATCH v15 00/11] Add interpret-trailers builtin","fromName":"Christian Couder","fromEmail":"chriscool@tuxfamily.org","sentAt":"2014-09-20T13:45:03Z","receivedAt":"2014-09-20T13:45:03Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"This patch series implements a new command:\n\n        git interpret-trailers\n\nand an infrastructure to process trailers that can be reused,\nfor example in \"commit.c\".\n\n1) Rationale\n\nThis command should help with RFC 822 style headers, called\n\"trailers\", that are found at the end of commit messages.\n\n(Note that these headers do not follow and are not intended to\nfollow many rules that are in RFC 822. For example they do not\nfollow the line breaking rules, the encoding rules and probably\nmany other rules.)\n\nFor a long time, these trailers have become a de facto standard\nway to add helpful information into commit messages.\n\nUntil now git commit has only supported the well known\n\"Signed-off-by: \" trailer, that is used by many projects like\nthe Linux kernel and Git.\n\nIt is better to keep builtin/commit.c uncontaminated by any more\nhard-wired logic, like what we have for the signed-off-by line.  Any\nnew things can and should be doable in hooks, and this filter would\nhelp writing these hooks.\n\nAnd that is why the design goal of the filter is to make it at least\nas powerful as the built-in logic we have for signed-off-by lines;\nthat would allow us to later eject the hard-wired logic for\nsigned-off-by line from the main codepath, if/when we wanted to.\n\nAlternatively, we could build a library-ish API around this filter\ncode and replace the hard-wired logic for signed-off-by line with a\ncall into that API, if/when we wanted to, but that requires (in\naddition to the \"at least as powerful as the built-in logic\") that\nthe implementation of this stand-alone filter can be cleanly made\ninto a reusable library, so that is a bit higher bar to cross than\n\"everything can be doable with hooks\" alternative.\n\n2) Current state\n\nCurrently the usage string of this command is:\n\ngit interpret-trailers [--trim-empty] [(--trailer <token>[(=|:)<value>])...] [<file>...]\n\nThe following features are implemented:\n\n        - the result is printed on stdout\n        - the --trailer arguments are interpreted\n        - messages read from <file>... or stdin are interpreted\n        - the \"trailer.separators\" option in the config is interpreted\n        - the \"trailer.where\" option is interpreted\n        - the \"trailer.ifexists\" option is interpreted\n        - the \"trailer.ifmissing\" option is interpreted\n        - the \"trailer.<token>.key\" options are interpreted\n        - the \"trailer.<token>.where\" options are interpreted\n        - the \"trailer.<token>.ifexist\" options are interpreted\n        - the \"trailer.<token>.ifmissing\" options are interpreted\n        - the \"trailer.<token>.command\" config works\n        - $ARG can be used in commands\n        - messages can contain a patch\n        - lines in messages starting with a comment char are ignored\n        - there are 49 tests\n        - there is some documentation\n        - there are examples in the documentation\n\n3) Changes since version 14, thanks to Jeff and Junio\n\n* renamed alnum_len() to token_len_without_separator()\n  and improve comment before this function (patch 1/11\n  and 4/11)\n\nChristian Couder (11):\n  trailer: add data structures and basic functions\n  trailer: process trailers from input message and arguments\n  trailer: read and process config information\n  trailer: process command line trailer arguments\n  trailer: parse trailers from file or stdin\n  trailer: put all the processing together and print\n  trailer: add interpret-trailers command\n  trailer: add tests for \"git interpret-trailers\"\n  trailer: execute command from 'trailer.<name>.command'\n  trailer: add tests for commands in config file\n  Documentation: add documentation for 'git interpret-trailers'\n\n .gitignore                               |   1 +\n Documentation/git-interpret-trailers.txt | 313 ++++++++++++\n Makefile                                 |   2 +\n builtin.h                                |   1 +\n builtin/interpret-trailers.c             |  44 ++\n command-list.txt                         |   1 +\n git.c                                    |   1 +\n t/t7513-interpret-trailers.sh            | 851 ++++++++++++++++++++++++++++++\n trailer.c                                | 852 +++++++++++++++++++++++++++++++\n trailer.h                                |   6 +\n 10 files changed, 2072 insertions(+)\n create mode 100644 Documentation/git-interpret-trailers.txt\n create mode 100644 builtin/interpret-trailers.c\n create mode 100755 t/t7513-interpret-trailers.sh\n create mode 100644 trailer.c\n create mode 100644 trailer.h\n\n-- \n2.0.3.960.g41c6e4c\n"},{"id":"249685","messageId":"20140920134515.18999.40219.chriscool@tuxfamily.org","threadId":"37601","inReplyTo":"20140920134048.18999.79434.chriscool@tuxfamily.org","subject":"[PATCH v15 01/11] trailer: add data structures and basic functions","fromName":"Christian Couder","fromEmail":"chriscool@tuxfamily.org","sentAt":"2014-09-20T13:45:04Z","receivedAt":"2014-09-20T13:45:04Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"We will use a doubly linked list to store all information\nabout trailers and their configuration.\n\nThis way we can easily remove or add trailers to or from\ntrailer lists while traversing the lists in either direction.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Makefile  |  1 +\n trailer.c | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 70 insertions(+)\n create mode 100644 trailer.c\n\ndiff --git a/Makefile b/Makefile\nindex 63a210d..ef82972 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -888,6 +888,7 @@ LIB_OBJS += submodule-config.o\n LIB_OBJS += symlinks.o\n LIB_OBJS += tag.o\n LIB_OBJS += trace.o\n+LIB_OBJS += trailer.o\n LIB_OBJS += transport.o\n LIB_OBJS += transport-helper.o\n LIB_OBJS += tree-diff.o\ndiff --git a/trailer.c b/trailer.c\nnew file mode 100644\nindex 0000000..ac323b1\n--- /dev/null\n+++ b/trailer.c\n@@ -0,0 +1,69 @@\n+#include \"cache.h\"\n+/*\n+ * Copyright (c) 2013, 2014 Christian Couder <chriscool@tuxfamily.org>\n+ */\n+\n+enum action_where { WHERE_END, WHERE_AFTER, WHERE_BEFORE, WHERE_START };\n+enum action_if_exists { EXISTS_ADD_IF_DIFFERENT_NEIGHBOR, EXISTS_ADD_IF_DIFFERENT,\n+\t\t\tEXISTS_ADD, EXISTS_REPLACE, EXISTS_DO_NOTHING };\n+enum action_if_missing { MISSING_ADD, MISSING_DO_NOTHING };\n+\n+struct conf_info {\n+\tchar *name;\n+\tchar *key;\n+\tchar *command;\n+\tenum action_where where;\n+\tenum action_if_exists if_exists;\n+\tenum action_if_missing if_missing;\n+};\n+\n+static struct conf_info default_conf_info;\n+\n+struct trailer_item {\n+\tstruct trailer_item *previous;\n+\tstruct trailer_item *next;\n+\tconst char *token;\n+\tconst char *value;\n+\tstruct conf_info conf;\n+};\n+\n+static struct trailer_item *first_conf_item;\n+\n+static char *separators = \":\";\n+\n+static int after_or_end(enum action_where where)\n+{\n+\treturn (where == WHERE_AFTER) || (where == WHERE_END);\n+}\n+\n+/*\n+ * Return the length of the string not including any final\n+ * punctuation. E.g., the input \"Signed-off-by:\" would return\n+ * 13, stripping the trailing punctuation but retaining\n+ * internal punctuation.\n+ */\n+static size_t token_len_without_separator(const char *token, size_t len)\n+{\n+\twhile (len > 0 && !isalnum(token[len - 1]))\n+\t\tlen--;\n+\treturn len;\n+}\n+\n+static int same_token(struct trailer_item *a, struct trailer_item *b)\n+{\n+\tsize_t a_len = token_len_without_separator(a->token, strlen(a->token));\n+\tsize_t b_len = token_len_without_separator(b->token, strlen(b->token));\n+\tsize_t min_len = (a_len > b_len) ? b_len : a_len;\n+\n+\treturn !strncasecmp(a->token, b->token, min_len);\n+}\n+\n+static int same_value(struct trailer_item *a, struct trailer_item *b)\n+{\n+\treturn !strcasecmp(a->value, b->value);\n+}\n+\n+static int same_trailer(struct trailer_item *a, struct trailer_item *b)\n+{\n+\treturn same_token(a, b) && same_value(a, b);\n+}\n-- \n2.0.3.960.g41c6e4c\n"},{"id":"249694","messageId":"20140920134515.18999.12257.chriscool@tuxfamily.org","threadId":"37601","inReplyTo":"20140920134048.18999.79434.chriscool@tuxfamily.org","subject":"[PATCH v15 02/11] trailer: process trailers from input message and arguments","fromName":"Christian Couder","fromEmail":"chriscool@tuxfamily.org","sentAt":"2014-09-20T13:45:05Z","receivedAt":"2014-09-20T13:45:05Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"Implement the logic to process trailers from the input message\nand from arguments.\n\nAt the beginning trailers from the input message are in their\nown \"in_tok\" doubly linked list, and trailers from arguments\nare in their own \"arg_tok\" doubly linked list.\n\nThe lists are traversed and when an \"arg_tok\" should be \"applied\",\nit is removed from its list and inserted into the \"in_tok\" list.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n trailer.c | 210 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 210 insertions(+)\n\ndiff --git a/trailer.c b/trailer.c\nindex ac323b1..be0ad65 100644\n--- a/trailer.c\n+++ b/trailer.c\n@@ -67,3 +67,213 @@ static int same_trailer(struct trailer_item *a, struct trailer_item *b)\n {\n \treturn same_token(a, b) && same_value(a, b);\n }\n+\n+static void free_trailer_item(struct trailer_item *item)\n+{\n+\tfree(item->conf.name);\n+\tfree(item->conf.key);\n+\tfree(item->conf.command);\n+\tfree((char *)item->token);\n+\tfree((char *)item->value);\n+\tfree(item);\n+}\n+\n+static void update_last(struct trailer_item **last)\n+{\n+\tif (*last)\n+\t\twhile ((*last)->next != NULL)\n+\t\t\t*last = (*last)->next;\n+}\n+\n+static void update_first(struct trailer_item **first)\n+{\n+\tif (*first)\n+\t\twhile ((*first)->previous != NULL)\n+\t\t\t*first = (*first)->previous;\n+}\n+\n+static void add_arg_to_input_list(struct trailer_item *on_tok,\n+\t\t\t\t  struct trailer_item *arg_tok,\n+\t\t\t\t  struct trailer_item **first,\n+\t\t\t\t  struct trailer_item **last)\n+{\n+\tif (after_or_end(arg_tok->conf.where)) {\n+\t\targ_tok->next = on_tok->next;\n+\t\ton_tok->next = arg_tok;\n+\t\targ_tok->previous = on_tok;\n+\t\tif (arg_tok->next)\n+\t\t\targ_tok->next->previous = arg_tok;\n+\t\tupdate_last(last);\n+\t} else {\n+\t\targ_tok->previous = on_tok->previous;\n+\t\ton_tok->previous = arg_tok;\n+\t\targ_tok->next = on_tok;\n+\t\tif (arg_tok->previous)\n+\t\t\targ_tok->previous->next = arg_tok;\n+\t\tupdate_first(first);\n+\t}\n+}\n+\n+static int check_if_different(struct trailer_item *in_tok,\n+\t\t\t      struct trailer_item *arg_tok,\n+\t\t\t      int check_all)\n+{\n+\tenum action_where where = arg_tok->conf.where;\n+\tdo {\n+\t\tif (!in_tok)\n+\t\t\treturn 1;\n+\t\tif (same_trailer(in_tok, arg_tok))\n+\t\t\treturn 0;\n+\t\t/*\n+\t\t * if we want to add a trailer after another one,\n+\t\t * we have to check those before this one\n+\t\t */\n+\t\tin_tok = after_or_end(where) ? in_tok->previous : in_tok->next;\n+\t} while (check_all);\n+\treturn 1;\n+}\n+\n+static void remove_from_list(struct trailer_item *item,\n+\t\t\t     struct trailer_item **first,\n+\t\t\t     struct trailer_item **last)\n+{\n+\tstruct trailer_item *next = item->next;\n+\tstruct trailer_item *previous = item->previous;\n+\n+\tif (next) {\n+\t\titem->next->previous = previous;\n+\t\titem->next = NULL;\n+\t} else if (last)\n+\t\t*last = previous;\n+\n+\tif (previous) {\n+\t\titem->previous->next = next;\n+\t\titem->previous = NULL;\n+\t} else if (first)\n+\t\t*first = next;\n+}\n+\n+static struct trailer_item *remove_first(struct trailer_item **first)\n+{\n+\tstruct trailer_item *item = *first;\n+\t*first = item->next;\n+\tif (item->next) {\n+\t\titem->next->previous = NULL;\n+\t\titem->next = NULL;\n+\t}\n+\treturn item;\n+}\n+\n+static void apply_arg_if_exists(struct trailer_item *in_tok,\n+\t\t\t\tstruct trailer_item *arg_tok,\n+\t\t\t\tstruct trailer_item *on_tok,\n+\t\t\t\tstruct trailer_item **in_tok_first,\n+\t\t\t\tstruct trailer_item **in_tok_last)\n+{\n+\tswitch (arg_tok->conf.if_exists) {\n+\tcase EXISTS_DO_NOTHING:\n+\t\tfree_trailer_item(arg_tok);\n+\t\tbreak;\n+\tcase EXISTS_REPLACE:\n+\t\tadd_arg_to_input_list(on_tok, arg_tok,\n+\t\t\t\t      in_tok_first, in_tok_last);\n+\t\tremove_from_list(in_tok, in_tok_first, in_tok_last);\n+\t\tfree_trailer_item(in_tok);\n+\t\tbreak;\n+\tcase EXISTS_ADD:\n+\t\tadd_arg_to_input_list(on_tok, arg_tok,\n+\t\t\t\t      in_tok_first, in_tok_last);\n+\t\tbreak;\n+\tcase EXISTS_ADD_IF_DIFFERENT:\n+\t\tif (check_if_different(in_tok, arg_tok, 1))\n+\t\t\tadd_arg_to_input_list(on_tok, arg_tok,\n+\t\t\t\t\t      in_tok_first, in_tok_last);\n+\t\telse\n+\t\t\tfree_trailer_item(arg_tok);\n+\t\tbreak;\n+\tcase EXISTS_ADD_IF_DIFFERENT_NEIGHBOR:\n+\t\tif (check_if_different(on_tok, arg_tok, 0))\n+\t\t\tadd_arg_to_input_list(on_tok, arg_tok,\n+\t\t\t\t\t      in_tok_first, in_tok_last);\n+\t\telse\n+\t\t\tfree_trailer_item(arg_tok);\n+\t\tbreak;\n+\t}\n+}\n+\n+static void apply_arg_if_missing(struct trailer_item **in_tok_first,\n+\t\t\t\t struct trailer_item **in_tok_last,\n+\t\t\t\t struct trailer_item *arg_tok)\n+{\n+\tstruct trailer_item **in_tok;\n+\tenum action_where where;\n+\n+\tswitch (arg_tok->conf.if_missing) {\n+\tcase MISSING_DO_NOTHING:\n+\t\tfree_trailer_item(arg_tok);\n+\t\tbreak;\n+\tcase MISSING_ADD:\n+\t\twhere = arg_tok->conf.where;\n+\t\tin_tok = after_or_end(where) ? in_tok_last : in_tok_first;\n+\t\tif (*in_tok) {\n+\t\t\tadd_arg_to_input_list(*in_tok, arg_tok,\n+\t\t\t\t\t      in_tok_first, in_tok_last);\n+\t\t} else {\n+\t\t\t*in_tok_first = arg_tok;\n+\t\t\t*in_tok_last = arg_tok;\n+\t\t}\n+\t\tbreak;\n+\t}\n+}\n+\n+static int find_same_and_apply_arg(struct trailer_item **in_tok_first,\n+\t\t\t\t   struct trailer_item **in_tok_last,\n+\t\t\t\t   struct trailer_item *arg_tok)\n+{\n+\tstruct trailer_item *in_tok;\n+\tstruct trailer_item *on_tok;\n+\tstruct trailer_item *following_tok;\n+\n+\tenum action_where where = arg_tok->conf.where;\n+\tint middle = (where == WHERE_AFTER) || (where == WHERE_BEFORE);\n+\tint backwards = after_or_end(where);\n+\tstruct trailer_item *start_tok = backwards ? *in_tok_last : *in_tok_first;\n+\n+\tfor (in_tok = start_tok; in_tok; in_tok = following_tok) {\n+\t\tfollowing_tok = backwards ? in_tok->previous : in_tok->next;\n+\t\tif (!same_token(in_tok, arg_tok))\n+\t\t\tcontinue;\n+\t\ton_tok = middle ? in_tok : start_tok;\n+\t\tapply_arg_if_exists(in_tok, arg_tok, on_tok,\n+\t\t\t\t    in_tok_first, in_tok_last);\n+\t\treturn 1;\n+\t}\n+\treturn 0;\n+}\n+\n+static void process_trailers_lists(struct trailer_item **in_tok_first,\n+\t\t\t\t   struct trailer_item **in_tok_last,\n+\t\t\t\t   struct trailer_item **arg_tok_first)\n+{\n+\tstruct trailer_item *arg_tok;\n+\tstruct trailer_item *next_arg;\n+\n+\tif (!*arg_tok_first)\n+\t\treturn;\n+\n+\tfor (arg_tok = *arg_tok_first; arg_tok; arg_tok = next_arg) {\n+\t\tint applied = 0;\n+\n+\t\tnext_arg = arg_tok->next;\n+\t\tremove_from_list(arg_tok, arg_tok_first, NULL);\n+\n+\t\tapplied = find_same_and_apply_arg(in_tok_first,\n+\t\t\t\t\t\t  in_tok_last,\n+\t\t\t\t\t\t  arg_tok);\n+\n+\t\tif (!applied)\n+\t\t\tapply_arg_if_missing(in_tok_first,\n+\t\t\t\t\t     in_tok_last,\n+\t\t\t\t\t     arg_tok);\n+\t}\n+}\n-- \n2.0.3.960.g41c6e4c\n"},{"id":"249693","messageId":"20140920134515.18999.70218.chriscool@tuxfamily.org","threadId":"37601","inReplyTo":"20140920134048.18999.79434.chriscool@tuxfamily.org","subject":"[PATCH v15 03/11] trailer: read and process config information","fromName":"Christian Couder","fromEmail":"chriscool@tuxfamily.org","sentAt":"2014-09-20T13:45:06Z","receivedAt":"2014-09-20T13:45:06Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"Read the configuration to get trailer information, and then process\nit and store it in a doubly linked list.\n\nThe config information is stored in the list whose first item is\npointed to by:\n\nstatic struct trailer_item *first_conf_item;\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n trailer.c | 185 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 185 insertions(+)\n\ndiff --git a/trailer.c b/trailer.c\nindex be0ad65..668dc33 100644\n--- a/trailer.c\n+++ b/trailer.c\n@@ -277,3 +277,188 @@ static void process_trailers_lists(struct trailer_item **in_tok_first,\n \t\t\t\t\t     arg_tok);\n \t}\n }\n+\n+static int set_where(struct conf_info *item, const char *value)\n+{\n+\tif (!strcasecmp(\"after\", value))\n+\t\titem->where = WHERE_AFTER;\n+\telse if (!strcasecmp(\"before\", value))\n+\t\titem->where = WHERE_BEFORE;\n+\telse if (!strcasecmp(\"end\", value))\n+\t\titem->where = WHERE_END;\n+\telse if (!strcasecmp(\"start\", value))\n+\t\titem->where = WHERE_START;\n+\telse\n+\t\treturn -1;\n+\treturn 0;\n+}\n+\n+static int set_if_exists(struct conf_info *item, const char *value)\n+{\n+\tif (!strcasecmp(\"addIfDifferent\", value))\n+\t\titem->if_exists = EXISTS_ADD_IF_DIFFERENT;\n+\telse if (!strcasecmp(\"addIfDifferentNeighbor\", value))\n+\t\titem->if_exists = EXISTS_ADD_IF_DIFFERENT_NEIGHBOR;\n+\telse if (!strcasecmp(\"add\", value))\n+\t\titem->if_exists = EXISTS_ADD;\n+\telse if (!strcasecmp(\"replace\", value))\n+\t\titem->if_exists = EXISTS_REPLACE;\n+\telse if (!strcasecmp(\"doNothing\", value))\n+\t\titem->if_exists = EXISTS_DO_NOTHING;\n+\telse\n+\t\treturn -1;\n+\treturn 0;\n+}\n+\n+static int set_if_missing(struct conf_info *item, const char *value)\n+{\n+\tif (!strcasecmp(\"doNothing\", value))\n+\t\titem->if_missing = MISSING_DO_NOTHING;\n+\telse if (!strcasecmp(\"add\", value))\n+\t\titem->if_missing = MISSING_ADD;\n+\telse\n+\t\treturn -1;\n+\treturn 0;\n+}\n+\n+static void duplicate_conf(struct conf_info *dst, struct conf_info *src)\n+{\n+\t*dst = *src;\n+\tif (src->name)\n+\t\tdst->name = xstrdup(src->name);\n+\tif (src->key)\n+\t\tdst->key = xstrdup(src->key);\n+\tif (src->command)\n+\t\tdst->command = xstrdup(src->command);\n+}\n+\n+static struct trailer_item *get_conf_item(const char *name)\n+{\n+\tstruct trailer_item *item;\n+\tstruct trailer_item *previous;\n+\n+\t/* Look up item with same name */\n+\tfor (previous = NULL, item = first_conf_item;\n+\t     item;\n+\t     previous = item, item = item->next) {\n+\t\tif (!strcasecmp(item->conf.name, name))\n+\t\t\treturn item;\n+\t}\n+\n+\t/* Item does not already exists, create it */\n+\titem = xcalloc(sizeof(struct trailer_item), 1);\n+\tduplicate_conf(&item->conf, &default_conf_info);\n+\titem->conf.name = xstrdup(name);\n+\n+\tif (!previous)\n+\t\tfirst_conf_item = item;\n+\telse {\n+\t\tprevious->next = item;\n+\t\titem->previous = previous;\n+\t}\n+\n+\treturn item;\n+}\n+\n+enum trailer_info_type { TRAILER_KEY, TRAILER_COMMAND, TRAILER_WHERE,\n+\t\t\t TRAILER_IF_EXISTS, TRAILER_IF_MISSING };\n+\n+static struct {\n+\tconst char *name;\n+\tenum trailer_info_type type;\n+} trailer_config_items[] = {\n+\t{ \"key\", TRAILER_KEY },\n+\t{ \"command\", TRAILER_COMMAND },\n+\t{ \"where\", TRAILER_WHERE },\n+\t{ \"ifexists\", TRAILER_IF_EXISTS },\n+\t{ \"ifmissing\", TRAILER_IF_MISSING }\n+};\n+\n+static int git_trailer_default_config(const char *conf_key, const char *value, void *cb)\n+{\n+\tconst char *trailer_item, *variable_name;\n+\n+\tif (!skip_prefix(conf_key, \"trailer.\", &trailer_item))\n+\t\treturn 0;\n+\n+\tvariable_name = strrchr(trailer_item, '.');\n+\tif (!variable_name) {\n+\t\tif (!strcmp(trailer_item, \"where\")) {\n+\t\t\tif (set_where(&default_conf_info, value) < 0)\n+\t\t\t\twarning(_(\"unknown value '%s' for key '%s'\"),\n+\t\t\t\t\tvalue, conf_key);\n+\t\t} else if (!strcmp(trailer_item, \"ifexists\")) {\n+\t\t\tif (set_if_exists(&default_conf_info, value) < 0)\n+\t\t\t\twarning(_(\"unknown value '%s' for key '%s'\"),\n+\t\t\t\t\tvalue, conf_key);\n+\t\t} else if (!strcmp(trailer_item, \"ifmissing\")) {\n+\t\t\tif (set_if_missing(&default_conf_info, value) < 0)\n+\t\t\t\twarning(_(\"unknown value '%s' for key '%s'\"),\n+\t\t\t\t\tvalue, conf_key);\n+\t\t} else if (!strcmp(trailer_item, \"separators\")) {\n+\t\t\tseparators = xstrdup(value);\n+\t\t}\n+\t}\n+\treturn 0;\n+}\n+\n+static int git_trailer_config(const char *conf_key, const char *value, void *cb)\n+{\n+\tconst char *trailer_item, *variable_name;\n+\tstruct trailer_item *item;\n+\tstruct conf_info *conf;\n+\tchar *name = NULL;\n+\tenum trailer_info_type type;\n+\tint i;\n+\n+\tif (!skip_prefix(conf_key, \"trailer.\", &trailer_item))\n+\t\treturn 0;\n+\n+\tvariable_name = strrchr(trailer_item, '.');\n+\tif (!variable_name)\n+\t\treturn 0;\n+\n+\tvariable_name++;\n+\tfor (i = 0; i < ARRAY_SIZE(trailer_config_items); i++) {\n+\t\tif (strcmp(trailer_config_items[i].name, variable_name))\n+\t\t\tcontinue;\n+\t\tname = xstrndup(trailer_item,  variable_name - trailer_item - 1);\n+\t\ttype = trailer_config_items[i].type;\n+\t\tbreak;\n+\t}\n+\n+\tif (!name)\n+\t\treturn 0;\n+\n+\titem = get_conf_item(name);\n+\tconf = &item->conf;\n+\tfree(name);\n+\n+\tswitch (type) {\n+\tcase TRAILER_KEY:\n+\t\tif (conf->key)\n+\t\t\twarning(_(\"more than one %s\"), conf_key);\n+\t\tconf->key = xstrdup(value);\n+\t\tbreak;\n+\tcase TRAILER_COMMAND:\n+\t\tif (conf->command)\n+\t\t\twarning(_(\"more than one %s\"), conf_key);\n+\t\tconf->command = xstrdup(value);\n+\t\tbreak;\n+\tcase TRAILER_WHERE:\n+\t\tif (set_where(conf, value))\n+\t\t\twarning(_(\"unknown value '%s' for key '%s'\"), value, conf_key);\n+\t\tbreak;\n+\tcase TRAILER_IF_EXISTS:\n+\t\tif (set_if_exists(conf, value))\n+\t\t\twarning(_(\"unknown value '%s' for key '%s'\"), value, conf_key);\n+\t\tbreak;\n+\tcase TRAILER_IF_MISSING:\n+\t\tif (set_if_missing(conf, value))\n+\t\t\twarning(_(\"unknown value '%s' for key '%s'\"), value, conf_key);\n+\t\tbreak;\n+\tdefault:\n+\t\tdie(\"internal bug in trailer.c\");\n+\t}\n+\treturn 0;\n+}\n-- \n2.0.3.960.g41c6e4c\n"},{"id":"249686","messageId":"20140920134515.18999.32085.chriscool@tuxfamily.org","threadId":"37601","inReplyTo":"20140920134048.18999.79434.chriscool@tuxfamily.org","subject":"[PATCH v15 04/11] trailer: process command line trailer arguments","fromName":"Christian Couder","fromEmail":"chriscool@tuxfamily.org","sentAt":"2014-09-20T13:45:07Z","receivedAt":"2014-09-20T13:45:07Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"Parse the trailer command line arguments and put\nthe result into an arg_tok doubly linked list.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n trailer.c | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 125 insertions(+)\n\ndiff --git a/trailer.c b/trailer.c\nindex 668dc33..bf58980 100644\n--- a/trailer.c\n+++ b/trailer.c\n@@ -1,4 +1,5 @@\n #include \"cache.h\"\n+#include \"string-list.h\"\n /*\n  * Copyright (c) 2013, 2014 Christian Couder <chriscool@tuxfamily.org>\n  */\n@@ -462,3 +463,127 @@ static int git_trailer_config(const char *conf_key, const char *value, void *cb)\n \t}\n \treturn 0;\n }\n+\n+static int parse_trailer(struct strbuf *tok, struct strbuf *val, const char *trailer)\n+{\n+\tsize_t len;\n+\tstruct strbuf seps = STRBUF_INIT;\n+\tstrbuf_addstr(&seps, separators);\n+\tstrbuf_addch(&seps, '=');\n+\tlen = strcspn(trailer, seps.buf);\n+\tstrbuf_release(&seps);\n+\tif (len == 0)\n+\t\treturn error(_(\"empty trailer token in trailer '%s'\"), trailer);\n+\tif (len < strlen(trailer)) {\n+\t\tstrbuf_add(tok, trailer, len);\n+\t\tstrbuf_trim(tok);\n+\t\tstrbuf_addstr(val, trailer + len + 1);\n+\t\tstrbuf_trim(val);\n+\t} else {\n+\t\tstrbuf_addstr(tok, trailer);\n+\t\tstrbuf_trim(tok);\n+\t}\n+\treturn 0;\n+}\n+\n+\n+static void duplicate_conf(struct conf_info *dst, struct conf_info *src)\n+{\n+\t*dst = *src;\n+\tif (src->name)\n+\t\tdst->name = xstrdup(src->name);\n+\tif (src->key)\n+\t\tdst->key = xstrdup(src->key);\n+\tif (src->command)\n+\t\tdst->command = xstrdup(src->command);\n+}\n+\n+static const char *token_from_item(struct trailer_item *item)\n+{\n+\tif (item->conf.key)\n+\t\treturn item->conf.key;\n+\n+\treturn item->conf.name;\n+}\n+\n+static struct trailer_item *new_trailer_item(struct trailer_item *conf_item,\n+\t\t\t\t\t     char *tok, char *val)\n+{\n+\tstruct trailer_item *new = xcalloc(sizeof(*new), 1);\n+\tnew->value = val;\n+\n+\tif (conf_item) {\n+\t\tduplicate_conf(&new->conf, &conf_item->conf);\n+\t\tnew->token = xstrdup(token_from_item(conf_item));\n+\t\tfree(tok);\n+\t} else {\n+\t\tduplicate_conf(&new->conf, &default_conf_info);\n+\t\tnew->token = tok;\n+\t}\n+\n+\treturn new;\n+}\n+\n+static int token_matches_item(const char *tok, struct trailer_item *item, int tok_len)\n+{\n+\tif (!strncasecmp(tok, item->conf.name, tok_len))\n+\t\treturn 1;\n+\treturn item->conf.key ? !strncasecmp(tok, item->conf.key, tok_len) : 0;\n+}\n+\n+static struct trailer_item *create_trailer_item(const char *string)\n+{\n+\tstruct strbuf tok = STRBUF_INIT;\n+\tstruct strbuf val = STRBUF_INIT;\n+\tstruct trailer_item *item;\n+\tint tok_len;\n+\n+\tif (parse_trailer(&tok, &val, string))\n+\t\treturn NULL;\n+\n+\ttok_len = token_len_without_separator(tok.buf, tok.len);\n+\n+\t/* Lookup if the token matches something in the config */\n+\tfor (item = first_conf_item; item; item = item->next) {\n+\t\tif (token_matches_item(tok.buf, item, tok_len)) {\n+\t\t\tstrbuf_release(&tok);\n+\t\t\treturn new_trailer_item(item,\n+\t\t\t\t\t\tNULL,\n+\t\t\t\t\t\tstrbuf_detach(&val, NULL));\n+\t\t}\n+\t}\n+\n+\treturn new_trailer_item(NULL,\n+\t\t\t\tstrbuf_detach(&tok, NULL),\n+\t\t\t\tstrbuf_detach(&val, NULL));\n+}\n+\n+static void add_trailer_item(struct trailer_item **first,\n+\t\t\t     struct trailer_item **last,\n+\t\t\t     struct trailer_item *new)\n+{\n+\tif (!new)\n+\t\treturn;\n+\tif (!*last) {\n+\t\t*first = new;\n+\t\t*last = new;\n+\t} else {\n+\t\t(*last)->next = new;\n+\t\tnew->previous = *last;\n+\t\t*last = new;\n+\t}\n+}\n+\n+static struct trailer_item *process_command_line_args(struct string_list *trailers)\n+{\n+\tstruct trailer_item *arg_tok_first = NULL;\n+\tstruct trailer_item *arg_tok_last = NULL;\n+\tstruct string_list_item *tr;\n+\n+\tfor_each_string_list_item(tr, trailers) {\n+\t\tstruct trailer_item *new = create_trailer_item(tr->string);\n+\t\tadd_trailer_item(&arg_tok_first, &arg_tok_last, new);\n+\t}\n+\n+\treturn arg_tok_first;\n+}\n-- \n2.0.3.960.g41c6e4c\n"},{"id":"249695","messageId":"20140920134515.18999.62698.chriscool@tuxfamily.org","threadId":"37601","inReplyTo":"20140920134048.18999.79434.chriscool@tuxfamily.org","subject":"[PATCH v15 05/11] trailer: parse trailers from file or stdin","fromName":"Christian Couder","fromEmail":"chriscool@tuxfamily.org","sentAt":"2014-09-20T13:45:08Z","receivedAt":"2014-09-20T13:45:08Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"Read trailers from a file or from stdin, parse the trailers and then\nput the result into a doubly linked list.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n trailer.c | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 123 insertions(+)\n\ndiff --git a/trailer.c b/trailer.c\nindex bf58980..c3e096f 100644\n--- a/trailer.c\n+++ b/trailer.c\n@@ -69,6 +69,14 @@ static int same_trailer(struct trailer_item *a, struct trailer_item *b)\n \treturn same_token(a, b) && same_value(a, b);\n }\n \n+static inline int contains_only_spaces(const char *str)\n+{\n+\tconst char *s = str;\n+\twhile (*s && isspace(*s))\n+\t\ts++;\n+\treturn !*s;\n+}\n+\n static void free_trailer_item(struct trailer_item *item)\n {\n \tfree(item->conf.name);\n@@ -587,3 +595,118 @@ static struct trailer_item *process_command_line_args(struct string_list *traile\n \n \treturn arg_tok_first;\n }\n+\n+static struct strbuf **read_input_file(const char *file)\n+{\n+\tstruct strbuf **lines;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\n+\tif (file) {\n+\t\tif (strbuf_read_file(&sb, file, 0) < 0)\n+\t\t\tdie_errno(_(\"could not read input file '%s'\"), file);\n+\t} else {\n+\t\tif (strbuf_read(&sb, fileno(stdin), 0) < 0)\n+\t\t\tdie_errno(_(\"could not read from stdin\"));\n+\t}\n+\n+\tlines = strbuf_split(&sb, '\\n');\n+\n+\tstrbuf_release(&sb);\n+\n+\treturn lines;\n+}\n+\n+/*\n+ * Return the (0 based) index of the start of the patch or the line\n+ * count if there is no patch in the message.\n+ */\n+static int find_patch_start(struct strbuf **lines, int count)\n+{\n+\tint i;\n+\n+\t/* Get the start of the patch part if any */\n+\tfor (i = 0; i < count; i++) {\n+\t\tif (starts_with(lines[i]->buf, \"---\"))\n+\t\t\treturn i;\n+\t}\n+\n+\treturn count;\n+}\n+\n+/*\n+ * Return the (0 based) index of the first trailer line or count if\n+ * there are no trailers. Trailers are searched only in the lines from\n+ * index (count - 1) down to index 0.\n+ */\n+static int find_trailer_start(struct strbuf **lines, int count)\n+{\n+\tint start, only_spaces = 1;\n+\n+\t/*\n+\t * Get the start of the trailers by looking starting from the end\n+\t * for a line with only spaces before lines with one separator.\n+\t */\n+\tfor (start = count - 1; start >= 0; start--) {\n+\t\tif (lines[start]->buf[0] == comment_line_char)\n+\t\t\tcontinue;\n+\t\tif (contains_only_spaces(lines[start]->buf)) {\n+\t\t\tif (only_spaces)\n+\t\t\t\tcontinue;\n+\t\t\treturn start + 1;\n+\t\t}\n+\t\tif (strcspn(lines[start]->buf, separators) < lines[start]->len) {\n+\t\t\tif (only_spaces)\n+\t\t\t\tonly_spaces = 0;\n+\t\t\tcontinue;\n+\t\t}\n+\t\treturn count;\n+\t}\n+\n+\treturn only_spaces ? count : 0;\n+}\n+\n+static int has_blank_line_before(struct strbuf **lines, int start)\n+{\n+\tfor (;start >= 0; start--) {\n+\t\tif (lines[start]->buf[0] == comment_line_char)\n+\t\t\tcontinue;\n+\t\treturn contains_only_spaces(lines[start]->buf);\n+\t}\n+\treturn 0;\n+}\n+\n+static void print_lines(struct strbuf **lines, int start, int end)\n+{\n+\tint i;\n+\tfor (i = start; lines[i] && i < end; i++)\n+\t\tprintf(\"%s\", lines[i]->buf);\n+}\n+\n+static int process_input_file(struct strbuf **lines,\n+\t\t\t      struct trailer_item **in_tok_first,\n+\t\t\t      struct trailer_item **in_tok_last)\n+{\n+\tint count = 0;\n+\tint patch_start, trailer_start, i;\n+\n+\t/* Get the line count */\n+\twhile (lines[count])\n+\t\tcount++;\n+\n+\tpatch_start = find_patch_start(lines, count);\n+\ttrailer_start = find_trailer_start(lines, patch_start);\n+\n+\t/* Print lines before the trailers as is */\n+\tprint_lines(lines, 0, trailer_start);\n+\n+\tif (!has_blank_line_before(lines, trailer_start - 1))\n+\t\tprintf(\"\\n\");\n+\n+\t/* Parse trailer lines */\n+\tfor (i = trailer_start; i < patch_start; i++) {\n+\t\tstruct trailer_item *new = create_trailer_item(lines[i]->buf);\n+\t\tadd_trailer_item(in_tok_first, in_tok_last, new);\n+\t}\n+\n+\treturn patch_start;\n+}\n-- \n2.0.3.960.g41c6e4c\n"},{"id":"249692","messageId":"20140920134515.18999.8387.chriscool@tuxfamily.org","threadId":"37601","inReplyTo":"20140920134048.18999.79434.chriscool@tuxfamily.org","subject":"[PATCH v15 06/11] trailer: put all the processing together and print","fromName":"Christian Couder","fromEmail":"chriscool@tuxfamily.org","sentAt":"2014-09-20T13:45:09Z","receivedAt":"2014-09-20T13:45:09Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"This patch adds the process_trailers() function that\ncalls all the previously added processing functions\nand then prints the results on the standard output.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n trailer.c | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++----------\n trailer.h |  6 +++++\n 2 files changed, 75 insertions(+), 12 deletions(-)\n create mode 100644 trailer.h\n\ndiff --git a/trailer.c b/trailer.c\nindex c3e096f..094c6e8 100644\n--- a/trailer.c\n+++ b/trailer.c\n@@ -1,5 +1,6 @@\n #include \"cache.h\"\n #include \"string-list.h\"\n+#include \"trailer.h\"\n /*\n  * Copyright (c) 2013, 2014 Christian Couder <chriscool@tuxfamily.org>\n  */\n@@ -87,6 +88,35 @@ static void free_trailer_item(struct trailer_item *item)\n \tfree(item);\n }\n \n+static char last_non_space_char(const char *s)\n+{\n+\tint i;\n+\tfor (i = strlen(s) - 1; i >= 0; i--)\n+\t\tif (!isspace(s[i]))\n+\t\t\treturn s[i];\n+\treturn '\\0';\n+}\n+\n+static void print_tok_val(const char *tok, const char *val)\n+{\n+\tchar c = last_non_space_char(tok);\n+\tif (!c)\n+\t\treturn;\n+\tif (strchr(separators, c))\n+\t\tprintf(\"%s%s\\n\", tok, val);\n+\telse\n+\t\tprintf(\"%s%c %s\\n\", tok, separators[0], val);\n+}\n+\n+static void print_all(struct trailer_item *first, int trim_empty)\n+{\n+\tstruct trailer_item *item;\n+\tfor (item = first; item; item = item->next) {\n+\t\tif (!trim_empty || strlen(item->value) > 0)\n+\t\t\tprint_tok_val(item->token, item->value);\n+\t}\n+}\n+\n static void update_last(struct trailer_item **last)\n {\n \tif (*last)\n@@ -494,18 +524,6 @@ static int parse_trailer(struct strbuf *tok, struct strbuf *val, const char *tra\n \treturn 0;\n }\n \n-\n-static void duplicate_conf(struct conf_info *dst, struct conf_info *src)\n-{\n-\t*dst = *src;\n-\tif (src->name)\n-\t\tdst->name = xstrdup(src->name);\n-\tif (src->key)\n-\t\tdst->key = xstrdup(src->key);\n-\tif (src->command)\n-\t\tdst->command = xstrdup(src->command);\n-}\n-\n static const char *token_from_item(struct trailer_item *item)\n {\n \tif (item->conf.key)\n@@ -710,3 +728,42 @@ static int process_input_file(struct strbuf **lines,\n \n \treturn patch_start;\n }\n+\n+static void free_all(struct trailer_item **first)\n+{\n+\twhile (*first) {\n+\t\tstruct trailer_item *item = remove_first(first);\n+\t\tfree_trailer_item(item);\n+\t}\n+}\n+\n+void process_trailers(const char *file, int trim_empty, struct string_list *trailers)\n+{\n+\tstruct trailer_item *in_tok_first = NULL;\n+\tstruct trailer_item *in_tok_last = NULL;\n+\tstruct trailer_item *arg_tok_first;\n+\tstruct strbuf **lines;\n+\tint patch_start;\n+\n+\t/* Default config must be setup first */\n+\tgit_config(git_trailer_default_config, NULL);\n+\tgit_config(git_trailer_config, NULL);\n+\n+\tlines = read_input_file(file);\n+\n+\t/* Print the lines before the trailers */\n+\tpatch_start = process_input_file(lines, &in_tok_first, &in_tok_last);\n+\n+\targ_tok_first = process_command_line_args(trailers);\n+\n+\tprocess_trailers_lists(&in_tok_first, &in_tok_last, &arg_tok_first);\n+\n+\tprint_all(in_tok_first, trim_empty);\n+\n+\tfree_all(&in_tok_first);\n+\n+\t/* Print the lines after the trailers as is */\n+\tprint_lines(lines, patch_start, INT_MAX);\n+\n+\tstrbuf_list_free(lines);\n+}\ndiff --git a/trailer.h b/trailer.h\nnew file mode 100644\nindex 0000000..8eb25d5\n--- /dev/null\n+++ b/trailer.h\n@@ -0,0 +1,6 @@\n+#ifndef TRAILER_H\n+#define TRAILER_H\n+\n+void process_trailers(const char *file, int trim_empty, struct string_list *trailers);\n+\n+#endif /* TRAILER_H */\n-- \n2.0.3.960.g41c6e4c\n"},{"id":"249687","messageId":"20140920134515.18999.41045.chriscool@tuxfamily.org","threadId":"37601","inReplyTo":"20140920134048.18999.79434.chriscool@tuxfamily.org","subject":"[PATCH v15 07/11] trailer: add interpret-trailers command","fromName":"Christian Couder","fromEmail":"chriscool@tuxfamily.org","sentAt":"2014-09-20T13:45:10Z","receivedAt":"2014-09-20T13:45:10Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"This patch adds the \"git interpret-trailers\" command.\nThis command uses the previously added process_trailers()\nfunction in trailer.c.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n .gitignore                   |  1 +\n Makefile                     |  1 +\n builtin.h                    |  1 +\n builtin/interpret-trailers.c | 44 ++++++++++++++++++++++++++++++++++++++++++++\n git.c                        |  1 +\n 5 files changed, 48 insertions(+)\n create mode 100644 builtin/interpret-trailers.c\n\ndiff --git a/.gitignore b/.gitignore\nindex cf0d191..85593de 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -74,6 +74,7 @@\n /git-index-pack\n /git-init\n /git-init-db\n+/git-interpret-trailers\n /git-instaweb\n /git-log\n /git-ls-files\ndiff --git a/Makefile b/Makefile\nindex ef82972..0b06ae0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -953,6 +953,7 @@ BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\n+BUILTIN_OBJS += builtin/interpret-trailers.o\n BUILTIN_OBJS += builtin/log.o\n BUILTIN_OBJS += builtin/ls-files.o\n BUILTIN_OBJS += builtin/ls-remote.o\ndiff --git a/builtin.h b/builtin.h\nindex 5d91f31..b87df70 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -73,6 +73,7 @@ extern int cmd_hash_object(int argc, const char **argv, const char *prefix);\n extern int cmd_help(int argc, const char **argv, const char *prefix);\n extern int cmd_index_pack(int argc, const char **argv, const char *prefix);\n extern int cmd_init_db(int argc, const char **argv, const char *prefix);\n+extern int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);\n extern int cmd_log(int argc, const char **argv, const char *prefix);\n extern int cmd_log_reflog(int argc, const char **argv, const char *prefix);\n extern int cmd_ls_files(int argc, const char **argv, const char *prefix);\ndiff --git a/builtin/interpret-trailers.c b/builtin/interpret-trailers.c\nnew file mode 100644\nindex 0000000..46838d2\n--- /dev/null\n+++ b/builtin/interpret-trailers.c\n@@ -0,0 +1,44 @@\n+/*\n+ * Builtin \"git interpret-trailers\"\n+ *\n+ * Copyright (c) 2013, 2014 Christian Couder <chriscool@tuxfamily.org>\n+ *\n+ */\n+\n+#include \"cache.h\"\n+#include \"builtin.h\"\n+#include \"parse-options.h\"\n+#include \"string-list.h\"\n+#include \"trailer.h\"\n+\n+static const char * const git_interpret_trailers_usage[] = {\n+\tN_(\"git interpret-trailers [--trim-empty] [(--trailer <token>[(=|:)<value>])...] [<file>...]\"),\n+\tNULL\n+};\n+\n+int cmd_interpret_trailers(int argc, const char **argv, const char *prefix)\n+{\n+\tint trim_empty = 0;\n+\tstruct string_list trailers = STRING_LIST_INIT_DUP;\n+\n+\tstruct option options[] = {\n+\t\tOPT_BOOL(0, \"trim-empty\", &trim_empty, N_(\"trim empty trailers\")),\n+\t\tOPT_STRING_LIST(0, \"trailer\", &trailers, N_(\"trailer\"),\n+\t\t\t\tN_(\"trailer(s) to add\")),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     git_interpret_trailers_usage, 0);\n+\n+\tif (argc) {\n+\t\tint i;\n+\t\tfor (i = 0; i < argc; i++)\n+\t\t\tprocess_trailers(argv[i], trim_empty, &trailers);\n+\t} else\n+\t\tprocess_trailers(NULL, trim_empty, &trailers);\n+\n+\tstring_list_clear(&trailers, 0);\n+\n+\treturn 0;\n+}\ndiff --git a/git.c b/git.c\nindex 5ebb32f..e327a90 100644\n--- a/git.c\n+++ b/git.c\n@@ -417,6 +417,7 @@ static struct cmd_struct commands[] = {\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY },\n \t{ \"init\", cmd_init_db, NO_SETUP },\n \t{ \"init-db\", cmd_init_db, NO_SETUP },\n+\t{ \"interpret-trailers\", cmd_interpret_trailers, RUN_SETUP },\n \t{ \"log\", cmd_log, RUN_SETUP },\n \t{ \"ls-files\", cmd_ls_files, RUN_SETUP },\n \t{ \"ls-remote\", cmd_ls_remote, RUN_SETUP_GENTLY },\n-- \n2.0.3.960.g41c6e4c\n"},{"id":"249689","messageId":"20140920134515.18999.48965.chriscool@tuxfamily.org","threadId":"37601","inReplyTo":"20140920134048.18999.79434.chriscool@tuxfamily.org","subject":"[PATCH v15 08/11] trailer: add tests for \"git interpret-trailers\"","fromName":"Christian Couder","fromEmail":"chriscool@tuxfamily.org","sentAt":"2014-09-20T13:45:11Z","receivedAt":"2014-09-20T13:45:11Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n t/t7513-interpret-trailers.sh | 726 ++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 726 insertions(+)\n create mode 100755 t/t7513-interpret-trailers.sh\n\ndiff --git a/t/t7513-interpret-trailers.sh b/t/t7513-interpret-trailers.sh\nnew file mode 100755\nindex 0000000..fa37565\n--- /dev/null\n+++ b/t/t7513-interpret-trailers.sh\n@@ -0,0 +1,726 @@\n+#!/bin/sh\n+#\n+# Copyright (c) 2013, 2014 Christian Couder\n+#\n+\n+test_description='git interpret-trailers'\n+\n+. ./test-lib.sh\n+\n+# When we want one trailing space at the end of each line, let's use sed\n+# to make sure that these spaces are not removed by any automatic tool.\n+\n+test_expect_success 'setup' '\n+\t: >empty &&\n+\tcat >basic_message <<-\\EOF &&\n+\t\tsubject\n+\n+\t\tbody\n+\tEOF\n+\tcat >complex_message_body <<-\\EOF &&\n+\t\tmy subject\n+\n+\t\tmy body which is long\n+\t\tand contains some special\n+\t\tchars like : = ? !\n+\n+\tEOF\n+\tsed -e \"s/ Z\\$/ /\" >complex_message_trailers <<-\\EOF &&\n+\t\tFixes: Z\n+\t\tAcked-by: Z\n+\t\tReviewed-by: Z\n+\t\tSigned-off-by: Z\n+\tEOF\n+\tcat >basic_patch <<-\\EOF\n+\t\t---\n+\t\t foo.txt | 2 +-\n+\t\t 1 file changed, 1 insertion(+), 1 deletion(-)\n+\n+\t\tdiff --git a/foo.txt b/foo.txt\n+\t\tindex 0353767..1d91aa1 100644\n+\t\t--- a/foo.txt\n+\t\t+++ b/foo.txt\n+\t\t@@ -1,3 +1,3 @@\n+\n+\t\t-bar\n+\t\t+baz\n+\n+\t\t--\n+\t\t1.9.rc0.11.ga562ddc\n+\n+\tEOF\n+'\n+\n+test_expect_success 'without config' '\n+\tsed -e \"s/ Z\\$/ /\" >expected <<-\\EOF &&\n+\n+\t\tack: Peff\n+\t\tReviewed-by: Z\n+\t\tAcked-by: Johan\n+\tEOF\n+\tgit interpret-trailers --trailer \"ack = Peff\" --trailer \"Reviewed-by\" \\\n+\t\t--trailer \"Acked-by: Johan\" empty >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'without config in another order' '\n+\tsed -e \"s/ Z\\$/ /\" >expected <<-\\EOF &&\n+\n+\t\tAcked-by: Johan\n+\t\tReviewed-by: Z\n+\t\tack: Peff\n+\tEOF\n+\tgit interpret-trailers --trailer \"Acked-by: Johan\" --trailer \"Reviewed-by\" \\\n+\t\t--trailer \"ack = Peff\" empty >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success '--trim-empty without config' '\n+\tcat >expected <<-\\EOF &&\n+\n+\t\tack: Peff\n+\t\tAcked-by: Johan\n+\tEOF\n+\tgit interpret-trailers --trim-empty --trailer ack=Peff \\\n+\t\t--trailer \"Reviewed-by\" --trailer \"Acked-by: Johan\" \\\n+\t\t--trailer \"sob:\" empty >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with config setup' '\n+\tgit config trailer.ack.key \"Acked-by: \" &&\n+\tcat >expected <<-\\EOF &&\n+\n+\t\tAcked-by: Peff\n+\tEOF\n+\tgit interpret-trailers --trim-empty --trailer \"ack = Peff\" empty >actual &&\n+\ttest_cmp expected actual &&\n+\tgit interpret-trailers --trim-empty --trailer \"Acked-by = Peff\" empty >actual &&\n+\ttest_cmp expected actual &&\n+\tgit interpret-trailers --trim-empty --trailer \"Acked-by :Peff\" empty >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with config setup and \":=\" as separators' '\n+\tgit config trailer.separators \":=\" &&\n+\tgit config trailer.ack.key \"Acked-by= \" &&\n+\tcat >expected <<-\\EOF &&\n+\n+\t\tAcked-by= Peff\n+\tEOF\n+\tgit interpret-trailers --trim-empty --trailer \"ack = Peff\" empty >actual &&\n+\ttest_cmp expected actual &&\n+\tgit interpret-trailers --trim-empty --trailer \"Acked-by= Peff\" empty >actual &&\n+\ttest_cmp expected actual &&\n+\tgit interpret-trailers --trim-empty --trailer \"Acked-by : Peff\" empty >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with config setup and \"%\" as separators' '\n+\tgit config trailer.separators \"%\" &&\n+\tcat >expected <<-\\EOF &&\n+\n+\t\tbug% 42\n+\t\tcount% 10\n+\t\tbug% 422\n+\tEOF\n+\tgit interpret-trailers --trim-empty --trailer \"bug = 42\" \\\n+\t\t--trailer count%10 --trailer \"test: stuff\" \\\n+\t\t--trailer \"bug % 422\" empty >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with \"%\" as separators and a message with trailers' '\n+\tcat >special_message <<-\\EOF &&\n+\t\tSpecial Message\n+\n+\t\tbug% 42\n+\t\tcount% 10\n+\t\tbug% 422\n+\tEOF\n+\tcat >expected <<-\\EOF &&\n+\t\tSpecial Message\n+\n+\t\tbug% 42\n+\t\tcount% 10\n+\t\tbug% 422\n+\t\tcount% 100\n+\tEOF\n+\tgit interpret-trailers --trailer count%100 \\\n+\t\tspecial_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with config setup and \":=#\" as separators' '\n+\tgit config trailer.separators \":=#\" &&\n+\tgit config trailer.bug.key \"Bug #\" &&\n+\tcat >expected <<-\\EOF &&\n+\n+\t\tBug #42\n+\tEOF\n+\tgit interpret-trailers --trim-empty --trailer \"bug = 42\" empty >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with commit basic message' '\n+\tcat basic_message >expected &&\n+\techo >>expected &&\n+\tgit interpret-trailers <basic_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with basic patch' '\n+\tcat basic_message >input &&\n+\tcat basic_patch >>input &&\n+\tcat basic_message >expected &&\n+\techo >>expected &&\n+\tcat basic_patch >>expected &&\n+\tgit interpret-trailers <input >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with commit complex message as argument' '\n+\tcat complex_message_body complex_message_trailers >complex_message &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tReviewed-by: Z\n+\t\tSigned-off-by: Z\n+\tEOF\n+\tgit interpret-trailers complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with 2 files arguments' '\n+\tcat basic_message >>expected &&\n+\techo >>expected &&\n+\tcat basic_patch >>expected &&\n+\tgit interpret-trailers complex_message input >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with message that has comments' '\n+\tcat basic_message >>message_with_comments &&\n+\tsed -e \"s/ Z\\$/ /\" >>message_with_comments <<-\\EOF &&\n+\t\t# comment\n+\n+\t\t# other comment\n+\t\tCc: Z\n+\t\t# yet another comment\n+\t\tReviewed-by: Johan\n+\t\tReviewed-by: Z\n+\t\t# last comment\n+\n+\tEOF\n+\tcat basic_patch >>message_with_comments &&\n+\tcat basic_message >expected &&\n+\tcat >>expected <<-\\EOF &&\n+\t\t# comment\n+\n+\t\tReviewed-by: Johan\n+\t\tCc: Peff\n+\tEOF\n+\tcat basic_patch >>expected &&\n+\tgit interpret-trailers --trim-empty --trailer \"Cc: Peff\" message_with_comments >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with commit complex message and trailer args' '\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tReviewed-by: Z\n+\t\tSigned-off-by: Z\n+\t\tAcked-by= Peff\n+\t\tBug #42\n+\tEOF\n+\tgit interpret-trailers --trailer \"ack: Peff\" \\\n+\t\t--trailer \"bug: 42\" <complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with complex patch, args and --trim-empty' '\n+\tcat complex_message >complex_patch &&\n+\tcat basic_patch >>complex_patch &&\n+\tcat complex_message_body >expected &&\n+\tcat >>expected <<-\\EOF &&\n+\t\tAcked-by= Peff\n+\t\tBug #42\n+\tEOF\n+\tcat basic_patch >>expected &&\n+\tgit interpret-trailers --trim-empty --trailer \"ack: Peff\" \\\n+\t\t--trailer \"bug: 42\" <complex_patch >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"where = before\"' '\n+\tgit config trailer.bug.where \"before\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tReviewed-by: Z\n+\t\tSigned-off-by: Z\n+\t\tAcked-by= Peff\n+\tEOF\n+\tgit interpret-trailers --trailer \"ack: Peff\" \\\n+\t\t--trailer \"bug: 42\" complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"where = after\"' '\n+\tgit config trailer.ack.where \"after\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tAcked-by= Peff\n+\t\tReviewed-by: Z\n+\t\tSigned-off-by: Z\n+\tEOF\n+\tgit interpret-trailers --trailer \"ack: Peff\" \\\n+\t\t--trailer \"bug: 42\" complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"where = end\"' '\n+\tgit config trailer.review.key \"Reviewed-by\" &&\n+\tgit config trailer.review.where \"end\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tAcked-by= Peff\n+\t\tReviewed-by: Z\n+\t\tSigned-off-by: Z\n+\t\tReviewed-by: Junio\n+\t\tReviewed-by: Johannes\n+\tEOF\n+\tgit interpret-trailers --trailer \"ack: Peff\" \\\n+\t\t--trailer \"Reviewed-by: Junio\" --trailer \"Reviewed-by: Johannes\" \\\n+\t\tcomplex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"where = start\"' '\n+\tgit config trailer.review.key \"Reviewed-by\" &&\n+\tgit config trailer.review.where \"start\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tReviewed-by: Johannes\n+\t\tReviewed-by: Junio\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tAcked-by= Peff\n+\t\tReviewed-by: Z\n+\t\tSigned-off-by: Z\n+\tEOF\n+\tgit interpret-trailers --trailer \"ack: Peff\" \\\n+\t\t--trailer \"Reviewed-by: Junio\" --trailer \"Reviewed-by: Johannes\" \\\n+\t\tcomplex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"where = before\" for a token in the middle of the message' '\n+\tgit config trailer.review.key \"Reviewed-by:\" &&\n+\tgit config trailer.review.where \"before\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tAcked-by= Peff\n+\t\tReviewed-by:Johan\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\tEOF\n+\tgit interpret-trailers --trailer \"ack: Peff\" --trailer \"bug: 42\" \\\n+\t\t--trailer \"review: Johan\" <complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"where = before\" and --trim-empty' '\n+\tcat complex_message_body >expected &&\n+\tcat >>expected <<-\\EOF &&\n+\t\tBug #46\n+\t\tBug #42\n+\t\tAcked-by= Peff\n+\t\tReviewed-by:Johan\n+\tEOF\n+\tgit interpret-trailers --trim-empty --trailer \"ack: Peff\" \\\n+\t\t--trailer \"bug: 42\" --trailer \"review: Johan\" \\\n+\t\t--trailer \"Bug: 46\" <complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'the default is \"ifExists = addIfDifferentNeighbor\"' '\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tAcked-by= Peff\n+\t\tAcked-by= Junio\n+\t\tAcked-by= Peff\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\tEOF\n+\tgit interpret-trailers --trailer \"ack: Peff\" --trailer \"review:\" \\\n+\t\t--trailer \"ack: Junio\" --trailer \"bug: 42\" --trailer \"ack: Peff\" \\\n+\t\t--trailer \"ack: Peff\" <complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'default \"ifExists\" is now \"addIfDifferent\"' '\n+\tgit config trailer.ifexists \"addIfDifferent\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tAcked-by= Peff\n+\t\tAcked-by= Junio\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\tEOF\n+\tgit interpret-trailers --trailer \"ack: Peff\" --trailer \"review:\" \\\n+\t\t--trailer \"ack: Junio\" --trailer \"bug: 42\" --trailer \"ack: Peff\" \\\n+\t\t--trailer \"ack: Peff\" <complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"ifExists = addIfDifferent\" with \"where = end\"' '\n+\tgit config trailer.ack.ifExists \"addIfDifferent\" &&\n+\tgit config trailer.ack.where \"end\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\t\tAcked-by= Peff\n+\tEOF\n+\tgit interpret-trailers --trailer \"ack: Peff\" --trailer \"review:\" \\\n+\t\t--trailer \"bug: 42\" --trailer \"ack: Peff\" \\\n+\t\t<complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"ifExists = addIfDifferent\" with \"where = before\"' '\n+\tgit config trailer.ack.ifExists \"addIfDifferent\" &&\n+\tgit config trailer.ack.where \"before\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tFixes: Z\n+\t\tAcked-by= Peff\n+\t\tAcked-by= Z\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\tEOF\n+\tgit interpret-trailers --trailer \"ack: Peff\" --trailer \"review:\" \\\n+\t\t--trailer \"bug: 42\" --trailer \"ack: Peff\" \\\n+\t\t<complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"ifExists = addIfDifferentNeighbor\" with \"where = end\"' '\n+\tgit config trailer.ack.ifExists \"addIfDifferentNeighbor\" &&\n+\tgit config trailer.ack.where \"end\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\t\tAcked-by= Peff\n+\t\tAcked-by= Junio\n+\t\tTested-by: Jakub\n+\t\tAcked-by= Junio\n+\t\tAcked-by= Peff\n+\tEOF\n+\tgit interpret-trailers --trailer \"ack: Peff\" --trailer \"review:\" \\\n+\t\t--trailer \"ack: Junio\" --trailer \"bug: 42\" \\\n+\t\t--trailer \"Tested-by: Jakub\" --trailer \"ack: Junio\" \\\n+\t\t--trailer \"ack: Junio\" --trailer \"ack: Peff\" <complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"ifExists = addIfDifferentNeighbor\"  with \"where = after\"' '\n+\tgit config trailer.ack.ifExists \"addIfDifferentNeighbor\" &&\n+\tgit config trailer.ack.where \"after\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tAcked-by= Peff\n+\t\tAcked-by= Junio\n+\t\tAcked-by= Peff\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\t\tTested-by: Jakub\n+\tEOF\n+\tgit interpret-trailers --trailer \"ack: Peff\" --trailer \"review:\" \\\n+\t\t--trailer \"ack: Junio\" --trailer \"bug: 42\" \\\n+\t\t--trailer \"Tested-by: Jakub\" --trailer \"ack: Junio\" \\\n+\t\t--trailer \"ack: Junio\" --trailer \"ack: Peff\" <complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"ifExists = addIfDifferentNeighbor\" and --trim-empty' '\n+\tgit config trailer.ack.ifExists \"addIfDifferentNeighbor\" &&\n+\tcat complex_message_body >expected &&\n+\tcat >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tAcked-by= Peff\n+\t\tAcked-by= Junio\n+\t\tAcked-by= Peff\n+\tEOF\n+\tgit interpret-trailers --trim-empty --trailer \"ack: Peff\" \\\n+\t\t--trailer \"Acked-by= Peff\" --trailer \"review:\" \\\n+\t\t--trailer \"ack: Junio\" --trailer \"bug: 42\" \\\n+\t\t--trailer \"ack: Peff\" <complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"ifExists = add\" with \"where = end\"' '\n+\tgit config trailer.ack.ifExists \"add\" &&\n+\tgit config trailer.ack.where \"end\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\t\tAcked-by= Peff\n+\t\tAcked-by= Peff\n+\t\tTested-by: Jakub\n+\t\tAcked-by= Junio\n+\t\tTested-by: Johannes\n+\t\tAcked-by= Peff\n+\tEOF\n+\tgit interpret-trailers --trailer \"ack: Peff\" \\\n+\t\t--trailer \"Acked-by= Peff\" --trailer \"review:\" \\\n+\t\t--trailer \"Tested-by: Jakub\" --trailer \"ack: Junio\" \\\n+\t\t--trailer \"bug: 42\" --trailer \"Tested-by: Johannes\" \\\n+\t\t--trailer \"ack: Peff\" <complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"ifExists = add\" with \"where = after\"' '\n+\tgit config trailer.ack.ifExists \"add\" &&\n+\tgit config trailer.ack.where \"after\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tAcked-by= Peff\n+\t\tAcked-by= Peff\n+\t\tAcked-by= Junio\n+\t\tAcked-by= Peff\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\tEOF\n+\tgit interpret-trailers --trailer \"ack: Peff\" \\\n+\t\t--trailer \"Acked-by= Peff\" --trailer \"review:\" \\\n+\t\t--trailer \"ack: Junio\" --trailer \"bug: 42\" \\\n+\t\t--trailer \"ack: Peff\" <complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"ifExists = replace\"' '\n+\tgit config trailer.fix.key \"Fixes: \" &&\n+\tgit config trailer.fix.ifExists \"replace\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tAcked-by= Z\n+\t\tAcked-by= Junio\n+\t\tAcked-by= Peff\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\t\tFixes: 22\n+\tEOF\n+\tgit interpret-trailers --trailer \"review:\" \\\n+\t\t--trailer \"fix=53\" --trailer \"ack: Junio\" --trailer \"fix=22\" \\\n+\t\t--trailer \"bug: 42\" --trailer \"ack: Peff\" \\\n+\t\t<complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"ifExists = replace\" with \"where = after\"' '\n+\tgit config trailer.fix.where \"after\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tFixes: 22\n+\t\tAcked-by= Z\n+\t\tAcked-by= Junio\n+\t\tAcked-by= Peff\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\tEOF\n+\tgit interpret-trailers --trailer \"review:\" \\\n+\t\t--trailer \"fix=53\" --trailer \"ack: Junio\" --trailer \"fix=22\" \\\n+\t\t--trailer \"bug: 42\" --trailer \"ack: Peff\" \\\n+\t\t<complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"ifExists = doNothing\"' '\n+\tgit config trailer.fix.ifExists \"doNothing\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tAcked-by= Junio\n+\t\tAcked-by= Peff\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\tEOF\n+\tgit interpret-trailers --trailer \"review:\" --trailer \"fix=53\" \\\n+\t\t--trailer \"ack: Junio\" --trailer \"fix=22\" \\\n+\t\t--trailer \"bug: 42\" --trailer \"ack: Peff\" \\\n+\t\t<complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'the default is \"ifMissing = add\"' '\n+\tgit config trailer.cc.key \"Cc: \" &&\n+\tgit config trailer.cc.where \"before\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tCc: Linus\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tAcked-by= Junio\n+\t\tAcked-by= Peff\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\tEOF\n+\tgit interpret-trailers --trailer \"review:\" --trailer \"fix=53\" \\\n+\t\t--trailer \"cc=Linus\" --trailer \"ack: Junio\" \\\n+\t\t--trailer \"fix=22\" --trailer \"bug: 42\" --trailer \"ack: Peff\" \\\n+\t\t<complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'when default \"ifMissing\" is \"doNothing\"' '\n+\tgit config trailer.ifmissing \"doNothing\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tAcked-by= Junio\n+\t\tAcked-by= Peff\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\tEOF\n+\tgit interpret-trailers --trailer \"review:\" --trailer \"fix=53\" \\\n+\t\t--trailer \"cc=Linus\" --trailer \"ack: Junio\" \\\n+\t\t--trailer \"fix=22\" --trailer \"bug: 42\" --trailer \"ack: Peff\" \\\n+\t\t<complex_message >actual &&\n+\ttest_cmp expected actual &&\n+\tgit config trailer.ifmissing \"add\"\n+'\n+\n+test_expect_success 'using \"ifMissing = add\" with \"where = end\"' '\n+\tgit config trailer.cc.key \"Cc: \" &&\n+\tgit config trailer.cc.where \"end\" &&\n+\tgit config trailer.cc.ifMissing \"add\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tAcked-by= Junio\n+\t\tAcked-by= Peff\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\t\tCc: Linus\n+\tEOF\n+\tgit interpret-trailers --trailer \"review:\" --trailer \"fix=53\" \\\n+\t\t--trailer \"ack: Junio\" --trailer \"fix=22\" \\\n+\t\t--trailer \"bug: 42\" --trailer \"cc=Linus\" --trailer \"ack: Peff\" \\\n+\t\t<complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"ifMissing = add\" with \"where = before\"' '\n+\tgit config trailer.cc.key \"Cc: \" &&\n+\tgit config trailer.cc.where \"before\" &&\n+\tgit config trailer.cc.ifMissing \"add\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tCc: Linus\n+\t\tBug #42\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tAcked-by= Junio\n+\t\tAcked-by= Peff\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\tEOF\n+\tgit interpret-trailers --trailer \"review:\" --trailer \"fix=53\" \\\n+\t\t--trailer \"ack: Junio\" --trailer \"fix=22\" \\\n+\t\t--trailer \"bug: 42\" --trailer \"cc=Linus\" --trailer \"ack: Peff\" \\\n+\t\t<complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'using \"ifMissing = doNothing\"' '\n+\tgit config trailer.cc.ifMissing \"doNothing\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tAcked-by= Junio\n+\t\tAcked-by= Peff\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\tEOF\n+\tgit interpret-trailers --trailer \"review:\" --trailer \"fix=53\" \\\n+\t\t--trailer \"cc=Linus\" --trailer \"ack: Junio\" \\\n+\t\t--trailer \"fix=22\" --trailer \"bug: 42\" --trailer \"ack: Peff\" \\\n+\t\t<complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'default \"where\" is now \"after\"' '\n+\tgit config trailer.where \"after\" &&\n+\tgit config --unset trailer.ack.where &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tBug #42\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tAcked-by= Peff\n+\t\tAcked-by= Peff\n+\t\tAcked-by= Junio\n+\t\tAcked-by= Peff\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\t\tTested-by: Jakub\n+\t\tTested-by: Johannes\n+\tEOF\n+\tgit interpret-trailers --trailer \"ack: Peff\" \\\n+\t\t--trailer \"Acked-by= Peff\" --trailer \"review:\" \\\n+\t\t--trailer \"Tested-by: Jakub\" --trailer \"ack: Junio\" \\\n+\t\t--trailer \"bug: 42\" --trailer \"Tested-by: Johannes\" \\\n+\t\t--trailer \"ack: Peff\" <complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_done\n-- \n2.0.3.960.g41c6e4c\n"},{"id":"249691","messageId":"20140920134515.18999.38603.chriscool@tuxfamily.org","threadId":"37601","inReplyTo":"20140920134048.18999.79434.chriscool@tuxfamily.org","subject":"[PATCH v15 09/11] trailer: execute command from 'trailer.<name>.command'","fromName":"Christian Couder","fromEmail":"chriscool@tuxfamily.org","sentAt":"2014-09-20T13:45:12Z","receivedAt":"2014-09-20T13:45:12Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"Let the user specify a command that will give on its standard output\nthe value to use for the specified trailer.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n trailer.c | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-\n 1 file changed, 84 insertions(+), 1 deletion(-)\n\ndiff --git a/trailer.c b/trailer.c\nindex 094c6e8..50f9547 100644\n--- a/trailer.c\n+++ b/trailer.c\n@@ -1,5 +1,7 @@\n #include \"cache.h\"\n #include \"string-list.h\"\n+#include \"run-command.h\"\n+#include \"string-list.h\"\n #include \"trailer.h\"\n /*\n  * Copyright (c) 2013, 2014 Christian Couder <chriscool@tuxfamily.org>\n@@ -33,6 +35,8 @@ static struct trailer_item *first_conf_item;\n \n static char *separators = \":\";\n \n+#define TRAILER_ARG_STRING \"$ARG\"\n+\n static int after_or_end(enum action_where where)\n {\n \treturn (where == WHERE_AFTER) || (where == WHERE_END);\n@@ -78,6 +82,13 @@ static inline int contains_only_spaces(const char *str)\n \treturn !*s;\n }\n \n+static inline void strbuf_replace(struct strbuf *sb, const char *a, const char *b)\n+{\n+\tconst char *ptr = strstr(sb->buf, a);\n+\tif (ptr)\n+\t\tstrbuf_splice(sb, ptr - sb->buf, strlen(a), b, strlen(b));\n+}\n+\n static void free_trailer_item(struct trailer_item *item)\n {\n \tfree(item->conf.name);\n@@ -203,6 +214,63 @@ static struct trailer_item *remove_first(struct trailer_item **first)\n \treturn item;\n }\n \n+static int read_from_command(struct child_process *cp, struct strbuf *buf)\n+{\n+\tif (run_command(cp))\n+\t\treturn error(\"running trailer command '%s' failed\", cp->argv[0]);\n+\tif (strbuf_read(buf, cp->out, 1024) < 1)\n+\t\treturn error(\"reading from trailer command '%s' failed\", cp->argv[0]);\n+\tstrbuf_trim(buf);\n+\treturn 0;\n+}\n+\n+static const char *apply_command(const char *command, const char *arg)\n+{\n+\tstruct strbuf cmd = STRBUF_INIT;\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct child_process cp;\n+\tconst char *argv[] = {NULL, NULL};\n+\tconst char *result;\n+\n+\tstrbuf_addstr(&cmd, command);\n+\tif (arg)\n+\t\tstrbuf_replace(&cmd, TRAILER_ARG_STRING, arg);\n+\n+\targv[0] = cmd.buf;\n+\tmemset(&cp, 0, sizeof(cp));\n+\tcp.argv = argv;\n+\tcp.env = local_repo_env;\n+\tcp.no_stdin = 1;\n+\tcp.out = -1;\n+\tcp.use_shell = 1;\n+\n+\tif (read_from_command(&cp, &buf)) {\n+\t\tstrbuf_release(&buf);\n+\t\tresult = xstrdup(\"\");\n+\t} else\n+\t\tresult = strbuf_detach(&buf, NULL);\n+\n+\tstrbuf_release(&cmd);\n+\treturn result;\n+}\n+\n+static void apply_item_command(struct trailer_item *in_tok, struct trailer_item *arg_tok)\n+{\n+\tif (arg_tok->conf.command) {\n+\t\tconst char *arg;\n+\t\tif (arg_tok->value && arg_tok->value[0]) {\n+\t\t\targ = arg_tok->value;\n+\t\t} else {\n+\t\t\tif (in_tok && in_tok->value)\n+\t\t\t\targ = xstrdup(in_tok->value);\n+\t\t\telse\n+\t\t\t\targ = xstrdup(\"\");\n+\t\t}\n+\t\targ_tok->value = apply_command(arg_tok->conf.command, arg);\n+\t\tfree((char *)arg);\n+\t}\n+}\n+\n static void apply_arg_if_exists(struct trailer_item *in_tok,\n \t\t\t\tstruct trailer_item *arg_tok,\n \t\t\t\tstruct trailer_item *on_tok,\n@@ -214,16 +282,19 @@ static void apply_arg_if_exists(struct trailer_item *in_tok,\n \t\tfree_trailer_item(arg_tok);\n \t\tbreak;\n \tcase EXISTS_REPLACE:\n+\t\tapply_item_command(in_tok, arg_tok);\n \t\tadd_arg_to_input_list(on_tok, arg_tok,\n \t\t\t\t      in_tok_first, in_tok_last);\n \t\tremove_from_list(in_tok, in_tok_first, in_tok_last);\n \t\tfree_trailer_item(in_tok);\n \t\tbreak;\n \tcase EXISTS_ADD:\n+\t\tapply_item_command(in_tok, arg_tok);\n \t\tadd_arg_to_input_list(on_tok, arg_tok,\n \t\t\t\t      in_tok_first, in_tok_last);\n \t\tbreak;\n \tcase EXISTS_ADD_IF_DIFFERENT:\n+\t\tapply_item_command(in_tok, arg_tok);\n \t\tif (check_if_different(in_tok, arg_tok, 1))\n \t\t\tadd_arg_to_input_list(on_tok, arg_tok,\n \t\t\t\t\t      in_tok_first, in_tok_last);\n@@ -231,6 +302,7 @@ static void apply_arg_if_exists(struct trailer_item *in_tok,\n \t\t\tfree_trailer_item(arg_tok);\n \t\tbreak;\n \tcase EXISTS_ADD_IF_DIFFERENT_NEIGHBOR:\n+\t\tapply_item_command(in_tok, arg_tok);\n \t\tif (check_if_different(on_tok, arg_tok, 0))\n \t\t\tadd_arg_to_input_list(on_tok, arg_tok,\n \t\t\t\t\t      in_tok_first, in_tok_last);\n@@ -254,6 +326,7 @@ static void apply_arg_if_missing(struct trailer_item **in_tok_first,\n \tcase MISSING_ADD:\n \t\twhere = arg_tok->conf.where;\n \t\tin_tok = after_or_end(where) ? in_tok_last : in_tok_first;\n+\t\tapply_item_command(NULL, arg_tok);\n \t\tif (*in_tok) {\n \t\t\tadd_arg_to_input_list(*in_tok, arg_tok,\n \t\t\t\t\t      in_tok_first, in_tok_last);\n@@ -536,7 +609,7 @@ static struct trailer_item *new_trailer_item(struct trailer_item *conf_item,\n \t\t\t\t\t     char *tok, char *val)\n {\n \tstruct trailer_item *new = xcalloc(sizeof(*new), 1);\n-\tnew->value = val;\n+\tnew->value = val ? val : xstrdup(\"\");\n \n \tif (conf_item) {\n \t\tduplicate_conf(&new->conf, &conf_item->conf);\n@@ -605,7 +678,17 @@ static struct trailer_item *process_command_line_args(struct string_list *traile\n \tstruct trailer_item *arg_tok_first = NULL;\n \tstruct trailer_item *arg_tok_last = NULL;\n \tstruct string_list_item *tr;\n+\tstruct trailer_item *item;\n+\n+\t/* Add a trailer item for each configured trailer with a command */\n+\tfor (item = first_conf_item; item; item = item->next) {\n+\t\tif (item->conf.command) {\n+\t\t\tstruct trailer_item *new = new_trailer_item(item, NULL, NULL);\n+\t\t\tadd_trailer_item(&arg_tok_first, &arg_tok_last, new);\n+\t\t}\n+\t}\n \n+\t/* Add a trailer item for each trailer on the command line */\n \tfor_each_string_list_item(tr, trailers) {\n \t\tstruct trailer_item *new = create_trailer_item(tr->string);\n \t\tadd_trailer_item(&arg_tok_first, &arg_tok_last, new);\n-- \n2.0.3.960.g41c6e4c\n"},{"id":"249688","messageId":"20140920134515.18999.78477.chriscool@tuxfamily.org","threadId":"37601","inReplyTo":"20140920134048.18999.79434.chriscool@tuxfamily.org","subject":"[PATCH v15 10/11] trailer: add tests for commands in config file","fromName":"Christian Couder","fromEmail":"chriscool@tuxfamily.org","sentAt":"2014-09-20T13:45:13Z","receivedAt":"2014-09-20T13:45:13Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"And add a few other tests for some special cases.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n t/t7513-interpret-trailers.sh | 125 ++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 125 insertions(+)\n\ndiff --git a/t/t7513-interpret-trailers.sh b/t/t7513-interpret-trailers.sh\nindex fa37565..c5b86ff 100755\n--- a/t/t7513-interpret-trailers.sh\n+++ b/t/t7513-interpret-trailers.sh\n@@ -723,4 +723,129 @@ test_expect_success 'default \"where\" is now \"after\"' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'with simple command' '\n+\tgit config trailer.sign.key \"Signed-off-by: \" &&\n+\tgit config trailer.sign.where \"after\" &&\n+\tgit config trailer.sign.ifExists \"addIfDifferentNeighbor\" &&\n+\tgit config trailer.sign.command \"echo \\\"A U Thor <author@example.com>\\\"\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\t\tSigned-off-by: A U Thor <author@example.com>\n+\tEOF\n+\tgit interpret-trailers --trailer \"review:\" --trailer \"fix=22\" \\\n+\t\t<complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with command using commiter information' '\n+\tgit config trailer.sign.ifExists \"addIfDifferent\" &&\n+\tgit config trailer.sign.command \"echo \\\"\\$GIT_COMMITTER_NAME <\\$GIT_COMMITTER_EMAIL>\\\"\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\t\tSigned-off-by: C O Mitter <committer@example.com>\n+\tEOF\n+\tgit interpret-trailers --trailer \"review:\" --trailer \"fix=22\" \\\n+\t\t<complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with command using author information' '\n+\tgit config trailer.sign.key \"Signed-off-by: \" &&\n+\tgit config trailer.sign.where \"after\" &&\n+\tgit config trailer.sign.ifExists \"addIfDifferentNeighbor\" &&\n+\tgit config trailer.sign.command \"echo \\\"\\$GIT_AUTHOR_NAME <\\$GIT_AUTHOR_EMAIL>\\\"\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-\\EOF &&\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\t\tSigned-off-by: A U Thor <author@example.com>\n+\tEOF\n+\tgit interpret-trailers --trailer \"review:\" --trailer \"fix=22\" \\\n+\t\t<complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'setup a commit' '\n+\techo \"Content of the first commit.\" > a.txt &&\n+\tgit add a.txt &&\n+\tgit commit -m \"Add file a.txt\"\n+'\n+\n+test_expect_success 'with command using $ARG' '\n+\tgit config trailer.fix.ifExists \"replace\" &&\n+\tgit config trailer.fix.command \"git log -1 --oneline --format=\\\"%h (%s)\\\" --abbrev-commit --abbrev=14 \\$ARG\" &&\n+\tFIXED=$(git log -1 --oneline --format=\"%h (%s)\" --abbrev-commit --abbrev=14 HEAD) &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-EOF &&\n+\t\tFixes: $FIXED\n+\t\tAcked-by= Z\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\t\tSigned-off-by: A U Thor <author@example.com>\n+\tEOF\n+\tgit interpret-trailers --trailer \"review:\" --trailer \"fix=HEAD\" \\\n+\t\t<complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with failing command using $ARG' '\n+\tgit config trailer.fix.ifExists \"replace\" &&\n+\tgit config trailer.fix.command \"false \\$ARG\" &&\n+\tcat complex_message_body >expected &&\n+\tsed -e \"s/ Z\\$/ /\" >>expected <<-EOF &&\n+\t\tFixes: Z\n+\t\tAcked-by= Z\n+\t\tReviewed-by:\n+\t\tSigned-off-by: Z\n+\t\tSigned-off-by: A U Thor <author@example.com>\n+\tEOF\n+\tgit interpret-trailers --trailer \"review:\" --trailer \"fix=HEAD\" \\\n+\t\t<complex_message >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with empty tokens' '\n+\tgit config --unset trailer.fix.command &&\n+\tcat >expected <<-EOF &&\n+\n+\t\tSigned-off-by: A U Thor <author@example.com>\n+\tEOF\n+\tgit interpret-trailers --trailer \":\" --trailer \":test\" >actual <<-EOF &&\n+\tEOF\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with command but no key' '\n+\tgit config --unset trailer.sign.key &&\n+\tcat >expected <<-EOF &&\n+\n+\t\tsign: A U Thor <author@example.com>\n+\tEOF\n+\tgit interpret-trailers >actual <<-EOF &&\n+\tEOF\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'with no command and no key' '\n+\tgit config --unset trailer.review.key &&\n+\tcat >expected <<-EOF &&\n+\n+\t\treview: Junio\n+\t\tsign: A U Thor <author@example.com>\n+\tEOF\n+\tgit interpret-trailers --trailer \"review:Junio\" >actual <<-EOF &&\n+\tEOF\n+\ttest_cmp expected actual\n+'\n+\n test_done\n-- \n2.0.3.960.g41c6e4c\n"},{"id":"249690","messageId":"20140920134515.18999.58095.chriscool@tuxfamily.org","threadId":"37601","inReplyTo":"20140920134048.18999.79434.chriscool@tuxfamily.org","subject":"[PATCH v15 11/11] Documentation: add documentation for 'git interpret-trailers'","fromName":"Christian Couder","fromEmail":"chriscool@tuxfamily.org","sentAt":"2014-09-20T13:45:14Z","receivedAt":"2014-09-20T13:45:14Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"While at it add git-interpret-trailers to \"command-list.txt\".\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/git-interpret-trailers.txt | 313 +++++++++++++++++++++++++++++++\n command-list.txt                         |   1 +\n 2 files changed, 314 insertions(+)\n create mode 100644 Documentation/git-interpret-trailers.txt\n\ndiff --git a/Documentation/git-interpret-trailers.txt b/Documentation/git-interpret-trailers.txt\nnew file mode 100644\nindex 0000000..db1e9da\n--- /dev/null\n+++ b/Documentation/git-interpret-trailers.txt\n@@ -0,0 +1,313 @@\n+git-interpret-trailers(1)\n+=========================\n+\n+NAME\n+----\n+git-interpret-trailers - help add stuctured information into commit messages\n+\n+SYNOPSIS\n+--------\n+[verse]\n+'git interpret-trailers' [--trim-empty] [(--trailer <token>[(=|:)<value>])...] [<file>...]\n+\n+DESCRIPTION\n+-----------\n+Help adding 'trailers' lines, that look similar to RFC 822 e-mail\n+headers, at the end of the otherwise free-form part of a commit\n+message.\n+\n+This command reads some patches or commit messages from either the\n+<file> arguments or the standard input if no <file> is specified. Then\n+this command applies the arguments passed using the `--trailer`\n+option, if any, to the commit message part of each input file. The\n+result is emitted on the standard output.\n+\n+Some configuration variables control the way the `--trailer` arguments\n+are applied to each commit message and the way any existing trailer in\n+the commit message is changed. They also make it possible to\n+automatically add some trailers.\n+\n+By default, a '<token>=<value>' or '<token>:<value>' argument given\n+using `--trailer` will be appended after the existing trailers only if\n+the last trailer has a different (<token>, <value>) pair (or if there\n+is no existing trailer). The <token> and <value> parts will be trimmed\n+to remove starting and trailing whitespace, and the resulting trimmed\n+<token> and <value> will appear in the message like this:\n+\n+------------------------------------------------\n+token: value\n+------------------------------------------------\n+\n+This means that the trimmed <token> and <value> will be separated by\n+`': '` (one colon followed by one space).\n+\n+By default the new trailer will appear at the end of all the existing\n+trailers. If there is no existing trailer, the new trailer will appear\n+after the commit message part of the ouput, and, if there is no line\n+with only spaces at the end of the commit message part, one blank line\n+will be added before the new trailer.\n+\n+Existing trailers are extracted from the input message by looking for\n+a group of one or more lines that contain a colon (by default), where\n+the group is preceded by one or more empty (or whitespace-only) lines.\n+The group must either be at the end of the message or be the last\n+non-whitespace lines before a line that starts with '---'. Such three\n+minus signs start the patch part of the message.\n+\n+When reading trailers, there can be whitespaces before and after the\n+token, the separator and the value. There can also be whitespaces\n+indide the token and the value.\n+\n+Note that 'trailers' do not follow and are not intended to follow many\n+rules for RFC 822 headers. For example they do not follow the line\n+folding rules, the encoding rules and probably many other rules.\n+\n+OPTIONS\n+-------\n+--trim-empty::\n+\tIf the <value> part of any trailer contains only whitespace,\n+\tthe whole trailer will be removed from the resulting message.\n+\tThis apply to existing trailers as well as new trailers.\n+\n+--trailer <token>[(=|:)<value>]::\n+\tSpecify a (<token>, <value>) pair that should be applied as a\n+\ttrailer to the input messages. See the description of this\n+\tcommand.\n+\n+CONFIGURATION VARIABLES\n+-----------------------\n+\n+trailer.separators::\n+\tThis option tells which characters are recognized as trailer\n+\tseparators. By default only ':' is recognized as a trailer\n+\tseparator, except that '=' is always accepted on the command\n+\tline for compatibility with other git commands.\n++\n+The first character given by this option will be the default character\n+used when another separator is not specified in the config for this\n+trailer.\n++\n+For example, if the value for this option is \"%=$\", then only lines\n+using the format '<token><sep><value>' with <sep> containing '%', '='\n+or '$' and then spaces will be considered trailers. And '%' will be\n+the default separator used, so by default trailers will appear like:\n+'<token>% <value>' (one percent sign and one space will appear between\n+the token and the value).\n+\n+trailer.where::\n+\tThis option tells where a new trailer will be added.\n++\n+This can be `end`, which is the default, `start`, `after` or `before`.\n++\n+If it is `end`, then each new trailer will appear at the end of the\n+existing trailers.\n++\n+If it is `start`, then each new trailer will appear at the start,\n+instead of the end, of the existing trailers.\n++\n+If it is `after`, then each new trailer will appear just after the\n+last trailer with the same <token>.\n++\n+If it is `before`, then each new trailer will appear just before the\n+first trailer with the same <token>.\n+\n+trailer.ifexists::\n+\tThis option makes it possible to choose what action will be\n+\tperformed when there is already at least one trailer with the\n+\tsame <token> in the message.\n++\n+The valid values for this option are: `addIfDifferentNeighbor` (this\n+is the default), `addIfDifferent`, `add`, `overwrite` or `doNothing`.\n++\n+With `addIfDifferentNeighbor`, a new trailer will be added only if no\n+trailer with the same (<token>, <value>) pair is above or below the line\n+where the new trailer will be added.\n++\n+With `addIfDifferent`, a new trailer will be added only if no trailer\n+with the same (<token>, <value>) pair is already in the message.\n++\n+With `add`, a new trailer will be added, even if some trailers with\n+the same (<token>, <value>) pair are already in the message.\n++\n+With `replace`, an existing trailer with the same <token> will be\n+deleted and the new trailer will be added. The deleted trailer will be\n+the closest one (with the same <token>) to the place where the new one\n+will be added.\n++\n+With `doNothing`, nothing will be done; that is no new trailer will be\n+added if there is already one with the same <token> in the message.\n+\n+trailer.ifmissing::\n+\tThis option makes it possible to choose what action will be\n+\tperformed when there is not yet any trailer with the same\n+\t<token> in the message.\n++\n+The valid values for this option are: `add` (this is the default) and\n+`doNothing`.\n++\n+With `add`, a new trailer will be added.\n++\n+With `doNothing`, nothing will be done.\n+\n+trailer.<token>.key::\n+\tThis `key` will be used instead of <token> in the trailer. At\n+\tthe end of this key, a separator can appear and then some\n+\tspace characters. By default the only valid separator is ':',\n+\tbut this can be changed using the `trailer.separators` config\n+\tvariable.\n++\n+If there is a separator, then the key will be used instead of both the\n+<token> and the default separator when adding the trailer.\n+\n+trailer.<token>.where::\n+\tThis option takes the same values as the 'trailer.where'\n+\tconfiguration variable and it overrides what is specified by\n+\tthat option for trailers with the specified <token>.\n+\n+trailer.<token>.ifexist::\n+\tThis option takes the same values as the 'trailer.ifexist'\n+\tconfiguration variable and it overrides what is specified by\n+\tthat option for trailers with the specified <token>.\n+\n+trailer.<token>.ifmissing::\n+\tThis option takes the same values as the 'trailer.ifmissing'\n+\tconfiguration variable and it overrides what is specified by\n+\tthat option for trailers with the specified <token>.\n+\n+trailer.<token>.command::\n+\tThis option can be used to specify a shell command that will\n+\tbe called to automatically add or modify a trailer with the\n+\tspecified <token>.\n++\n+When this option is specified, the behavior is as if a special\n+'<token>=<value>' argument were added at the beginning of the command\n+line, where <value> is taken to be the standard output of the\n+specified command with any leading and trailing whitespace trimmed\n+off.\n++\n+If the command contains the `$ARG` string, this string will be\n+replaced with the <value> part of an existing trailer with the same\n+<token>, if any, before the command is launched.\n++\n+If some '<token>=<value>' arguments are also passed on the command\n+line, when a 'trailer.<token>.command' is configured, the command will\n+also be executed for each of these arguments. And the <value> part of\n+these arguments, if any, will be used to replace the `$ARG` string in\n+the command.\n+\n+EXAMPLES\n+--------\n+\n+* Configure a 'sign' trailer with a 'Signed-off-by' key, and then\n+  add two of these trailers to a message:\n++\n+------------\n+$ git config trailer.sign.key \"Signed-off-by\"\n+$ cat msg.txt\n+subject\n+\n+message\n+$ cat msg.txt | git interpret-trailers --trailer 'sign: Alice <alice@example.com>' --trailer 'sign: Bob <bob@example.com>'\n+subject\n+\n+message\n+\n+Signed-off-by: Alice <alice@example.com>\n+Signed-off-by: Bob <bob@example.com>\n+------------\n+\n+* Extract the last commit as a patch, and add a 'Cc' and a\n+  'Reviewed-by' trailer to it:\n++\n+------------\n+$ git format-patch -1\n+0001-foo.patch\n+$ git interpret-trailers --trailer 'Cc: Alice <alice@example.com>' --trailer 'Reviewed-by: Bob <bob@example.com>' 0001-foo.patch >0001-bar.patch\n+------------\n+\n+* Configure a 'sign' trailer with a command to automatically add a\n+  'Signed-off-by: ' with the author information only if there is no\n+  'Signed-off-by: ' already, and show how it works:\n++\n+------------\n+$ git config trailer.sign.key \"Signed-off-by: \"\n+$ git config trailer.sign.ifmissing add\n+$ git config trailer.sign.ifexists doNothing\n+$ git config trailer.sign.command 'echo \"$(git config user.name) <$(git config user.email)>\"'\n+$ git interpret-trailers <<EOF\n+> EOF\n+\n+Signed-off-by: Bob <bob@example.com>\n+$ git interpret-trailers <<EOF\n+> Signed-off-by: Alice <alice@example.com>\n+> EOF\n+\n+Signed-off-by: Alice <alice@example.com>\n+------------\n+\n+* Configure a 'fix' trailer with a key that contains a '#' and no\n+  space after this character, and show how it works:\n++\n+------------\n+$ git config trailer.separators \":#\"\n+$ git config trailer.fix.key \"Fix #\"\n+$ echo \"subject\" | git interpret-trailers --trailer fix=42\n+subject\n+\n+Fix #42\n+------------\n+\n+* Configure a 'see' trailer with a command to show the subject of a\n+  commit that is related, and show how it works:\n++\n+------------\n+$ git config trailer.see.key \"See-also: \"\n+$ git config trailer.see.ifExists \"replace\"\n+$ git config trailer.see.ifMissing \"doNothing\"\n+$ git config trailer.see.command \"git log -1 --oneline --format=\\\"%h (%s)\\\" --abbrev-commit --abbrev=14 \\$ARG\"\n+$ git interpret-trailers <<EOF\n+> subject\n+> \n+> message\n+> \n+> see: HEAD~2\n+> EOF\n+subject\n+\n+message\n+\n+See-also: fe3187489d69c4 (subject of related commit)\n+------------\n+\n+* Configure a commit template with some trailers with empty values,\n+  then configure a commit-msg hook that uses 'git interpret-trailers'\n+  to remove trailers with empty values and to add a 'git-version'\n+  trailer:\n++\n+------------\n+$ cat >commit_template.txt <<EOF\n+> ***subject***\n+> \n+> ***message***\n+> \n+> Fixes: \n+> Cc: \n+> Reviewed-by: \n+> Signed-off-by: \n+> EOF\n+$ git config commit.template commit_template.txt\n+$ cat >.git/hooks/commit-msg <<EOF\n+> #!/bin/sh\n+> git interpret-trailers --trim-empty --trailer \"git-version: \\$(git describe)\" \"\\$1\" > \"\\$1.new\"\n+> mv \"\\$1.new\" \"\\$1\"\n+> EOF\n+$ chmod +x .git/hooks/commit-msg\n+------------\n+\n+SEE ALSO\n+--------\n+linkgit:git-commit[1], linkgit:git-format-patch[1], linkgit:git-config[1]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/command-list.txt b/command-list.txt\nindex a3ff0c9..f1eae08 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -62,6 +62,7 @@ git-imap-send                           foreignscminterface\n git-index-pack                          plumbingmanipulators\n git-init                                mainporcelain common\n git-instaweb                            ancillaryinterrogators\n+git-interpret-trailers                  purehelpers\n gitk                                    mainporcelain\n git-log                                 mainporcelain common\n git-ls-files                            plumbinginterrogators\n-- \n2.0.3.960.g41c6e4c\n"},{"id":"249757","messageId":"xmqqk34vjqni.fsf@gitster.dls.corp.google.com","threadId":"37601","inReplyTo":"20140920134515.18999.58095.chriscool@tuxfamily.org","subject":"Re: [PATCH v15 11/11] Documentation: add documentation for 'git interpret-trailers'","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-22T21:23:29Z","receivedAt":"2014-09-22T21:23:29Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"This has quite a few \"trailing whitespace\" errors in the patch in\nthe documentation part.  It would mean that people who try to follow\nalong the examples would have difficult time deciphering where\ntrailing whitespaces are necessary.\n\nFor example\n\n> +* Configure a commit template with some trailers with empty values,\n> +  then configure a commit-msg hook that uses 'git interpret-trailers'\n> +  to remove trailers with empty values and to add a 'git-version'\n> +  trailer:\n> ++\n> +------------\n> +$ cat >commit_template.txt <<EOF\n> +> ***subject***\n> +> \n> +> ***message***\n> +> \n> +> Fixes: \n> +> Cc: \n> ...\n\nThis presents the example in the least useful way.  The leading \"> \"\nare coming from $PS2 so people cannot cut & paste to follow along on\ntheir interactive shell session, but cutting and pasting from a\nmachine-readable copy of the document would be the only way to\npreserve the trailing SP on lines that begin with Fixes:, Cc: and\nfriends.  On the other hand, those who type each line while\neyeballing the documentation (because it is not cut-and-paste ready\ndue to $PS2) may not spot the trailing SP on these lines.\n\nCan we think of a way to make the necessary trailing SP stand out?\n"}]}