Merge branch 'tb/shortlog-group'

"git shortlog" learned to group by the "format" string.

* tb/shortlog-group:
  shortlog: implement `--group=committer` in terms of `--group=<format>`
  shortlog: implement `--group=author` in terms of `--group=<format>`
  shortlog: extract `shortlog_finish_setup()`
  shortlog: support arbitrary commit format `--group`s
  shortlog: extract `--group` fragment for translation
  shortlog: make trailer insertion a noop when appropriate
  shortlog: accept `--date`-related options
This commit is contained in:
Taylor Blau 2022-10-30 21:04:42 -04:00
commit c112d8d9c2
5 changed files with 113 additions and 27 deletions

View file

@ -47,6 +47,11 @@ OPTIONS
Each pretty-printed commit will be rewrapped before it is shown. Each pretty-printed commit will be rewrapped before it is shown.
--date=<format>::
Show dates formatted according to the given date string. (See
the `--date` option in the "Commit Formatting" section of
linkgit:git-log[1]). Useful with `--group=format:<format>`.
--group=<type>:: --group=<type>::
Group commits based on `<type>`. If no `--group` option is Group commits based on `<type>`. If no `--group` option is
specified, the default is `author`. `<type>` is one of: specified, the default is `author`. `<type>` is one of:
@ -59,6 +64,9 @@ OPTIONS
example, if your project uses `Reviewed-by` trailers, you might want example, if your project uses `Reviewed-by` trailers, you might want
to see who has been reviewing with to see who has been reviewing with
`git shortlog -ns --group=trailer:reviewed-by`. `git shortlog -ns --group=trailer:reviewed-by`.
- `format:<format>`, any string accepted by the `--format` option of
'git log'. (See the "PRETTY FORMATS" section of
linkgit:git-log[1].)
+ +
Note that commits that do not include the trailer will not be counted. Note that commits that do not include the trailer will not be counted.
Likewise, commits with multiple trailers (e.g., multiple signoffs) may Likewise, commits with multiple trailers (e.g., multiple signoffs) may

View file

