]> git.ipfire.org Git - thirdparty/git.git/commitdiff
shortlog: allow multiple groups to be specified
authorJeff King <peff@peff.net>
Sun, 27 Sep 2020 08:40:15 +0000 (04:40 -0400)
committerJunio C Hamano <gitster@pobox.com>
Sun, 27 Sep 2020 19:21:05 +0000 (12:21 -0700)
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 <peff@peff.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/git-shortlog.txt
builtin/log.c
builtin/shortlog.c
shortlog.h
t/t4201-shortlog.sh

index 3db0db2da038aad90e8b6f8c7e393cb76f672cde..fd93cd41e90c7d7b6c5b9dcbc904681a0b247254 100644 (file)
@@ -51,6 +51,7 @@ OPTIONS
        Group commits based on `<type>`. If no `--group` option is
        specified, the default is `author`. `<type>` is one of:
 +
+--
  - `author`, commits are grouped by author
  - `committer`, commits are grouped by committer (the same as `-c`)
  - `trailer:<field>`, the `<field>` is interpreted as a case-insensitive
@@ -68,6 +69,12 @@ Shortlog will attempt to parse each trailer value as a `name <email>`
 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::
index d104d5c6889ba2d2ffaaeafa08fbfcd759d8bc92..ad0c5230cf70945486ecae14fb6f502617535d27 100644 (file)
@@ -1197,6 +1197,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]);
 
index 28133aec6854084bd12d112f8f2382c7198bda3a..0a5c4968f64e1655b5cf16e3555209d52dae065c 100644 (file)
@@ -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,21 +246,20 @@ 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);
 }
 
@@ -265,6 +267,7 @@ 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 : "<none>";
 
-       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 @@ parse_done:
        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);
index 89c2dbc5e66b4a5bb56a6825cfe06bf1d856ab77..64be879b241b1bd3daf8d5a30cf736f98211e745 100644 (file)
@@ -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;
index a62ee9ed55e7b6401b7d971a57b7939a7282443e..3d5c4a2086afbaa5f5ae275c957efb6ee809953b 100755 (executable)
@@ -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 <a@example.com>
+       Another-trailer: User B <b@example.com>
+       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 <author@example.com>
+       Some-trailer: User B <b@example.com>
+       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 <log
+'
+
 test_done