Skip to content

Commit 63d24fa

Browse files
peffgitster
authored andcommitted
shortlog: allow multiple groups to be specified
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 <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 56d5dde commit 63d24fa

File tree

5 files changed

+127
-29
lines changed

5 files changed

+127
-29
lines changed

Documentation/git-shortlog.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ OPTIONS
5151
Group commits based on `<type>`. If no `--group` option is
5252
specified, the default is `author`. `<type>` is one of:
5353
+
54+
--
5455
- `author`, commits are grouped by author
5556
- `committer`, commits are grouped by committer (the same as `-c`)
5657
- `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>`
6869
identity. If successful, the mailmap is applied and the email is omitted
6970
unless the `--email` option is specified. If the value cannot be parsed
7071
as an identity, it will be taken literally and completely.
72+
--
73+
+
74+
If `--group` is specified multiple times, commits are counted under each
75+
value (but again, only once per unique value in that commit). For
76+
example, `git shortlog --group=author --group=trailer:co-authored-by`
77+
counts both authors and co-authors.
7178

7279
-c::
7380
--committer::

builtin/log.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,7 @@ static void make_cover_letter(struct rev_info *rev, int use_stdout,
11971197
log.in1 = 2;
11981198
log.in2 = 4;
11991199
log.file = rev->diffopt.file;
1200+
log.groups = SHORTLOG_GROUP_AUTHOR;
12001201
for (i = 0; i < nr; i++)
12011202
shortlog_add_commit(&log, list[i]);
12021203

builtin/shortlog.c

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,10 @@ static void read_from_stdin(struct shortlog *log)
130130
static const char *committer_match[2] = { "Commit: ", "committer " };
131131
const char **match;
132132

133-
switch (log->group) {
133+
if (HAS_MULTI_BITS(log->groups))
134+
die(_("using multiple --group options with stdin is not supported"));
135+
136+
switch (log->groups) {
134137
case SHORTLOG_GROUP_AUTHOR:
135138
match = author_match;
136139
break;
@@ -221,13 +224,13 @@ static void strset_clear(struct strset *ss)
221224
}
222225

223226
static void insert_records_from_trailers(struct shortlog *log,
227+
struct strset *dups,
224228
struct commit *commit,
225229
struct pretty_print_context *ctx,
226230
const char *oneline)
227231
{
228232
struct trailer_iterator iter;
229233
const char *commit_buffer, *body;
230-
struct strset dups = STRSET_INIT;
231234
struct strbuf ident = STRBUF_INIT;
232235

233236
/*
@@ -243,28 +246,28 @@ static void insert_records_from_trailers(struct shortlog *log,
243246
while (trailer_iterator_advance(&iter)) {
244247
const char *value = iter.val.buf;
245248

246-
if (strcasecmp(iter.key.buf, log->trailer))
249+
if (!string_list_has_string(&log->trailers, iter.key.buf))
247250
continue;
248251

249252
strbuf_reset(&ident);
250253
if (!parse_ident(log, &ident, value))
251254
value = ident.buf;
252255

253-
if (strset_check_and_add(&dups, value))
256+
if (strset_check_and_add(dups, value))
254257
continue;
255258
insert_one_record(log, value, oneline);
256259
}
257260
trailer_iterator_release(&iter);
258261

259262
strbuf_release(&ident);
260-
strset_clear(&dups);
261263
unuse_commit_buffer(commit, commit_buffer);
262264
}
263265

264266
void shortlog_add_commit(struct shortlog *log, struct commit *commit)
265267
{
266268
struct strbuf ident = STRBUF_INIT;
267269
struct strbuf oneline = STRBUF_INIT;
270+
struct strset dups = STRSET_INIT;
268271
struct pretty_print_context ctx = {0};
269272
const char *oneline_str;
270273

@@ -282,24 +285,29 @@ void shortlog_add_commit(struct shortlog *log, struct commit *commit)
282285
}
283286
oneline_str = oneline.len ? oneline.buf : "<none>";
284287

285-
switch (log->group) {
286-
case SHORTLOG_GROUP_AUTHOR:
288+
if (log->groups & SHORTLOG_GROUP_AUTHOR) {
289+
strbuf_reset(&ident);
287290
format_commit_message(commit,
288291
log->email ? "%aN <%aE>" : "%aN",
289292
&ident, &ctx);
290-
insert_one_record(log, ident.buf, oneline_str);
291-
break;
292-
case SHORTLOG_GROUP_COMMITTER:
293+
if (!HAS_MULTI_BITS(log->groups) ||
294+
!strset_check_and_add(&dups, ident.buf))
295+
insert_one_record(log, ident.buf, oneline_str);
296+
}
297+
if (log->groups & SHORTLOG_GROUP_COMMITTER) {
298+
strbuf_reset(&ident);
293299
format_commit_message(commit,
294300
log->email ? "%cN <%cE>" : "%cN",
295301
&ident, &ctx);
296-
insert_one_record(log, ident.buf, oneline_str);
297-
break;
298-
case SHORTLOG_GROUP_TRAILER:
299-
insert_records_from_trailers(log, commit, &ctx, oneline_str);
300-
break;
302+
if (!HAS_MULTI_BITS(log->groups) ||
303+
!strset_check_and_add(&dups, ident.buf))
304+
insert_one_record(log, ident.buf, oneline_str);
305+
}
306+
if (log->groups & SHORTLOG_GROUP_TRAILER) {
307+
insert_records_from_trailers(log, &dups, commit, &ctx, oneline_str);
301308
}
302309

310+
strset_clear(&dups);
303311
strbuf_release(&ident);
304312
strbuf_release(&oneline);
305313
}
@@ -366,14 +374,16 @@ static int parse_group_option(const struct option *opt, const char *arg, int uns
366374
struct shortlog *log = opt->value;
367375
const char *field;
368376

369-
if (unset || !strcasecmp(arg, "author"))
370-
log->group = SHORTLOG_GROUP_AUTHOR;
377+
if (unset) {
378+
log->groups = 0;
379+
string_list_clear(&log->trailers, 0);
380+
} else if (!strcasecmp(arg, "author"))
381+
log->groups |= SHORTLOG_GROUP_AUTHOR;
371382
else if (!strcasecmp(arg, "committer"))
372-
log->group = SHORTLOG_GROUP_COMMITTER;
383+
log->groups |= SHORTLOG_GROUP_COMMITTER;
373384
else if (skip_prefix(arg, "trailer:", &field)) {
374-
log->group = SHORTLOG_GROUP_TRAILER;
375-
free(log->trailer);
376-
log->trailer = xstrdup(field);
385+
log->groups |= SHORTLOG_GROUP_TRAILER;
386+
string_list_append(&log->trailers, field);
377387
} else
378388
return error(_("unknown group type: %s"), arg);
379389

@@ -391,6 +401,8 @@ void shortlog_init(struct shortlog *log)
391401
log->wrap = DEFAULT_WRAPLEN;
392402
log->in1 = DEFAULT_INDENT1;
393403
log->in2 = DEFAULT_INDENT2;
404+
log->trailers.strdup_strings = 1;
405+
log->trailers.cmp = strcasecmp;
394406
}
395407

396408
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)
400412
int nongit = !startup_info->have_repository;
401413

402414
const struct option options[] = {
403-
OPT_SET_INT('c', "committer", &log.group,
404-
N_("Group by committer rather than author"),
405-
SHORTLOG_GROUP_COMMITTER),
415+
OPT_BIT('c', "committer", &log.groups,
416+
N_("Group by committer rather than author"),
417+
SHORTLOG_GROUP_COMMITTER),
406418
OPT_BOOL('n', "numbered", &log.sort_by_number,
407419
N_("sort output according to the number of commits per author")),
408420
OPT_BOOL('s', "summary", &log.summary,
@@ -454,6 +466,10 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)
454466
log.abbrev = rev.abbrev;
455467
log.file = rev.diffopt.file;
456468

469+
if (!log.groups)
470+
log.groups = SHORTLOG_GROUP_AUTHOR;
471+
string_list_sort(&log.trailers);
472+
457473
/* assume HEAD if from a tty */
458474
if (!nongit && !rev.pending.nr && isatty(0))
459475
add_head_to_pending(&rev);

shortlog.h

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ struct shortlog {
1717
int abbrev;
1818

1919
enum {
20-
SHORTLOG_GROUP_AUTHOR = 0,
21-
SHORTLOG_GROUP_COMMITTER,
22-
SHORTLOG_GROUP_TRAILER,
23-
} group;
24-
char *trailer;
20+
SHORTLOG_GROUP_AUTHOR = (1 << 0),
21+
SHORTLOG_GROUP_COMMITTER = (1 << 1),
22+
SHORTLOG_GROUP_TRAILER = (1 << 2),
23+
} groups;
24+
struct string_list trailers;
2525

2626
char *common_repo_prefix;
2727
int email;

t/t4201-shortlog.sh

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,4 +282,78 @@ test_expect_success 'shortlog de-duplicates trailers in a single commit' '
282282
test_cmp expect actual
283283
'
284284

285+
test_expect_success 'shortlog can match multiple groups' '
286+
git commit --allow-empty -F - <<-\EOF &&
287+
subject one
288+
289+
this has two trailers that are distinct from the author; it will count
290+
3 times in the output
291+
292+
Some-trailer: User A <[email protected]>
293+
Another-trailer: User B <[email protected]>
294+
EOF
295+
296+
git commit --allow-empty -F - <<-\EOF &&
297+
subject two
298+
299+
this one has two trailers, one of which is a duplicate with the author;
300+
it will only be counted once for them
301+
302+
Another-trailer: A U Thor <[email protected]>
303+
Some-trailer: User B <[email protected]>
304+
EOF
305+
306+
cat >expect <<-\EOF &&
307+
2 A U Thor
308+
2 User B
309+
1 User A
310+
EOF
311+
git shortlog -ns \
312+
--group=author \
313+
--group=trailer:some-trailer \
314+
--group=trailer:another-trailer \
315+
-2 HEAD >actual &&
316+
test_cmp expect actual
317+
'
318+
319+
test_expect_success 'set up option selection tests' '
320+
git commit --allow-empty -F - <<-\EOF
321+
subject
322+
323+
body
324+
325+
Trailer-one: value-one
326+
Trailer-two: value-two
327+
EOF
328+
'
329+
330+
test_expect_success '--no-group resets group list to author' '
331+
cat >expect <<-\EOF &&
332+
1 A U Thor
333+
EOF
334+
git shortlog -ns \
335+
--group=committer \
336+
--group=trailer:trailer-one \
337+
--no-group \
338+
-1 HEAD >actual &&
339+
test_cmp expect actual
340+
'
341+
342+
test_expect_success '--no-group resets trailer list' '
343+
cat >expect <<-\EOF &&
344+
1 value-two
345+
EOF
346+
git shortlog -ns \
347+
--group=trailer:trailer-one \
348+
--no-group \
349+
--group=trailer:trailer-two \
350+
-1 HEAD >actual &&
351+
test_cmp expect actual
352+
'
353+
354+
test_expect_success 'stdin with multiple groups reports error' '
355+
git log >log &&
356+
test_must_fail git shortlog --group=author --group=committer <log
357+
'
358+
285359
test_done

0 commit comments

Comments
 (0)