mirror of
https://github.com/git/git.git
synced 2024-12-18 06:14:59 +08:00
fa83cc834d
Several Git commands have subcommands to implement mutually exclusive "operation modes", and they usually parse their subcommand argument with a bunch of if-else if statements. Teach parse-options to handle subcommands as well, which will result in shorter and simpler code with consistent error handling and error messages on unknown or missing subcommand, and it will also make possible for our Bash completion script to handle subcommands programmatically. The approach is guided by the following observations: - Most subcommands [1] are implemented in dedicated functions, and most of those functions [2] either have a signature matching the 'int cmd_foo(int argc, const char **argc, const char *prefix)' signature of builtin commands or can be trivially converted to that signature, because they miss only that last prefix parameter or have no parameters at all. - Subcommand arguments only have long form, and they have no double dash prefix, no negated form, and no description, and they don't take any arguments, and can't be abbreviated. - There must be exactly one subcommand among the arguments, or zero if the command has a default operation mode. - All arguments following the subcommand are considered to be arguments of the subcommand, and, conversely, arguments meant for the subcommand may not preceed the subcommand. So in the end subcommand declaration and parsing would look something like this: parse_opt_subcommand_fn *fn = NULL; struct option builtin_commit_graph_options[] = { OPT_STRING(0, "object-dir", &opts.obj_dir, N_("dir"), N_("the object directory to store the graph")), OPT_SUBCOMMAND("verify", &fn, graph_verify), OPT_SUBCOMMAND("write", &fn, graph_write), OPT_END(), }; argc = parse_options(argc, argv, prefix, options, builtin_commit_graph_usage, 0); return fn(argc, argv, prefix); Here each OPT_SUBCOMMAND specifies the name of the subcommand and the function implementing it, and the address of the same 'fn' subcommand function pointer. parse_options() then processes the arguments until it finds the first argument matching one of the subcommands, sets 'fn' to the function associated with that subcommand, and returns, leaving the rest of the arguments unprocessed. If none of the listed subcommands is found among the arguments, parse_options() will show usage and abort. If a command has a default operation mode, 'fn' should be initialized to the function implementing that mode, and parse_options() should be invoked with the PARSE_OPT_SUBCOMMAND_OPTIONAL flag. In this case parse_options() won't error out when not finding any subcommands, but will return leaving 'fn' unchanged. Note that if that default operation mode has any --options, then the PARSE_OPT_KEEP_UNKNOWN_OPT flag is necessary as well (otherwise parse_options() would error out upon seeing the unknown option meant to the default operation mode). Some thoughts about the implementation: - The same pointer to 'fn' must be specified as 'value' for each OPT_SUBCOMMAND, because there can be only one set of mutually exclusive subcommands; parse_options() will BUG() otherwise. There are other ways to tell parse_options() where to put the function associated with the subcommand given on the command line, but I didn't like them: - Change parse_options()'s signature by adding a pointer to subcommand function to be set to the function associated with the given subcommand, affecting all callsites, even those that don't have subcommands. - Introduce a specific parse_options_and_subcommand() variant with that extra funcion parameter. - I decided against automatically calling the subcommand function from within parse_options(), because: - There are commands that have to perform additional actions after option parsing but before calling the function implementing the specified subcommand. - The return code of the subcommand is usually the return code of the git command, but preserving the return code of the automatically called subcommand function would have made the API awkward. - Also add a OPT_SUBCOMMAND_F() variant to allow specifying an option flag: we have two subcommands that are purposefully excluded from completion ('git remote rm' and 'git stash save'), so they'll have to be specified with the PARSE_OPT_NOCOMPLETE flag. - Some of the 'parse_opt_flags' don't make sense with subcommands, and using them is probably just an oversight or misunderstanding. Therefore parse_options() will BUG() when invoked with any of the following flags while the options array contains at least one OPT_SUBCOMMAND: - PARSE_OPT_KEEP_DASHDASH: parse_options() stops parsing arguments when encountering a "--" argument, so it doesn't make sense to expect and keep one before a subcommand, because it would prevent the parsing of the subcommand. However, this flag is allowed in combination with the PARSE_OPT_SUBCOMMAND_OPTIONAL flag, because the double dash might be meaningful for the command's default operation mode, e.g. to disambiguate refs and pathspecs. - PARSE_OPT_STOP_AT_NON_OPTION: As its name suggests, this flag tells parse_options() to stop as soon as it encouners a non-option argument, but subcommands are by definition not options... so how could they be parsed, then?! - PARSE_OPT_KEEP_UNKNOWN: This flag can be used to collect any unknown --options and then pass them to a different command or subsystem. Surely if a command has subcommands, then this functionality should rather be delegated to one of those subcommands, and not performed by the command itself. However, this flag is allowed in combination with the PARSE_OPT_SUBCOMMAND_OPTIONAL flag, making possible to pass --options to the default operation mode. - If the command with subcommands has a default operation mode, then all arguments to the command must preceed the arguments of the subcommand. AFAICT we don't have any commands where this makes a difference, because in those commands either only the command accepts any arguments ('notes' and 'remote'), or only the default subcommand ('reflog' and 'stash'), but never both. - The 'argv' array passed to subcommand functions currently starts with the name of the subcommand. Keep this behavior. AFAICT no subcommand functions depend on the actual content of 'argv[0]', but the parse_options() call handling their options expects that the options start at argv[1]. - To support handling subcommands programmatically in our Bash completion script, 'git cmd --git-completion-helper' will now list both subcommands and regular --options, if any. This means that the completion script will have to separate subcommands (i.e. words without a double dash prefix) from --options on its own, but that's rather easy to do, and it's not much work either, because the number of subcommands a command might have is rather low, and those commands accept only a single --option or none at all. An alternative would be to introduce a separate option that lists only subcommands, but then the completion script would need not one but two git invocations and command substitutions for commands with subcommands. Note that this change doesn't affect the behavior of our Bash completion script, because when completing the --option of a command with subcommands, e.g. for 'git notes --<TAB>', then all subcommands will be filtered out anyway, as none of them will match the word to be completed starting with that double dash prefix. [1] Except 'git rerere', because many of its subcommands are implemented in the bodies of the if-else if statements parsing the command's subcommand argument. [2] Except 'credential', 'credential-store' and 'fsmonitor--daemon', because some of the functions implementing their subcommands take special parameters. Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
483 lines
12 KiB
C
483 lines
12 KiB
C
#include "builtin.h"
|
|
#include "cache.h"
|
|
#include "config.h"
|
|
#include "commit.h"
|
|
#include "diff.h"
|
|
#include "string-list.h"
|
|
#include "revision.h"
|
|
#include "utf8.h"
|
|
#include "mailmap.h"
|
|
#include "shortlog.h"
|
|
#include "parse-options.h"
|
|
#include "trailer.h"
|
|
#include "strmap.h"
|
|
|
|
static char const * const shortlog_usage[] = {
|
|
N_("git shortlog [<options>] [<revision-range>] [[--] <path>...]"),
|
|
N_("git log --pretty=short | git shortlog [<options>]"),
|
|
NULL
|
|
};
|
|
|
|
/*
|
|
* The util field of our string_list_items will contain one of two things:
|
|
*
|
|
* - if --summary is not in use, it will point to a string list of the
|
|
* oneline subjects assigned to this author
|
|
*
|
|
* - if --summary is in use, we don't need that list; we only need to know
|
|
* its size. So we abuse the pointer slot to store our integer counter.
|
|
*
|
|
* This macro accesses the latter.
|
|
*/
|
|
#define UTIL_TO_INT(x) ((intptr_t)(x)->util)
|
|
|
|
static int compare_by_counter(const void *a1, const void *a2)
|
|
{
|
|
const struct string_list_item *i1 = a1, *i2 = a2;
|
|
return UTIL_TO_INT(i2) - UTIL_TO_INT(i1);
|
|
}
|
|
|
|
static int compare_by_list(const void *a1, const void *a2)
|
|
{
|
|
const struct string_list_item *i1 = a1, *i2 = a2;
|
|
const struct string_list *l1 = i1->util, *l2 = i2->util;
|
|
|
|
if (l1->nr < l2->nr)
|
|
return 1;
|
|
else if (l1->nr == l2->nr)
|
|
return 0;
|
|
else
|
|
return -1;
|
|
}
|
|
|
|
static void insert_one_record(struct shortlog *log,
|
|
const char *ident,
|
|
const char *oneline)
|
|
{
|
|
struct string_list_item *item;
|
|
|
|
item = string_list_insert(&log->list, ident);
|
|
|
|
if (log->summary)
|
|
item->util = (void *)(UTIL_TO_INT(item) + 1);
|
|
else {
|
|
char *buffer;
|
|
struct strbuf subject = STRBUF_INIT;
|
|
const char *eol;
|
|
|
|
/* Skip any leading whitespace, including any blank lines. */
|
|
while (*oneline && isspace(*oneline))
|
|
oneline++;
|
|
eol = strchr(oneline, '\n');
|
|
if (!eol)
|
|
eol = oneline + strlen(oneline);
|
|
if (starts_with(oneline, "[PATCH")) {
|
|
char *eob = strchr(oneline, ']');
|
|
if (eob && (!eol || eob < eol))
|
|
oneline = eob + 1;
|
|
}
|
|
while (*oneline && isspace(*oneline) && *oneline != '\n')
|
|
oneline++;
|
|
format_subject(&subject, oneline, " ");
|
|
buffer = strbuf_detach(&subject, NULL);
|
|
|
|
if (!item->util) {
|
|
item->util = xmalloc(sizeof(struct string_list));
|
|
string_list_init_nodup(item->util);
|
|
}
|
|
string_list_append(item->util, buffer);
|
|
}
|
|
}
|
|
|
|
static int parse_ident(struct shortlog *log,
|
|
struct strbuf *out, const char *in)
|
|
{
|
|
const char *mailbuf, *namebuf;
|
|
size_t namelen, maillen;
|
|
struct ident_split ident;
|
|
|
|
if (split_ident_line(&ident, in, strlen(in)))
|
|
return -1;
|
|
|
|
namebuf = ident.name_begin;
|
|
mailbuf = ident.mail_begin;
|
|
namelen = ident.name_end - ident.name_begin;
|
|
maillen = ident.mail_end - ident.mail_begin;
|
|
|
|
map_user(&log->mailmap, &mailbuf, &maillen, &namebuf, &namelen);
|
|
strbuf_add(out, namebuf, namelen);
|
|
if (log->email)
|
|
strbuf_addf(out, " <%.*s>", (int)maillen, mailbuf);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void read_from_stdin(struct shortlog *log)
|
|
{
|
|
struct strbuf ident = STRBUF_INIT;
|
|
struct strbuf mapped_ident = STRBUF_INIT;
|
|
struct strbuf oneline = STRBUF_INIT;
|
|
static const char *author_match[2] = { "Author: ", "author " };
|
|
static const char *committer_match[2] = { "Commit: ", "committer " };
|
|
const char **match;
|
|
|
|
if (HAS_MULTI_BITS(log->groups))
|
|
die(_("using multiple --group options with stdin is not supported"));
|
|
|
|
switch (log->groups) {
|
|
case SHORTLOG_GROUP_AUTHOR:
|
|
match = author_match;
|
|
break;
|
|
case SHORTLOG_GROUP_COMMITTER:
|
|
match = committer_match;
|
|
break;
|
|
case SHORTLOG_GROUP_TRAILER:
|
|
die(_("using --group=trailer with stdin is not supported"));
|
|
default:
|
|
BUG("unhandled shortlog group");
|
|
}
|
|
|
|
while (strbuf_getline_lf(&ident, stdin) != EOF) {
|
|
const char *v;
|
|
if (!skip_prefix(ident.buf, match[0], &v) &&
|
|
!skip_prefix(ident.buf, match[1], &v))
|
|
continue;
|
|
while (strbuf_getline_lf(&oneline, stdin) != EOF &&
|
|
oneline.len)
|
|
; /* discard headers */
|
|
while (strbuf_getline_lf(&oneline, stdin) != EOF &&
|
|
!oneline.len)
|
|
; /* discard blanks */
|
|
|
|
strbuf_reset(&mapped_ident);
|
|
if (parse_ident(log, &mapped_ident, v) < 0)
|
|
continue;
|
|
|
|
insert_one_record(log, mapped_ident.buf, oneline.buf);
|
|
}
|
|
strbuf_release(&ident);
|
|
strbuf_release(&mapped_ident);
|
|
strbuf_release(&oneline);
|
|
}
|
|
|
|
static void insert_records_from_trailers(struct shortlog *log,
|
|
struct strset *dups,
|
|
struct commit *commit,
|
|
struct pretty_print_context *ctx,
|
|
const char *oneline)
|
|
{
|
|
struct trailer_iterator iter;
|
|
const char *commit_buffer, *body;
|
|
struct strbuf ident = STRBUF_INIT;
|
|
|
|
/*
|
|
* Using format_commit_message("%B") would be simpler here, but
|
|
* this saves us copying the message.
|
|
*/
|
|
commit_buffer = logmsg_reencode(commit, NULL, ctx->output_encoding);
|
|
body = strstr(commit_buffer, "\n\n");
|
|
if (!body)
|
|
return;
|
|
|
|
trailer_iterator_init(&iter, body);
|
|
while (trailer_iterator_advance(&iter)) {
|
|
const char *value = iter.val.buf;
|
|
|
|
if (!string_list_has_string(&log->trailers, iter.key.buf))
|
|
continue;
|
|
|
|
strbuf_reset(&ident);
|
|
if (!parse_ident(log, &ident, value))
|
|
value = ident.buf;
|
|
|
|
if (!strset_add(dups, value))
|
|
continue;
|
|
insert_one_record(log, value, oneline);
|
|
}
|
|
trailer_iterator_release(&iter);
|
|
|
|
strbuf_release(&ident);
|
|
unuse_commit_buffer(commit, commit_buffer);
|
|
}
|
|
|
|
void shortlog_add_commit(struct shortlog *log, struct commit *commit)
|
|
{
|
|
struct strbuf ident = STRBUF_INIT;
|
|
struct strbuf oneline = STRBUF_INIT;
|
|
struct strset dups = STRSET_INIT;
|
|
struct pretty_print_context ctx = {0};
|
|
const char *oneline_str;
|
|
|
|
ctx.fmt = CMIT_FMT_USERFORMAT;
|
|
ctx.abbrev = log->abbrev;
|
|
ctx.print_email_subject = 1;
|
|
ctx.date_mode.type = DATE_NORMAL;
|
|
ctx.output_encoding = get_log_output_encoding();
|
|
|
|
if (!log->summary) {
|
|
if (log->user_format)
|
|
pretty_print_commit(&ctx, commit, &oneline);
|
|
else
|
|
format_commit_message(commit, "%s", &oneline, &ctx);
|
|
}
|
|
oneline_str = oneline.len ? oneline.buf : "<none>";
|
|
|
|
if (log->groups & SHORTLOG_GROUP_AUTHOR) {
|
|
strbuf_reset(&ident);
|
|
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);
|
|
strbuf_release(&ident);
|
|
strbuf_release(&oneline);
|
|
}
|
|
|
|
static void get_from_rev(struct rev_info *rev, struct shortlog *log)
|
|
{
|
|
struct commit *commit;
|
|
|
|
if (prepare_revision_walk(rev))
|
|
die(_("revision walk setup failed"));
|
|
while ((commit = get_revision(rev)) != NULL)
|
|
shortlog_add_commit(log, commit);
|
|
}
|
|
|
|
static int parse_uint(char const **arg, int comma, int defval)
|
|
{
|
|
unsigned long ul;
|
|
int ret;
|
|
char *endp;
|
|
|
|
ul = strtoul(*arg, &endp, 10);
|
|
if (*endp && *endp != comma)
|
|
return -1;
|
|
if (ul > INT_MAX)
|
|
return -1;
|
|
ret = *arg == endp ? defval : (int)ul;
|
|
*arg = *endp ? endp + 1 : endp;
|
|
return ret;
|
|
}
|
|
|
|
static const char wrap_arg_usage[] = "-w[<width>[,<indent1>[,<indent2>]]]";
|
|
#define DEFAULT_WRAPLEN 76
|
|
#define DEFAULT_INDENT1 6
|
|
#define DEFAULT_INDENT2 9
|
|
|
|
static int parse_wrap_args(const struct option *opt, const char *arg, int unset)
|
|
{
|
|
struct shortlog *log = opt->value;
|
|
|
|
log->wrap_lines = !unset;
|
|
if (unset)
|
|
return 0;
|
|
if (!arg) {
|
|
log->wrap = DEFAULT_WRAPLEN;
|
|
log->in1 = DEFAULT_INDENT1;
|
|
log->in2 = DEFAULT_INDENT2;
|
|
return 0;
|
|
}
|
|
|
|
log->wrap = parse_uint(&arg, ',', DEFAULT_WRAPLEN);
|
|
log->in1 = parse_uint(&arg, ',', DEFAULT_INDENT1);
|
|
log->in2 = parse_uint(&arg, '\0', DEFAULT_INDENT2);
|
|
if (log->wrap < 0 || log->in1 < 0 || log->in2 < 0)
|
|
return error(wrap_arg_usage);
|
|
if (log->wrap &&
|
|
((log->in1 && log->wrap <= log->in1) ||
|
|
(log->in2 && log->wrap <= log->in2)))
|
|
return error(wrap_arg_usage);
|
|
return 0;
|
|
}
|
|
|
|
static int parse_group_option(const struct option *opt, const char *arg, int unset)
|
|
{
|
|
struct shortlog *log = opt->value;
|
|
const char *field;
|
|
|
|
if (unset) {
|
|
log->groups = 0;
|
|
string_list_clear(&log->trailers, 0);
|
|
} else if (!strcasecmp(arg, "author"))
|
|
log->groups |= SHORTLOG_GROUP_AUTHOR;
|
|
else if (!strcasecmp(arg, "committer"))
|
|
log->groups |= SHORTLOG_GROUP_COMMITTER;
|
|
else if (skip_prefix(arg, "trailer:", &field)) {
|
|
log->groups |= SHORTLOG_GROUP_TRAILER;
|
|
string_list_append(&log->trailers, field);
|
|
} else
|
|
return error(_("unknown group type: %s"), arg);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
void shortlog_init(struct shortlog *log)
|
|
{
|
|
memset(log, 0, sizeof(*log));
|
|
|
|
read_mailmap(&log->mailmap);
|
|
|
|
log->list.strdup_strings = 1;
|
|
log->wrap = DEFAULT_WRAPLEN;
|
|
log->in1 = DEFAULT_INDENT1;
|
|
log->in2 = DEFAULT_INDENT2;
|
|
log->trailers.strdup_strings = 1;
|
|
log->trailers.cmp = strcasecmp;
|
|
}
|
|
|
|
int cmd_shortlog(int argc, const char **argv, const char *prefix)
|
|
{
|
|
struct shortlog log = { STRING_LIST_INIT_NODUP };
|
|
struct rev_info rev;
|
|
int nongit = !startup_info->have_repository;
|
|
|
|
const struct option options[] = {
|
|
OPT_BIT('c', "committer", &log.groups,
|
|
N_("group by committer rather than author"),
|
|
SHORTLOG_GROUP_COMMITTER),
|
|
OPT_BOOL('n', "numbered", &log.sort_by_number,
|
|
N_("sort output according to the number of commits per author")),
|
|
OPT_BOOL('s', "summary", &log.summary,
|
|
N_("suppress commit descriptions, only provides commit count")),
|
|
OPT_BOOL('e', "email", &log.email,
|
|
N_("show the email address of each author")),
|
|
OPT_CALLBACK_F('w', NULL, &log, N_("<w>[,<i1>[,<i2>]]"),
|
|
N_("linewrap output"), PARSE_OPT_OPTARG,
|
|
&parse_wrap_args),
|
|
OPT_CALLBACK(0, "group", &log, N_("field"),
|
|
N_("group by field"), parse_group_option),
|
|
OPT_END(),
|
|
};
|
|
|
|
struct parse_opt_ctx_t ctx;
|
|
|
|
git_config(git_default_config, NULL);
|
|
shortlog_init(&log);
|
|
repo_init_revisions(the_repository, &rev, prefix);
|
|
parse_options_start(&ctx, argc, argv, prefix, options,
|
|
PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0);
|
|
|
|
for (;;) {
|
|
switch (parse_options_step(&ctx, options, shortlog_usage)) {
|
|
case PARSE_OPT_NON_OPTION:
|
|
case PARSE_OPT_UNKNOWN:
|
|
break;
|
|
case PARSE_OPT_HELP:
|
|
case PARSE_OPT_ERROR:
|
|
case PARSE_OPT_SUBCOMMAND:
|
|
exit(129);
|
|
case PARSE_OPT_COMPLETE:
|
|
exit(0);
|
|
case PARSE_OPT_DONE:
|
|
goto parse_done;
|
|
}
|
|
parse_revision_opt(&rev, &ctx, options, shortlog_usage);
|
|
}
|
|
parse_done:
|
|
revision_opts_finish(&rev);
|
|
argc = parse_options_end(&ctx);
|
|
|
|
if (nongit && argc > 1) {
|
|
error(_("too many arguments given outside repository"));
|
|
usage_with_options(shortlog_usage, options);
|
|
}
|
|
|
|
if (setup_revisions(argc, argv, &rev, NULL) != 1) {
|
|
error(_("unrecognized argument: %s"), argv[1]);
|
|
usage_with_options(shortlog_usage, options);
|
|
}
|
|
|
|
log.user_format = rev.commit_format == CMIT_FMT_USERFORMAT;
|
|
log.abbrev = rev.abbrev;
|
|
log.file = rev.diffopt.file;
|
|
|
|
if (!log.groups)
|
|
log.groups = SHORTLOG_GROUP_AUTHOR;
|
|
string_list_sort(&log.trailers);
|
|
|
|
/* assume HEAD if from a tty */
|
|
if (!nongit && !rev.pending.nr && isatty(0))
|
|
add_head_to_pending(&rev);
|
|
if (rev.pending.nr == 0) {
|
|
if (isatty(0))
|
|
fprintf(stderr, _("(reading log message from standard input)\n"));
|
|
read_from_stdin(&log);
|
|
}
|
|
else
|
|
get_from_rev(&rev, &log);
|
|
|
|
release_revisions(&rev);
|
|
|
|
shortlog_output(&log);
|
|
if (log.file != stdout)
|
|
fclose(log.file);
|
|
return 0;
|
|
}
|
|
|
|
static void add_wrapped_shortlog_msg(struct strbuf *sb, const char *s,
|
|
const struct shortlog *log)
|
|
{
|
|
strbuf_add_wrapped_text(sb, s, log->in1, log->in2, log->wrap);
|
|
strbuf_addch(sb, '\n');
|
|
}
|
|
|
|
void shortlog_output(struct shortlog *log)
|
|
{
|
|
size_t i, j;
|
|
struct strbuf sb = STRBUF_INIT;
|
|
|
|
if (log->sort_by_number)
|
|
STABLE_QSORT(log->list.items, log->list.nr,
|
|
log->summary ? compare_by_counter : compare_by_list);
|
|
for (i = 0; i < log->list.nr; i++) {
|
|
const struct string_list_item *item = &log->list.items[i];
|
|
if (log->summary) {
|
|
fprintf(log->file, "%6d\t%s\n",
|
|
(int)UTIL_TO_INT(item), item->string);
|
|
} else {
|
|
struct string_list *onelines = item->util;
|
|
fprintf(log->file, "%s (%"PRIuMAX"):\n",
|
|
item->string, (uintmax_t)onelines->nr);
|
|
for (j = onelines->nr; j >= 1; j--) {
|
|
const char *msg = onelines->items[j - 1].string;
|
|
|
|
if (log->wrap_lines) {
|
|
strbuf_reset(&sb);
|
|
add_wrapped_shortlog_msg(&sb, msg, log);
|
|
fwrite(sb.buf, sb.len, 1, log->file);
|
|
}
|
|
else
|
|
fprintf(log->file, " %s\n", msg);
|
|
}
|
|
putc('\n', log->file);
|
|
onelines->strdup_strings = 1;
|
|
string_list_clear(onelines, 0);
|
|
free(onelines);
|
|
}
|
|
|
|
log->list.items[i].util = NULL;
|
|
}
|
|
|
|
strbuf_release(&sb);
|
|
log->list.strdup_strings = 1;
|
|
string_list_clear(&log->list, 1);
|
|
clear_mailmap(&log->mailmap);
|
|
}
|