shortlog: support arbitrary commit format `--group`s

In addition to generating a shortlog based on committer, author, or the identity in one or more specified trailers, it can be useful to generate a shortlog based on an arbitrary commit format. This can be used, for example, to generate a distribution of commit activity over time, like so: $ git shortlog --group='%cd' --date='format:%Y-%m' -s v2.37.0.. 117 2022-06 274 2022-07 324 2022-08 263 2022-09 7 2022-10 Arbitrary commit formats can be used. In fact, `git shortlog`'s default behavior (to count by commit authors) can be emulated as follows: $ git shortlog --group='%aN <%aE>' ... and future patches will make the default behavior (as well as `--committer`, and `--group=trailer:<trailer>`) special cases of the more flexible `--group` option. Note also that the SHORTLOG_GROUP_FORMAT enum value is used only to designate that `--group:<format>` is in use when in stdin mode to declare that the combination is invalid. Signed-off-by: Taylor Blau <me@ttaylorr.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Taylor Blau committed Oct 24, 2022 at 14:55 UTC 3dc95e09e1355ebde472bd5be37aa7b29ef774d3
4 files changed +78 -2
Documentation/git-shortlog.txt
+4 -1
@@ -50,7 +50,7 @@ OPTIONS
50 --date=<format>::
51 Show dates formatted according to the given date string. (See
52 the `--date` option in the "Commit Formatting" section of
53 - linkgit:git-log[1]).
53 + linkgit:git-log[1]). Useful with `--group=format:<format>`.
54
55 --group=<type>::
56 Group commits based on `<type>`. If no `--group` option is
@@ -64,6 +64,9 @@ OPTIONS
64 example, if your project uses `Reviewed-by` trailers, you might want
65 to see who has been reviewing with
66 `git shortlog -ns --group=trailer:reviewed-by`.
67 + - `format:<format>`, any string accepted by the `--format` option of
68 + 'git log'. (See the "PRETTY FORMATS" section of
69 + linkgit:git-log[1].)
70 +
71 Note that commits that do not include the trailer will not be counted.
72 Likewise, commits with multiple trailers (e.g., multiple signoffs) may
builtin/shortlog.c
+40 -1
@@ -133,6 +133,8 @@ static void read_from_stdin(struct shortlog *log)
133 break;
134 case SHORTLOG_GROUP_TRAILER:
135 die(_("using %s with stdin is not supported"), "--group=trailer");
136 + case SHORTLOG_GROUP_FORMAT:
137 + die(_("using %s with stdin is not supported"), "--group=format");
138 default:
139 BUG("unhandled shortlog group");
140 }
@@ -203,6 +205,32 @@ static void insert_records_from_trailers(struct shortlog *log,
205 unuse_commit_buffer(commit, commit_buffer);
206 }
207
208 +static int shortlog_needs_dedup(const struct shortlog *log)
209 +{
210 + return HAS_MULTI_BITS(log->groups) || log->format.nr > 1 || log->trailers.nr;
211 +}
212 +
213 +static void insert_records_from_format(struct shortlog *log,
214 + struct strset *dups,
215 + struct commit *commit,
216 + struct pretty_print_context *ctx,
217 + const char *oneline)
218 +{
219 + struct strbuf buf = STRBUF_INIT;
220 + struct string_list_item *item;
221 +
222 + for_each_string_list_item(item, &log->format) {
223 + strbuf_reset(&buf);
224 +
225 + format_commit_message(commit, item->string, &buf, ctx);
226 +
227 + if (!shortlog_needs_dedup(log) || strset_add(dups, buf.buf))
228 + insert_one_record(log, buf.buf, oneline);
229 + }
230 +
231 + strbuf_release(&buf);
232 +}
233 +
234 void shortlog_add_commit(struct shortlog *log, struct commit *commit)
235 {
236 struct strbuf ident = STRBUF_INIT;
@@ -244,6 +272,7 @@ void shortlog_add_commit(struct shortlog *log, struct commit *commit)
272 insert_one_record(log, ident.buf, oneline_str);
273 }
274 insert_records_from_trailers(log, &dups, commit, &ctx, oneline_str);
275 + insert_records_from_format(log, &dups, commit, &ctx, oneline_str);
276
277 strset_clear(&dups);
278 strbuf_release(&ident);
@@ -315,6 +344,7 @@ static int parse_group_option(const struct option *opt, const char *arg, int uns
344 if (unset) {
345 log->groups = 0;
346 string_list_clear(&log->trailers, 0);
347 + string_list_clear(&log->format, 0);
348 } else if (!strcasecmp(arg, "author"))
349 log->groups |= SHORTLOG_GROUP_AUTHOR;
350 else if (!strcasecmp(arg, "committer"))
@@ -322,8 +352,15 @@ static int parse_group_option(const struct option *opt, const char *arg, int uns
352 else if (skip_prefix(arg, "trailer:", &field)) {
353 log->groups |= SHORTLOG_GROUP_TRAILER;
354 string_list_append(&log->trailers, field);
325 - } else
355 + } else if (skip_prefix(arg, "format:", &field)) {
356 + log->groups |= SHORTLOG_GROUP_FORMAT;
357 + string_list_append(&log->format, field);
358 + } else if (strchr(arg, '%')) {
359 + log->groups |= SHORTLOG_GROUP_FORMAT;
360 + string_list_append(&log->format, arg);
361 + } else {
362 return error(_("unknown group type: %s"), arg);
363 + }
364
365 return 0;
366 }
@@ -341,6 +378,7 @@ void shortlog_init(struct shortlog *log)
378 log->in2 = DEFAULT_INDENT2;
379 log->trailers.strdup_strings = 1;
380 log->trailers.cmp = strcasecmp;
381 + log->format.strdup_strings = 1;
382 }
383
384 int cmd_shortlog(int argc, const char **argv, const char *prefix)
@@ -481,4 +519,5 @@ void shortlog_output(struct shortlog *log)
519 log->list.strdup_strings = 1;
520 string_list_clear(&log->list, 1);
521 clear_mailmap(&log->mailmap);
522 + string_list_clear(&log->format, 0);
523 }
shortlog.h
+2
@@ -22,8 +22,10 @@ struct shortlog {
22 SHORTLOG_GROUP_AUTHOR = (1 << 0),
23 SHORTLOG_GROUP_COMMITTER = (1 << 1),
24 SHORTLOG_GROUP_TRAILER = (1 << 2),
25 + SHORTLOG_GROUP_FORMAT = (1 << 3),
26 } groups;
27 struct string_list trailers;
28 + struct string_list format;
29
30 int email;
31 struct string_list mailmap;
t/t4201-shortlog.sh
+32
@@ -244,6 +244,26 @@ test_expect_success 'shortlog --group=trailer:signed-off-by' '
244 test_cmp expect actual
245 '
246
247 +test_expect_success 'shortlog --group=format' '
248 + git shortlog -s --date="format:%Y" --group="format:%cN (%cd)" \
249 + HEAD >actual &&
250 + cat >expect <<-\EOF &&
251 + 4 C O Mitter (2005)
252 + 1 Sin Nombre (2005)
253 + EOF
254 + test_cmp expect actual
255 +'
256 +
257 +test_expect_success 'shortlog --group=<format> DWIM' '
258 + git shortlog -s --date="format:%Y" --group="%cN (%cd)" HEAD >actual &&
259 + test_cmp expect actual
260 +'
261 +
262 +test_expect_success 'shortlog bogus --group' '
263 + test_must_fail git shortlog --group=bogus HEAD 2>err &&
264 + grep "unknown group type" err
265 +'
266 +
267 test_expect_success 'trailer idents are split' '
268 cat >expect <<-\EOF &&
269 2 C O Mitter
@@ -326,6 +346,18 @@ test_expect_success 'shortlog can match multiple groups' '
346 test_cmp expect actual
347 '
348
349 +test_expect_success 'shortlog can match multiple format groups' '
350 + GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" \
351 + git commit --allow-empty -m "identical names" &&
352 + test_tick &&
353 + cat >expect <<-\EOF &&
354 + 2 A U Thor
355 + 1 C O Mitter
356 + EOF
357 + git shortlog -ns --group="%cn" --group="%an" -2 HEAD >actual &&
358 + test_cmp expect actual
359 +'
360 +
361 test_expect_success 'set up option selection tests' '
362 git commit --allow-empty -F - <<-\EOF
363 subject