@ -1334,6 +1334,7 @@ static void make_cover_letter(struct rev_info *rev, int use_separate_file,
log.in2 = 4; log.in2 = 4;
log.file = rev->diffopt.file; log.file = rev->diffopt.file;
log.groups = SHORTLOG_GROUP_AUTHOR; log.groups = SHORTLOG_GROUP_AUTHOR;
shortlog_finish_setup(&log);
for (i = 0; i < nr; i++) for (i = 0; i < nr; i++)
shortlog_add_commit(&log, list[i]); shortlog_add_commit(&log, list[i]);

View file

@ -132,7 +132,9 @@ static void read_from_stdin(struct shortlog *log)
match = committer_match; match = committer_match;
break; break;
case SHORTLOG_GROUP_TRAILER: case SHORTLOG_GROUP_TRAILER:
die(_("using --group=trailer with stdin is not supported")); die(_("using %s with stdin is not supported"), "--group=trailer");
case SHORTLOG_GROUP_FORMAT:
die(_("using %s with stdin is not supported"), "--group=format");
default: default:
BUG("unhandled shortlog group"); BUG("unhandled shortlog group");
} }
@ -170,6 +172,9 @@ static void insert_records_from_trailers(struct shortlog *log,
const char *commit_buffer, *body; const char *commit_buffer, *body;
struct strbuf ident = STRBUF_INIT; struct strbuf ident = STRBUF_INIT;
if (!log->trailers.nr)
return;
/* /*
* Using format_commit_message("%B") would be simpler here, but * Using format_commit_message("%B") would be simpler here, but
* this saves us copying the message. * this saves us copying the message.
@ -200,9 +205,34 @@ static void insert_records_from_trailers(struct shortlog *log,
unuse_commit_buffer(commit, commit_buffer); unuse_commit_buffer(commit, commit_buffer);
} }
static int shortlog_needs_dedup(const struct shortlog *log)
{
return HAS_MULTI_BITS(log->groups) || log->format.nr > 1 || log->trailers.nr;
}
static void insert_records_from_format(struct shortlog *log,
struct strset *dups,
struct commit *commit,
struct pretty_print_context *ctx,
const char *oneline)
{
struct strbuf buf = STRBUF_INIT;
struct string_list_item *item;
for_each_string_list_item(item, &log->format) {
strbuf_reset(&buf);
format_commit_message(commit, item->string, &buf, ctx);
if (!shortlog_needs_dedup(log) || strset_add(dups, buf.buf))
insert_one_record(log, buf.buf, oneline);
}
strbuf_release(&buf);
}
void shortlog_add_commit(struct shortlog *log, struct commit *commit) void shortlog_add_commit(struct shortlog *log, struct commit *commit)
{ {
struct strbuf ident = STRBUF_INIT;
struct strbuf oneline = STRBUF_INIT; struct strbuf oneline = STRBUF_INIT;
struct strset dups = STRSET_INIT; struct strset dups = STRSET_INIT;
struct pretty_print_context ctx = {0}; struct pretty_print_context ctx = {0};
@ -211,7 +241,7 @@ void shortlog_add_commit(struct shortlog *log, struct commit *commit)
ctx.fmt = CMIT_FMT_USERFORMAT; ctx.fmt = CMIT_FMT_USERFORMAT;
ctx.abbrev = log->abbrev; ctx.abbrev = log->abbrev;
ctx.print_email_subject = 1; ctx.print_email_subject = 1;
ctx.date_mode.type = DATE_NORMAL; ctx.date_mode = log->date_mode;
ctx.output_encoding = get_log_output_encoding(); ctx.output_encoding = get_log_output_encoding();
if (!log->summary) { if (!log->summary) {
@ -222,30 +252,10 @@ void shortlog_add_commit(struct shortlog *log, struct commit *commit)
} }
oneline_str = oneline.len ? oneline.buf : "<none>"; oneline_str = oneline.len ? oneline.buf : "<none>";
if (log->groups & SHORTLOG_GROUP_AUTHOR) { insert_records_from_trailers(log, &dups, commit, &ctx, oneline_str);
strbuf_reset(&ident); insert_records_from_format(log, &dups, commit, &ctx, oneline_str);
format_commit_message(commit,
log->email ? "%aN <%aE>" : "%aN",
&ident, &ctx);
if (!HAS_MULTI_BITS(log->groups) ||
strset_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);
if (!HAS_MULTI_BITS(log->groups) ||
strset_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); strset_clear(&dups);
strbuf_release(&ident);
strbuf_release(&oneline); strbuf_release(&oneline);
} }
@ -314,6 +324,7 @@ static int parse_group_option(const struct option *opt, const char *arg, int uns
if (unset) { if (unset) {
log->groups = 0; log->groups = 0;
string_list_clear(&log->trailers, 0); string_list_clear(&log->trailers, 0);
string_list_clear(&log->format, 0);
} else if (!strcasecmp(arg, "author")) } else if (!strcasecmp(arg, "author"))
log->groups |= SHORTLOG_GROUP_AUTHOR; log->groups |= SHORTLOG_GROUP_AUTHOR;
else if (!strcasecmp(arg, "committer")) else if (!strcasecmp(arg, "committer"))
@ -321,8 +332,15 @@ static int parse_group_option(const struct option *opt, const char *arg, int uns
else if (skip_prefix(arg, "trailer:", &field)) { else if (skip_prefix(arg, "trailer:", &field)) {
log->groups |= SHORTLOG_GROUP_TRAILER; log->groups |= SHORTLOG_GROUP_TRAILER;
string_list_append(&log->trailers, field); string_list_append(&log->trailers, field);
} else } else if (skip_prefix(arg, "format:", &field)) {
log->groups |= SHORTLOG_GROUP_FORMAT;
string_list_append(&log->format, field);
} else if (strchr(arg, '%')) {
log->groups |= SHORTLOG_GROUP_FORMAT;
string_list_append(&log->format, arg);
} else {
return error(_("unknown group type: %s"), arg); return error(_("unknown group type: %s"), arg);
}
return 0; return 0;
} }
@ -340,6 +358,19 @@ void shortlog_init(struct shortlog *log)
log->in2 = DEFAULT_INDENT2; log->in2 = DEFAULT_INDENT2;
log->trailers.strdup_strings = 1; log->trailers.strdup_strings = 1;
log->trailers.cmp = strcasecmp; log->trailers.cmp = strcasecmp;
log->format.strdup_strings = 1;
}
void shortlog_finish_setup(struct shortlog *log)
{
if (log->groups & SHORTLOG_GROUP_AUTHOR)
string_list_append(&log->format,
log->email ? "%aN <%aE>" : "%aN");
if (log->groups & SHORTLOG_GROUP_COMMITTER)
string_list_append(&log->format,
log->email ? "%cN <%cE>" : "%cN");
string_list_sort(&log->trailers);
} }
int cmd_shortlog(int argc, const char **argv, const char *prefix) int cmd_shortlog(int argc, const char **argv, const char *prefix)
@ -407,10 +438,11 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)
log.user_format = rev.commit_format == CMIT_FMT_USERFORMAT; log.user_format = rev.commit_format == CMIT_FMT_USERFORMAT;
log.abbrev = rev.abbrev; log.abbrev = rev.abbrev;
log.file = rev.diffopt.file; log.file = rev.diffopt.file;
log.date_mode = rev.date_mode;
if (!log.groups) if (!log.groups)
log.groups = SHORTLOG_GROUP_AUTHOR; log.groups = SHORTLOG_GROUP_AUTHOR;
string_list_sort(&log.trailers); shortlog_finish_setup(&log);
/* assume HEAD if from a tty */ /* assume HEAD if from a tty */
if (!nongit && !rev.pending.nr && isatty(0)) if (!nongit && !rev.pending.nr && isatty(0))
@ -479,4 +511,5 @@ void shortlog_output(struct shortlog *log)
log->list.strdup_strings = 1; log->list.strdup_strings = 1;
string_list_clear(&log->list, 1); string_list_clear(&log->list, 1);
clear_mailmap(&log->mailmap); clear_mailmap(&log->mailmap);
string_list_clear(&log->format, 0);
} }

