From patchwork Sun Sep 27 08:39:52 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jeff King X-Patchwork-Id: 11801977 Return-Path: Received: from mail.kernel.org (pdx-korg-mail-1.web.codeaurora.org [172.30.200.123]) by pdx-korg-patchwork-2.web.codeaurora.org (Postfix) with ESMTP id A807092C for ; Sun, 27 Sep 2020 08:39:55 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 9A31C207BC for ; Sun, 27 Sep 2020 08:39:55 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1730560AbgI0Ijx (ORCPT ); Sun, 27 Sep 2020 04:39:53 -0400 Received: from cloud.peff.net ([104.130.231.41]:42374 "EHLO cloud.peff.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1730513AbgI0Ijx (ORCPT ); Sun, 27 Sep 2020 04:39:53 -0400 Received: (qmail 29136 invoked by uid 109); 27 Sep 2020 08:39:53 -0000 Received: from Unknown (HELO peff.net) (10.0.1.2) by cloud.peff.net (qpsmtpd/0.94) with ESMTP; Sun, 27 Sep 2020 08:39:53 +0000 Authentication-Results: cloud.peff.net; auth=none Received: (qmail 4038 invoked by uid 111); 27 Sep 2020 08:39:55 -0000 Received: from coredump.intra.peff.net (HELO sigill.intra.peff.net) (10.0.0.2) by peff.net (qpsmtpd/0.94) with (TLS_AES_256_GCM_SHA384 encrypted) ESMTPS; Sun, 27 Sep 2020 04:39:55 -0400 Authentication-Results: peff.net; auth=none Date: Sun, 27 Sep 2020 04:39:52 -0400 From: Jeff King To: git@vger.kernel.org Cc: Eric Sunshine , Martin =?utf-8?b?w4VncmVu?= Subject: [PATCH v2 1/8] shortlog: change "author" variables to "ident" Message-ID: <20200927083952.GA2465761@coredump.intra.peff.net> References: <20200927083933.GA2222823@coredump.intra.peff.net> MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: <20200927083933.GA2222823@coredump.intra.peff.net> Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org We already match "committer", and we're about to start matching more things. Let's use a more neutral variable to avoid confusion. Signed-off-by: Jeff King --- builtin/shortlog.c | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/builtin/shortlog.c b/builtin/shortlog.c index c856c58bb5..edcf2e0d54 100644 --- a/builtin/shortlog.c +++ b/builtin/shortlog.c @@ -49,12 +49,12 @@ static int compare_by_list(const void *a1, const void *a2) } static void insert_one_record(struct shortlog *log, - const char *author, + const char *ident, const char *oneline) { struct string_list_item *item; - item = string_list_insert(&log->list, author); + item = string_list_insert(&log->list, ident); if (log->summary) item->util = (void *)(UTIL_TO_INT(item) + 1); @@ -97,8 +97,8 @@ static void insert_one_record(struct shortlog *log, } } -static int parse_stdin_author(struct shortlog *log, - struct strbuf *out, const char *in) +static int parse_stdin_ident(struct shortlog *log, + struct strbuf *out, const char *in) { const char *mailbuf, *namebuf; size_t namelen, maillen; @@ -122,18 +122,18 @@ static int parse_stdin_author(struct shortlog *log, static void read_from_stdin(struct shortlog *log) { - struct strbuf author = STRBUF_INIT; - struct strbuf mapped_author = STRBUF_INIT; + struct strbuf ident = STRBUF_INIT; + struct strbuf mapped_ident = STRBUF_INIT; struct strbuf oneline = STRBUF_INIT; static const char *author_match[2] = { "Author: ", "author " }; static const char *committer_match[2] = { "Commit: ", "committer " }; const char **match; match = log->committer ? committer_match : author_match; - while (strbuf_getline_lf(&author, stdin) != EOF) { + while (strbuf_getline_lf(&ident, stdin) != EOF) { const char *v; - if (!skip_prefix(author.buf, match[0], &v) && - !skip_prefix(author.buf, match[1], &v)) + if (!skip_prefix(ident.buf, match[0], &v) && + !skip_prefix(ident.buf, match[1], &v)) continue; while (strbuf_getline_lf(&oneline, stdin) != EOF && oneline.len) @@ -142,20 +142,20 @@ static void read_from_stdin(struct shortlog *log) !oneline.len) ; /* discard blanks */ - strbuf_reset(&mapped_author); - if (parse_stdin_author(log, &mapped_author, v) < 0) + strbuf_reset(&mapped_ident); + if (parse_stdin_ident(log, &mapped_ident, v) < 0) continue; - insert_one_record(log, mapped_author.buf, oneline.buf); + insert_one_record(log, mapped_ident.buf, oneline.buf); } - strbuf_release(&author); - strbuf_release(&mapped_author); + strbuf_release(&ident); + strbuf_release(&mapped_ident); strbuf_release(&oneline); } void shortlog_add_commit(struct shortlog *log, struct commit *commit) { - struct strbuf author = STRBUF_INIT; + struct strbuf ident = STRBUF_INIT; struct strbuf oneline = STRBUF_INIT; struct pretty_print_context ctx = {0}; const char *fmt; @@ -170,17 +170,17 @@ void shortlog_add_commit(struct shortlog *log, struct commit *commit) (log->email ? "%cN <%cE>" : "%cN") : (log->email ? "%aN <%aE>" : "%aN"); - format_commit_message(commit, fmt, &author, &ctx); + format_commit_message(commit, fmt, &ident, &ctx); if (!log->summary) { if (log->user_format) pretty_print_commit(&ctx, commit, &oneline); else format_commit_message(commit, "%s", &oneline, &ctx); } - insert_one_record(log, author.buf, oneline.len ? oneline.buf : ""); + insert_one_record(log, ident.buf, oneline.len ? oneline.buf : ""); - strbuf_release(&author); + strbuf_release(&ident); strbuf_release(&oneline); } From patchwork Sun Sep 27 08:39:59 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jeff King X-Patchwork-Id: 11801979 Return-Path: Received: from mail.kernel.org (pdx-korg-mail-1.web.codeaurora.org [172.30.200.123]) by pdx-korg-patchwork-2.web.codeaurora.org (Postfix) with ESMTP id 3CC426CA for ; Sun, 27 Sep 2020 08:40:03 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 2895F207BC for ; Sun, 27 Sep 2020 08:40:03 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1730564AbgI0IkB (ORCPT ); Sun, 27 Sep 2020 04:40:01 -0400 Received: from cloud.peff.net ([104.130.231.41]:42388 "EHLO cloud.peff.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1730513AbgI0IkA (ORCPT ); Sun, 27 Sep 2020 04:40:00 -0400 Received: (qmail 29150 invoked by uid 109); 27 Sep 2020 08:39:59 -0000 Received: from Unknown (HELO peff.net) (10.0.1.2) by cloud.peff.net (qpsmtpd/0.94) with ESMTP; Sun, 27 Sep 2020 08:39:59 +0000 Authentication-Results: cloud.peff.net; auth=none Received: (qmail 4065 invoked by uid 111); 27 Sep 2020 08:40:01 -0000 Received: from coredump.intra.peff.net (HELO sigill.intra.peff.net) (10.0.0.2) by peff.net (qpsmtpd/0.94) with (TLS_AES_256_GCM_SHA384 encrypted) ESMTPS; Sun, 27 Sep 2020 04:40:01 -0400 Authentication-Results: peff.net; auth=none Date: Sun, 27 Sep 2020 04:39:59 -0400 From: Jeff King To: git@vger.kernel.org Cc: Eric Sunshine , Martin =?utf-8?b?w4VncmVu?= Subject: [PATCH v2 2/8] shortlog: add grouping option Message-ID: <20200927083959.GB2465761@coredump.intra.peff.net> References: <20200927083933.GA2222823@coredump.intra.peff.net> MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: <20200927083933.GA2222823@coredump.intra.peff.net> Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org In preparation for adding more grouping types, let's refactor the committer/author grouping code and add a user-facing option that binds them together. In particular: - the main option is now "--group", to make it clear that the various group types are mutually exclusive. The "--committer" option is an alias for "--group=committer". - we keep an enum rather than a binary flag, to prepare for more values - we prefer switch statements to ternary assignment, since other group types will need more custom code Signed-off-by: Jeff King --- Documentation/git-shortlog.txt | 9 +++++- builtin/shortlog.c | 59 +++++++++++++++++++++++++++------- shortlog.h | 6 +++- t/t4201-shortlog.sh | 5 +++ 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/Documentation/git-shortlog.txt b/Documentation/git-shortlog.txt index a72ea7f7ba..6496d313c1 100644 --- a/Documentation/git-shortlog.txt +++ b/Documentation/git-shortlog.txt @@ -47,9 +47,16 @@ OPTIONS Each pretty-printed commit will be rewrapped before it is shown. +--group=:: + Group commits based on ``. If no `--group` option is + specified, the default is `author`. `` is one of: ++ + - `author`, commits are grouped by author + - `committer`, commits are grouped by committer (the same as `-c`) + -c:: --committer:: - Collect and show committer identities instead of authors. + This is an alias for `--group=committer`. -w[[,[,]]]:: Linewrap the output by wrapping each line at `width`. The first diff --git a/builtin/shortlog.c b/builtin/shortlog.c index edcf2e0d54..880ce19304 100644 --- a/builtin/shortlog.c +++ b/builtin/shortlog.c @@ -129,7 +129,17 @@ static void read_from_stdin(struct shortlog *log) static const char *committer_match[2] = { "Commit: ", "committer " }; const char **match; - match = log->committer ? committer_match : author_match; + switch (log->group) { + case SHORTLOG_GROUP_AUTHOR: + match = author_match; + break; + case SHORTLOG_GROUP_COMMITTER: + match = committer_match; + break; + default: + BUG("unhandled shortlog group"); + } + while (strbuf_getline_lf(&ident, stdin) != EOF) { const char *v; if (!skip_prefix(ident.buf, match[0], &v) && @@ -158,27 +168,36 @@ void shortlog_add_commit(struct shortlog *log, struct commit *commit) struct strbuf ident = STRBUF_INIT; struct strbuf oneline = STRBUF_INIT; struct pretty_print_context ctx = {0}; - const char *fmt; + const char *oneline_str; ctx.fmt = CMIT_FMT_USERFORMAT; ctx.abbrev = log->abbrev; ctx.print_email_subject = 1; ctx.date_mode.type = DATE_NORMAL; ctx.output_encoding = get_log_output_encoding(); - fmt = log->committer ? - (log->email ? "%cN <%cE>" : "%cN") : - (log->email ? "%aN <%aE>" : "%aN"); - - format_commit_message(commit, fmt, &ident, &ctx); if (!log->summary) { if (log->user_format) pretty_print_commit(&ctx, commit, &oneline); else format_commit_message(commit, "%s", &oneline, &ctx); } - - insert_one_record(log, ident.buf, oneline.len ? oneline.buf : ""); + oneline_str = oneline.len ? oneline.buf : ""; + + switch (log->group) { + case SHORTLOG_GROUP_AUTHOR: + format_commit_message(commit, + log->email ? "%aN <%aE>" : "%aN", + &ident, &ctx); + insert_one_record(log, ident.buf, oneline_str); + break; + case SHORTLOG_GROUP_COMMITTER: + format_commit_message(commit, + log->email ? "%cN <%cE>" : "%cN", + &ident, &ctx); + insert_one_record(log, ident.buf, oneline_str); + break; + } strbuf_release(&ident); strbuf_release(&oneline); @@ -241,6 +260,21 @@ static int parse_wrap_args(const struct option *opt, const char *arg, int unset) return 0; } +static int parse_group_option(const struct option *opt, const char *arg, int unset) +{ + struct shortlog *log = opt->value; + + if (unset || !strcasecmp(arg, "author")) + log->group = SHORTLOG_GROUP_AUTHOR; + else if (!strcasecmp(arg, "committer")) + log->group = SHORTLOG_GROUP_COMMITTER; + else + return error(_("unknown group type: %s"), arg); + + return 0; +} + + void shortlog_init(struct shortlog *log) { memset(log, 0, sizeof(*log)); @@ -260,8 +294,9 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix) int nongit = !startup_info->have_repository; const struct option options[] = { - OPT_BOOL('c', "committer", &log.committer, - N_("Group by committer rather than author")), + OPT_SET_INT('c', "committer", &log.group, + N_("Group by committer rather than author"), + SHORTLOG_GROUP_COMMITTER), OPT_BOOL('n', "numbered", &log.sort_by_number, N_("sort output according to the number of commits per author")), OPT_BOOL('s', "summary", &log.summary, @@ -271,6 +306,8 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix) OPT_CALLBACK_F('w', NULL, &log, N_("[,[,]]"), N_("Linewrap output"), PARSE_OPT_OPTARG, &parse_wrap_args), + OPT_CALLBACK(0, "group", &log, N_("field"), + N_("Group by field"), parse_group_option), OPT_END(), }; diff --git a/shortlog.h b/shortlog.h index 2fa61c4294..876a52158d 100644 --- a/shortlog.h +++ b/shortlog.h @@ -15,7 +15,11 @@ struct shortlog { int in2; int user_format; int abbrev; - int committer; + + enum { + SHORTLOG_GROUP_AUTHOR = 0, + SHORTLOG_GROUP_COMMITTER, + } group; char *common_repo_prefix; int email; diff --git a/t/t4201-shortlog.sh b/t/t4201-shortlog.sh index d3a7ce6bbb..65e4468746 100755 --- a/t/t4201-shortlog.sh +++ b/t/t4201-shortlog.sh @@ -215,4 +215,9 @@ test_expect_success 'shortlog --committer (external)' ' test_cmp expect actual ' +test_expect_success '--group=committer is the same as --committer' ' + git shortlog -ns --group=committer HEAD >actual && + test_cmp expect actual +' + test_done From patchwork Sun Sep 27 08:40:01 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jeff King X-Patchwork-Id: 11801981 Return-Path: Received: from mail.kernel.org (pdx-korg-mail-1.web.codeaurora.org [172.30.200.123]) by pdx-korg-patchwork-2.web.codeaurora.org (Postfix) with ESMTP id 208516CA for ; Sun, 27 Sep 2020 08:40:06 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 12885207BC for ; Sun, 27 Sep 2020 08:40:06 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1730576AbgI0IkD (ORCPT ); Sun, 27 Sep 2020 04:40:03 -0400 Received: from cloud.peff.net ([104.130.231.41]:42390 "EHLO cloud.peff.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1730513AbgI0IkD (ORCPT ); Sun, 27 Sep 2020 04:40:03 -0400 Received: (qmail 29157 invoked by uid 109); 27 Sep 2020 08:40:02 -0000 Received: from Unknown (HELO peff.net) (10.0.1.2) by cloud.peff.net (qpsmtpd/0.94) with ESMTP; Sun, 27 Sep 2020 08:40:02 +0000 Authentication-Results: cloud.peff.net; auth=none Received: (qmail 4070 invoked by uid 111); 27 Sep 2020 08:40:04 -0000 Received: from coredump.intra.peff.net (HELO sigill.intra.peff.net) (10.0.0.2) by peff.net (qpsmtpd/0.94) with (TLS_AES_256_GCM_SHA384 encrypted) ESMTPS; Sun, 27 Sep 2020 04:40:04 -0400 Authentication-Results: peff.net; auth=none Date: Sun, 27 Sep 2020 04:40:01 -0400 From: Jeff King To: git@vger.kernel.org Cc: Eric Sunshine , Martin =?utf-8?b?w4VncmVu?= Subject: [PATCH v2 3/8] trailer: add interface for iterating over commit trailers Message-ID: <20200927084001.GC2465761@coredump.intra.peff.net> References: <20200927083933.GA2222823@coredump.intra.peff.net> MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: <20200927083933.GA2222823@coredump.intra.peff.net> Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org The trailer code knows how to parse out the trailers and re-format them, but there's no easy way to iterate over the trailers (you can use trailer_info, but you have to then do a bunch of extra parsing). Let's add an iteration interface that makes this easy to do. Signed-off-by: Jeff King --- trailer.c | 36 ++++++++++++++++++++++++++++++++++++ trailer.h | 45 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/trailer.c b/trailer.c index 68dabc2556..3f7391d793 100644 --- a/trailer.c +++ b/trailer.c @@ -1183,3 +1183,39 @@ void format_trailers_from_commit(struct strbuf *out, const char *msg, format_trailer_info(out, &info, opts); trailer_info_release(&info); } + +void trailer_iterator_init(struct trailer_iterator *iter, const char *msg) +{ + struct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT; + strbuf_init(&iter->key, 0); + strbuf_init(&iter->val, 0); + opts.no_divider = 1; + trailer_info_get(&iter->info, msg, &opts); + iter->cur = 0; +} + +int trailer_iterator_advance(struct trailer_iterator *iter) +{ + while (iter->cur < iter->info.trailer_nr) { + char *trailer = iter->info.trailers[iter->cur++]; + int separator_pos = find_separator(trailer, separators); + + if (separator_pos < 1) + continue; /* not a real trailer */ + + strbuf_reset(&iter->key); + strbuf_reset(&iter->val); + parse_trailer(&iter->key, &iter->val, NULL, + trailer, separator_pos); + unfold_value(&iter->val); + return 1; + } + return 0; +} + +void trailer_iterator_release(struct trailer_iterator *iter) +{ + trailer_info_release(&iter->info); + strbuf_release(&iter->val); + strbuf_release(&iter->key); +} diff --git a/trailer.h b/trailer.h index 203acf4ee1..cd93e7ddea 100644 --- a/trailer.h +++ b/trailer.h @@ -2,8 +2,7 @@ #define TRAILER_H #include "list.h" - -struct strbuf; +#include "strbuf.h" enum trailer_where { WHERE_DEFAULT, @@ -103,4 +102,46 @@ void trailer_info_release(struct trailer_info *info); void format_trailers_from_commit(struct strbuf *out, const char *msg, const struct process_trailer_options *opts); +/* + * An interface for iterating over the trailers found in a particular commit + * message. Use like: + * + * struct trailer_iterator iter; + * trailer_iterator_init(&iter, msg); + * while (trailer_iterator_advance(&iter)) + * ... do something with iter.key and iter.val ... + * trailer_iterator_release(&iter); + */ +struct trailer_iterator { + struct strbuf key; + struct strbuf val; + + /* private */ + struct trailer_info info; + size_t cur; +}; + +/* + * Initialize "iter" in preparation for walking over the trailers in the commit + * message "msg". The "msg" pointer must remain valid until the iterator is + * released. + * + * After initializing, note that key/val will not yet point to any trailer. + * Call advance() to parse the first one (if any). + */ +void trailer_iterator_init(struct trailer_iterator *iter, const char *msg); + +/* + * Advance to the next trailer of the iterator. Returns 0 if there is no such + * trailer, and 1 otherwise. The key and value of the trailer can be + * fetched from the iter->key and iter->value fields (which are valid + * only until the next advance). + */ +int trailer_iterator_advance(struct trailer_iterator *iter); + +/* + * Release all resources associated with the trailer iteration. + */ +void trailer_iterator_release(struct trailer_iterator *iter); + #endif /* TRAILER_H */ From patchwork Sun Sep 27 08:40:04 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jeff King X-Patchwork-Id: 11801983 Return-Path: Received: from mail.kernel.org (pdx-korg-mail-1.web.codeaurora.org [172.30.200.123]) by pdx-korg-patchwork-2.web.codeaurora.org (Postfix) with ESMTP id 8239092C for ; Sun, 27 Sep 2020 08:40:07 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 73E0C23977 for ; Sun, 27 Sep 2020 08:40:07 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1730583AbgI0IkG (ORCPT ); Sun, 27 Sep 2020 04:40:06 -0400 Received: from cloud.peff.net ([104.130.231.41]:42402 "EHLO cloud.peff.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1730513AbgI0IkG (ORCPT ); Sun, 27 Sep 2020 04:40:06 -0400 Received: (qmail 29170 invoked by uid 109); 27 Sep 2020 08:40:05 -0000 Received: from Unknown (HELO peff.net) (10.0.1.2) by cloud.peff.net (qpsmtpd/0.94) with ESMTP; Sun, 27 Sep 2020 08:40:05 +0000 Authentication-Results: cloud.peff.net; auth=none Received: (qmail 4075 invoked by uid 111); 27 Sep 2020 08:40:07 -0000 Received: from coredump.intra.peff.net (HELO sigill.intra.peff.net) (10.0.0.2) by peff.net (qpsmtpd/0.94) with (TLS_AES_256_GCM_SHA384 encrypted) ESMTPS; Sun, 27 Sep 2020 04:40:07 -0400 Authentication-Results: peff.net; auth=none Date: Sun, 27 Sep 2020 04:40:04 -0400 From: Jeff King To: git@vger.kernel.org Cc: Eric Sunshine , Martin =?utf-8?b?w4VncmVu?= Subject: [PATCH v2 4/8] shortlog: match commit trailers with --group Message-ID: <20200927084004.GD2465761@coredump.intra.peff.net> References: <20200927083933.GA2222823@coredump.intra.peff.net> MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: <20200927083933.GA2222823@coredump.intra.peff.net> Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org If a project uses commit trailers, this patch lets you use shortlog to see who is performing each action. For example, running: git shortlog -ns --group=trailer:reviewed-by in git.git shows who has reviewed. You can even use a custom format to see things like who has helped whom: git shortlog --format="...helped %an (%ad)" \ --group=trailer:helped-by Signed-off-by: Jeff King --- Documentation/git-shortlog.txt | 13 ++++++++++ builtin/shortlog.c | 44 +++++++++++++++++++++++++++++++++- shortlog.h | 2 ++ t/t4201-shortlog.sh | 14 +++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/Documentation/git-shortlog.txt b/Documentation/git-shortlog.txt index 6496d313c1..edd6cda58a 100644 --- a/Documentation/git-shortlog.txt +++ b/Documentation/git-shortlog.txt @@ -53,6 +53,19 @@ OPTIONS + - `author`, commits are grouped by author - `committer`, commits are grouped by committer (the same as `-c`) + - `trailer:`, the `` is interpreted as a case-insensitive + commit message trailer (see linkgit:git-interpret-trailers[1]). For + example, if your project uses `Reviewed-by` trailers, you might want + to see who has been reviewing with + `git shortlog -ns --group=trailer:reviewed-by`. ++ +Note that commits that do not include the trailer will not be counted. +Likewise, commits with multiple trailers (e.g., multiple signoffs) may +be counted more than once. ++ +The contents of each trailer value are taken literally and completely. +No mailmap is applied, and the `-e` option has no effect (if the trailer +contains a username and email, they are both always shown). -c:: --committer:: diff --git a/builtin/shortlog.c b/builtin/shortlog.c index 880ce19304..e1d9ee909f 100644 --- a/builtin/shortlog.c +++ b/builtin/shortlog.c @@ -9,6 +9,7 @@ #include "mailmap.h" #include "shortlog.h" #include "parse-options.h" +#include "trailer.h" static char const * const shortlog_usage[] = { N_("git shortlog [] [] [[--] ...]"), @@ -136,6 +137,8 @@ static void read_from_stdin(struct shortlog *log) case SHORTLOG_GROUP_COMMITTER: match = committer_match; break; + case SHORTLOG_GROUP_TRAILER: + die(_("using --group=trailer with stdin is not supported")); default: BUG("unhandled shortlog group"); } @@ -163,6 +166,37 @@ static void read_from_stdin(struct shortlog *log) strbuf_release(&oneline); } +static void insert_records_from_trailers(struct shortlog *log, + struct commit *commit, + struct pretty_print_context *ctx, + const char *oneline) +{ + struct trailer_iterator iter; + const char *commit_buffer, *body; + + /* + * Using format_commit_message("%B") would be simpler here, but + * this saves us copying the message. + */ + commit_buffer = logmsg_reencode(commit, NULL, ctx->output_encoding); + body = strstr(commit_buffer, "\n\n"); + if (!body) + return; + + trailer_iterator_init(&iter, body); + while (trailer_iterator_advance(&iter)) { + const char *value = iter.val.buf; + + if (strcasecmp(iter.key.buf, log->trailer)) + continue; + + insert_one_record(log, value, oneline); + } + trailer_iterator_release(&iter); + + unuse_commit_buffer(commit, commit_buffer); +} + void shortlog_add_commit(struct shortlog *log, struct commit *commit) { struct strbuf ident = STRBUF_INIT; @@ -197,6 +231,9 @@ void shortlog_add_commit(struct shortlog *log, struct commit *commit) &ident, &ctx); insert_one_record(log, ident.buf, oneline_str); break; + case SHORTLOG_GROUP_TRAILER: + insert_records_from_trailers(log, commit, &ctx, oneline_str); + break; } strbuf_release(&ident); @@ -263,12 +300,17 @@ static int parse_wrap_args(const struct option *opt, const char *arg, int unset) static int parse_group_option(const struct option *opt, const char *arg, int unset) { struct shortlog *log = opt->value; + const char *field; if (unset || !strcasecmp(arg, "author")) log->group = SHORTLOG_GROUP_AUTHOR; else if (!strcasecmp(arg, "committer")) log->group = SHORTLOG_GROUP_COMMITTER; - else + else if (skip_prefix(arg, "trailer:", &field)) { + log->group = SHORTLOG_GROUP_TRAILER; + free(log->trailer); + log->trailer = xstrdup(field); + } else return error(_("unknown group type: %s"), arg); return 0; diff --git a/shortlog.h b/shortlog.h index 876a52158d..89c2dbc5e6 100644 --- a/shortlog.h +++ b/shortlog.h @@ -19,7 +19,9 @@ struct shortlog { enum { SHORTLOG_GROUP_AUTHOR = 0, SHORTLOG_GROUP_COMMITTER, + SHORTLOG_GROUP_TRAILER, } group; + char *trailer; char *common_repo_prefix; int email; diff --git a/t/t4201-shortlog.sh b/t/t4201-shortlog.sh index 65e4468746..e97d891a71 100755 --- a/t/t4201-shortlog.sh +++ b/t/t4201-shortlog.sh @@ -220,4 +220,18 @@ test_expect_success '--group=committer is the same as --committer' ' test_cmp expect actual ' +test_expect_success 'shortlog --group=trailer:signed-off-by' ' + git commit --allow-empty -m foo -s && + GIT_COMMITTER_NAME="SOB One" \ + GIT_COMMITTER_EMAIL=sob@example.com \ + git commit --allow-empty -m foo -s && + git commit --allow-empty --amend --no-edit -s && + cat >expect <<-\EOF && + 2 C O Mitter + 1 SOB One + EOF + git shortlog -ns --group=trailer:signed-off-by HEAD >actual && + test_cmp expect actual +' + test_done From patchwork Sun Sep 27 08:40:07 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jeff King X-Patchwork-Id: 11801985 Return-Path: Received: from mail.kernel.org (pdx-korg-mail-1.web.codeaurora.org [172.30.200.123]) by pdx-korg-patchwork-2.web.codeaurora.org (Postfix) with ESMTP id 88EA492C for ; Sun, 27 Sep 2020 08:40:09 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 7AFFE207BC for ; Sun, 27 Sep 2020 08:40:09 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1730590AbgI0IkI (ORCPT ); Sun, 27 Sep 2020 04:40:08 -0400 Received: from cloud.peff.net ([104.130.231.41]:42410 "EHLO cloud.peff.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1730513AbgI0IkI (ORCPT ); Sun, 27 Sep 2020 04:40:08 -0400 Received: (qmail 29181 invoked by uid 109); 27 Sep 2020 08:40:07 -0000 Received: from Unknown (HELO peff.net) (10.0.1.2) by cloud.peff.net (qpsmtpd/0.94) with ESMTP; Sun, 27 Sep 2020 08:40:07 +0000 Authentication-Results: cloud.peff.net; auth=none Received: (qmail 4106 invoked by uid 111); 27 Sep 2020 08:40:09 -0000 Received: from coredump.intra.peff.net (HELO sigill.intra.peff.net) (10.0.0.2) by peff.net (qpsmtpd/0.94) with (TLS_AES_256_GCM_SHA384 encrypted) ESMTPS; Sun, 27 Sep 2020 04:40:09 -0400 Authentication-Results: peff.net; auth=none Date: Sun, 27 Sep 2020 04:40:07 -0400 From: Jeff King To: git@vger.kernel.org Cc: Eric Sunshine , Martin =?utf-8?b?w4VncmVu?= Subject: [PATCH v2 5/8] shortlog: de-duplicate trailer values Message-ID: <20200927084007.GE2465761@coredump.intra.peff.net> References: <20200927083933.GA2222823@coredump.intra.peff.net> MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: <20200927083933.GA2222823@coredump.intra.peff.net> Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org The current documentation is vague about what happens with --group=trailer:signed-off-by when we see a commit with: Signed-off-by: One Signed-off-by: Two Signed-off-by: One We clearly should credit both "One" and "Two", but should "One" get credited twice? The current code does so, but mostly because that was the easiest thing to do. It's probably more useful to count each commit at most once. This will become especially important when we allow values from multiple sources in a future patch. Signed-off-by: Jeff King --- Documentation/git-shortlog.txt | 3 +- builtin/shortlog.c | 58 ++++++++++++++++++++++++++++++++++ t/t4201-shortlog.sh | 28 ++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/Documentation/git-shortlog.txt b/Documentation/git-shortlog.txt index edd6cda58a..9e94613e13 100644 --- a/Documentation/git-shortlog.txt +++ b/Documentation/git-shortlog.txt @@ -61,7 +61,8 @@ OPTIONS + Note that commits that do not include the trailer will not be counted. Likewise, commits with multiple trailers (e.g., multiple signoffs) may -be counted more than once. +be counted more than once (but only once per unique trailer value in +that commit). + The contents of each trailer value are taken literally and completely. No mailmap is applied, and the `-e` option has no effect (if the trailer diff --git a/builtin/shortlog.c b/builtin/shortlog.c index e1d9ee909f..d2d8103dd3 100644 --- a/builtin/shortlog.c +++ b/builtin/shortlog.c @@ -166,13 +166,68 @@ static void read_from_stdin(struct shortlog *log) strbuf_release(&oneline); } +struct strset_item { + struct hashmap_entry ent; + char value[FLEX_ARRAY]; +}; + +struct strset { + struct hashmap map; +}; + +#define STRSET_INIT { { NULL } } + +static int strset_item_hashcmp(const void *hash_data, + const struct hashmap_entry *entry, + const struct hashmap_entry *entry_or_key, + const void *keydata) +{ + const struct strset_item *a, *b; + + a = container_of(entry, const struct strset_item, ent); + if (keydata) + return strcmp(a->value, keydata); + + b = container_of(entry_or_key, const struct strset_item, ent); + return strcmp(a->value, b->value); +} + +/* + * Adds "str" to the set if it was not already present; returns true if it was + * already there. + */ +static int strset_check_and_add(struct strset *ss, const char *str) +{ + unsigned int hash = strhash(str); + struct strset_item *item; + + if (!ss->map.table) + hashmap_init(&ss->map, strset_item_hashcmp, NULL, 0); + + if (hashmap_get_from_hash(&ss->map, hash, str)) + return 1; + + FLEX_ALLOC_STR(item, value, str); + hashmap_entry_init(&item->ent, hash); + hashmap_add(&ss->map, &item->ent); + return 0; +} + +static void strset_clear(struct strset *ss) +{ + if (!ss->map.table) + return; + hashmap_free_entries(&ss->map, struct strset_item, ent); +} + static void insert_records_from_trailers(struct shortlog *log, struct commit *commit, struct pretty_print_context *ctx, const char *oneline) { struct trailer_iterator iter; const char *commit_buffer, *body; + struct strset dups = STRSET_INIT; /* * Using format_commit_message("%B") would be simpler here, but @@ -190,10 +245,13 @@ static void insert_records_from_trailers(struct shortlog *log, if (strcasecmp(iter.key.buf, log->trailer)) continue; + if (strset_check_and_add(&dups, value)) + continue; insert_one_record(log, value, oneline); } trailer_iterator_release(&iter); + strset_clear(&dups); unuse_commit_buffer(commit, commit_buffer); } diff --git a/t/t4201-shortlog.sh b/t/t4201-shortlog.sh index e97d891a71..83dbbc44e8 100755 --- a/t/t4201-shortlog.sh +++ b/t/t4201-shortlog.sh @@ -234,4 +234,32 @@ test_expect_success 'shortlog --group=trailer:signed-off-by' ' test_cmp expect actual ' +test_expect_success 'shortlog de-duplicates trailers in a single commit' ' + git commit --allow-empty -F - <<-\EOF && + subject one + + this message has two distinct values, plus a repeat + + Repeated-trailer: Foo + Repeated-trailer: Bar + Repeated-trailer: Foo + EOF + + git commit --allow-empty -F - <<-\EOF && + subject two + + similar to the previous, but without the second distinct value + + Repeated-trailer: Foo + Repeated-trailer: Foo + EOF + + cat >expect <<-\EOF && + 2 Foo + 1 Bar + EOF + git shortlog -ns --group=trailer:repeated-trailer -2 HEAD >actual && + test_cmp expect actual +' + test_done From patchwork Sun Sep 27 08:40:09 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jeff King X-Patchwork-Id: 11801987 Return-Path: Received: from mail.kernel.org (pdx-korg-mail-1.web.codeaurora.org [172.30.200.123]) by pdx-korg-patchwork-2.web.codeaurora.org (Postfix) with ESMTP id D48256CA for ; Sun, 27 Sep 2020 08:40:11 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id C73CC23998 for ; Sun, 27 Sep 2020 08:40:11 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1730593AbgI0IkL (ORCPT ); Sun, 27 Sep 2020 04:40:11 -0400 Received: from cloud.peff.net ([104.130.231.41]:42422 "EHLO cloud.peff.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1730513AbgI0IkK (ORCPT ); Sun, 27 Sep 2020 04:40:10 -0400 Received: (qmail 29195 invoked by uid 109); 27 Sep 2020 08:40:10 -0000 Received: from Unknown (HELO peff.net) (10.0.1.2) by cloud.peff.net (qpsmtpd/0.94) with ESMTP; Sun, 27 Sep 2020 08:40:10 +0000 Authentication-Results: cloud.peff.net; auth=none Received: (qmail 4111 invoked by uid 111); 27 Sep 2020 08:40:12 -0000 Received: from coredump.intra.peff.net (HELO sigill.intra.peff.net) (10.0.0.2) by peff.net (qpsmtpd/0.94) with (TLS_AES_256_GCM_SHA384 encrypted) ESMTPS; Sun, 27 Sep 2020 04:40:12 -0400 Authentication-Results: peff.net; auth=none Date: Sun, 27 Sep 2020 04:40:09 -0400 From: Jeff King To: git@vger.kernel.org Cc: Eric Sunshine , Martin =?utf-8?b?w4VncmVu?= Subject: [PATCH v2 6/8] shortlog: rename parse_stdin_ident() Message-ID: <20200927084009.GF2465761@coredump.intra.peff.net> References: <20200927083933.GA2222823@coredump.intra.peff.net> MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: <20200927083933.GA2222823@coredump.intra.peff.net> Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org This function is actually useful for parsing any identity, whether from stdin or not. We'll need it for handling trailers. Signed-off-by: Jeff King --- builtin/shortlog.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/shortlog.c b/builtin/shortlog.c index d2d8103dd3..e6f4faec7c 100644 --- a/builtin/shortlog.c +++ b/builtin/shortlog.c @@ -98,8 +98,8 @@ static void insert_one_record(struct shortlog *log, } } -static int parse_stdin_ident(struct shortlog *log, - struct strbuf *out, const char *in) +static int parse_ident(struct shortlog *log, + struct strbuf *out, const char *in) { const char *mailbuf, *namebuf; size_t namelen, maillen; @@ -156,7 +156,7 @@ static void read_from_stdin(struct shortlog *log) ; /* discard blanks */ strbuf_reset(&mapped_ident); - if (parse_stdin_ident(log, &mapped_ident, v) < 0) + if (parse_ident(log, &mapped_ident, v) < 0) continue; insert_one_record(log, mapped_ident.buf, oneline.buf); From patchwork Sun Sep 27 08:40:11 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jeff King X-Patchwork-Id: 11801989 Return-Path: Received: from mail.kernel.org (pdx-korg-mail-1.web.codeaurora.org [172.30.200.123]) by pdx-korg-patchwork-2.web.codeaurora.org (Postfix) with ESMTP id 527DB6CA for ; Sun, 27 Sep 2020 08:40:14 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 42DDD207BC for ; Sun, 27 Sep 2020 08:40:14 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1730596AbgI0IkN (ORCPT ); Sun, 27 Sep 2020 04:40:13 -0400 Received: from cloud.peff.net ([104.130.231.41]:42434 "EHLO cloud.peff.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1730513AbgI0IkN (ORCPT ); Sun, 27 Sep 2020 04:40:13 -0400 Received: (qmail 29206 invoked by uid 109); 27 Sep 2020 08:40:12 -0000 Received: from Unknown (HELO peff.net) (10.0.1.2) by cloud.peff.net (qpsmtpd/0.94) with ESMTP; Sun, 27 Sep 2020 08:40:12 +0000 Authentication-Results: cloud.peff.net; auth=none Received: (qmail 4142 invoked by uid 111); 27 Sep 2020 08:40:14 -0000 Received: from coredump.intra.peff.net (HELO sigill.intra.peff.net) (10.0.0.2) by peff.net (qpsmtpd/0.94) with (TLS_AES_256_GCM_SHA384 encrypted) ESMTPS; Sun, 27 Sep 2020 04:40:14 -0400 Authentication-Results: peff.net; auth=none Date: Sun, 27 Sep 2020 04:40:11 -0400 From: Jeff King To: git@vger.kernel.org Cc: Eric Sunshine , Martin =?utf-8?b?w4VncmVu?= Subject: [PATCH v2 7/8] shortlog: parse trailer idents Message-ID: <20200927084011.GG2465761@coredump.intra.peff.net> References: <20200927083933.GA2222823@coredump.intra.peff.net> MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: <20200927083933.GA2222823@coredump.intra.peff.net> Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org Trailers don't necessarily contain name/email identity values, so shortlog has so far treated them as opaque strings. However, since many trailers do contain identities, it's useful to treat them as such when they can be parsed. That lets "-e" work as usual, as well as mailmap. When they can't be parsed, we'll continue with the old behavior of treating them as a single string (there's no new test for that here, since the existing tests cover a trailer like this). Signed-off-by: Jeff King --- Documentation/git-shortlog.txt | 7 ++++--- builtin/shortlog.c | 6 ++++++ t/t4201-shortlog.sh | 20 ++++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Documentation/git-shortlog.txt b/Documentation/git-shortlog.txt index 9e94613e13..3db0db2da0 100644 --- a/Documentation/git-shortlog.txt +++ b/Documentation/git-shortlog.txt @@ -64,9 +64,10 @@ Likewise, commits with multiple trailers (e.g., multiple signoffs) may be counted more than once (but only once per unique trailer value in that commit). + -The contents of each trailer value are taken literally and completely. -No mailmap is applied, and the `-e` option has no effect (if the trailer -contains a username and email, they are both always shown). +Shortlog will attempt to parse each trailer value as a `name ` +identity. If successful, the mailmap is applied and the email is omitted +unless the `--email` option is specified. If the value cannot be parsed +as an identity, it will be taken literally and completely. -c:: --committer:: diff --git a/builtin/shortlog.c b/builtin/shortlog.c index e6f4faec7c..28133aec68 100644 --- a/builtin/shortlog.c +++ b/builtin/shortlog.c @@ -228,6 +228,7 @@ static void insert_records_from_trailers(struct shortlog *log, struct trailer_iterator iter; const char *commit_buffer, *body; struct strset dups = STRSET_INIT; + struct strbuf ident = STRBUF_INIT; /* * Using format_commit_message("%B") would be simpler here, but @@ -245,12 +246,17 @@ static void insert_records_from_trailers(struct shortlog *log, if (strcasecmp(iter.key.buf, log->trailer)) continue; + strbuf_reset(&ident); + if (!parse_ident(log, &ident, value)) + value = ident.buf; + if (strset_check_and_add(&dups, value)) continue; insert_one_record(log, value, oneline); } trailer_iterator_release(&iter); + strbuf_release(&ident); strset_clear(&dups); unuse_commit_buffer(commit, commit_buffer); } diff --git a/t/t4201-shortlog.sh b/t/t4201-shortlog.sh index 83dbbc44e8..a62ee9ed55 100755 --- a/t/t4201-shortlog.sh +++ b/t/t4201-shortlog.sh @@ -230,10 +230,30 @@ test_expect_success 'shortlog --group=trailer:signed-off-by' ' 2 C O Mitter 1 SOB One EOF + git shortlog -nse --group=trailer:signed-off-by HEAD >actual && + test_cmp expect actual +' + +test_expect_success 'trailer idents are split' ' + cat >expect <<-\EOF && + 2 C O Mitter + 1 SOB One + EOF git shortlog -ns --group=trailer:signed-off-by HEAD >actual && test_cmp expect actual ' +test_expect_success 'trailer idents are mailmapped' ' + cat >expect <<-\EOF && + 2 C O Mitter + 1 Another Name + EOF + echo "Another Name " >mail.map && + git -c mailmap.file=mail.map shortlog -ns \ + --group=trailer:signed-off-by HEAD >actual && + test_cmp expect actual +' + test_expect_success 'shortlog de-duplicates trailers in a single commit' ' git commit --allow-empty -F - <<-\EOF && subject one From patchwork Sun Sep 27 08:40:15 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jeff King X-Patchwork-Id: 11801991 Return-Path: Received: from mail.kernel.org (pdx-korg-mail-1.web.codeaurora.org [172.30.200.123]) by pdx-korg-patchwork-2.web.codeaurora.org (Postfix) with ESMTP id 67DF16CA for ; Sun, 27 Sep 2020 08:40:18 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 5557923998 for ; Sun, 27 Sep 2020 08:40:18 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1730601AbgI0IkR (ORCPT ); Sun, 27 Sep 2020 04:40:17 -0400 Received: from cloud.peff.net ([104.130.231.41]:42440 "EHLO cloud.peff.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1730513AbgI0IkR (ORCPT ); Sun, 27 Sep 2020 04:40:17 -0400 Received: (qmail 29215 invoked by uid 109); 27 Sep 2020 08:40:15 -0000 Received: from Unknown (HELO peff.net) (10.0.1.2) by cloud.peff.net (qpsmtpd/0.94) with ESMTP; Sun, 27 Sep 2020 08:40:15 +0000 Authentication-Results: cloud.peff.net; auth=none Received: (qmail 4147 invoked by uid 111); 27 Sep 2020 08:40:17 -0000 Received: from coredump.intra.peff.net (HELO sigill.intra.peff.net) (10.0.0.2) by peff.net (qpsmtpd/0.94) with (TLS_AES_256_GCM_SHA384 encrypted) ESMTPS; Sun, 27 Sep 2020 04:40:17 -0400 Authentication-Results: peff.net; auth=none Date: Sun, 27 Sep 2020 04:40:15 -0400 From: Jeff King To: git@vger.kernel.org Cc: Eric Sunshine , Martin =?utf-8?b?w4VncmVu?= Subject: [PATCH v2 8/8] shortlog: allow multiple groups to be specified Message-ID: <20200927084015.GH2465761@coredump.intra.peff.net> References: <20200927083933.GA2222823@coredump.intra.peff.net> MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: <20200927083933.GA2222823@coredump.intra.peff.net> Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org Now that shortlog supports reading from trailers, it can be useful to combine counts from multiple trailers, or between trailers and authors. This can be done manually by post-processing the output from multiple runs, but it's non-trivial to make sure that each name/commit pair is counted only once. This patch teaches shortlog to accept multiple --group options on the command line, and pull data from all of them. That makes it possible to run: git shortlog -ns --group=author --group=trailer:co-authored-by to get a shortlog that counts authors and co-authors equally. The implementation is mostly straightforward. The "group" enum becomes a bitfield, and the trailer key becomes a list. I didn't bother implementing the multi-group semantics for reading from stdin. It would be possible to do, but the existing matching code makes it awkward, and I doubt anybody cares. The duplicate suppression we used for trailers now covers authors and committers as well (though in non-trailer single-group mode we can skip the hash insertion and lookup, since we only see one value per commit). There is one subtlety: we now care about the case when no group bit is set (in which case we default to showing the author). The caller in builtin/log.c needs to be adapted to ask explicitly for authors, rather than relying on shortlog_init(). It would be possible with some gymnastics to make this keep working as-is, but it's not worth it for a single caller. Signed-off-by: Jeff King --- Documentation/git-shortlog.txt | 7 ++++ builtin/log.c | 1 + builtin/shortlog.c | 64 ++++++++++++++++++----------- shortlog.h | 10 ++--- t/t4201-shortlog.sh | 74 ++++++++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 29 deletions(-) diff --git a/Documentation/git-shortlog.txt b/Documentation/git-shortlog.txt index 3db0db2da0..fd93cd41e9 100644 --- a/Documentation/git-shortlog.txt +++ b/Documentation/git-shortlog.txt @@ -51,6 +51,7 @@ OPTIONS Group commits based on ``. If no `--group` option is specified, the default is `author`. `` is one of: + +-- - `author`, commits are grouped by author - `committer`, commits are grouped by committer (the same as `-c`) - `trailer:`, the `` is interpreted as a case-insensitive @@ -68,6 +69,12 @@ Shortlog will attempt to parse each trailer value as a `name ` identity. If successful, the mailmap is applied and the email is omitted unless the `--email` option is specified. If the value cannot be parsed as an identity, it will be taken literally and completely. +-- ++ +If `--group` is specified multiple times, commits are counted under each +value (but again, only once per unique value in that commit). For +example, `git shortlog --group=author --group=trailer:co-authored-by` +counts both authors and co-authors. -c:: --committer:: diff --git a/builtin/log.c b/builtin/log.c index b8824d898f..7f27e9eca1 100644 --- a/builtin/log.c +++ b/builtin/log.c @@ -1195,6 +1195,7 @@ static void make_cover_letter(struct rev_info *rev, int use_stdout, log.in1 = 2; log.in2 = 4; log.file = rev->diffopt.file; + log.groups = SHORTLOG_GROUP_AUTHOR; for (i = 0; i < nr; i++) shortlog_add_commit(&log, list[i]); diff --git a/builtin/shortlog.c b/builtin/shortlog.c index 28133aec68..0a5c4968f6 100644 --- a/builtin/shortlog.c +++ b/builtin/shortlog.c @@ -130,7 +130,10 @@ static void read_from_stdin(struct shortlog *log) static const char *committer_match[2] = { "Commit: ", "committer " }; const char **match; - switch (log->group) { + if (HAS_MULTI_BITS(log->groups)) + die(_("using multiple --group options with stdin is not supported")); + + switch (log->groups) { case SHORTLOG_GROUP_AUTHOR: match = author_match; break; @@ -221,13 +224,13 @@ static void strset_clear(struct strset *ss) } static void insert_records_from_trailers(struct shortlog *log, + struct strset *dups, struct commit *commit, struct pretty_print_context *ctx, const char *oneline) { struct trailer_iterator iter; const char *commit_buffer, *body; - struct strset dups = STRSET_INIT; struct strbuf ident = STRBUF_INIT; /* @@ -243,28 +246,28 @@ static void insert_records_from_trailers(struct shortlog *log, while (trailer_iterator_advance(&iter)) { const char *value = iter.val.buf; - if (strcasecmp(iter.key.buf, log->trailer)) + if (!string_list_has_string(&log->trailers, iter.key.buf)) continue; strbuf_reset(&ident); if (!parse_ident(log, &ident, value)) value = ident.buf; - if (strset_check_and_add(&dups, value)) + if (strset_check_and_add(dups, value)) continue; insert_one_record(log, value, oneline); } trailer_iterator_release(&iter); strbuf_release(&ident); - strset_clear(&dups); unuse_commit_buffer(commit, commit_buffer); } void shortlog_add_commit(struct shortlog *log, struct commit *commit) { struct strbuf ident = STRBUF_INIT; struct strbuf oneline = STRBUF_INIT; + struct strset dups = STRSET_INIT; struct pretty_print_context ctx = {0}; const char *oneline_str; @@ -282,24 +285,29 @@ void shortlog_add_commit(struct shortlog *log, struct commit *commit) } oneline_str = oneline.len ? oneline.buf : ""; - switch (log->group) { - case SHORTLOG_GROUP_AUTHOR: + if (log->groups & SHORTLOG_GROUP_AUTHOR) { + strbuf_reset(&ident); format_commit_message(commit, log->email ? "%aN <%aE>" : "%aN", &ident, &ctx); - insert_one_record(log, ident.buf, oneline_str); - break; - case SHORTLOG_GROUP_COMMITTER: + if (!HAS_MULTI_BITS(log->groups) || + !strset_check_and_add(&dups, ident.buf)) + insert_one_record(log, ident.buf, oneline_str); + } + if (log->groups & SHORTLOG_GROUP_COMMITTER) { + strbuf_reset(&ident); format_commit_message(commit, log->email ? "%cN <%cE>" : "%cN", &ident, &ctx); - insert_one_record(log, ident.buf, oneline_str); - break; - case SHORTLOG_GROUP_TRAILER: - insert_records_from_trailers(log, commit, &ctx, oneline_str); - break; + if (!HAS_MULTI_BITS(log->groups) || + !strset_check_and_add(&dups, ident.buf)) + insert_one_record(log, ident.buf, oneline_str); + } + if (log->groups & SHORTLOG_GROUP_TRAILER) { + insert_records_from_trailers(log, &dups, commit, &ctx, oneline_str); } + strset_clear(&dups); strbuf_release(&ident); strbuf_release(&oneline); } @@ -366,14 +374,16 @@ static int parse_group_option(const struct option *opt, const char *arg, int uns struct shortlog *log = opt->value; const char *field; - if (unset || !strcasecmp(arg, "author")) - log->group = SHORTLOG_GROUP_AUTHOR; + if (unset) { + log->groups = 0; + string_list_clear(&log->trailers, 0); + } else if (!strcasecmp(arg, "author")) + log->groups |= SHORTLOG_GROUP_AUTHOR; else if (!strcasecmp(arg, "committer")) - log->group = SHORTLOG_GROUP_COMMITTER; + log->groups |= SHORTLOG_GROUP_COMMITTER; else if (skip_prefix(arg, "trailer:", &field)) { - log->group = SHORTLOG_GROUP_TRAILER; - free(log->trailer); - log->trailer = xstrdup(field); + log->groups |= SHORTLOG_GROUP_TRAILER; + string_list_append(&log->trailers, field); } else return error(_("unknown group type: %s"), arg); @@ -391,6 +401,8 @@ void shortlog_init(struct shortlog *log) log->wrap = DEFAULT_WRAPLEN; log->in1 = DEFAULT_INDENT1; log->in2 = DEFAULT_INDENT2; + log->trailers.strdup_strings = 1; + log->trailers.cmp = strcasecmp; } int cmd_shortlog(int argc, const char **argv, const char *prefix) @@ -400,9 +412,9 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix) int nongit = !startup_info->have_repository; const struct option options[] = { - OPT_SET_INT('c', "committer", &log.group, - N_("Group by committer rather than author"), - SHORTLOG_GROUP_COMMITTER), + OPT_BIT('c', "committer", &log.groups, + N_("Group by committer rather than author"), + SHORTLOG_GROUP_COMMITTER), OPT_BOOL('n', "numbered", &log.sort_by_number, N_("sort output according to the number of commits per author")), OPT_BOOL('s', "summary", &log.summary, @@ -454,6 +466,10 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix) log.abbrev = rev.abbrev; log.file = rev.diffopt.file; + if (!log.groups) + log.groups = SHORTLOG_GROUP_AUTHOR; + string_list_sort(&log.trailers); + /* assume HEAD if from a tty */ if (!nongit && !rev.pending.nr && isatty(0)) add_head_to_pending(&rev); diff --git a/shortlog.h b/shortlog.h index 89c2dbc5e6..64be879b24 100644 --- a/shortlog.h +++ b/shortlog.h @@ -17,11 +17,11 @@ struct shortlog { int abbrev; enum { - SHORTLOG_GROUP_AUTHOR = 0, - SHORTLOG_GROUP_COMMITTER, - SHORTLOG_GROUP_TRAILER, - } group; - char *trailer; + SHORTLOG_GROUP_AUTHOR = (1 << 0), + SHORTLOG_GROUP_COMMITTER = (1 << 1), + SHORTLOG_GROUP_TRAILER = (1 << 2), + } groups; + struct string_list trailers; char *common_repo_prefix; int email; diff --git a/t/t4201-shortlog.sh b/t/t4201-shortlog.sh index a62ee9ed55..3d5c4a2086 100755 --- a/t/t4201-shortlog.sh +++ b/t/t4201-shortlog.sh @@ -282,4 +282,78 @@ test_expect_success 'shortlog de-duplicates trailers in a single commit' ' test_cmp expect actual ' +test_expect_success 'shortlog can match multiple groups' ' + git commit --allow-empty -F - <<-\EOF && + subject one + + this has two trailers that are distinct from the author; it will count + 3 times in the output + + Some-trailer: User A + Another-trailer: User B + EOF + + git commit --allow-empty -F - <<-\EOF && + subject two + + this one has two trailers, one of which is a duplicate with the author; + it will only be counted once for them + + Another-trailer: A U Thor + Some-trailer: User B + EOF + + cat >expect <<-\EOF && + 2 A U Thor + 2 User B + 1 User A + EOF + git shortlog -ns \ + --group=author \ + --group=trailer:some-trailer \ + --group=trailer:another-trailer \ + -2 HEAD >actual && + test_cmp expect actual +' + +test_expect_success 'set up option selection tests' ' + git commit --allow-empty -F - <<-\EOF + subject + + body + + Trailer-one: value-one + Trailer-two: value-two + EOF +' + +test_expect_success '--no-group resets group list to author' ' + cat >expect <<-\EOF && + 1 A U Thor + EOF + git shortlog -ns \ + --group=committer \ + --group=trailer:trailer-one \ + --no-group \ + -1 HEAD >actual && + test_cmp expect actual +' + +test_expect_success '--no-group resets trailer list' ' + cat >expect <<-\EOF && + 1 value-two + EOF + git shortlog -ns \ + --group=trailer:trailer-one \ + --no-group \ + --group=trailer:trailer-two \ + -1 HEAD >actual && + test_cmp expect actual +' + +test_expect_success 'stdin with multiple groups reports error' ' + git log >log && + test_must_fail git shortlog --group=author --group=committer