From 38ed9faa1d9bfa72b33f59afdd0a09c17cb8dba0 Mon Sep 17 00:00:00 2001 From: Jiamu Sun <39@barroit.sh> Date: Tue, 10 Mar 2026 20:41:00 +0900 Subject: [PATCH 1/8] parseopt: extract subcommand handling from parse_options_step() Move the subcommand branch out of parse_options_step() into a new handle_subcommand() helper. Also, make parse_subcommand() return a simple success/failure status. This removes the switch over impossible parse_opt_result values and makes the non-option path easier to follow and maintain. Signed-off-by: Jiamu Sun <39@barroit.sh> Signed-off-by: Junio C Hamano --- parse-options.c | 86 ++++++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 40 deletions(-) diff --git a/parse-options.c b/parse-options.c index c9cafc21b9..33f26d6b61 100644 --- a/parse-options.c +++ b/parse-options.c @@ -605,17 +605,44 @@ static enum parse_opt_result parse_nodash_opt(struct parse_opt_ctx_t *p, return PARSE_OPT_ERROR; } -static enum parse_opt_result parse_subcommand(const char *arg, - const struct option *options) +static int parse_subcommand(const char *arg, const struct option *options) { - for (; options->type != OPTION_END; options++) - if (options->type == OPTION_SUBCOMMAND && - !strcmp(options->long_name, arg)) { - *(parse_opt_subcommand_fn **)options->value = options->subcommand_fn; - return PARSE_OPT_SUBCOMMAND; - } + for (; options->type != OPTION_END; options++) { + parse_opt_subcommand_fn **opt_val; - return PARSE_OPT_UNKNOWN; + if (options->type != OPTION_SUBCOMMAND || + strcmp(options->long_name, arg)) + continue; + + opt_val = options->value; + *opt_val = options->subcommand_fn; + return 0; + } + + return -1; +} + +static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx, + const char *arg, + const struct option *options, + const char * const usagestr[]) +{ + int err = parse_subcommand(arg, options); + + if (!err) + return PARSE_OPT_SUBCOMMAND; + + /* + * arg is neither a short or long option nor a subcommand. Since this + * command has a default operation mode, we have to treat this arg and + * all remaining args as args meant to that default operation mode. + * So we are done parsing. + */ + if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL) + return PARSE_OPT_DONE; + + error(_("unknown subcommand: `%s'"), arg); + usage_with_options(usagestr, options); } static void check_typos(const char *arg, const struct option *options) @@ -990,38 +1017,17 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx, if (*arg != '-' || !arg[1]) { if (parse_nodash_opt(ctx, arg, options) == 0) continue; - if (!ctx->has_subcommands) { - if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION) - return PARSE_OPT_NON_OPTION; - ctx->out[ctx->cpidx++] = ctx->argv[0]; - continue; - } - switch (parse_subcommand(arg, options)) { - case PARSE_OPT_SUBCOMMAND: - return PARSE_OPT_SUBCOMMAND; - case PARSE_OPT_UNKNOWN: - if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL) - /* - * arg is neither a short or long - * option nor a subcommand. Since - * this command has a default - * operation mode, we have to treat - * this arg and all remaining args - * as args meant to that default - * operation mode. - * So we are done parsing. - */ - return PARSE_OPT_DONE; - error(_("unknown subcommand: `%s'"), arg); - usage_with_options(usagestr, options); - case PARSE_OPT_COMPLETE: - case PARSE_OPT_HELP: - case PARSE_OPT_ERROR: - case PARSE_OPT_DONE: - case PARSE_OPT_NON_OPTION: - /* Impossible. */ - BUG("parse_subcommand() cannot return these"); + + if (ctx->has_subcommands) { + return handle_subcommand(ctx, arg, options, + usagestr); } + + if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION) + return PARSE_OPT_NON_OPTION; + + ctx->out[ctx->cpidx++] = ctx->argv[0]; + continue; } /* lone -h asks for help */ From db8b01619c1ad127ba4b550eefc9d1bd424d788e Mon Sep 17 00:00:00 2001 From: Jiamu Sun <39@barroit.sh> Date: Tue, 10 Mar 2026 20:41:01 +0900 Subject: [PATCH 2/8] help: make autocorrect handling reusable Move config parsing and prompt/delay handling into autocorrect.c and expose them in autocorrect.h. This makes autocorrect reusable regardless of which target links against it. Signed-off-by: Jiamu Sun <39@barroit.sh> Signed-off-by: Junio C Hamano --- Makefile | 1 + autocorrect.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++ autocorrect.h | 16 ++++++++++++ help.c | 64 +++------------------------------------------ 4 files changed, 93 insertions(+), 60 deletions(-) create mode 100644 autocorrect.c create mode 100644 autocorrect.h diff --git a/Makefile b/Makefile index f3264d0a37..6111631c2c 100644 --- a/Makefile +++ b/Makefile @@ -1098,6 +1098,7 @@ LIB_OBJS += archive-tar.o LIB_OBJS += archive-zip.o LIB_OBJS += archive.o LIB_OBJS += attr.o +LIB_OBJS += autocorrect.o LIB_OBJS += base85.o LIB_OBJS += bisect.o LIB_OBJS += blame.o diff --git a/autocorrect.c b/autocorrect.c new file mode 100644 index 0000000000..1037f03201 --- /dev/null +++ b/autocorrect.c @@ -0,0 +1,72 @@ +#include "git-compat-util.h" +#include "autocorrect.h" +#include "config.h" +#include "parse.h" +#include "strbuf.h" +#include "prompt.h" +#include "gettext.h" + +static int parse_autocorrect(const char *value) +{ + switch (git_parse_maybe_bool_text(value)) { + case 1: + return AUTOCORRECT_IMMEDIATELY; + case 0: + return AUTOCORRECT_SHOW; + default: /* other random text */ + break; + } + + if (!strcmp(value, "prompt")) + return AUTOCORRECT_PROMPT; + if (!strcmp(value, "never")) + return AUTOCORRECT_NEVER; + if (!strcmp(value, "immediate")) + return AUTOCORRECT_IMMEDIATELY; + if (!strcmp(value, "show")) + return AUTOCORRECT_SHOW; + + return 0; +} + +void autocorr_resolve_config(const char *var, const char *value, + const struct config_context *ctx, void *data) +{ + int *out = data; + + if (!strcmp(var, "help.autocorrect")) { + int v = parse_autocorrect(value); + + if (!v) { + v = git_config_int(var, value, ctx->kvi); + if (v < 0 || v == 1) + v = AUTOCORRECT_IMMEDIATELY; + } + + *out = v; + } +} + +void autocorr_confirm(int autocorrect, const char *assumed) +{ + if (autocorrect == AUTOCORRECT_IMMEDIATELY) { + fprintf_ln(stderr, + _("Continuing under the assumption that you meant '%s'."), + assumed); + } else if (autocorrect == AUTOCORRECT_PROMPT) { + char *answer; + struct strbuf msg = STRBUF_INIT; + + strbuf_addf(&msg, _("Run '%s' instead [y/N]? "), assumed); + answer = git_prompt(msg.buf, PROMPT_ECHO); + strbuf_release(&msg); + + if (!(starts_with(answer, "y") || starts_with(answer, "Y"))) + exit(1); + } else { + fprintf_ln(stderr, + _("Continuing in %0.1f seconds, assuming that you meant '%s'."), + (float)autocorrect / 10.0, assumed); + sleep_millisec(autocorrect * 100); + } +} diff --git a/autocorrect.h b/autocorrect.h new file mode 100644 index 0000000000..45609990c7 --- /dev/null +++ b/autocorrect.h @@ -0,0 +1,16 @@ +#ifndef AUTOCORRECT_H +#define AUTOCORRECT_H + +#define AUTOCORRECT_SHOW (-4) +#define AUTOCORRECT_PROMPT (-3) +#define AUTOCORRECT_NEVER (-2) +#define AUTOCORRECT_IMMEDIATELY (-1) + +struct config_context; + +void autocorr_resolve_config(const char *var, const char *value, + const struct config_context *ctx, void *data); + +void autocorr_confirm(int autocorr, const char *assumed); + +#endif /* AUTOCORRECT_H */ diff --git a/help.c b/help.c index 95f576c5c8..6be3ec9dfb 100644 --- a/help.c +++ b/help.c @@ -22,6 +22,7 @@ #include "repository.h" #include "alias.h" #include "utf8.h" +#include "autocorrect.h" #ifndef NO_CURL #include "git-curl-compat.h" /* For LIBCURL_VERSION only */ @@ -541,34 +542,6 @@ struct help_unknown_cmd_config { struct cmdnames aliases; }; -#define AUTOCORRECT_SHOW (-4) -#define AUTOCORRECT_PROMPT (-3) -#define AUTOCORRECT_NEVER (-2) -#define AUTOCORRECT_IMMEDIATELY (-1) - -static int parse_autocorrect(const char *value) -{ - switch (git_parse_maybe_bool_text(value)) { - case 1: - return AUTOCORRECT_IMMEDIATELY; - case 0: - return AUTOCORRECT_SHOW; - default: /* other random text */ - break; - } - - if (!strcmp(value, "prompt")) - return AUTOCORRECT_PROMPT; - if (!strcmp(value, "never")) - return AUTOCORRECT_NEVER; - if (!strcmp(value, "immediate")) - return AUTOCORRECT_IMMEDIATELY; - if (!strcmp(value, "show")) - return AUTOCORRECT_SHOW; - - return 0; -} - static int git_unknown_cmd_config(const char *var, const char *value, const struct config_context *ctx, void *cb) @@ -577,17 +550,7 @@ static int git_unknown_cmd_config(const char *var, const char *value, const char *subsection, *key; size_t subsection_len; - if (!strcmp(var, "help.autocorrect")) { - int v = parse_autocorrect(value); - - if (!v) { - v = git_config_int(var, value, ctx->kvi); - if (v < 0 || v == 1) - v = AUTOCORRECT_IMMEDIATELY; - } - - cfg->autocorrect = v; - } + autocorr_resolve_config(var, value, ctx, &cfg->autocorrect); /* Also use aliases for command lookup */ if (!parse_config_key(var, "alias", &subsection, &subsection_len, @@ -724,27 +687,8 @@ char *help_unknown_cmd(const char *cmd) _("WARNING: You called a Git command named '%s', " "which does not exist."), cmd); - if (cfg.autocorrect == AUTOCORRECT_IMMEDIATELY) - fprintf_ln(stderr, - _("Continuing under the assumption that " - "you meant '%s'."), - assumed); - else if (cfg.autocorrect == AUTOCORRECT_PROMPT) { - char *answer; - struct strbuf msg = STRBUF_INIT; - strbuf_addf(&msg, _("Run '%s' instead [y/N]? "), assumed); - answer = git_prompt(msg.buf, PROMPT_ECHO); - strbuf_release(&msg); - if (!(starts_with(answer, "y") || - starts_with(answer, "Y"))) - exit(1); - } else { - fprintf_ln(stderr, - _("Continuing in %0.1f seconds, " - "assuming that you meant '%s'."), - (float)cfg.autocorrect/10.0, assumed); - sleep_millisec(cfg.autocorrect * 100); - } + + autocorr_confirm(cfg.autocorrect, assumed); cmdnames_release(&cfg.aliases); cmdnames_release(&main_cmds); From c8ac3d589a848be7752325703b5c4cd8b0f34cfa Mon Sep 17 00:00:00 2001 From: Jiamu Sun <39@barroit.sh> Date: Tue, 10 Mar 2026 20:41:02 +0900 Subject: [PATCH 3/8] help: move tty check for autocorrection to autocorrect.c TTY checking is the autocorrect config parser's responsibility. It must ensure the parsed value is correct and reliable. Thus, move the check to autocorr_resolve_config(). Signed-off-by: Jiamu Sun <39@barroit.sh> Signed-off-by: Junio C Hamano --- autocorrect.c | 50 +++++++++++++++++++++++++++++--------------------- autocorrect.h | 20 ++++++++++++++------ help.c | 17 ++++++----------- 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/autocorrect.c b/autocorrect.c index 1037f03201..50d7f116d8 100644 --- a/autocorrect.c +++ b/autocorrect.c @@ -6,7 +6,7 @@ #include "prompt.h" #include "gettext.h" -static int parse_autocorrect(const char *value) +static enum autocorr_mode parse_autocorrect(const char *value) { switch (git_parse_maybe_bool_text(value)) { case 1: @@ -19,41 +19,49 @@ static int parse_autocorrect(const char *value) if (!strcmp(value, "prompt")) return AUTOCORRECT_PROMPT; - if (!strcmp(value, "never")) + else if (!strcmp(value, "never")) return AUTOCORRECT_NEVER; - if (!strcmp(value, "immediate")) + else if (!strcmp(value, "immediate")) return AUTOCORRECT_IMMEDIATELY; - if (!strcmp(value, "show")) + else if (!strcmp(value, "show")) return AUTOCORRECT_SHOW; - - return 0; + else + return AUTOCORRECT_DELAY; } void autocorr_resolve_config(const char *var, const char *value, const struct config_context *ctx, void *data) { - int *out = data; + struct autocorr *conf = data; - if (!strcmp(var, "help.autocorrect")) { - int v = parse_autocorrect(value); + if (strcmp(var, "help.autocorrect")) + return; - if (!v) { - v = git_config_int(var, value, ctx->kvi); - if (v < 0 || v == 1) - v = AUTOCORRECT_IMMEDIATELY; - } + conf->mode = parse_autocorrect(value); - *out = v; + /* + * Disable autocorrection prompt in a non-interactive session. + */ + if (conf->mode == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2))) + conf->mode = AUTOCORRECT_NEVER; + + if (conf->mode == AUTOCORRECT_DELAY) { + conf->delay = git_config_int(var, value, ctx->kvi); + + if (!conf->delay) + conf->mode = AUTOCORRECT_SHOW; + else if (conf->delay <= 1) + conf->mode = AUTOCORRECT_IMMEDIATELY; } } -void autocorr_confirm(int autocorrect, const char *assumed) +void autocorr_confirm(struct autocorr *conf, const char *assumed) { - if (autocorrect == AUTOCORRECT_IMMEDIATELY) { + if (conf->mode == AUTOCORRECT_IMMEDIATELY) { fprintf_ln(stderr, _("Continuing under the assumption that you meant '%s'."), assumed); - } else if (autocorrect == AUTOCORRECT_PROMPT) { + } else if (conf->mode == AUTOCORRECT_PROMPT) { char *answer; struct strbuf msg = STRBUF_INIT; @@ -63,10 +71,10 @@ void autocorr_confirm(int autocorrect, const char *assumed) if (!(starts_with(answer, "y") || starts_with(answer, "Y"))) exit(1); - } else { + } else if (conf->mode == AUTOCORRECT_DELAY) { fprintf_ln(stderr, _("Continuing in %0.1f seconds, assuming that you meant '%s'."), - (float)autocorrect / 10.0, assumed); - sleep_millisec(autocorrect * 100); + conf->delay / 10.0, assumed); + sleep_millisec(conf->delay * 100); } } diff --git a/autocorrect.h b/autocorrect.h index 45609990c7..ce4a68379f 100644 --- a/autocorrect.h +++ b/autocorrect.h @@ -1,16 +1,24 @@ #ifndef AUTOCORRECT_H #define AUTOCORRECT_H -#define AUTOCORRECT_SHOW (-4) -#define AUTOCORRECT_PROMPT (-3) -#define AUTOCORRECT_NEVER (-2) -#define AUTOCORRECT_IMMEDIATELY (-1) - struct config_context; +enum autocorr_mode { + AUTOCORRECT_SHOW, + AUTOCORRECT_NEVER, + AUTOCORRECT_PROMPT, + AUTOCORRECT_IMMEDIATELY, + AUTOCORRECT_DELAY, +}; + +struct autocorr { + enum autocorr_mode mode; + int delay; +}; + void autocorr_resolve_config(const char *var, const char *value, const struct config_context *ctx, void *data); -void autocorr_confirm(int autocorr, const char *assumed); +void autocorr_confirm(struct autocorr *conf, const char *assumed); #endif /* AUTOCORRECT_H */ diff --git a/help.c b/help.c index 6be3ec9dfb..566d33299b 100644 --- a/help.c +++ b/help.c @@ -538,7 +538,7 @@ int is_in_cmdlist(struct cmdnames *c, const char *s) } struct help_unknown_cmd_config { - int autocorrect; + struct autocorr autocorr; struct cmdnames aliases; }; @@ -550,7 +550,7 @@ static int git_unknown_cmd_config(const char *var, const char *value, const char *subsection, *key; size_t subsection_len; - autocorr_resolve_config(var, value, ctx, &cfg->autocorrect); + autocorr_resolve_config(var, value, ctx, &cfg->autocorr); /* Also use aliases for command lookup */ if (!parse_config_key(var, "alias", &subsection, &subsection_len, @@ -607,13 +607,7 @@ char *help_unknown_cmd(const char *cmd) read_early_config(the_repository, git_unknown_cmd_config, &cfg); - /* - * Disable autocorrection prompt in a non-interactive session - */ - if ((cfg.autocorrect == AUTOCORRECT_PROMPT) && (!isatty(0) || !isatty(2))) - cfg.autocorrect = AUTOCORRECT_NEVER; - - if (cfg.autocorrect == AUTOCORRECT_NEVER) { + if (cfg.autocorr.mode == AUTOCORRECT_NEVER) { fprintf_ln(stderr, _("git: '%s' is not a git command. See 'git --help'."), cmd); exit(1); } @@ -679,7 +673,8 @@ char *help_unknown_cmd(const char *cmd) n++) ; /* still counting */ } - if (cfg.autocorrect && cfg.autocorrect != AUTOCORRECT_SHOW && n == 1 && + + if (cfg.autocorr.mode != AUTOCORRECT_SHOW && n == 1 && SIMILAR_ENOUGH(best_similarity)) { char *assumed = xstrdup(main_cmds.names[0]->name); @@ -688,7 +683,7 @@ char *help_unknown_cmd(const char *cmd) "which does not exist."), cmd); - autocorr_confirm(cfg.autocorrect, assumed); + autocorr_confirm(&cfg.autocorr, assumed); cmdnames_release(&cfg.aliases); cmdnames_release(&main_cmds); From 2a48e5668be6d6ffa8c59ecfc39d0df7585ce752 Mon Sep 17 00:00:00 2001 From: Jiamu Sun <39@barroit.sh> Date: Tue, 10 Mar 2026 20:41:03 +0900 Subject: [PATCH 4/8] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINTONLY AUTOCORRECT_SHOW is ambiguous. Its purpose is to show commands similar to the unknown one and take no other action. Rename it to fit the semantics. Signed-off-by: Jiamu Sun <39@barroit.sh> Signed-off-by: Junio C Hamano --- autocorrect.c | 6 +++--- autocorrect.h | 2 +- help.c | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/autocorrect.c b/autocorrect.c index 50d7f116d8..9c4b691fb0 100644 --- a/autocorrect.c +++ b/autocorrect.c @@ -12,7 +12,7 @@ static enum autocorr_mode parse_autocorrect(const char *value) case 1: return AUTOCORRECT_IMMEDIATELY; case 0: - return AUTOCORRECT_SHOW; + return AUTOCORRECT_HINTONLY; default: /* other random text */ break; } @@ -24,7 +24,7 @@ static enum autocorr_mode parse_autocorrect(const char *value) else if (!strcmp(value, "immediate")) return AUTOCORRECT_IMMEDIATELY; else if (!strcmp(value, "show")) - return AUTOCORRECT_SHOW; + return AUTOCORRECT_HINTONLY; else return AUTOCORRECT_DELAY; } @@ -49,7 +49,7 @@ void autocorr_resolve_config(const char *var, const char *value, conf->delay = git_config_int(var, value, ctx->kvi); if (!conf->delay) - conf->mode = AUTOCORRECT_SHOW; + conf->mode = AUTOCORRECT_HINTONLY; else if (conf->delay <= 1) conf->mode = AUTOCORRECT_IMMEDIATELY; } diff --git a/autocorrect.h b/autocorrect.h index ce4a68379f..be4e3e8b20 100644 --- a/autocorrect.h +++ b/autocorrect.h @@ -4,7 +4,7 @@ struct config_context; enum autocorr_mode { - AUTOCORRECT_SHOW, + AUTOCORRECT_HINTONLY, AUTOCORRECT_NEVER, AUTOCORRECT_PROMPT, AUTOCORRECT_IMMEDIATELY, diff --git a/help.c b/help.c index 566d33299b..6158545e48 100644 --- a/help.c +++ b/help.c @@ -674,7 +674,7 @@ char *help_unknown_cmd(const char *cmd) ; /* still counting */ } - if (cfg.autocorr.mode != AUTOCORRECT_SHOW && n == 1 && + if (cfg.autocorr.mode != AUTOCORRECT_HINTONLY && n == 1 && SIMILAR_ENOUGH(best_similarity)) { char *assumed = xstrdup(main_cmds.names[0]->name); From 46483152a40c803dd0cddcbf9ce2248f9cc23fc6 Mon Sep 17 00:00:00 2001 From: Jiamu Sun <39@barroit.sh> Date: Tue, 10 Mar 2026 20:41:04 +0900 Subject: [PATCH 5/8] autocorrect: provide config resolution API Add autocorr_resolve(). This resolves and populates the correct values for autocorrect config. Make autocorrect config callback internal. The API is meant to provide a high-level way to retrieve the config. Allowing access to the config callback from outside violates that intent. Additionally, in some cases, without access to the config callback, two config iterations cannot be merged into one, which can hurt performance. This is fine, as the code path that calls autocorr_resolve() is cold. Signed-off-by: Jiamu Sun <39@barroit.sh> Signed-off-by: Junio C Hamano --- autocorrect.c | 15 ++++++++++++--- autocorrect.h | 5 +---- help.c | 37 +++++++++++++++---------------------- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/autocorrect.c b/autocorrect.c index 9c4b691fb0..63fa331ef5 100644 --- a/autocorrect.c +++ b/autocorrect.c @@ -1,3 +1,5 @@ +#define USE_THE_REPOSITORY_VARIABLE + #include "git-compat-util.h" #include "autocorrect.h" #include "config.h" @@ -29,13 +31,13 @@ static enum autocorr_mode parse_autocorrect(const char *value) return AUTOCORRECT_DELAY; } -void autocorr_resolve_config(const char *var, const char *value, - const struct config_context *ctx, void *data) +static int resolve_autocorr(const char *var, const char *value, + const struct config_context *ctx, void *data) { struct autocorr *conf = data; if (strcmp(var, "help.autocorrect")) - return; + return 0; conf->mode = parse_autocorrect(value); @@ -53,6 +55,13 @@ void autocorr_resolve_config(const char *var, const char *value, else if (conf->delay <= 1) conf->mode = AUTOCORRECT_IMMEDIATELY; } + + return 0; +} + +void autocorr_resolve(struct autocorr *conf) +{ + read_early_config(the_repository, resolve_autocorr, conf); } void autocorr_confirm(struct autocorr *conf, const char *assumed) diff --git a/autocorrect.h b/autocorrect.h index be4e3e8b20..5d82c49903 100644 --- a/autocorrect.h +++ b/autocorrect.h @@ -1,8 +1,6 @@ #ifndef AUTOCORRECT_H #define AUTOCORRECT_H -struct config_context; - enum autocorr_mode { AUTOCORRECT_HINTONLY, AUTOCORRECT_NEVER, @@ -16,8 +14,7 @@ struct autocorr { int delay; }; -void autocorr_resolve_config(const char *var, const char *value, - const struct config_context *ctx, void *data); +void autocorr_resolve(struct autocorr *conf); void autocorr_confirm(struct autocorr *conf, const char *assumed); diff --git a/help.c b/help.c index 6158545e48..4adb1fcf07 100644 --- a/help.c +++ b/help.c @@ -537,32 +537,24 @@ int is_in_cmdlist(struct cmdnames *c, const char *s) return 0; } -struct help_unknown_cmd_config { - struct autocorr autocorr; - struct cmdnames aliases; -}; - -static int git_unknown_cmd_config(const char *var, const char *value, - const struct config_context *ctx, - void *cb) +static int resolve_aliases(const char *var, const char *value UNUSED, + const struct config_context *ctx UNUSED, void *data) { - struct help_unknown_cmd_config *cfg = cb; + struct cmdnames *aliases = data; const char *subsection, *key; size_t subsection_len; - autocorr_resolve_config(var, value, ctx, &cfg->autocorr); - /* Also use aliases for command lookup */ if (!parse_config_key(var, "alias", &subsection, &subsection_len, &key)) { if (subsection) { /* [alias "name"] command = value */ if (!strcmp(key, "command")) - add_cmdname(&cfg->aliases, subsection, + add_cmdname(aliases, subsection, subsection_len); } else { /* alias.name = value */ - add_cmdname(&cfg->aliases, key, strlen(key)); + add_cmdname(aliases, key, strlen(key)); } } @@ -599,22 +591,24 @@ static const char bad_interpreter_advice[] = char *help_unknown_cmd(const char *cmd) { - struct help_unknown_cmd_config cfg = { 0 }; + struct cmdnames aliases = { 0 }; + struct autocorr autocorr = { 0 }; int i, n, best_similarity = 0; struct cmdnames main_cmds = { 0 }; struct cmdnames other_cmds = { 0 }; struct cmdname_help *common_cmds; - read_early_config(the_repository, git_unknown_cmd_config, &cfg); + autocorr_resolve(&autocorr); - if (cfg.autocorr.mode == AUTOCORRECT_NEVER) { + if (autocorr.mode == AUTOCORRECT_NEVER) { fprintf_ln(stderr, _("git: '%s' is not a git command. See 'git --help'."), cmd); exit(1); } load_command_list("git-", &main_cmds, &other_cmds); + read_early_config(the_repository, resolve_aliases, &aliases); - add_cmd_list(&main_cmds, &cfg.aliases); + add_cmd_list(&main_cmds, &aliases); add_cmd_list(&main_cmds, &other_cmds); QSORT(main_cmds.names, main_cmds.cnt, cmdname_compare); uniq(&main_cmds); @@ -674,18 +668,17 @@ char *help_unknown_cmd(const char *cmd) ; /* still counting */ } - if (cfg.autocorr.mode != AUTOCORRECT_HINTONLY && n == 1 && + if (autocorr.mode != AUTOCORRECT_HINTONLY && n == 1 && SIMILAR_ENOUGH(best_similarity)) { char *assumed = xstrdup(main_cmds.names[0]->name); fprintf_ln(stderr, - _("WARNING: You called a Git command named '%s', " - "which does not exist."), + _("WARNING: You called a Git command named '%s', which does not exist."), cmd); - autocorr_confirm(&cfg.autocorr, assumed); + autocorr_confirm(&autocorr, assumed); - cmdnames_release(&cfg.aliases); + cmdnames_release(&aliases); cmdnames_release(&main_cmds); cmdnames_release(&other_cmds); return assumed; From bdd1ed4ad245280efcabf243ecbe27973d526e39 Mon Sep 17 00:00:00 2001 From: Jiamu Sun <39@barroit.sh> Date: Tue, 10 Mar 2026 20:41:05 +0900 Subject: [PATCH 6/8] parseopt: autocorrect mistyped subcommands Try to autocorrect the mistyped mandatory subcommand before showing an error and exiting. Subcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL are skipped. In autocorrect_subcommand(), AUTOCORR_HINTONLY does the same as AUTOCORR_NEVER, because builtins have a limited number of subcommands. Those lists are currently not too large. Therefore, displaying all subcommands via usage_with_options() is good enough here. This also keeps the autocorrection handling simple. Use a dynamic threshold for similar_enough() to check if the result is usable. This can yield more accurate typo corrections. Even though subcommands are often short, they can still vary across builtins. And in the current implementation, a fixed threshold can't do better on both short and long subcommands at the same time. Signed-off-by: Jiamu Sun <39@barroit.sh> Signed-off-by: Junio C Hamano --- parse-options.c | 76 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/parse-options.c b/parse-options.c index 33f26d6b61..227bc74991 100644 --- a/parse-options.c +++ b/parse-options.c @@ -6,6 +6,8 @@ #include "strbuf.h" #include "string-list.h" #include "utf8.h" +#include "autocorrect.h" +#include "levenshtein.h" static int disallow_abbreviated_options; @@ -622,13 +624,72 @@ static int parse_subcommand(const char *arg, const struct option *options) return -1; } +static void find_subcommands(struct string_list *list, + const struct option *options) +{ + for (; options->type != OPTION_END; options++) { + if (options->type == OPTION_SUBCOMMAND) + string_list_append(list, options->long_name); + } +} + +static int similar_enough(const char *cmd, unsigned int dist) +{ + size_t len = strlen(cmd); + unsigned int threshold = len < 3 ? 1 : len < 6 ? 3 : 6; + + return dist < threshold; +} + +static const char *autocorrect_subcommand(const char *cmd, + struct string_list *cmds) +{ + struct autocorr autocorr = { 0 }; + unsigned int min = UINT_MAX; + unsigned int ties = 0; + struct string_list_item *cand; + struct string_list_item *best = NULL; + + autocorr_resolve(&autocorr); + + if (autocorr.mode == AUTOCORRECT_NEVER || + autocorr.mode == AUTOCORRECT_HINTONLY) + return NULL; + + for_each_string_list_item(cand, cmds) { + unsigned int dist = levenshtein(cmd, cand->string, 0, 2, 1, 3); + + if (dist < min) { + min = dist; + best = cand; + ties = 0; + } else if (dist == min) { + ties++; + } + } + + if (!ties && similar_enough(cmd, min)) { + fprintf_ln(stderr, + _("WARNING: You called a subcommand named '%s', which does not exist."), + cmd); + + autocorr_confirm(&autocorr, best->string); + return best->string; + } + + return NULL; +} + static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx, const char *arg, const struct option *options, const char * const usagestr[]) { - int err = parse_subcommand(arg, options); + int err; + const char *assumed; + struct string_list cmds = STRING_LIST_INIT_NODUP; + err = parse_subcommand(arg, options); if (!err) return PARSE_OPT_SUBCOMMAND; @@ -641,8 +702,17 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx, if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL) return PARSE_OPT_DONE; - error(_("unknown subcommand: `%s'"), arg); - usage_with_options(usagestr, options); + find_subcommands(&cmds, options); + assumed = autocorrect_subcommand(arg, &cmds); + + if (!assumed) { + error(_("unknown subcommand: `%s'"), arg); + usage_with_options(usagestr, options); + } + + string_list_clear(&cmds, 0); + parse_subcommand(assumed, options); + return PARSE_OPT_SUBCOMMAND; } static void check_typos(const char *arg, const struct option *options) From 94d7bdf64651dde699b2cb03d506f36fd26acc4b Mon Sep 17 00:00:00 2001 From: Jiamu Sun <39@barroit.sh> Date: Tue, 10 Mar 2026 20:41:06 +0900 Subject: [PATCH 7/8] parseopt: enable subcommand autocorrection for git-remote and git-notes Add PARSE_OPT_SUBCOMMAND_AUTOCORR to enable autocorrection for subcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL. Use it for git-remote and git-notes, so mistyped subcommands can be automatically corrected, and builtin entry points no longer need to handle the unknown subcommand error path themselves. This is safe for these two builtins, because they either resolve to a single subcommand or take no subcommand at all. This means that if the subcommand parser encounters an unknown argument, it must be a mistyped subcommand. Signed-off-by: Jiamu Sun <39@barroit.sh> Signed-off-by: Junio C Hamano --- builtin/notes.c | 10 +++------- builtin/remote.c | 12 ++++-------- parse-options.c | 16 +++++++++------- parse-options.h | 1 + 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/builtin/notes.c b/builtin/notes.c index 9af602bdd7..087eb898a4 100644 --- a/builtin/notes.c +++ b/builtin/notes.c @@ -1149,14 +1149,10 @@ int cmd_notes(int argc, repo_config(the_repository, git_default_config, NULL); argc = parse_options(argc, argv, prefix, options, git_notes_usage, - PARSE_OPT_SUBCOMMAND_OPTIONAL); - if (!fn) { - if (argc) { - error(_("unknown subcommand: `%s'"), argv[0]); - usage_with_options(git_notes_usage, options); - } + PARSE_OPT_SUBCOMMAND_OPTIONAL | + PARSE_OPT_SUBCOMMAND_AUTOCORR); + if (!fn) fn = list; - } if (override_notes_ref) { struct strbuf sb = STRBUF_INIT; diff --git a/builtin/remote.c b/builtin/remote.c index 0fddaa1773..9415f6cb03 100644 --- a/builtin/remote.c +++ b/builtin/remote.c @@ -1953,15 +1953,11 @@ int cmd_remote(int argc, }; argc = parse_options(argc, argv, prefix, options, builtin_remote_usage, - PARSE_OPT_SUBCOMMAND_OPTIONAL); + PARSE_OPT_SUBCOMMAND_OPTIONAL | + PARSE_OPT_SUBCOMMAND_AUTOCORR); - if (fn) { + if (fn) return !!fn(argc, argv, prefix, repo); - } else { - if (argc) { - error(_("unknown subcommand: `%s'"), argv[0]); - usage_with_options(builtin_remote_usage, options); - } + else return !!show_all(); - } } diff --git a/parse-options.c b/parse-options.c index 227bc74991..6ac4bd30e2 100644 --- a/parse-options.c +++ b/parse-options.c @@ -693,14 +693,16 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx, if (!err) return PARSE_OPT_SUBCOMMAND; - /* - * arg is neither a short or long option nor a subcommand. Since this - * command has a default operation mode, we have to treat this arg and - * all remaining args as args meant to that default operation mode. - * So we are done parsing. - */ - if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL) + if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL && + !(ctx->flags & PARSE_OPT_SUBCOMMAND_AUTOCORR)) { + /* + * arg is neither a short or long option nor a subcommand. + * Since this command has a default operation mode, we have to + * treat this arg and all remaining args as args meant to that + * default operation mode. So we are done parsing. + */ return PARSE_OPT_DONE; + } find_subcommands(&cmds, options); assumed = autocorrect_subcommand(arg, &cmds); diff --git a/parse-options.h b/parse-options.h index 706de9729f..f29ac33789 100644 --- a/parse-options.h +++ b/parse-options.h @@ -40,6 +40,7 @@ enum parse_opt_flags { PARSE_OPT_ONE_SHOT = 1 << 5, PARSE_OPT_SHELL_EVAL = 1 << 6, PARSE_OPT_SUBCOMMAND_OPTIONAL = 1 << 7, + PARSE_OPT_SUBCOMMAND_AUTOCORR = 1 << 8, }; enum parse_opt_option_flags { From 92e2a91a77d89e4c12c1dea932197a682b94c80d Mon Sep 17 00:00:00 2001 From: Jiamu Sun <39@barroit.sh> Date: Tue, 10 Mar 2026 20:41:07 +0900 Subject: [PATCH 8/8] help: add tests for subcommand autocorrection These tests cover default behavior (help.autocorrect is unset), no correction, immediate correction, delayed correction, and rejection when the typo is too dissimilar. Signed-off-by: Jiamu Sun <39@barroit.sh> Signed-off-by: Junio C Hamano --- t/t9004-autocorrect-subcommand.sh | 51 +++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100755 t/t9004-autocorrect-subcommand.sh diff --git a/t/t9004-autocorrect-subcommand.sh b/t/t9004-autocorrect-subcommand.sh new file mode 100755 index 0000000000..d10031659b --- /dev/null +++ b/t/t9004-autocorrect-subcommand.sh @@ -0,0 +1,51 @@ +#!/bin/sh + +test_description='subcommand auto-correction test + +Test autocorrection for subcommands with different +help.autocorrect mode.' + +. ./test-lib.sh + +test_expect_success 'setup' " + echo '^error: unknown subcommand: ' >grep_unknown +" + +test_expect_success 'default is not to autocorrect' ' + test_must_fail git worktree lsit 2>actual && + head -n1 actual >first && test_grep -f grep_unknown first +' + +for mode in false no off 0 show never +do + test_expect_success "'$mode' disables autocorrection" " + test_config help.autocorrect $mode && + + test_must_fail git worktree lsit 2>actual && + head -n1 actual >first && test_grep -f grep_unknown first + " +done + +for mode in -39 immediate 1 +do + test_expect_success "autocorrect immediately with '$mode'" - <<-EOT + test_config help.autocorrect $mode && + + git worktree lsit 2>actual && + test_grep "you meant 'list'\.$" actual + EOT +done + +test_expect_success 'delay path is executed' - <<-\EOT + test_config help.autocorrect 2 && + + git worktree lsit 2>actual && + test_grep '^Continuing in 0.2 seconds, ' actual +EOT + +test_expect_success 'deny if too dissimilar' - <<-\EOT + test_must_fail git remote rensnr 2>actual && + head -n1 actual >first && test_grep -f grep_unknown first +EOT + +test_done