Merge branch 'ps/parse-options-subcommand-groups' into jch

The parse-options API has been updated to support grouping
subcommands, and the 'git refs' command has been updated to use this
new feature to categorize its subcommands in its help output.

* ps/parse-options-subcommand-groups:
  builtin/refs: introduce subcommand groups
  parse-options: allow grouping subcommands
  parse-options: extract functions to print single option
  parse-options: fix completion format when first option is skipped
jch
Junio C Hamano 2026-10-12 09:25:55 -07:00
commit e1fde7a744
9 changed files with 150 additions and 93 deletions

View File

@ -11,6 +11,7 @@ SYNOPSIS
[synopsis]
git refs migrate --ref-storage-format=<format> [--no-reflog] [--dry-run]
git refs verify [--strict] [--verbose]
git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
[(--sort=<key>)...] [--format=<format>]
[--include-root-refs] [--points-at=<object>]
@ -19,7 +20,6 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
[(--exclude=<pattern>)...] [--start-after=<marker>]
[ --stdin | (<pattern>...)]
git refs exists <ref>
git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>
git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]

View File

@ -243,6 +243,7 @@ with `flags` set to `0`.
Start an option group. `description` is a short string that
describes the group or an empty string.
Start the description with an upper-case letter.
Groups apply to options and subcommands that have a help string.

`OPT_HIDDEN_GROUP(description)`::
Like `OPT_GROUP()`, but the group header carries
@ -362,7 +363,8 @@ with `flags` set to `0`.

`OPT_SUBCOMMAND(long, &fn_ptr, subcommand_fn)`::
Define a subcommand. `subcommand_fn` is put into `fn_ptr` when
this subcommand is used.
this subcommand is used. The subcommand is not listed in the
usage output.

The last element of the array must be `OPT_END()`.


View File

