Skip to content

Commit 2fa8aac

Browse files
committed
Merge branch 'jk/shortlog-group-by-trailer'
"git shortlog" has been taught to group commits by the contents of the trailer lines, like "Reviewed-by:", "Coauthored-by:", etc. * jk/shortlog-group-by-trailer: shortlog: allow multiple groups to be specified shortlog: parse trailer idents shortlog: rename parse_stdin_ident() shortlog: de-duplicate trailer values shortlog: match commit trailers with --group trailer: add interface for iterating over commit trailers shortlog: add grouping option shortlog: change "author" variables to "ident"
2 parents ea1f611 + 63d24fa commit 2fa8aac

File tree

7 files changed

+444
-31
lines changed

7 files changed

+444
-31
lines changed

Documentation/git-shortlog.txt

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,38 @@ OPTIONS
4747

4848
Each pretty-printed commit will be rewrapped before it is shown.
4949

50+
--group=<type>::
51+
Group commits based on `<type>`. If no `--group` option is
52+
specified, the default is `author`. `<type>` is one of:
53+
+
54+
--
55+
- `author`, commits are grouped by author
56+
- `committer`, commits are grouped by committer (the same as `-c`)
57+
- `trailer:<field>`, the `<field>` is interpreted as a case-insensitive
58+
commit message trailer (see linkgit:git-interpret-trailers[1]). For
59+
example, if your project uses `Reviewed-by` trailers, you might want
60+
to see who has been reviewing with
61+
`git shortlog -ns --group=trailer:reviewed-by`.
62+
+
63+
Note that commits that do not include the trailer will not be counted.
64+
Likewise, commits with multiple trailers (e.g., multiple signoffs) may
65+
be counted more than once (but only once per unique trailer value in
66+
that commit).
67+
+
68+
Shortlog will attempt to parse each trailer value as a `name <email>`
69+
identity. If successful, the mailmap is applied and the email is omitted
70+
unless the `--email` option is specified. If the value cannot be parsed
71+
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.
78+
5079
-c::
5180
--committer::
52-
Collect and show committer identities instead of authors.
81+
This is an alias for `--group=committer`.
5382

5483
-w[<width>[,<indent1>[,<indent2>]]]::
5584
Linewrap the output by wrapping each line at `width`. The first

builtin/log.c

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

builtin/shortlog.c

Lines changed: 186 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "mailmap.h"
1010
#include "shortlog.h"
1111
#include "parse-options.h"
12+
#include "trailer.h"
1213

1314
static char const * const shortlog_usage[] = {
1415
N_("git shortlog [<options>] [<revision-range>] [[--] <path>...]"),
@@ -49,12 +50,12 @@ static int compare_by_list(const void *a1, const void *a2)
4950
}
5051

5152
static void insert_one_record(struct shortlog *log,
52-
const char *author,
53+
const char *ident,
5354
const char *oneline)
5455
{
5556
struct string_list_item *item;
5657

57-
item = string_list_insert(&log->list, author);
58+
item = string_list_insert(&log->list, ident);
5859

5960
if (log->summary)
6061
item->util = (void *)(UTIL_TO_INT(item) + 1);
@@ -97,8 +98,8 @@ static void insert_one_record(struct shortlog *log,
9798
}
9899
}
99100