View file

@ -2,6 +2,7 @@
#define SHORTLOG_H #define SHORTLOG_H
#include "string-list.h" #include "string-list.h"
#include "date.h"
struct commit; struct commit;
@ -15,13 +16,16 @@ struct shortlog {
int in2; int in2;
int user_format; int user_format;
int abbrev; int abbrev;
struct date_mode date_mode;
enum { enum {
SHORTLOG_GROUP_AUTHOR = (1 << 0), SHORTLOG_GROUP_AUTHOR = (1 << 0),
SHORTLOG_GROUP_COMMITTER = (1 << 1), SHORTLOG_GROUP_COMMITTER = (1 << 1),
SHORTLOG_GROUP_TRAILER = (1 << 2), SHORTLOG_GROUP_TRAILER = (1 << 2),
SHORTLOG_GROUP_FORMAT = (1 << 3),
} groups; } groups;
struct string_list trailers; struct string_list trailers;
struct string_list format;
int email; int email;
struct string_list mailmap; struct string_list mailmap;
@ -29,6 +33,7 @@ struct shortlog {
}; };
void shortlog_init(struct shortlog *log); void shortlog_init(struct shortlog *log);
void shortlog_finish_setup(struct shortlog *log);
void shortlog_add_commit(struct shortlog *log, struct commit *commit); void shortlog_add_commit(struct shortlog *log, struct commit *commit);

View file

@ -83,6 +83,13 @@ test_expect_success 'pretty format' '
test_cmp expect log.predictable test_cmp expect log.predictable
' '
test_expect_success 'pretty format (with --date)' '
sed "s/SUBJECT/2005-04-07 OBJECT_NAME/" expect.template >expect &&
git shortlog --format="%ad %H" --date=short HEAD >log &&
fuzz log >log.predictable &&
test_cmp expect log.predictable
'
test_expect_success '--abbrev' ' test_expect_success '--abbrev' '
sed s/SUBJECT/OBJID/ expect.template >expect && sed s/SUBJECT/OBJID/ expect.template >expect &&
git shortlog --format="%h" --abbrev=35 HEAD >log && git shortlog --format="%h" --abbrev=35 HEAD >log &&
@ -237,6 +244,26 @@ test_expect_success 'shortlog --group=trailer:signed-off-by' '
test_cmp expect actual test_cmp expect actual
' '
test_expect_success 'shortlog --group=format' '
git shortlog -s --date="format:%Y" --group="format:%cN (%cd)" \
HEAD >actual &&
cat >expect <<-\EOF &&
4 C O Mitter (2005)
1 Sin Nombre (2005)
EOF
test_cmp expect actual
'
test_expect_success 'shortlog --group=<format> DWIM' '
git shortlog -s --date="format:%Y" --group="%cN (%cd)" HEAD >actual &&
test_cmp expect actual
'
test_expect_success 'shortlog bogus --group' '
test_must_fail git shortlog --group=bogus HEAD 2>err &&
grep "unknown group type" err
'
test_expect_success 'trailer idents are split' ' test_expect_success 'trailer idents are split' '
cat >expect <<-\EOF && cat >expect <<-\EOF &&
2 C O Mitter 2 C O Mitter
@ -319,6 +346,18 @@ test_expect_success 'shortlog can match multiple groups' '
test_cmp expect actual test_cmp expect actual
' '
test_expect_success 'shortlog can match multiple format groups' '
GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" \
git commit --allow-empty -m "identical names" &&
test_tick &&
cat >expect <<-\EOF &&
2 A U Thor
1 C O Mitter
EOF
git shortlog -ns --group="%cn" --group="%an" -2 HEAD >actual &&
test_cmp expect actual
'
test_expect_success 'set up option selection tests' ' test_expect_success 'set up option selection tests' '
git commit --allow-empty -F - <<-\EOF git commit --allow-empty -F - <<-\EOF
subject subject