@ -383,9 +383,9 @@ int cmd_refs(int argc,
const char * const refs_usage[] = {
REFS_MIGRATE_USAGE,
REFS_VERIFY_USAGE,
REFS_OPTIMIZE_USAGE,
"git refs list " COMMON_USAGE_FOR_EACH_REF,
REFS_EXISTS_USAGE,
REFS_OPTIMIZE_USAGE,
REFS_CREATE_USAGE,
REFS_DELETE_USAGE,
REFS_UPDATE_USAGE,
@ -394,15 +394,27 @@ int cmd_refs(int argc,
};
parse_opt_subcommand_fn *fn = NULL;
struct option opts[] = {
OPT_SUBCOMMAND("migrate", &fn, cmd_refs_migrate),
OPT_SUBCOMMAND("verify", &fn, cmd_refs_verify),
OPT_SUBCOMMAND("list", &fn, cmd_refs_list),
OPT_SUBCOMMAND("exists", &fn, cmd_refs_exists),
OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
OPT_SUBCOMMAND("create", &fn, cmd_refs_create),
OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
OPT_SUBCOMMAND("rename", &fn, cmd_refs_rename),
OPT_GROUP(N_("Reference database")),
OPT_SUBCOMMAND_F("migrate", &fn, cmd_refs_migrate,
N_("migrate the reference database to a different format"), 0),
OPT_SUBCOMMAND_F("verify", &fn, cmd_refs_verify,
N_("verify the consistency of the reference database"), 0),
OPT_SUBCOMMAND_F("optimize", &fn, cmd_refs_optimize,
N_("optimize the reference database"), 0),
OPT_GROUP(N_("Reading references")),
OPT_SUBCOMMAND_F("list", &fn, cmd_refs_list,
N_("list references"), 0),
OPT_SUBCOMMAND_F("exists", &fn, cmd_refs_exists,
N_("check whether a reference exists"), 0),
OPT_GROUP(N_("Writing references")),
OPT_SUBCOMMAND_F("create", &fn, cmd_refs_create,
N_("create a new reference"), 0),
OPT_SUBCOMMAND_F("delete", &fn, cmd_refs_delete,
N_("delete a reference"), 0),
OPT_SUBCOMMAND_F("update", &fn, cmd_refs_update,
N_("update an existing reference"), 0),
OPT_SUBCOMMAND_F("rename", &fn, cmd_refs_rename,
N_("rename a reference"), 0),
OPT_END(),
};


View File

@ -1940,7 +1940,7 @@ int cmd_remote(int argc,
OPT__VERBOSE(&verbose, N_("be verbose; must be placed before a subcommand")),
OPT_SUBCOMMAND("add", &fn, add),
OPT_SUBCOMMAND("rename", &fn, mv),
OPT_SUBCOMMAND_F("rm", &fn, rm, PARSE_OPT_NOCOMPLETE),
OPT_SUBCOMMAND_F("rm", &fn, rm, NULL, PARSE_OPT_NOCOMPLETE),
OPT_SUBCOMMAND("remove", &fn, rm),
OPT_SUBCOMMAND("set-head", &fn, set_head),
OPT_SUBCOMMAND("set-branches", &fn, set_branches),

View File

@ -2431,7 +2431,7 @@ int cmd_stash(int argc,
OPT_SUBCOMMAND("push", &fn, push_stash_unassumed),
OPT_SUBCOMMAND("export", &fn, export_stash),
OPT_SUBCOMMAND("import", &fn, import_stash),
OPT_SUBCOMMAND_F("save", &fn, save_stash, PARSE_OPT_NOCOMPLETE),
OPT_SUBCOMMAND_F("save", &fn, save_stash, NULL, PARSE_OPT_NOCOMPLETE),
OPT_END()
};
const char **args_copy;

View File

@ -845,6 +845,7 @@ static int show_gitcomp(const struct option *opts, int show_all)
{
const struct option *original_opts = opts;
int nr_noopts = 0;
bool shown = false;

for (; opts->type != OPTION_END; opts++) {
const char *prefix = "--";
@ -882,8 +883,9 @@ static int show_gitcomp(const struct option *opts, int show_all)
suffix = "=";
if (starts_with(opts->long_name, "no-"))
nr_noopts++;
printf("%s%s%s%s", opts == original_opts ? "" : " ",
printf("%s%s%s%s", shown ? " " : "",
prefix, opts->long_name, suffix);
shown = true;
}
show_negated_gitcomp(original_opts, show_all, -1);
show_negated_gitcomp(original_opts, show_all, nr_noopts);
@ -1323,6 +1325,100 @@ static const struct option *find_option_by_long_name(const struct option *opts,
return NULL;
}

static int usage_print_flag(const struct option *opt,
FILE *outfile,
const char **positive_name)
{
int off = 0;

if (opt->short_name) {
if (opt->flags & PARSE_OPT_NODASH)
off += fprintf(outfile, "%c", opt->short_name);
else
off += fprintf(outfile, "-%c", opt->short_name);
}
if (opt->long_name && opt->short_name)
off += fprintf(outfile, ", ");
if (opt->long_name) {
const char *long_name = opt->long_name;
if ((opt->flags & PARSE_OPT_NONEG) ||
skip_prefix(long_name, "no-", positive_name))
off += fprintf(outfile, "--%s", long_name);
else
off += fprintf(outfile, "--[no-]%s", long_name);
}

if (opt->type == OPTION_NUMBER)
off += utf8_fprintf(outfile, _("-NUM"));

if ((opt->flags & PARSE_OPT_LITERAL_ARGHELP) ||
!(opt->flags & PARSE_OPT_NOARG))
off += usage_argh(opt, outfile);

return off;
}

static void usage_print_option(const struct option *opt,
const struct option *all_opts,
int full,
int *need_newline,
FILE *outfile)
{
const char *positive_name = NULL;
const char *cp, *np;
size_t pos;

if (opt->type == OPTION_SUBCOMMAND && !opt->help)
return;
if (!full && (opt->flags & PARSE_OPT_HIDDEN))
return;
if (opt->type == OPTION_GROUP) {
fputc('\n', outfile);
*need_newline = 0;
if (*opt->help)
fprintf(outfile, "%s\n", _(opt->help));
return;
}

if (*need_newline) {
fputc('\n', outfile);
*need_newline = 0;
}

pos = usage_indent(outfile);
if (opt->type == OPTION_SUBCOMMAND)
pos += fprintf(outfile, "%s", opt->long_name);
else
pos += usage_print_flag(opt, outfile, &positive_name);

if (opt->type == OPTION_ALIAS) {
usage_padding(outfile, pos);
fprintf_ln(outfile, _("alias of --%s"),
(const char *)opt->value);
return;
}

for (cp = opt->help ? _(opt->help) : ""; *cp; cp = np) {
np = strchrnul(cp, '\n');
if (*np)
np++;
usage_padding(outfile, pos);
fwrite(cp, 1, np - cp, outfile);
pos = 0;
}
fputc('\n', outfile);

if (positive_name) {
if (find_option_by_long_name(all_opts, positive_name))
return;
pos = usage_indent(outfile);
pos += fprintf(outfile, "--%s", positive_name);
usage_padding(outfile, pos);
fprintf_ln(outfile, _("opposite of --no-%s"),
positive_name);
}
}

static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t *ctx,
const char * const *usagestr,
const struct option *opts,
@ -1408,81 +1504,9 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t
}

need_newline = 1;

for (; opts->type != OPTION_END; opts++) {
size_t pos;
const char *cp, *np;
const char *positive_name = NULL;

if (opts->type == OPTION_SUBCOMMAND)
continue;
if (!full && (opts->flags & PARSE_OPT_HIDDEN))
continue;
if (opts->type == OPTION_GROUP) {
fputc('\n', outfile);
need_newline = 0;
if (*opts->help)
fprintf(outfile, "%s\n", _(opts->help));
continue;
}

if (need_newline) {
fputc('\n', outfile);
need_newline = 0;
}

pos = usage_indent(outfile);
if (opts->short_name) {
if (opts->flags & PARSE_OPT_NODASH)
pos += fprintf(outfile, "%c", opts->short_name);
else
pos += fprintf(outfile, "-%c", opts->short_name);
}
if (opts->long_name && opts->short_name)
pos += fprintf(outfile, ", ");
if (opts->long_name) {
const char *long_name = opts->long_name;
if ((opts->flags & PARSE_OPT_NONEG) ||
skip_prefix(long_name, "no-", &positive_name))
pos += fprintf(outfile, "--%s", long_name);
else
pos += fprintf(outfile, "--[no-]%s", long_name);
}

if (opts->type == OPTION_NUMBER)
pos += utf8_fprintf(outfile, _("-NUM"));

if ((opts->flags & PARSE_OPT_LITERAL_ARGHELP) ||
!(opts->flags & PARSE_OPT_NOARG))
pos += usage_argh(opts, outfile);

if (opts->type == OPTION_ALIAS) {
usage_padding(outfile, pos);
fprintf_ln(outfile, _("alias of --%s"),
(const char *)opts->value);
continue;
}

for (cp = opts->help ? _(opts->help) : ""; *cp; cp = np) {
np = strchrnul(cp, '\n');
if (*np)
np++;
usage_padding(outfile, pos);
fwrite(cp, 1, np - cp, outfile);
pos = 0;
}
fputc('\n', outfile);

if (positive_name) {
if (find_option_by_long_name(all_opts, positive_name))
continue;
pos = usage_indent(outfile);
pos += fprintf(outfile, "--%s", positive_name);
usage_padding(outfile, pos);
fprintf_ln(outfile, _("opposite of --no-%s"),
positive_name);
}
}
for (; opts->type != OPTION_END; opts++)
usage_print_option(opts, all_opts, full,
&need_newline, outfile);
fputc('\n', outfile);

if (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)

View File

@ -396,14 +396,15 @@ static char *parse_options_noop_ignored_value MAYBE_UNUSED;

#define OPT_ALIAS(s, l, source_long_name) OPT_ALIAS_F(s, l, source_long_name, 0)

#define OPT_SUBCOMMAND_F(l, v, fn, f) { \
#define OPT_SUBCOMMAND_F(l, v, fn, h, f) { \
.type = OPTION_SUBCOMMAND, \
.long_name = (l), \
.value = (v), \
.help = (h), \
.flags = (f), \
.subcommand_fn = (fn), \
}
#define OPT_SUBCOMMAND(l, v, fn) OPT_SUBCOMMAND_F((l), (v), (fn), 0)
#define OPT_SUBCOMMAND(l, v, fn) OPT_SUBCOMMAND_F((l), (v), (fn), NULL, 0)

/*
* parse_options() will filter out the processed options and leave the

View File

@ -351,8 +351,10 @@ static int parse_subcommand__cmd(int argc, const char **argv,
parse_opt_subcommand_fn *fn = NULL;
int opt = 0;
struct option options[] = {
OPT_SUBCOMMAND("subcmd-one", &fn, subcmd_one),
OPT_GROUP("Subcommands"),
OPT_SUBCOMMAND_F("subcmd-one", &fn, subcmd_one, "the first subcommand", 0),
OPT_SUBCOMMAND("subcmd-two", &fn, subcmd_two),
OPT_GROUP("Options"),
OPT_INTEGER('o', "opt", &opt, "an integer option"),
OPT_END()
};

View File

@ -629,6 +629,22 @@ test_expect_success 'KEEP_UNKNOWN_OPT | NO_INTERNAL_HELP works' '
test_cmp expect actual
'

test_expect_success 'subcommand - usage lists subcommands with help text under their group' '
test-tool parse-subcommand cmd -h >actual &&
cat >expect <<-\EOF &&
usage: <...> cmd subcmd-one
or: <...> cmd subcmd-two

Subcommands
subcmd-one the first subcommand

Options
-o, --[no-]opt <n> an integer option

EOF
test_cmp expect actual
'

test_expect_success 'subcommand - no subcommand shows error and usage' '
test_expect_code 129 test-tool parse-subcommand cmd 2>err &&
test_grep "^error: need a subcommand" err &&