{"thread":{"id":"45001","subject":"[RFC] Proof of concept: Support multiple authors","startedAt":"2017-01-29T18:06:45Z","lastAt":"2017-01-31T01:12:53Z","messageCount":5,"participants":["Cornelius Schumacher","Christian Couder","Junio C Hamano"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"310510","messageId":"1485713194-11782-1-git-send-email-schumacher@kde.org","threadId":"45001","inReplyTo":null,"subject":"[RFC] Proof of concept: Support multiple authors","fromName":"Cornelius Schumacher","fromEmail":"schumacher@kde.org","sentAt":"2017-01-29T18:06:34Z","receivedAt":"2017-01-29T18:06:45Z","isPatch":false,"sender":{"key":"schumacher@kde.org","avatar":"https://gravatar.com/avatar/f1089e3b59bb8e14182f3befdfa38faa3212b1b77a6baff2d0edf94b34382c20?d=mp&s=160"},"body":"This patch is a proof of concept implementation of support for\nmultiple authors. It adds an optional `authors` header to commits\nwhich is set when there are authors configured in the git config.\n\nA new command `git-authors` is used to manage the authors settings.\nAuthors are identified by initials and their names and emails are\nset in a `.git_authors_map` file.\n\nSigned-off-by: Cornelius Schumacher <schumacher@kde.org>\n---\n\nWhen doing pair programming we have to work around the limitation that\ngit can only have a single author in each commit. There are some tools\nwhich help with that such as [git-duet] [1], but there are still some\nlimits, because the information about multiple authors is not reflected\nin the native git data model.\n\nHere is a proposal how to change that and implement native support for\nmultiple authors in git. It comes with a patch as a proof of concept.\nThe patch by no means is finished, it doesn't cover all cases and needs\nmore tests and error handling. It's meant as an illustration of the\nconcept.\n\nThe basic idea is to introduce a new optional `authors` header in\ncommits which contains a list of authors. The header is set in each new\ncommit when there is an entry `authors.current` in the git config listing\nthe current authors. When this config is not there the behavior falls\nback to the current standard behavior.\n\nWhen the header is there it is treated in the same way as the author\nheader. It's preserved on merges and similar operations, is displayed in\ngit show, and used to create a list of `From` addresses in `format-patch`.\nEmail supports multiple `From` addresses as specified in section 3.6.2 of\nRFC 5322.\n\nWhen multiple authors are configured, they still write the standard author\nheader to keep backwards compatibility. The first author is used as author\nand committer. In the future it might be good to implement something like\nautomatic rotation of the order of authors to give credit in a fair way.\n\nTo make it easier to work with the authors there is a new command\n`git-authors`. It sets the list of authors using initials as shortcut for\nthe full configuration with name and email. The mapping of initials to\nnames and email addresses is taken from a file `.git_authors_map` in the\nhome directory of the users. This way it's possible to quickly set a list\nof authors by running a command such as `git authors ab cd`. This is\nuseful when doing pair programming because the people working together\nusually switch quite frequently and using the command with the intials is\nquicker and less error-prone than editing the configuration with full\nnames and emails.\n\nThe command also supports setting a single author, setting more than two\nauthors or clearing the configuration for multiple authors to go back to\nthe standard behavior without the new authors header.\n\nThe concept of the command and the mappings file is similar to what\ngit-duet does, so that it should be familiar to many people doing pair\nprogramming. The behavior of git doesn't change when the new feature is\nnot used and when it's used it should be backwards compatible so that it\ndoesn't break existing functionality. This should make a smooth transition\nfor users who choose to make use of it.\n\nAdding support for multiple authors would make the life of developers doing\npair programming easier. It would be useful in itself, but it would also\nneed support by other tools around git to use its full potential. This\nmight take a while, but I think it's worth the effort.\n\nI'm willing to continue to work on this and create a patch which is suitable\nfor inclusion in git.\n\nWhat do you think?\n\n[1]: https://github.com/git-duet/git-duet\n\n .gitignore              |   1 +\n Makefile                |   3 +\n authors.c               | 205 ++++++++++++++++++++++++++++++++++++++++++++++++\n authors.h               |  29 +++++++\n builtin.h               |   1 +\n builtin/am.c            |  18 ++++-\n builtin/authors.c       |  82 +++++++++++++++++++\n builtin/commit-tree.c   |   2 +-\n builtin/commit.c        |  56 +++++++++++--\n builtin/merge.c         |   4 +-\n cache.h                 |   1 +\n commit.c                |  17 ++--\n commit.h                |   6 +-\n git.c                   |   1 +\n ident.c                 |   2 +-\n mailinfo.c              |  17 ++++\n mailinfo.h              |   2 +\n notes-cache.c           |   2 +-\n notes-utils.c           |   2 +-\n pretty.c                |  79 ++++++++++++++++++-\n t/helper/.gitignore     |   1 +\n t/helper/test-authors.c |  42 ++++++++++\n t/t9904-authors.sh      |  32 ++++++++\n 23 files changed, 581 insertions(+), 24 deletions(-)\n create mode 100644 authors.c\n create mode 100644 authors.h\n create mode 100644 builtin/authors.c\n create mode 100644 t/helper/test-authors.c\n create mode 100755 t/t9904-authors.sh\n\ndiff --git a/.gitignore b/.gitignore\nindex 6722f78..1323907 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -16,6 +16,7 @@\n /git-apply\n /git-archimport\n /git-archive\n+/git-authors\n /git-bisect\n /git-bisect--helper\n /git-blame\ndiff --git a/Makefile b/Makefile\nindex 27afd0f..22f4b28 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -597,6 +597,7 @@ X =\n\n PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS))\n\n+TEST_PROGRAMS_NEED_X += test-authors\n TEST_PROGRAMS_NEED_X += test-chmtime\n TEST_PROGRAMS_NEED_X += test-ctype\n TEST_PROGRAMS_NEED_X += test-config\n@@ -700,6 +701,7 @@ LIB_OBJS += archive-tar.o\n LIB_OBJS += archive-zip.o\n LIB_OBJS += argv-array.o\n LIB_OBJS += attr.o\n+LIB_OBJS += authors.o\n LIB_OBJS += base85.o\n LIB_OBJS += bisect.o\n LIB_OBJS += blob.o\n@@ -859,6 +861,7 @@ BUILTIN_OBJS += builtin/am.o\n BUILTIN_OBJS += builtin/annotate.o\n BUILTIN_OBJS += builtin/apply.o\n BUILTIN_OBJS += builtin/archive.o\n+BUILTIN_OBJS += builtin/authors.o\n BUILTIN_OBJS += builtin/bisect--helper.o\n BUILTIN_OBJS += builtin/blame.o\n BUILTIN_OBJS += builtin/branch.o\ndiff --git a/authors.c b/authors.c\nnew file mode 100644\nindex 0000000..ba782a0\n--- /dev/null\n+++ b/authors.c\n@@ -0,0 +1,205 @@\n+#include \"cache.h\"\n+#include \"authors.h\"\n+#include \"string.h\"\n+#include \"strbuf.h\"\n+\n+/*\n+ * given an authors line, split the fields\n+ * to allow the caller to parse it.\n+ * Signal a success by returning 0.\n+ */\n+int split_authors_line(struct authors_split *split, const char *line, int len)\n+{\n+\tconst char *cp;\n+\n+\tmemset(split, 0, sizeof(*split));\n+\n+\tsplit->begin = line;\n+\n+\tfor (cp = line + len - 1; *cp != '>'; cp--)\n+\t\tif (cp == line) return -1;\n+\n+\tsplit->end = cp + 1;\n+\treturn 0;\n+}\n+\n+void read_authors_map_line(struct string_list *map, char *buffer)\n+{\n+\tint len = strlen(buffer);\n+\n+\tif (len && buffer[len - 1] == '\\n')\n+\t\tbuffer[--len] = 0;\n+\n+\tstring_list_insert(map, xstrdup(buffer));\n+}\n+\n+void read_authors_map_file(struct string_list *map)\n+{\n+\tchar buffer[1024];\n+\tFILE *f;\n+\tconst char *filename;\n+\tconst char *home;\n+\n+\thome = getenv(\"HOME\");\n+\tif (!home)\n+\t\tdie(\"HOME not set\");\n+\n+\tfilename = mkpathdup(\"%s/.git_authors_map\", home);\n+\n+\tf = fopen(filename, \"r\");\n+\tif (!f) {\n+\t\tif (errno == ENOENT) {\n+\t\t\twarning(\"~/.git_authors_map does not exist\");\n+\t\t\treturn;\n+\t\t}\n+\t\tdie_errno(\"unable to open authors map at %s\", filename);\n+\t}\n+\n+\twhile (fgets(buffer, sizeof(buffer), f) != NULL)\n+\t\tread_authors_map_line(map, buffer);\n+\tfclose(f);\n+}\n+\n+char *lookup_author(struct string_list *map, const char *author_abbr)\n+{\n+\tstruct string_list_item *author_item = NULL;\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, map) {\n+\t\tif (strncmp(item->string, author_abbr, strlen(author_abbr)) == 0 &&\n+\t\t    strlen(item->string) > strlen(author_abbr) &&\n+\t\t    *(item->string + strlen(author_abbr)) == ' ') {\n+\t\t\tauthor_item = item;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tif (!author_item)\n+\t\treturn NULL;\n+\n+\treturn xstrdup(author_item->string + strlen(author_abbr) + 1);\n+}\n+\n+const char *expand_authors(struct string_list *map, const char *author_shorts)\n+{\n+\tint i;\n+\tconst char *author_start = author_shorts;\n+\tconst char *author_end;\n+\tchar *author_short, *expanded_author;\n+\tstatic struct strbuf expanded_authors = STRBUF_INIT;\n+\n+\tstrbuf_reset(&expanded_authors);\n+\n+\tfor (i = 0; i <= strlen(author_shorts); i++) {\n+\t\tauthor_end = author_shorts + i;\n+\t\tif (*author_end == ' ' || *author_end == '\\0') {\n+\t\t\tauthor_short = xstrndup(author_start, author_end - author_start);\n+\t\t\texpanded_author = lookup_author(map, author_short);\n+\t\t\tif (!expanded_author)\n+\t\t\t\tdie(\"Could not expand author '%s'. Add it to the file ~/.git_authors_map.\", author_short);\n+\t\t\telse {\n+\t\t\t\tif (expanded_authors.len > 0)\n+\t\t\t\t\tstrbuf_addch(&expanded_authors, ',');\n+\t\t\t\tstrbuf_addstr(&expanded_authors, expanded_author);\n+\t\t\t\tfree(expanded_author);\n+\t\t\t}\n+\t\t\tfree(author_short);\n+\n+\t\t\tauthor_start = author_end + 1;\n+\t\t}\n+\t}\n+\n+\treturn expanded_authors.buf;\n+}\n+\n+const char *git_authors_info(void)\n+{\n+\tstatic struct strbuf authors_info = STRBUF_INIT;\n+\tconst char *authors_config = NULL;\n+\tconst char *date_str = NULL;\n+\tstruct string_list authors_map = STRING_LIST_INIT_NODUP;\n+\n+\tif (git_config_get_string_const(\"authors.current\", &authors_config))\n+\t\treturn NULL;\n+\n+\tread_authors_map_file(&authors_map);\n+\n+\tstrbuf_reset(&authors_info);\n+\tstrbuf_addstr(&authors_info, expand_authors(&authors_map, authors_config));\n+\n+\tstrbuf_addch(&authors_info, ' ');\n+\tdate_str = getenv(\"GIT_AUTHOR_DATE\");\n+\tif (date_str && date_str[0]) {\n+\t\tif (parse_date(date_str, &authors_info) < 0)\n+\t\t\tdie(\"invalid date format: %s\", date_str);\n+\t}\n+\telse\n+\t\tstrbuf_addstr(&authors_info, ident_default_date());\n+\n+\treturn authors_info.buf;\n+}\n+\n+const char *git_authors_first_info(const char *authors)\n+{\n+\tstatic struct strbuf authors_first_info = STRBUF_INIT;\n+\tstruct authors_split split;\n+\tconst char *cp;\n+\n+\tif (split_authors_line(&split, authors, strlen(authors)) < 0)\n+\t\tdie(\"invalid authors format: %s\", authors);\n+\n+\tfor (cp = split.begin; cp < split.end; cp++)\n+\t\tif (*cp == ',')\n+\t\t\tbreak;\n+\tstrbuf_add(&authors_first_info, split.begin, cp - split.begin);\n+\tstrbuf_add(&authors_first_info, split.end, strlen(authors));\n+\n+\treturn authors_first_info.buf;\n+}\n+\n+const char *authors_split_to_email_froms(const struct authors_split *authors)\n+{\n+\tstatic struct strbuf email_froms = STRBUF_INIT;\n+\tconst char *cp;\n+\n+\tstrbuf_reset(&email_froms);\n+\n+\tstrbuf_addstr(&email_froms, \"From: \");\n+\tfor(cp = authors->begin; cp < authors->end; cp++)\n+\t\tif (*cp == ',')\n+\t\t\tstrbuf_addstr(&email_froms, \"\\nFrom: \");\n+\t\telse\n+\t\t\tstrbuf_addch(&email_froms, *cp);\n+\tstrbuf_addch(&email_froms, '\\n');\n+\n+\treturn email_froms.buf;\n+}\n+\n+int has_multiple_authors(const char *authors)\n+{\n+\tconst char *cp = authors;\n+\n+\twhile (*cp != '\\0')\n+\t\tif (*cp++ == ',')\n+\t\t\treturn 1;\n+\treturn 0;\n+}\n+\n+const char *fmt_authors(const char *authors, const char *date_str)\n+{\n+\tstatic struct strbuf authors_info = STRBUF_INIT;\n+\n+\tstrbuf_reset(&authors_info);\n+\n+\tstrbuf_addstr(&authors_info, authors);\n+\n+\tstrbuf_addch(&authors_info, ' ');\n+\tif (date_str && date_str[0]) {\n+\t\tif (parse_date(date_str, &authors_info) < 0)\n+\t\t\tdie(\"invalid date format: %s\", date_str);\n+\t}\n+\telse\n+\t\tstrbuf_addstr(&authors_info, ident_default_date());\n+\n+\treturn authors_info.buf;\n+}\ndiff --git a/authors.h b/authors.h\nnew file mode 100644\nindex 0000000..aa96674\n--- /dev/null\n+++ b/authors.h\n@@ -0,0 +1,29 @@\n+#ifndef AUTHORS_H\n+#define AUTHORS_H\n+\n+#include \"string-list.h\"\n+\n+struct authors_split {\n+\tconst char *begin;\n+\tconst char *end;\n+};\n+/*\n+ * Signals an success with 0\n+ */\n+extern int split_authors_line(struct authors_split *, const char *, int);\n+\n+extern const char *git_authors_info(void);\n+extern const char *git_authors_first_info(const char *);\n+\n+extern void read_authors_map_file(struct string_list *);\n+\n+extern char *lookup_author(struct string_list *, const char *);\n+extern const char *expand_authors(struct string_list *, const char *);\n+\n+extern const char *authors_split_to_email_froms(const struct authors_split *);\n+\n+extern int has_multiple_authors(const char *);\n+\n+extern const char *fmt_authors(const char *, const char *);\n+\n+#endif /* AUTHORS_H */\ndiff --git a/builtin.h b/builtin.h\nindex b9122bc..fec370e 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -34,6 +34,7 @@ extern int cmd_am(int argc, const char **argv, const char *prefix);\n extern int cmd_annotate(int argc, const char **argv, const char *prefix);\n extern int cmd_apply(int argc, const char **argv, const char *prefix);\n extern int cmd_archive(int argc, const char **argv, const char *prefix);\n+extern int cmd_authors(int argc, const char **argv, const char *prefix);\n extern int cmd_bisect__helper(int argc, const char **argv, const char *prefix);\n extern int cmd_blame(int argc, const char **argv, const char *prefix);\n extern int cmd_branch(int argc, const char **argv, const char *prefix);\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 31fb605..cc131d4 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -30,6 +30,7 @@\n #include \"mailinfo.h\"\n #include \"apply.h\"\n #include \"string-list.h\"\n+#include \"authors.h\"\n\n /**\n  * Returns 1 if the file is empty or does not exist, 0 otherwise.\n@@ -106,6 +107,7 @@ struct am_state {\n \tchar *author_name;\n \tchar *author_email;\n \tchar *author_date;\n+\tchar *authors;\n \tchar *msg;\n \tsize_t msg_len;\n\n@@ -171,6 +173,7 @@ static void am_state_release(struct am_state *state)\n \tfree(state->author_name);\n \tfree(state->author_email);\n \tfree(state->author_date);\n+\tfree(state->authors);\n \tfree(state->msg);\n \targv_array_clear(&state->git_apply_opts);\n }\n@@ -1237,6 +1240,7 @@ static int parse_mail(struct am_state *state, const char *mail)\n \tstruct strbuf author_name = STRBUF_INIT;\n \tstruct strbuf author_date = STRBUF_INIT;\n \tstruct strbuf author_email = STRBUF_INIT;\n+\tstruct strbuf authors = STRBUF_INIT;\n \tint ret = 0;\n \tstruct mailinfo mi;\n\n@@ -1303,6 +1307,8 @@ static int parse_mail(struct am_state *state, const char *mail)\n \t\t\tstrbuf_addstr(&author_email, x);\n \t\telse if (skip_prefix(sb.buf, \"Date: \", &x))\n \t\t\tstrbuf_addstr(&author_date, x);\n+\t\telse if (skip_prefix(sb.buf, \"Authors: \", &x))\n+\t\t\tstrbuf_addstr(&authors, x);\n \t}\n \tfclose(fp);\n\n@@ -1333,6 +1339,10 @@ static int parse_mail(struct am_state *state, const char *mail)\n \tassert(!state->author_date);\n \tstate->author_date = strbuf_detach(&author_date, NULL);\n\n+\tassert(!state->authors);\n+\tif (authors.len > 0)\n+\t\tstate->authors = strbuf_detach(&authors, NULL);\n+\n \tassert(!state->msg);\n \tstate->msg = strbuf_detach(&msg, &state->msg_len);\n\n@@ -1341,6 +1351,7 @@ static int parse_mail(struct am_state *state, const char *mail)\n \tstrbuf_release(&author_date);\n \tstrbuf_release(&author_email);\n \tstrbuf_release(&author_name);\n+\tstrbuf_release(&authors);\n \tstrbuf_release(&sb);\n \tclear_mailinfo(&mi);\n \treturn ret;\n@@ -1678,6 +1689,7 @@ static void do_commit(const struct am_state *state)\n \tstruct commit_list *parents = NULL;\n \tconst char *reflog_msg, *author;\n \tstruct strbuf sb = STRBUF_INIT;\n+\tconst char *authors_info = NULL;\n\n \tif (run_hook_le(NULL, \"pre-applypatch\", NULL))\n \t\texit(1);\n@@ -1701,8 +1713,12 @@ static void do_commit(const struct am_state *state)\n \t\tsetenv(\"GIT_COMMITTER_DATE\",\n \t\t\tstate->ignore_date ? \"\" : state->author_date, 1);\n\n+\tif (state->authors)\n+\t\tauthors_info = fmt_authors(state->authors,\n+\t\t\t\tstate->ignore_date ? NULL : state->author_date);\n+\n \tif (commit_tree(state->msg, state->msg_len, tree.hash, parents, commit.hash,\n-\t\t\t\tauthor, state->sign_commit))\n+\t\t\t\tauthor, authors_info, state->sign_commit))\n \t\tdie(_(\"failed to write commit object\"));\n\n \treflog_msg = getenv(\"GIT_REFLOG_ACTION\");\ndiff --git a/builtin/authors.c b/builtin/authors.c\nnew file mode 100644\nindex 0000000..3342b91\n--- /dev/null\n+++ b/builtin/authors.c\n@@ -0,0 +1,82 @@\n+#include \"builtin.h\"\n+#include \"authors.h\"\n+#include \"parse-options.h\"\n+\n+static const char *const builtin_authors_usage[] = {\n+\tN_(\"git authors [<options>]\"),\n+\tNULL\n+};\n+\n+static int actions;\n+\n+#define ACTION_LIST (1<<0)\n+#define ACTION_GET (1<<1)\n+#define ACTION_SET (1<<2)\n+#define ACTION_CLEAR (1<<3)\n+\n+static struct option builtin_authors_options[] = {\n+\tOPT_BIT('c', \"clear\", &actions, N_(\"clear current authors\"), ACTION_CLEAR),\n+\tOPT_BIT('l', \"list\", &actions, N_(\"list all available authors\"), ACTION_LIST)\n+};\n+\n+static struct string_list authors_map = STRING_LIST_INIT_NODUP;\n+\n+int cmd_authors(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct string_list_item *item;\n+\n+\targc = parse_options(argc, argv, prefix, builtin_authors_options,\n+\t\t\t     builtin_authors_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\n+\tif (actions == 0) {\n+\tif (argc == 0)\n+\t\tactions = ACTION_GET;\n+\telse\n+\tactions = ACTION_SET;\n+\t}\n+\n+\tread_authors_map_file(&authors_map);\n+\n+\tif (actions == ACTION_LIST)\n+\t\tfor_each_string_list_item(item, &authors_map) {\n+\t\tprintf(\"%s\\n\", item->string);\n+\t}\n+\telse if (actions == ACTION_GET) {\n+\t\tconst char *authors_config = NULL;\n+\t\tconst char *expanded_authors;\n+\n+\t\tif (git_config_get_string_const(\"authors.current\", &authors_config))\n+\t\t\tdie(\"No current authors set. Use `git authors <initials> <initials> to set authors.\");\n+\n+\t\tprintf(\"Short:    %s\\n\", authors_config);\n+\n+\t\texpanded_authors = expand_authors(&authors_map, authors_config);\n+\n+\t\tprintf(\"Expanded: %s\\n\", expanded_authors);\n+\t}\n+\telse if (actions == ACTION_SET) {\n+\t\tint i;\n+\t\tstatic struct strbuf authors_info = STRBUF_INIT;\n+\t\tint lookup_error = 0;\n+\n+\t\tfor (i = 0; i < argc; ++i) {\n+\t\t\tif (!lookup_author(&authors_map, argv[i])) {\n+\t\t\t\tlookup_error--;\n+\t\t\t\terror(\"Couldn't find author '%s'\", argv[i]);\n+\t\t\t}\n+\t\t\tif (i > 0)\n+\t\t\t\tstrbuf_addch(&authors_info, ' ');\n+\t\t\t\tstrbuf_addstr(&authors_info, argv[i]);\n+\t\t}\n+\t\tif (lookup_error < 0)\n+\t\t\tdie(\"Add missing authors to ~/.git_authors_map\");\n+\n+\t\tgit_config_set(\"authors.current\", authors_info.buf);\n+\t}\n+\telse if (actions == ACTION_CLEAR) {\n+\t\tgit_config_set(\"authors.current\", NULL);\n+\t}\n+\n+\treturn 0;\n+}\ndiff --git a/builtin/commit-tree.c b/builtin/commit-tree.c\nindex 6050172..ecc2ce4 100644\n--- a/builtin/commit-tree.c\n+++ b/builtin/commit-tree.c\n@@ -118,7 +118,7 @@ int cmd_commit_tree(int argc, const char **argv, const char *prefix)\n \t}\n\n \tif (commit_tree(buffer.buf, buffer.len, tree_oid.hash, parents,\n-\t\t\tcommit_oid.hash, NULL, sign_commit)) {\n+\t\t\tcommit_oid.hash, NULL, NULL, sign_commit)) {\n \t\tstrbuf_release(&buffer);\n \t\treturn 1;\n \t}\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 711f96c..88038ce 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -33,6 +33,7 @@\n #include \"notes-utils.h\"\n #include \"mailmap.h\"\n #include \"sigchain.h\"\n+#include \"authors.h\"\n\n static const char * const builtin_commit_usage[] = {\n \tN_(\"git commit [<options>] [--] <pathspec>...\"),\n@@ -622,6 +623,29 @@ static void determine_author_info(struct strbuf *author_ident)\n \tfree(date);\n }\n\n+static void determine_authors_info(struct strbuf *authors_info)\n+{\n+\tconst char *current_authors_info;\n+\n+\tif (author_message) {\n+\t\tstruct authors_split authors;\n+\t\tsize_t len;\n+\t\tconst char *a;\n+\n+\t\ta = find_commit_header(author_message_buffer, \"authors\", &len);\n+\t\tif (a) {\n+\t\t\tif (split_authors_line(&authors, a, len) < 0)\n+\t\t\t\tdie(_(\"commit '%s' has malformed authors line\"), author_message);\n+\n+\t\t\tstrbuf_add(authors_info, a, len);\n+\t\t}\n+\t} else {\n+\t\tcurrent_authors_info = git_authors_info();\n+\t\tif (current_authors_info)\n+\t\t\tstrbuf_addstr(authors_info, current_authors_info);\n+\t}\n+}\n+\n static int author_date_is_interesting(void)\n {\n \treturn author_message || force_date;\n@@ -660,7 +684,8 @@ static void adjust_comment_line_char(const struct strbuf *sb)\n static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\t\t     struct commit *current_head,\n \t\t\t     struct wt_status *s,\n-\t\t\t     struct strbuf *author_ident)\n+\t\t\t     struct strbuf *author_ident,\n+\t\t\t     struct strbuf *authors_info)\n {\n \tstruct stat statbuf;\n \tstruct strbuf committer_ident = STRBUF_INIT;\n@@ -671,8 +696,15 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \tint clean_message_contents = (cleanup_mode != CLEANUP_NONE);\n \tint old_display_comment_prefix;\n\n-\t/* This checks and barfs if author is badly specified */\n-\tdetermine_author_info(author_ident);\n+\tdetermine_authors_info(authors_info);\n+\tif (authors_info->len > 0) {\n+\t\tstrbuf_addstr(author_ident, git_authors_first_info(authors_info->buf));\n+\t\tstrbuf_addstr(&committer_ident, git_authors_first_info(authors_info->buf));\n+\t} else {\n+\t\t/* This checks and barfs if author is badly specified */\n+\t\tdetermine_author_info(author_ident);\n+\t\tstrbuf_addstr(&committer_ident, git_committer_info(IDENT_STRICT));\n+\t}\n\n \tif (!no_verify && run_commit_hook(use_editor, index_file, \"pre-commit\", NULL))\n \t\treturn 0;\n@@ -800,11 +832,11 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \tstrbuf_release(&sb);\n\n \t/* This checks if committer ident is explicitly given */\n-\tstrbuf_addstr(&committer_ident, git_committer_info(IDENT_STRICT));\n \tif (use_editor && include_status) {\n \t\tint ident_shown = 0;\n \t\tint saved_color_setting;\n \t\tstruct ident_split ci, ai;\n+\t\tstruct authors_split authors_split;\n\n \t\tif (whence != FROM_COMMIT) {\n \t\t\tif (cleanup_mode == CLEANUP_SCISSORS)\n@@ -854,6 +886,14 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\tassert_split_ident(&ai, author_ident);\n \t\tassert_split_ident(&ci, &committer_ident);\n\n+\t\tif (authors_info->len > 0) {\n+\t\t\tsplit_authors_line(&authors_split, authors_info->buf, authors_info->len);\n+\t\t\tstatus_printf_ln(s, GIT_COLOR_NORMAL,\n+\t\t\t\t_(\"%s\"\n+\t\t\t\t\"Authors:   %.*s\"),\n+\t\t\t\tident_shown++ ? \"\" : \"\\n\",\n+\t\t\t\t(int)(authors_split.end - authors_split.begin), authors_split.begin);\n+\t\t}\n \t\tif (ident_cmp(&ai, &ci))\n \t\t\tstatus_printf_ln(s, GIT_COLOR_NORMAL,\n \t\t\t\t_(\"%s\"\n@@ -1637,6 +1677,7 @@ int cmd_commit(int argc, const char **argv, const char *prefix)\n\n \tstruct strbuf sb = STRBUF_INIT;\n \tstruct strbuf author_ident = STRBUF_INIT;\n+\tstruct strbuf authors_info = STRBUF_INIT;\n \tconst char *index_file, *reflog_msg;\n \tchar *nl;\n \tunsigned char sha1[20];\n@@ -1675,7 +1716,7 @@ int cmd_commit(int argc, const char **argv, const char *prefix)\n \t/* Set up everything for writing the commit object.  This includes\n \t   running hooks, writing the trees, and interacting with the user.  */\n \tif (!prepare_to_commit(index_file, prefix,\n-\t\t\t       current_head, &s, &author_ident)) {\n+\t\t\t       current_head, &s, &author_ident, &authors_info)) {\n \t\trollback_index_files();\n \t\treturn 1;\n \t}\n@@ -1762,11 +1803,14 @@ int cmd_commit(int argc, const char **argv, const char *prefix)\n \t}\n\n \tif (commit_tree_extended(sb.buf, sb.len, active_cache_tree->sha1,\n-\t\t\t parents, sha1, author_ident.buf, sign_commit, extra)) {\n+\t\t\t parents, sha1, author_ident.buf,\n+\t\t\t authors_info.len > 0 ? authors_info.buf : NULL,\n+\t\t\t sign_commit, extra)) {\n \t\trollback_index_files();\n \t\tdie(_(\"failed to write commit object\"));\n \t}\n \tstrbuf_release(&author_ident);\n+\tstrbuf_release(&authors_info);\n \tfree_commit_extra_headers(extra);\n\n \tnl = strchr(sb.buf, '\\n');\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex a96d4fb..a23599c 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -798,7 +798,7 @@ static int merge_trivial(struct commit *head, struct commit_list *remoteheads)\n \tpptr = commit_list_append(remoteheads->item, pptr);\n \tprepare_to_commit(remoteheads);\n \tif (commit_tree(merge_msg.buf, merge_msg.len, result_tree, parents,\n-\t\t\tresult_commit, NULL, sign_commit))\n+\t\t\tresult_commit, NULL, NULL, sign_commit))\n \t\tdie(_(\"failed to write commit object\"));\n \tfinish(head, remoteheads, result_commit, \"In-index merge\");\n \tdrop_save();\n@@ -823,7 +823,7 @@ static int finish_automerge(struct commit *head,\n \tstrbuf_addch(&merge_msg, '\\n');\n \tprepare_to_commit(remoteheads);\n \tif (commit_tree(merge_msg.buf, merge_msg.len, result_tree, parents,\n-\t\t\tresult_commit, NULL, sign_commit))\n+\t\t\tresult_commit, NULL, NULL, sign_commit))\n \t\tdie(_(\"failed to write commit object\"));\n \tstrbuf_addf(&buf, \"Merge made by the '%s' strategy.\", wt_strategy);\n \tfinish(head, remoteheads, result_commit, buf.buf);\ndiff --git a/cache.h b/cache.h\nindex 40f7ddd..3cb4a84 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1327,6 +1327,7 @@ extern const char *fmt_ident(const char *name, const char *email, const char *da\n extern const char *fmt_name(const char *name, const char *email);\n extern const char *ident_default_name(void);\n extern const char *ident_default_email(void);\n+extern const char *ident_default_date(void);\n extern const char *git_editor(void);\n extern const char *git_pager(int stdout_is_tty);\n extern int git_ident_config(const char *, const char *, void *);\ndiff --git a/commit.c b/commit.c\nindex 2cf8515..40eb1c4 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -11,6 +11,7 @@\n #include \"commit-slab.h\"\n #include \"prio-queue.h\"\n #include \"sha1-lookup.h\"\n+#include \"authors.h\"\n\n static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);\n\n@@ -1312,7 +1313,8 @@ static inline int standard_header_field(const char *field, size_t len)\n \t\t(len == 6 && !memcmp(field, \"parent \", 7)) ||\n \t\t(len == 6 && !memcmp(field, \"author \", 7)) ||\n \t\t(len == 9 && !memcmp(field, \"committer \", 10)) ||\n-\t\t(len == 8 && !memcmp(field, \"encoding \", 9)));\n+\t\t(len == 8 && !memcmp(field, \"encoding \", 9)) ||\n+\t\t(len == 7 && !memcmp(field, \"authors \", 8)));\n }\n\n static int excluded_header_field(const char *field, size_t len, const char **exclude)\n@@ -1388,14 +1390,14 @@ void free_commit_extra_headers(struct commit_extra_header *extra)\n int commit_tree(const char *msg, size_t msg_len,\n \t\tconst unsigned char *tree,\n \t\tstruct commit_list *parents, unsigned char *ret,\n-\t\tconst char *author, const char *sign_commit)\n+\t\tconst char *author, const char *authors, const char *sign_commit)\n {\n \tstruct commit_extra_header *extra = NULL, **tail = &extra;\n \tint result;\n\n \tappend_merge_tag_headers(parents, &tail);\n \tresult = commit_tree_extended(msg, msg_len, tree, parents, ret,\n-\t\t\t\t      author, sign_commit, extra);\n+\t\t\t\t      author, authors, sign_commit, extra);\n \tfree_commit_extra_headers(extra);\n \treturn result;\n }\n@@ -1518,7 +1520,8 @@ N_(\"Warning: commit message did not conform to UTF-8.\\n\"\n int commit_tree_extended(const char *msg, size_t msg_len,\n \t\t\t const unsigned char *tree,\n \t\t\t struct commit_list *parents, unsigned char *ret,\n-\t\t\t const char *author, const char *sign_commit,\n+\t\t\t const char *author, const char *authors,\n+\t\t\t const char *sign_commit,\n \t\t\t struct commit_extra_header *extra)\n {\n \tint result;\n@@ -1549,11 +1552,13 @@ int commit_tree_extended(const char *msg, size_t msg_len,\n\n \t/* Person/date information */\n \tif (!author)\n-\t\tauthor = git_author_info(IDENT_STRICT);\n+\t\tauthor = authors ? git_authors_first_info(authors) : git_author_info(IDENT_STRICT);\n \tstrbuf_addf(&buffer, \"author %s\\n\", author);\n-\tstrbuf_addf(&buffer, \"committer %s\\n\", git_committer_info(IDENT_STRICT));\n+\tstrbuf_addf(&buffer, \"committer %s\\n\", authors ? git_authors_first_info(authors) : git_committer_info(IDENT_STRICT));\n \tif (!encoding_is_utf8)\n \t\tstrbuf_addf(&buffer, \"encoding %s\\n\", git_commit_encoding);\n+\tif (authors)\n+\t\tstrbuf_addf(&buffer, \"authors %s\\n\", authors);\n\n \twhile (extra) {\n \t\tadd_extra_header(&buffer, extra);\ndiff --git a/commit.h b/commit.h\nindex 9c12abb..4880460 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -331,12 +331,14 @@ extern void append_merge_tag_headers(struct commit_list *parents,\n extern int commit_tree(const char *msg, size_t msg_len,\n \t\t       const unsigned char *tree,\n \t\t       struct commit_list *parents, unsigned char *ret,\n-\t\t       const char *author, const char *sign_commit);\n+\t\t       const char *author, const char *authors,\n+\t\t       const char *sign_commit);\n\n extern int commit_tree_extended(const char *msg, size_t msg_len,\n \t\t\t\tconst unsigned char *tree,\n \t\t\t\tstruct commit_list *parents, unsigned char *ret,\n-\t\t\t\tconst char *author, const char *sign_commit,\n+\t\t\t\tconst char *author, const char *authors,\n+\t\t\t\tconst char *sign_commit,\n \t\t\t\tstruct commit_extra_header *);\n\n extern struct commit_extra_header *read_commit_extra_headers(struct commit *, const char **);\ndiff --git a/git.c b/git.c\nindex b367cf6..b6fb852 100644\n--- a/git.c\n+++ b/git.c\n@@ -397,6 +397,7 @@ static struct cmd_struct commands[] = {\n \t{ \"annotate\", cmd_annotate, RUN_SETUP },\n \t{ \"apply\", cmd_apply, RUN_SETUP_GENTLY },\n \t{ \"archive\", cmd_archive, RUN_SETUP_GENTLY },\n+\t{ \"authors\", cmd_authors, RUN_SETUP },\n \t{ \"bisect--helper\", cmd_bisect__helper, RUN_SETUP },\n \t{ \"blame\", cmd_blame, RUN_SETUP },\n \t{ \"branch\", cmd_branch, RUN_SETUP },\ndiff --git a/ident.c b/ident.c\nindex ac4ae02..b538abc 100644\n--- a/ident.c\n+++ b/ident.c\n@@ -177,7 +177,7 @@ const char *ident_default_email(void)\n \treturn git_default_email.buf;\n }\n\n-static const char *ident_default_date(void)\n+const char *ident_default_date(void)\n {\n \tif (!git_default_date.len)\n \t\tdatestamp(&git_default_date);\ndiff --git a/mailinfo.c b/mailinfo.c\nindex a489d9d..43c85de 100644\n--- a/mailinfo.c\n+++ b/mailinfo.c\n@@ -1,6 +1,7 @@\n #include \"cache.h\"\n #include \"utf8.h\"\n #include \"strbuf.h\"\n+#include \"authors.h\"\n #include \"mailinfo.h\"\n\n static void cleanup_space(struct strbuf *sb)\n@@ -533,6 +534,18 @@ static int check_header(struct mailinfo *mi,\n {\n \tint i, ret = 0, len;\n \tstruct strbuf sb = STRBUF_INIT;\n+\tstruct strbuf from = STRBUF_INIT;\n+\n+\tif (overwrite && cmp_header(line, \"From\")) {\n+\t\tlen = strlen(\"From: \");\n+\t\tstrbuf_add(&from, line->buf + len, line->len - len);\n+\t\tdecode_header(mi, &from);\n+\n+\t\tif (mi->authors.len > 0)\n+\t\t\tstrbuf_addch(&mi->authors,',');\n+\t\tstrbuf_addstr(&mi->authors, from.buf);\n+\t}\n+\tstrbuf_release(&from);\n\n \t/* search for the interesting parts */\n \tfor (i = 0; header[i]; i++) {\n@@ -1068,6 +1081,8 @@ static void handle_info(struct mailinfo *mi)\n \t\t\tfprintf(mi->output, \"%s: %s\\n\", header[i], hdr->buf);\n \t\t}\n \t}\n+\tif (has_multiple_authors(mi->authors.buf))\n+\t\tfprintf(mi->output, \"Authors: %s\\n\", mi->authors.buf);\n \tfprintf(mi->output, \"\\n\");\n }\n\n@@ -1133,6 +1148,7 @@ void setup_mailinfo(struct mailinfo *mi)\n \tstrbuf_init(&mi->charset, 0);\n \tstrbuf_init(&mi->log_message, 0);\n \tstrbuf_init(&mi->inbody_header_accum, 0);\n+\tstrbuf_init(&mi->authors, 0);\n \tmi->header_stage = 1;\n \tmi->use_inbody_headers = 1;\n \tmi->content_top = mi->content;\n@@ -1147,6 +1163,7 @@ void clear_mailinfo(struct mailinfo *mi)\n \tstrbuf_release(&mi->email);\n \tstrbuf_release(&mi->charset);\n \tstrbuf_release(&mi->inbody_header_accum);\n+\tstrbuf_release(&mi->authors);\n \tfree(mi->message_id);\n\n \tfor (i = 0; mi->p_hdr_data[i]; i++)\ndiff --git a/mailinfo.h b/mailinfo.h\nindex 04a2535..d264d0d 100644\n--- a/mailinfo.h\n+++ b/mailinfo.h\n@@ -33,6 +33,8 @@ struct mailinfo {\n\n \tstruct strbuf log_message;\n \tint input_error;\n+\n+\tstruct strbuf authors;\n };\n\n extern void setup_mailinfo(struct mailinfo *);\ndiff --git a/notes-cache.c b/notes-cache.c\nindex 5dfc5cb..2b58add 100644\n--- a/notes-cache.c\n+++ b/notes-cache.c\n@@ -58,7 +58,7 @@ int notes_cache_write(struct notes_cache *c)\n \tif (write_notes_tree(&c->tree, tree_sha1))\n \t\treturn -1;\n \tif (commit_tree(c->validity, strlen(c->validity), tree_sha1, NULL,\n-\t\t\tcommit_sha1, NULL, NULL) < 0)\n+\t\t\tcommit_sha1, NULL, NULL, NULL) < 0)\n \t\treturn -1;\n \tif (update_ref(\"update notes cache\", c->tree.update_ref, commit_sha1,\n \t\t       NULL, 0, UPDATE_REFS_QUIET_ON_ERR) < 0)\ndiff --git a/notes-utils.c b/notes-utils.c\nindex 24a3361..8adf381 100644\n--- a/notes-utils.c\n+++ b/notes-utils.c\n@@ -26,7 +26,7 @@ void create_notes_commit(struct notes_tree *t, struct commit_list *parents,\n \t\t/* else: t->ref points to nothing, assume root/orphan commit */\n \t}\n\n-\tif (commit_tree(msg, msg_len, tree_sha1, parents, result_sha1, NULL, NULL))\n+\tif (commit_tree(msg, msg_len, tree_sha1, parents, result_sha1, NULL, NULL, NULL))\n \t\tdie(\"Failed to commit notes tree to database\");\n }\n\ndiff --git a/pretty.c b/pretty.c\nindex 5e68383..d505f2a 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -11,6 +11,7 @@\n #include \"reflog-walk.h\"\n #include \"gpg-interface.h\"\n #include \"trailer.h\"\n+#include \"authors.h\"\n\n static char *user_format;\n static struct cmt_fmt_map {\n@@ -510,6 +511,55 @@ void pp_user_info(struct pretty_print_context *pp,\n \t}\n }\n\n+void pp_authors_info(struct pretty_print_context *pp,\n+\t\t     struct strbuf *sb,\n+\t\t     const char *line, const char *encoding)\n+{\n+\tstruct ident_split ident;\n+\tchar *line_end;\n+\tstruct authors_split authors;\n+\n+\tif (pp->fmt == CMIT_FMT_ONELINE)\n+\t\treturn;\n+\n+\tline_end = strchrnul(line, '\\n');\n+\n+\tif (split_authors_line(&authors, line, line_end - line))\n+\t\treturn;\n+\n+\tif (cmit_fmt_is_mail(pp->fmt)) {\n+\t\tstrbuf_addstr(sb, authors_split_to_email_froms(&authors));\n+\t} else {\n+\t\tstrbuf_addstr(sb, \"Authors: \");\n+\t\tif (pp->fmt == CMIT_FMT_FULLER)\n+\t\t\tstrbuf_addstr(sb, \"    \");\n+\t\tstrbuf_add(sb, authors.begin, authors.end - authors.begin);\n+\t\tstrbuf_addch(sb, '\\n');\n+\t}\n+\n+\t/* Use the ident split for parsing the date part */\n+\tif (split_ident_line(&ident, line, line_end - line))\n+\t\treturn;\n+\n+\tswitch (pp->fmt) {\n+\tcase CMIT_FMT_MEDIUM:\n+\t\tstrbuf_addf(sb, \"Date:    %s\\n\",\n+\t\t\t    show_ident_date(&ident, &pp->date_mode));\n+\t\tbreak;\n+\tcase CMIT_FMT_EMAIL:\n+\tcase CMIT_FMT_MBOXRD:\n+\t\tstrbuf_addf(sb, \"Date: %s\\n\",\n+\t\t\t    show_ident_date(&ident, DATE_MODE(RFC2822)));\n+\t\tbreak;\n+\tcase CMIT_FMT_FULLER:\n+\t\tstrbuf_addf(sb, \"AuthorsDate: %s\\n\",\n+\t\t\t    show_ident_date(&ident, &pp->date_mode));\n+\t\tbreak;\n+\tdefault:\n+\t\tbreak;\n+\t}\n+}\n+\n static int is_blank_line(const char *line, int *len_p)\n {\n \tint len = *len_p;\n@@ -1544,6 +1594,23 @@ static void pp_header(struct pretty_print_context *pp,\n \t\t      struct strbuf *sb)\n {\n \tint parents_shown = 0;\n+\tint has_authors = 0;\n+\tconst char *msg = *msg_p;\n+\n+\t/* Check if header has `authors` header */\n+\tfor(;;) {\n+\t\tconst char *line = msg;\n+\t\tint linelen = get_one_line(msg);\n+\t\tif (!linelen)\n+\t\t\tbreak;\n+\t\tmsg += linelen;\n+\t\tif (linelen == 1)\n+\t\t\tbreak;\n+\t\tif (starts_with(line, \"authors \")) {\n+\t\t\thas_authors = 1;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n\n \tfor (;;) {\n \t\tconst char *name, *line = *msg_p;\n@@ -1582,13 +1649,19 @@ static void pp_header(struct pretty_print_context *pp,\n \t\t * FULLER shows both authors and dates.\n \t\t */\n \t\tif (skip_prefix(line, \"author \", &name)) {\n-\t\t\tstrbuf_grow(sb, linelen + 80);\n-\t\t\tpp_user_info(pp, \"Author\", sb, name, encoding);\n+\t\t\tif (!has_authors) {\n+\t\t\t\tstrbuf_grow(sb, linelen + 80);\n+\t\t\t\tpp_user_info(pp, \"Author\", sb, name, encoding);\n+\t\t\t}\n \t\t}\n \t\tif (skip_prefix(line, \"committer \", &name) &&\n \t\t    (pp->fmt == CMIT_FMT_FULL || pp->fmt == CMIT_FMT_FULLER)) {\n \t\t\tstrbuf_grow(sb, linelen + 80);\n-\t\t\tpp_user_info(pp, \"Commit\", sb, name, encoding);\n+\t\t\tpp_user_info(pp, has_authors ? \"Commit \" : \"Commit\", sb, name, encoding);\n+\t\t}\n+\t\tif (skip_prefix(line, \"authors \", &name)) {\n+\t\t\tstrbuf_grow(sb, linelen + 80);\n+\t\t\tpp_authors_info(pp, sb, name, encoding);\n \t\t}\n \t}\n }\ndiff --git a/t/helper/.gitignore b/t/helper/.gitignore\nindex d6e8b36..9ad4bbc 100644\n--- a/t/helper/.gitignore\n+++ b/t/helper/.gitignore\n@@ -1,3 +1,4 @@\n+/test-authors\n /test-chmtime\n /test-ctype\n /test-config\ndiff --git a/t/helper/test-authors.c b/t/helper/test-authors.c\nnew file mode 100644\nindex 0000000..ab93f9f\n--- /dev/null\n+++ b/t/helper/test-authors.c\n@@ -0,0 +1,42 @@\n+#include \"cache.h\"\n+#include \"authors.h\"\n+\n+static const char *usage_msg = \"\\n\"\n+\"  test-authors split [authors_info]\\n\"\n+\"  test-authors has-multiple-authors [authors]\\n\";\n+\n+static void test_split_authors(const char **argv)\n+{\n+\tstruct authors_split split;\n+\tint result;\n+\tstruct strbuf splitted = STRBUF_INIT;\n+\n+\tprintf(\"%s -> \",*argv);\n+\tresult = split_authors_line(&split, *argv, strlen(*argv));\n+\tif (result)\n+\t\tprintf(\"error\");\n+\telse {\n+\t\tstrbuf_add(&splitted, split.begin, split.end - split.begin);\n+\t\tprintf(splitted.buf);\n+\t}\n+\tprintf(\"\\n\");\n+}\n+\n+static void test_has_multiple_authors(const char **argv)\n+{\n+\tprintf(\"%s -> %s\\n\", *argv, has_multiple_authors(*argv) ? \"yes\" : \"no\");\n+}\n+\n+int cmd_main(int argc, const char **argv)\n+{\n+\targv++;\n+\tif (argc != 3)\n+\t\tusage(usage_msg);\n+\tif (!strcmp(*argv, \"split\"))\n+\t\ttest_split_authors(argv+1);\n+\telse if (!strcmp(*argv, \"has-multiple\"))\n+\t\ttest_has_multiple_authors(argv+1);\n+\telse\n+\t\tusage(usage_msg);\n+\treturn 0;\n+}\ndiff --git a/t/t9904-authors.sh b/t/t9904-authors.sh\nnew file mode 100755\nindex 0000000..eb7fffa\n--- /dev/null\n+++ b/t/t9904-authors.sh\n@@ -0,0 +1,32 @@\n+#!/bin/sh\n+\n+test_description='test authors'\n+\n+. ./test-lib.sh\n+\n+check_split() {\n+\techo \"$1 -> $2\" >expect\n+\ttest_expect_success \"split '$1'\" \"\n+\ttest-authors split '$1' >actual &&\n+\ttest_cmp expect actual\n+\t\"\n+}\n+\n+check_split 'xxx' 'error'\n+check_split 'Some Guy <sg@example.com> 1484387401 +0100' 'Some Guy <sg@example.com>'\n+check_split 'Some Guy <sg@example.com>,Another Pal <ap@example.com> 1484387401 +0100' 'Some Guy <sg@example.com>,Another Pal <ap@example.com>'\n+check_split 'Some Guy <sg@example.com>,Another Pal <ap@example.com>' 'Some Guy <sg@example.com>,Another Pal <ap@example.com>'\n+\n+check_has_multiple() {\n+\techo \"$1 -> $2\" >expect\n+\ttest_expect_success \"has multiple authors '$1'\" \"\n+\ttest-authors has-multiple '$1' >actual &&\n+\ttest_cmp expect actual\n+\t\"\n+}\n+\n+check_has_multiple 'abc' 'no'\n+check_has_multiple 'Some Guy <sg@example.com>' 'no'\n+check_has_multiple 'Some Guy <sg@example.com>,Another Pal <ap@example.com>' 'yes'\n+\n+test_done\n--\n2.6.6\n\n"},{"id":"310545","messageId":"CAP8UFD3=vaFupEDay-5vrMBwK_YJezysUUvySxnUUZxuW7m_WQ@mail.gmail.com","threadId":"45001","inReplyTo":"1485713194-11782-1-git-send-email-schumacher@kde.org","subject":"Re: [RFC] Proof of concept: Support multiple authors","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2017-01-30T17:56:42Z","receivedAt":"2017-01-30T18:33:57Z","isPatch":false,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"Hi,\n\nOn Sun, Jan 29, 2017 at 7:06 PM, Cornelius Schumacher\n<schumacher@kde.org> wrote:\n> This patch is a proof of concept implementation of support for\n> multiple authors. It adds an optional `authors` header to commits\n> which is set when there are authors configured in the git config.\n\nI am just wondering if you have read and taken into account the\nprevious threads on this mailing list about the same subject, like for\nexample this one:\n\nhttps://public-inbox.org/git/CAOvwQ4i_HL7XGnxZrVu3oSnsbnTyxbg8Vh6vzi4c1isSrrexYQ@mail.gmail.com/\n\n> A new command `git-authors` is used to manage the authors settings.\n> Authors are identified by initials and their names and emails are\n> set in a `.git_authors_map` file.\n>\n> Signed-off-by: Cornelius Schumacher <schumacher@kde.org>\n> ---\n>\n> When doing pair programming we have to work around the limitation that\n> git can only have a single author in each commit. There are some tools\n> which help with that such as [git-duet] [1], but there are still some\n> limits, because the information about multiple authors is not reflected\n> in the native git data model.\n>\n> Here is a proposal how to change that and implement native support for\n> multiple authors in git. It comes with a patch as a proof of concept.\n> The patch by no means is finished, it doesn't cover all cases and needs\n> more tests and error handling. It's meant as an illustration of the\n> concept.\n>\n> The basic idea is to introduce a new optional `authors` header in\n> commits which contains a list of authors. The header is set in each new\n> commit when there is an entry `authors.current` in the git config listing\n> the current authors. When this config is not there the behavior falls\n> back to the current standard behavior.\n>\n> When the header is there it is treated in the same way as the author\n> header. It's preserved on merges and similar operations, is displayed in\n> git show, and used to create a list of `From` addresses in `format-patch`.\n> Email supports multiple `From` addresses as specified in section 3.6.2 of\n> RFC 5322.\n>\n> When multiple authors are configured, they still write the standard author\n> header to keep backwards compatibility. The first author is used as author\n> and committer. In the future it might be good to implement something like\n> automatic rotation of the order of authors to give credit in a fair way.\n>\n> To make it easier to work with the authors there is a new command\n> `git-authors`. It sets the list of authors using initials as shortcut for\n> the full configuration with name and email. The mapping of initials to\n> names and email addresses is taken from a file `.git_authors_map` in the\n> home directory of the users. This way it's possible to quickly set a list\n> of authors by running a command such as `git authors ab cd`. This is\n> useful when doing pair programming because the people working together\n> usually switch quite frequently and using the command with the intials is\n> quicker and less error-prone than editing the configuration with full\n> names and emails.\n>\n> The command also supports setting a single author, setting more than two\n> authors or clearing the configuration for multiple authors to go back to\n> the standard behavior without the new authors header.\n>\n> The concept of the command and the mappings file is similar to what\n> git-duet does, so that it should be familiar to many people doing pair\n> programming. The behavior of git doesn't change when the new feature is\n> not used and when it's used it should be backwards compatible so that it\n> doesn't break existing functionality. This should make a smooth transition\n> for users who choose to make use of it.\n>\n> Adding support for multiple authors would make the life of developers doing\n> pair programming easier. It would be useful in itself, but it would also\n> need support by other tools around git to use its full potential.\n\nFrom what I recall from previous discussions, the most important\nquestion is: are you sure that it doesn't break any other tool?\n\n> This\n> might take a while, but I think it's worth the effort.\n>\n> I'm willing to continue to work on this and create a patch which is suitable\n> for inclusion in git.\n"},{"id":"310547","messageId":"1895528.rno9AclvBq@linux-7ekr","threadId":"45001","inReplyTo":"CAP8UFD3=vaFupEDay-5vrMBwK_YJezysUUvySxnUUZxuW7m_WQ@mail.gmail.com","subject":"Re: [RFC] Proof of concept: Support multiple authors","fromName":"Cornelius Schumacher","fromEmail":"schumacher@kde.org","sentAt":"2017-01-30T19:33:56Z","receivedAt":"2017-01-30T19:34:12Z","isPatch":false,"sender":{"key":"schumacher@kde.org","avatar":"https://gravatar.com/avatar/f1089e3b59bb8e14182f3befdfa38faa3212b1b77a6baff2d0edf94b34382c20?d=mp&s=160"},"body":"On Monday 30 January 2017 18:56:42 Christian Couder wrote:\n> On Sun, Jan 29, 2017 at 7:06 PM, Cornelius Schumacher\n> <schumacher@kde.org> wrote:\n> > This patch is a proof of concept implementation of support for\n> > multiple authors. It adds an optional `authors` header to commits\n> > which is set when there are authors configured in the git config.\n> \n> I am just wondering if you have read and taken into account the\n> previous threads on this mailing list about the same subject, like for\n> example this one:\n> \n> https://public-inbox.org/git/CAOvwQ4i_HL7XGnxZrVu3oSnsbnTyxbg8Vh6vzi4c1isSrr\n> exYQ@mail.gmail.com/\n\nThanks for the pointer. I have read what I could find about the topic and \ntried to take it into account. Conceptually I wouldn't want to alter the \nsemantics of the existing author field, but add optional information to \ncapture the nature of commits done by multiple people collaboratively, where \nattribution to a single author is not an adequate representation of how the \ncommit was done.\n\nMaybe it still would be too intrusive to add an additional header, and there \nwould be more elegant solutions to this problem. I would be very much \ninterested to hear about better ideas how to handle this. On the other hand it \nseems to be the most straight-forward solution to handle this on the same \nlevel as single author information. But maybe this is due to my still limited \nfamiliarity to the internals of git ;-)\n\nWhat I know from the experience of pair programming is that it is an actual \nproblem to not be able to represent this information in a native way. It would \nbenefit quite a number of programmers to improve that. I'm trying to find a \nsolution which does that and still is compatible with the design of git. Any \ncomments leading to an acceptable solution I highly appreciate.\n\n> > Adding support for multiple authors would make the life of developers\n> > doing\n> > pair programming easier. It would be useful in itself, but it would also\n> > need support by other tools around git to use its full potential.\n> \n> From what I recall from previous discussions, the most important\n> question is: are you sure that it doesn't break any other tool?\n\nI have tried with a few tools and didn't find breakage other than that the \nadditional information would not be taken into account. That of course doesn't \nmean that we could be sure that there are no tools which would break. Does \nanybody have hints on what tools would be most sensitive to such a change?\n\nI realize that it does take effort and time to implement such a feature in a \nway which doesn't create breakage. But I still would like to try how far we \ncould come with that., because maybe it actually can be done.\n\n-- \nCornelius Schumacher <schumacher@kde.org>\n"},{"id":"310554","messageId":"xmqqinowuvd7.fsf@gitster.mtv.corp.google.com","threadId":"45001","inReplyTo":"CAP8UFD3=vaFupEDay-5vrMBwK_YJezysUUvySxnUUZxuW7m_WQ@mail.gmail.com","subject":"Re: [RFC] Proof of concept: Support multiple authors","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2017-01-30T20:48:52Z","receivedAt":"2017-01-30T20:49:34Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> I am just wondering if you have read and taken into account the\n> previous threads on this mailing list about the same subject, like for\n> example this one:\n>\n> https://public-inbox.org/git/CAOvwQ4i_HL7XGnxZrVu3oSnsbnTyxbg8Vh6vzi4c1isSrrexYQ@mail.gmail.com/\n\nThanks for a starting-point link.\n\nIn that discussion, another discussion in the debian BTS is\nmentioned,\n\n  https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=451880\n\nwhich in turn has links to other, even earlier, discussions, but\nthey are all gmane links so it would be harder for those who do not\nuse its NNTP interface (which still works).  Here are their modern\ncounterparts ;-)\n\n  https://public-inbox.org/git/?q=gmane:83880\n  https://public-inbox.org/git/?q=gmane:146223\n  https://public-inbox.org/git/?q=gmane:146886\n\nThe older discussions already cited the cost to update both in-tree\nand out-of-tree tools not to barf when they see such a commit object\nand one of the reason why we would not want to do this, and Ted Ts'o\ngave us another excellent reason why not to do multiple author\nheader lines in a commit object header, i.e. \"How often is that all\nof the authors are completely equal?\"\n\nAnother thing that I didn't see brought up was that it is not enough\nto ensure all existing tools are updated not to barf on a commit\nwith multiple \"author\" line.  You need to define what it means to\nhave multiple authors and how they are treated by tools in a\nconsistent way.  Think \"shortlog\", for example.  The tool may be\nable to be tweaked not to barf, and you may be able to add a random\ndefinition of which of the multiple authors to group a single commit\nunder (the \"random definition\" could be \"show that same commit\nmultiple times, once for each author\" or it could be \"concatenate\nthe authors to create a single header to list co-authored commits\nunder, as if they were a single person\", or it could be something\nequally bogus), but I do not think any single solution satisfies\npeople's needs, and my gut feeling is that it is not worth to add\ntons of options and explain them to the end-users to support these\nrandom things that happen when there are multiple authors.\n\nIncidentally, there recently was a discussion on extending\nrequest-pull by adding a summary of commits by reviewers and\ntesters.\n\nhttps://public-inbox.org/git/20170115183051.3565-1-wsa@the-dreams.de/\n\nI would imagine, if it is to be implemented, it would boil down to\nteaching shortlog that the \"author\" line in the commit object header\nis not the only thing that matters, and that it should optionally\ntake notice of lines in the trailer block of the log message (e.g.\nperhaps \"Co-Authored-by: \" trailer suggested by $gmane/146223 above\nwould help there).\n\nMy advice to those who want to record credit to multiple authors is\nto treat the commit author line as recording the primary contact\nwhen there is a question on the commit and nothing else.  Anything\nwith richer semantics is better done in the trailer by enriching the\nsupport of trailer lines with interpret-trailers framework.\n"},{"id":"310583","messageId":"3204990.cGxpkETTLk@linux-7ekr","threadId":"45001","inReplyTo":"xmqqinowuvd7.fsf@gitster.mtv.corp.google.com","subject":"Re: [RFC] Proof of concept: Support multiple authors","fromName":"Cornelius Schumacher","fromEmail":"schumacher@kde.org","sentAt":"2017-01-31T00:54:53Z","receivedAt":"2017-01-31T01:12:53Z","isPatch":false,"sender":{"key":"schumacher@kde.org","avatar":"https://gravatar.com/avatar/f1089e3b59bb8e14182f3befdfa38faa3212b1b77a6baff2d0edf94b34382c20?d=mp&s=160"},"body":"On Monday 30 January 2017 12:48:52 Junio C Hamano wrote:\n> \n>   https://public-inbox.org/git/?q=gmane:83880\n>   https://public-inbox.org/git/?q=gmane:146223\n>   https://public-inbox.org/git/?q=gmane:146886\n\nThanks for putting the links together. That's very useful as a reference.\n\n> The older discussions already cited the cost to update both in-tree\n> and out-of-tree tools not to barf when they see such a commit object\n> and one of the reason why we would not want to do this, and Ted Ts'o\n> gave us another excellent reason why not to do multiple author\n> header lines in a commit object header, i.e. \"How often is that all\n> of the authors are completely equal?\"\n\nJust to give a bit of context: In the pair programming environment where I \nwork we usually use non-personalized workstations and switch the keyboard \nbetween the two people working together quite frequently, sometimes every few \nminutes, or even within writing a commit message. There the person pressing \nthe return button on the commit really does not have a special role. In this \nstyle of working I think it feels like the fairest and most practical \nassumption to treat all authors as equal.\n\n> My advice to those who want to record credit to multiple authors is\n> to treat the commit author line as recording the primary contact\n> when there is a question on the commit and nothing else.  Anything\n> with richer semantics is better done in the trailer by enriching the\n> support of trailer lines with interpret-trailers framework.\n\nThanks for the advice. I think I will explore this direction a little bit \nfurther and see how handling a situation of multiple authors could be better \ndone in this framework.\n\n-- \nCornelius Schumacher <schumacher@kde.org>\n"}]}