100-
static int parse_stdin_author(struct shortlog *log,
101-
struct strbuf *out, const char *in)
101+
static int parse_ident(struct shortlog *log,
102+
struct strbuf *out, const char *in)
102103
{
103104
const char *mailbuf, *namebuf;
104105
size_t namelen, maillen;
@@ -122,18 +123,33 @@ static int parse_stdin_author(struct shortlog *log,
122123

123124
static void read_from_stdin(struct shortlog *log)
124125
{
125-
struct strbuf author = STRBUF_INIT;
126-
struct strbuf mapped_author = STRBUF_INIT;
126+
struct strbuf ident = STRBUF_INIT;
127+
struct strbuf mapped_ident = STRBUF_INIT;
127128
struct strbuf oneline = STRBUF_INIT;
128129
static const char *author_match[2] = { "Author: ", "author " };
129130
static const char *committer_match[2] = { "Commit: ", "committer " };
130131
const char **match;
131132

132-
match = log->committer ? committer_match : author_match;
133-
while (strbuf_getline_lf(&author, stdin) != EOF) {
133+
if (HAS_MULTI_BITS(log->groups))
134+
die(_("using multiple --group options with stdin is not supported"));
135+
136+
switch (log->groups) {
137+
case SHORTLOG_GROUP_AUTHOR:
138+
match = author_match;
139+
break;
140+
case SHORTLOG_GROUP_COMMITTER:
141+
match = committer_match;
142+
break;
143+
case SHORTLOG_GROUP_TRAILER:
144+
die(_("using --group=trailer with stdin is not supported"));
145+
default:
146+
BUG("unhandled shortlog group");
147+
}
148+
149+
while (strbuf_getline_lf(&ident, stdin) != EOF) {
134150
const char *v;
135-
if (!skip_prefix(author.buf, match[0], &v) &&
136-
!skip_prefix(author.buf, match[1], &v))
151+
if (!skip_prefix(ident.buf, match[0], &v) &&
152+
!skip_prefix(ident.buf, match[1], &v))
137153
continue;
138154
while (strbuf_getline_lf(&oneline, stdin) != EOF &&
139155
oneline.len)
@@ -142,45 +158,157 @@ static void read_from_stdin(struct shortlog *log)
142158
!oneline.len)
143159
; /* discard blanks */
144160

145-
strbuf_reset(&mapped_author);
146-
if (parse_stdin_author(log, &mapped_author, v) < 0)
161+
strbuf_reset(&mapped_ident);
162+
if (parse_ident(log, &mapped_ident, v) < 0)
147163
continue;
148164

149-
insert_one_record(log, mapped_author.buf, oneline.buf);
165+
insert_one_record(log, mapped_ident.buf, oneline.buf);
150166
}
151-
strbuf_release(&author);
152-
strbuf_release(&mapped_author);
167+
strbuf_release(&ident);
168+
strbuf_release(&mapped_ident);
153169
strbuf_release(&oneline);
154170
}
155171

172+
struct strset_item {
173+
struct hashmap_entry ent;
174+
char value[FLEX_ARRAY];
175+
};
176+
177+
struct strset {
178+
struct hashmap map;
179+
};
180+
181+
#define STRSET_INIT { { NULL } }
182+
183+
static int strset_item_hashcmp(const void *hash_data,
184+
const struct hashmap_entry *entry,
185+
const struct hashmap_entry *entry_or_key,
186+
const void *keydata)
187+
{
188+
const struct strset_item *a, *b;
189+
190+
a = container_of(entry, const struct strset_item, ent);
191+
if (keydata)
192+
return strcmp(a->value, keydata);
193+
194+
b = container_of(entry_or_key, const struct strset_item, ent);
195+
return strcmp(a->value, b->value);
196+
}
197+
198+
/*
199+
* Adds "str" to the set if it was not already present; returns true if it was
200+
* already there.
201+
*/
202+
static int strset_check_and_add(struct strset *ss, const char *str)
203+
{
204+
unsigned int hash = strhash(str);
205+
struct strset_item *item;
206+
207+
if (!ss->map.table)
208+
hashmap_init(&ss->map, strset_item_hashcmp, NULL, 0);
209+
210+
if (hashmap_get_from_hash(&ss->map, hash, str))
211+
return 1;
212+
213+
FLEX_ALLOC_STR(item, value, str);
214+
hashmap_entry_init(&item->ent, hash);
215+
hashmap_add(&ss->map, &item->ent);
216+
return 0;
217+
}
218+
219+
static void strset_clear(struct strset *ss)
220+
{
221+
if (!ss->map.table)
222+
return;
223+
hashmap_free_entries(&ss->map, struct strset_item, ent);
224+
}
225+
226+
static void insert_records_from_trailers(struct shortlog *log,
227+
struct strset *dups,
228+
struct commit *commit,
229+
struct pretty_print_context *ctx,
230+
const char *oneline)
231+
{
232+
struct trailer_iterator iter;
233+
const char *commit_buffer, *body;
234+
struct strbuf ident = STRBUF_INIT;
235+
236+
/*
237+
* Using format_commit_message("%B") would be simpler here, but
238+
* this saves us copying the message.
239+
*/
240+
commit_buffer = logmsg_reencode(commit, NULL, ctx->output_encoding);
241+
body = strstr(commit_buffer, "\n\n");
242+
if (!body)
243+
return;
244+
245+
trailer_iterator_init(&iter, body);
246+
while (trailer_iterator_advance(&iter)) {
247+
const char *value = iter.val.buf;
248+
249+
if (!string_list_has_string(&log->trailers, iter.key.buf))
250+
continue;
251+
252+
strbuf_reset(&ident);
253+
if (!parse_ident(log, &ident, value))
254+
value = ident.buf;
255+
256+
if (strset_check_and_add(dups, value))
257+
continue;
258+
insert_one_record(log, value, oneline);
259+
}
260+
trailer_iterator_release(&iter);
261+
262+
strbuf_release(&ident);
263+
unuse_commit_buffer(commit, commit_buffer);
264+
}
265+
156266
void shortlog_add_commit(struct shortlog *log, struct commit *commit)
157267
{
158-
struct strbuf author = STRBUF_INIT;
268+
struct strbuf ident = STRBUF_INIT;
159269
struct strbuf oneline = STRBUF_INIT;
270+
struct strset dups = STRSET_INIT;
160271
struct pretty_print_context ctx = {0};
161-
const char *fmt;
272+
const char *oneline_str;
162273

163274
ctx.fmt = CMIT_FMT_USERFORMAT;
164275
ctx.abbrev = log->abbrev;
165276
ctx.print_email_subject = 1;
166277
ctx.date_mode.type = DATE_NORMAL;
167278
ctx.output_encoding = get_log_output_encoding();
168279

169-
fmt = log->committer ?
170-
(log->email ? "%cN <%cE>" : "%cN") :
171-
(log->email ? "%aN <%aE>" : "%aN");
172-
173-
format_commit_message(commit, fmt, &author, &ctx);
174280
if (!log->summary) {
175281
if (log->user_format)
176282
pretty_print_commit(&ctx, commit, &oneline);
177283
else
178284
format_commit_message(commit, "%s", &oneline, &ctx);
179285
}
286+
oneline_str = oneline.len ? oneline.buf : "<none>";
287+
288+
if (log->groups & SHORTLOG_GROUP_AUTHOR) {
289+
strbuf_reset(&ident);
290+
format_commit_message(commit,
291+
log->email ? "%aN <%aE>" : "%aN",
292+
&ident, &ctx);
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);
299+
format_commit_message(commit,
300+
log->email ? "%cN <%cE>" : "%cN",
301+
&ident, &ctx);
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);
308+
}
180309

181-
insert_one_record(log, author.buf, oneline.len ? oneline.buf : "<none>");
182-
183-
strbuf_release(&author);
310+
strset_clear(&dups);
311+
strbuf_release(&ident);
184312
strbuf_release(&oneline);
185313
}
186314

