diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc index 1c0cf73e49..601bc470fb 100644 --- a/Documentation/git-refs.adoc +++ b/Documentation/git-refs.adoc @@ -11,6 +11,7 @@ SYNOPSIS [synopsis] git refs migrate --ref-storage-format= [--no-reflog] [--dry-run] git refs verify [--strict] [--verbose] +git refs optimize [--all] [--no-prune] [--auto] [--include ] [--exclude ] git refs list [--count=] [--shell|--perl|--python|--tcl] [(--sort=)...] [--format=] [--include-root-refs] [--points-at=] @@ -19,7 +20,6 @@ git refs list [--count=] [--shell|--perl|--python|--tcl] [(--exclude=)...] [--start-after=] [ --stdin | (...)] git refs exists -git refs optimize [--all] [--no-prune] [--auto] [--include ] [--exclude ] git refs create [--message=] [--no-deref] [--create-reflog] git refs delete [--message=] [--no-deref] [] git refs update [--message=] [--no-deref] [--create-reflog] [] diff --git a/Documentation/technical/api-parse-options.adoc b/Documentation/technical/api-parse-options.adoc index 95b7924e84..6dfea34220 100644 --- a/Documentation/technical/api-parse-options.adoc +++ b/Documentation/technical/api-parse-options.adoc @@ -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()`. diff --git a/builtin/refs.c b/builtin/refs.c index 31aafb9974..0847dfdd5d 100644 --- a/builtin/refs.c +++ b/builtin/refs.c @@ -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(), }; diff --git a/builtin/remote.c b/builtin/remote.c index de989ea3ba..fb7e5b114f 100644 --- a/builtin/remote.c +++ b/builtin/remote.c @@ -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), diff --git a/builtin/stash.c b/builtin/stash.c index fa3deeecbe..9a747691bf 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -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; diff --git a/parse-options.c b/parse-options.c index 51a49792d1..283a0f1dc2 100644 --- a/parse-options.c +++ b/parse-options.c @@ -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) diff --git a/parse-options.h b/parse-options.h index a0b30f3c04..f68b51ceca 100644 --- a/parse-options.h +++ b/parse-options.h @@ -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 diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index f181f0c02d..950db78673 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -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() }; diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index 449fff4d34..ec55bb1414 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -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 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 &&