Git fork
at reftables-rust 535 lines 14 kB view raw
1#define USE_THE_REPOSITORY_VARIABLE 2 3#include "builtin.h" 4#include "config.h" 5#include "commit.h" 6#include "diff.h" 7#include "environment.h" 8#include "gettext.h" 9#include "string-list.h" 10#include "revision.h" 11#include "utf8.h" 12#include "mailmap.h" 13#include "setup.h" 14#include "shortlog.h" 15#include "parse-options.h" 16#include "trailer.h" 17#include "strmap.h" 18 19static char const * const shortlog_usage[] = { 20 N_("git shortlog [<options>] [<revision-range>] [[--] <path>...]"), 21 N_("git log --pretty=short | git shortlog [<options>]"), 22 NULL 23}; 24 25/* 26 * The util field of our string_list_items will contain one of two things: 27 * 28 * - if --summary is not in use, it will point to a string list of the 29 * oneline subjects assigned to this author 30 * 31 * - if --summary is in use, we don't need that list; we only need to know 32 * its size. So we abuse the pointer slot to store our integer counter. 33 * 34 * This macro accesses the latter. 35 */ 36#define UTIL_TO_INT(x) ((intptr_t)(x)->util) 37 38static int compare_by_counter(const void *a1, const void *a2) 39{ 40 const struct string_list_item *i1 = a1, *i2 = a2; 41 return UTIL_TO_INT(i2) - UTIL_TO_INT(i1); 42} 43 44static int compare_by_list(const void *a1, const void *a2) 45{ 46 const struct string_list_item *i1 = a1, *i2 = a2; 47 const struct string_list *l1 = i1->util, *l2 = i2->util; 48 49 if (l1->nr < l2->nr) 50 return 1; 51 else if (l1->nr == l2->nr) 52 return 0; 53 else 54 return -1; 55} 56 57static void insert_one_record(struct shortlog *log, 58 const char *ident, 59 const char *oneline) 60{ 61 struct string_list_item *item; 62 63 item = string_list_insert(&log->list, ident); 64 65 if (log->summary) 66 item->util = (void *)(UTIL_TO_INT(item) + 1); 67 else { 68 char *buffer; 69 struct strbuf subject = STRBUF_INIT; 70 const char *eol; 71 72 /* Skip any leading whitespace, including any blank lines. */ 73 while (*oneline && isspace(*oneline)) 74 oneline++; 75 eol = strchr(oneline, '\n'); 76 if (!eol) 77 eol = oneline + strlen(oneline); 78 if (starts_with(oneline, "[PATCH")) { 79 char *eob = strchr(oneline, ']'); 80 if (eob && (!eol || eob < eol)) 81 oneline = eob + 1; 82 } 83 while (*oneline && isspace(*oneline) && *oneline != '\n') 84 oneline++; 85 format_subject(&subject, oneline, " "); 86 buffer = strbuf_detach(&subject, NULL); 87 88 if (!item->util) { 89 item->util = xmalloc(sizeof(struct string_list)); 90 string_list_init_nodup(item->util); 91 } 92 string_list_append(item->util, buffer); 93 } 94} 95 96static int parse_ident(struct shortlog *log, 97 struct strbuf *out, const char *in) 98{ 99 const char *mailbuf, *namebuf; 100 size_t namelen, maillen; 101 struct ident_split ident; 102 103 if (split_ident_line(&ident, in, strlen(in))) 104 return -1; 105 106 namebuf = ident.name_begin; 107 mailbuf = ident.mail_begin; 108 namelen = ident.name_end - ident.name_begin; 109 maillen = ident.mail_end - ident.mail_begin; 110 111 map_user(&log->mailmap, &mailbuf, &maillen, &namebuf, &namelen); 112 strbuf_add(out, namebuf, namelen); 113 if (log->email) 114 strbuf_addf(out, " <%.*s>", (int)maillen, mailbuf); 115 116 return 0; 117} 118 119static void read_from_stdin(struct shortlog *log) 120{ 121 struct strbuf ident = STRBUF_INIT; 122 struct strbuf mapped_ident = STRBUF_INIT; 123 struct strbuf oneline = STRBUF_INIT; 124 static const char *author_match[2] = { "Author: ", "author " }; 125 static const char *committer_match[2] = { "Commit: ", "committer " }; 126 const char **match; 127 128 if (HAS_MULTI_BITS(log->groups)) 129 die(_("using multiple --group options with stdin is not supported")); 130 131 switch (log->groups) { 132 case SHORTLOG_GROUP_AUTHOR: 133 match = author_match; 134 break; 135 case SHORTLOG_GROUP_COMMITTER: 136 match = committer_match; 137 break; 138 case SHORTLOG_GROUP_TRAILER: 139 die(_("using %s with stdin is not supported"), "--group=trailer"); 140 case SHORTLOG_GROUP_FORMAT: 141 die(_("using %s with stdin is not supported"), "--group=format"); 142 default: 143 BUG("unhandled shortlog group"); 144 } 145 146 while (strbuf_getline_lf(&ident, stdin) != EOF) { 147 const char *v; 148 if (!skip_prefix(ident.buf, match[0], &v) && 149 !skip_prefix(ident.buf, match[1], &v)) 150 continue; 151 while (strbuf_getline_lf(&oneline, stdin) != EOF && 152 oneline.len) 153 ; /* discard headers */ 154 while (strbuf_getline_lf(&oneline, stdin) != EOF && 155 !oneline.len) 156 ; /* discard blanks */ 157 158 strbuf_reset(&mapped_ident); 159 if (parse_ident(log, &mapped_ident, v) < 0) 160 continue; 161 162 insert_one_record(log, mapped_ident.buf, oneline.buf); 163 } 164 strbuf_release(&ident); 165 strbuf_release(&mapped_ident); 166 strbuf_release(&oneline); 167} 168 169static void insert_records_from_trailers(struct shortlog *log, 170 struct strset *dups, 171 struct commit *commit, 172 struct pretty_print_context *ctx, 173 const char *oneline) 174{ 175 struct trailer_iterator iter; 176 const char *commit_buffer, *body; 177 struct strbuf ident = STRBUF_INIT; 178 179 if (!log->trailers.nr) 180 return; 181 182 /* 183 * Using repo_format_commit_message("%B") would be simpler here, but 184 * this saves us copying the message. 185 */ 186 commit_buffer = repo_logmsg_reencode(the_repository, commit, NULL, 187 ctx->output_encoding); 188 body = strstr(commit_buffer, "\n\n"); 189 if (!body) 190 goto out; 191 192 trailer_iterator_init(&iter, body); 193 while (trailer_iterator_advance(&iter)) { 194 const char *value = iter.val.buf; 195 196 if (!string_list_has_string(&log->trailers, iter.key.buf)) 197 continue; 198 199 strbuf_reset(&ident); 200 if (!parse_ident(log, &ident, value)) 201 value = ident.buf; 202 203 if (!strset_add(dups, value)) 204 continue; 205 insert_one_record(log, value, oneline); 206 } 207 trailer_iterator_release(&iter); 208 209out: 210 strbuf_release(&ident); 211 repo_unuse_commit_buffer(the_repository, commit, commit_buffer); 212} 213 214static int shortlog_needs_dedup(const struct shortlog *log) 215{ 216 return HAS_MULTI_BITS(log->groups) || log->format.nr > 1 || log->trailers.nr; 217} 218 219static void insert_records_from_format(struct shortlog *log, 220 struct strset *dups, 221 struct commit *commit, 222 struct pretty_print_context *ctx, 223 const char *oneline) 224{ 225 struct strbuf buf = STRBUF_INIT; 226 struct string_list_item *item; 227 228 for_each_string_list_item(item, &log->format) { 229 strbuf_reset(&buf); 230 231 repo_format_commit_message(the_repository, commit, 232 item->string, &buf, ctx); 233 234 if (!shortlog_needs_dedup(log) || strset_add(dups, buf.buf)) 235 insert_one_record(log, buf.buf, oneline); 236 } 237 238 strbuf_release(&buf); 239} 240 241void shortlog_add_commit(struct shortlog *log, struct commit *commit) 242{ 243 struct strbuf oneline = STRBUF_INIT; 244 struct strset dups = STRSET_INIT; 245 struct pretty_print_context ctx = {0}; 246 const char *oneline_str; 247 248 ctx.fmt = CMIT_FMT_USERFORMAT; 249 ctx.abbrev = log->abbrev; 250 ctx.date_mode = log->date_mode; 251 ctx.output_encoding = get_log_output_encoding(); 252 253 if (!log->summary) { 254 if (log->user_format) 255 pretty_print_commit(&ctx, commit, &oneline); 256 else 257 repo_format_commit_message(the_repository, commit, 258 "%s", &oneline, &ctx); 259 } 260 oneline_str = oneline.len ? oneline.buf : "<none>"; 261 262 insert_records_from_trailers(log, &dups, commit, &ctx, oneline_str); 263 insert_records_from_format(log, &dups, commit, &ctx, oneline_str); 264 265 strset_clear(&dups); 266 strbuf_release(&oneline); 267} 268 269static void get_from_rev(struct rev_info *rev, struct shortlog *log) 270{ 271 struct commit *commit; 272 273 if (prepare_revision_walk(rev)) 274 die(_("revision walk setup failed")); 275 while ((commit = get_revision(rev)) != NULL) 276 shortlog_add_commit(log, commit); 277} 278 279static int parse_uint(char const **arg, int comma, int defval) 280{ 281 unsigned long ul; 282 int ret; 283 char *endp; 284 285 ul = strtoul(*arg, &endp, 10); 286 if (*endp && *endp != comma) 287 return -1; 288 if (ul > INT_MAX) 289 return -1; 290 ret = *arg == endp ? defval : (int)ul; 291 *arg = *endp ? endp + 1 : endp; 292 return ret; 293} 294 295static const char wrap_arg_usage[] = "-w[<width>[,<indent1>[,<indent2>]]]"; 296#define DEFAULT_WRAPLEN 76 297#define DEFAULT_INDENT1 6 298#define DEFAULT_INDENT2 9 299 300static int parse_wrap_args(const struct option *opt, const char *arg, int unset) 301{ 302 struct shortlog *log = opt->value; 303 304 log->wrap_lines = !unset; 305 if (unset) 306 return 0; 307 if (!arg) { 308 log->wrap = DEFAULT_WRAPLEN; 309 log->in1 = DEFAULT_INDENT1; 310 log->in2 = DEFAULT_INDENT2; 311 return 0; 312 } 313 314 log->wrap = parse_uint(&arg, ',', DEFAULT_WRAPLEN); 315 log->in1 = parse_uint(&arg, ',', DEFAULT_INDENT1); 316 log->in2 = parse_uint(&arg, '\0', DEFAULT_INDENT2); 317 if (log->wrap < 0 || log->in1 < 0 || log->in2 < 0) 318 return error(wrap_arg_usage); 319 if (log->wrap && 320 ((log->in1 && log->wrap <= log->in1) || 321 (log->in2 && log->wrap <= log->in2))) 322 return error(wrap_arg_usage); 323 return 0; 324} 325 326static int parse_group_option(const struct option *opt, const char *arg, int unset) 327{ 328 struct shortlog *log = opt->value; 329 const char *field; 330 331 if (unset) { 332 log->groups = 0; 333 string_list_clear(&log->trailers, 0); 334 string_list_clear(&log->format, 0); 335 } else if (!strcasecmp(arg, "author")) 336 log->groups |= SHORTLOG_GROUP_AUTHOR; 337 else if (!strcasecmp(arg, "committer")) 338 log->groups |= SHORTLOG_GROUP_COMMITTER; 339 else if (skip_prefix(arg, "trailer:", &field)) { 340 log->groups |= SHORTLOG_GROUP_TRAILER; 341 string_list_append(&log->trailers, field); 342 } else if (skip_prefix(arg, "format:", &field)) { 343 log->groups |= SHORTLOG_GROUP_FORMAT; 344 string_list_append(&log->format, field); 345 } else if (strchr(arg, '%')) { 346 log->groups |= SHORTLOG_GROUP_FORMAT; 347 string_list_append(&log->format, arg); 348 } else { 349 return error(_("unknown group type: %s"), arg); 350 } 351 352 return 0; 353} 354 355 356void shortlog_init(struct shortlog *log) 357{ 358 memset(log, 0, sizeof(*log)); 359 360 read_mailmap(&log->mailmap); 361 362 log->list.strdup_strings = 1; 363 log->wrap = DEFAULT_WRAPLEN; 364 log->in1 = DEFAULT_INDENT1; 365 log->in2 = DEFAULT_INDENT2; 366 log->trailers.strdup_strings = 1; 367 log->trailers.cmp = strcasecmp; 368 log->format.strdup_strings = 1; 369} 370 371void shortlog_finish_setup(struct shortlog *log) 372{ 373 if (log->groups & SHORTLOG_GROUP_AUTHOR) 374 string_list_append(&log->format, 375 log->email ? "%aN <%aE>" : "%aN"); 376 if (log->groups & SHORTLOG_GROUP_COMMITTER) 377 string_list_append(&log->format, 378 log->email ? "%cN <%cE>" : "%cN"); 379 380 string_list_sort(&log->trailers); 381} 382 383int cmd_shortlog(int argc, 384 const char **argv, 385 const char *prefix, 386 struct repository *repo UNUSED) 387{ 388 struct shortlog log = { STRING_LIST_INIT_NODUP }; 389 struct rev_info rev; 390 int nongit = !startup_info->have_repository; 391 392 const struct option options[] = { 393 OPT_BIT('c', "committer", &log.groups, 394 N_("group by committer rather than author"), 395 SHORTLOG_GROUP_COMMITTER), 396 OPT_BOOL('n', "numbered", &log.sort_by_number, 397 N_("sort output according to the number of commits per author")), 398 OPT_BOOL('s', "summary", &log.summary, 399 N_("suppress commit descriptions, only provides commit count")), 400 OPT_BOOL('e', "email", &log.email, 401 N_("show the email address of each author")), 402 OPT_CALLBACK_F('w', NULL, &log, N_("<w>[,<i1>[,<i2>]]"), 403 N_("linewrap output"), PARSE_OPT_OPTARG, 404 &parse_wrap_args), 405 OPT_CALLBACK(0, "group", &log, N_("field"), 406 N_("group by field"), parse_group_option), 407 OPT_END(), 408 }; 409 410 struct parse_opt_ctx_t ctx; 411 412 /* 413 * NEEDSWORK: Later on we'll call parse_revision_opt which relies on 414 * the hash algorithm being set but since we are operating outside of a 415 * Git repository we cannot determine one. This is only needed because 416 * parse_revision_opt expects hexsz for --abbrev which is irrelevant 417 * for shortlog outside of a git repository. For now explicitly set 418 * SHA1, but ideally the parsing machinery would be split between 419 * git/nongit so that we do not have to do this. 420 */ 421 if (nongit && !the_hash_algo) 422 repo_set_hash_algo(the_repository, GIT_HASH_DEFAULT); 423 424 repo_config(the_repository, git_default_config, NULL); 425 shortlog_init(&log); 426 repo_init_revisions(the_repository, &rev, prefix); 427 parse_options_start(&ctx, argc, argv, prefix, options, 428 PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0); 429 430 for (;;) { 431 switch (parse_options_step(&ctx, options, shortlog_usage)) { 432 case PARSE_OPT_NON_OPTION: 433 case PARSE_OPT_UNKNOWN: 434 break; 435 case PARSE_OPT_HELP: 436 case PARSE_OPT_ERROR: 437 case PARSE_OPT_SUBCOMMAND: 438 exit(129); 439 case PARSE_OPT_COMPLETE: 440 exit(0); 441 case PARSE_OPT_DONE: 442 goto parse_done; 443 } 444 parse_revision_opt(&rev, &ctx, options, shortlog_usage); 445 } 446parse_done: 447 revision_opts_finish(&rev); 448 argc = parse_options_end(&ctx); 449 450 if (nongit && argc > 1) { 451 error(_("too many arguments given outside repository")); 452 usage_with_options(shortlog_usage, options); 453 } 454 455 if (!nongit && setup_revisions(argc, argv, &rev, NULL) != 1) { 456 error(_("unrecognized argument: %s"), argv[1]); 457 usage_with_options(shortlog_usage, options); 458 } 459 460 log.user_format = rev.commit_format == CMIT_FMT_USERFORMAT; 461 log.abbrev = rev.abbrev; 462 log.file = rev.diffopt.file; 463 log.date_mode = rev.date_mode; 464 465 if (!log.groups) 466 log.groups = SHORTLOG_GROUP_AUTHOR; 467 shortlog_finish_setup(&log); 468 469 /* assume HEAD if from a tty */ 470 if (!nongit && !rev.pending.nr && isatty(0)) 471 add_head_to_pending(&rev); 472 if (rev.pending.nr == 0) { 473 if (isatty(0)) 474 fprintf(stderr, _("(reading log message from standard input)\n")); 475 read_from_stdin(&log); 476 } 477 else 478 get_from_rev(&rev, &log); 479 480 shortlog_output(&log); 481 release_revisions(&rev); 482 return 0; 483} 484 485static void add_wrapped_shortlog_msg(struct strbuf *sb, const char *s, 486 const struct shortlog *log) 487{ 488 strbuf_add_wrapped_text(sb, s, log->in1, log->in2, log->wrap); 489 strbuf_addch(sb, '\n'); 490} 491 492void shortlog_output(struct shortlog *log) 493{ 494 size_t i, j; 495 struct strbuf sb = STRBUF_INIT; 496 497 if (log->sort_by_number) 498 STABLE_QSORT(log->list.items, log->list.nr, 499 log->summary ? compare_by_counter : compare_by_list); 500 for (i = 0; i < log->list.nr; i++) { 501 const struct string_list_item *item = &log->list.items[i]; 502 if (log->summary) { 503 fprintf(log->file, "%6d\t%s\n", 504 (int)UTIL_TO_INT(item), item->string); 505 } else { 506 struct string_list *onelines = item->util; 507 fprintf(log->file, "%s (%"PRIuMAX"):\n", 508 item->string, (uintmax_t)onelines->nr); 509 for (j = onelines->nr; j >= 1; j--) { 510 const char *msg = onelines->items[j - 1].string; 511 512 if (log->wrap_lines) { 513 strbuf_reset(&sb); 514 add_wrapped_shortlog_msg(&sb, msg, log); 515 fwrite(sb.buf, sb.len, 1, log->file); 516 } 517 else 518 fprintf(log->file, " %s\n", msg); 519 } 520 putc('\n', log->file); 521 onelines->strdup_strings = 1; 522 string_list_clear(onelines, 0); 523 free(onelines); 524 } 525 526 log->list.items[i].util = NULL; 527 } 528 529 strbuf_release(&sb); 530 log->list.strdup_strings = 1; 531 string_list_clear(&log->list, 1); 532 clear_mailmap(&log->mailmap); 533 string_list_clear(&log->format, 0); 534 string_list_clear(&log->trailers, 0); 535}