@@ -241,6 +369,28 @@ static int parse_wrap_args(const struct option *opt, const char *arg, int unset)
241369
return 0;
242370
}
243371

372+
static int parse_group_option(const struct option *opt, const char *arg, int unset)
373+
{
374+
struct shortlog *log = opt->value;
375+
const char *field;
376+
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;
382+
else if (!strcasecmp(arg, "committer"))
383+
log->groups |= SHORTLOG_GROUP_COMMITTER;
384+
else if (skip_prefix(arg, "trailer:", &field)) {
385+
log->groups |= SHORTLOG_GROUP_TRAILER;
386+
string_list_append(&log->trailers, field);
387+
} else
388+
return error(_("unknown group type: %s"), arg);
389+
390+
return 0;
391+
}
392+
393+
244394
void shortlog_init(struct shortlog *log)
245395
{
246396
memset(log, 0, sizeof(*log));
@@ -251,6 +401,8 @@ void shortlog_init(struct shortlog *log)
251401
log->wrap = DEFAULT_WRAPLEN;
252402
log->in1 = DEFAULT_INDENT1;
253403
log->in2 = DEFAULT_INDENT2;
404+
log->trailers.strdup_strings = 1;
405+
log->trailers.cmp = strcasecmp;
254406
}
255407

256408
int cmd_shortlog(int argc, const char **argv, const char *prefix)
@@ -260,8 +412,9 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)
260412
int nongit = !startup_info->have_repository;
261413

262414
const struct option options[] = {
263-
OPT_BOOL('c', "committer", &log.committer,
264-
N_("Group by committer rather than author")),
415+
OPT_BIT('c', "committer", &log.groups,
416+
N_("Group by committer rather than author"),
417+
SHORTLOG_GROUP_COMMITTER),
265418
OPT_BOOL('n', "numbered", &log.sort_by_number,
266419
N_("sort output according to the number of commits per author")),
267420
OPT_BOOL('s', "summary", &log.summary,
@@ -271,6 +424,8 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)
271424
OPT_CALLBACK_F('w', NULL, &log, N_("<w>[,<i1>[,<i2>]]"),
272425
N_("Linewrap output"), PARSE_OPT_OPTARG,
273426
&parse_wrap_args),
427+
OPT_CALLBACK(0, "group", &log, N_("field"),
428+
N_("Group by field"), parse_group_option),
274429
OPT_END(),
275430
};
276431

@@ -311,6 +466,10 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)
311466
log.abbrev = rev.abbrev;
312467
log.file = rev.diffopt.file;
313468

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

shortlog.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ struct shortlog {
1515
int in2;
1616
int user_format;
1717
int abbrev;
18-
int committer;
18+
19+
enum {
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;
1925

2026
char *common_repo_prefix;
2127
int email;

0 commit comments

Comments
 (0)