{"thread":{"id":"65168","subject":"[PATCH 0/5] parseopt: add subcommand autocorrection","startedAt":"2026-03-08T12:17:53Z","lastAt":"2026-06-06T14:23:24Z","messageCount":96,"participants":["Jiamu Sun","Junio C Hamano","Karthik Nayak","Aaron Plattner"],"isPatch":true,"patchVersion":1,"patchTotal":5},"messages":[{"id":"538196","messageId":"SY0P300MB08019805A8304105FA805EB1CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":null,"subject":"[PATCH 0/5] parseopt: add subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-08T12:17:20Z","receivedAt":"2026-03-08T12:17:53Z","isPatch":true,"body":"Git currently provides auto-correction for builtins and aliases, but\nlacks this functionality for subcommands parsed via the parse-options\nAPI. Subcommands are also commands, and typos will occur, too. Like:\n\n\tgit remote add-rul\n\nSo, this series introduces subcommand auto-correction.\n\nCurrently, builtins with mandatory subcommands enable autocorrection by\ndefault. However, those using PARSE_OPT_SUBCOMMAND_OPTIONAL skip it to\navoid treating valid unknown arguments as mistyped subcommands.\n\nThis series adds PARSE_OPT_SUBCOMMAND_AUTOCORR, allowing commands with\noptional subcommands to explicitly opt in to autocorrection. It then\nenables this flag for git-remote and git-notes.\n\nAdditionally, it extracts the existing autocorrection logic from help.c\nso subcommand handling can reuse the same config parsing and\nprompt/delay logic.\n\nJiamu Sun (5):\n  parseopt: extract subcommand handling from parse_options_step()\n  help: refactor command autocorrection handling\n  parseopt: autocorrect mistyped subcommands\n  parseopt: enable subcommand autocorrect for remote and notes\n  help: add tests for subcommand autocorrection\n\n Makefile                          |   1 +\n autocorrect.c                     |  92 +++++++++++++++++++\n autocorrect.h                     |  23 +++++\n builtin/notes.c                   |  10 +-\n builtin/remote.c                  |  12 +--\n help.c                            | 106 ++++-----------------\n parse-options.c                   | 147 +++++++++++++++++++++++-------\n parse-options.h                   |   1 +\n t/t9004-autocorrect-subcommand.sh |  49 ++++++++++\n 9 files changed, 306 insertions(+), 135 deletions(-)\n create mode 100644 autocorrect.c\n create mode 100644 autocorrect.h\n create mode 100755 t/t9004-autocorrect-subcommand.sh\n\n\nbase-commit: 795c338de725e13bd361214c6b768019fc45a2c1\n-- \n2.53.0\n\n"},{"id":"538197","messageId":"SY0P300MB0801422323C4C4185B9617A1CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB08019805A8304105FA805EB1CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH 1/5] parseopt: extract subcommand handling from parse_options_step()","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-08T12:17:21Z","receivedAt":"2026-03-08T12:17:55Z","isPatch":true,"body":"Move the subcommand branch out of parse_options_step() into a new\nhandle_subcommand() helper. Also, make parse_subcommand() return a\nsimple success/failure status.\n\nThis removes the switch over impossible parse_opt_result values and\nmakes the non-option path easier to follow and maintain.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n parse-options.c | 75 ++++++++++++++++++++++++++-----------------------\n 1 file changed, 40 insertions(+), 35 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex c9cafc21b903..6bb0c5697099 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -605,17 +605,43 @@ static enum parse_opt_result parse_nodash_opt(struct parse_opt_ctx_t *p,\n \treturn PARSE_OPT_ERROR;\n }\n \n-static enum parse_opt_result parse_subcommand(const char *arg,\n-\t\t\t\t\t      const struct option *options)\n+static int parse_subcommand(const char *arg, const struct option *options)\n {\n-\tfor (; options->type != OPTION_END; options++)\n-\t\tif (options->type == OPTION_SUBCOMMAND &&\n-\t\t    !strcmp(options->long_name, arg)) {\n-\t\t\t*(parse_opt_subcommand_fn **)options->value = options->subcommand_fn;\n-\t\t\treturn PARSE_OPT_SUBCOMMAND;\n-\t\t}\n+\tfor (; options->type != OPTION_END; options++) {\n+\t\tif (options->type != OPTION_SUBCOMMAND ||\n+\t\t    strcmp(options->long_name, arg))\n+\t\t\tcontinue;\n \n-\treturn PARSE_OPT_UNKNOWN;\n+\t\tparse_opt_subcommand_fn **opt_val = options->value;\n+\t\t*opt_val = options->subcommand_fn;\n+\n+\t\treturn 0;\n+\t}\n+\n+\treturn -1;\n+}\n+\n+static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n+\t\t\t\t\t       const char *arg,\n+\t\t\t\t\t       const struct option *options,\n+\t\t\t\t\t       const char * const usagestr[])\n+{\n+\tint err = parse_subcommand(arg, options);\n+\n+\tif (!err)\n+\t\treturn PARSE_OPT_SUBCOMMAND;\n+\n+\t/*\n+\t * arg is neither a short or long option nor a subcommand.  Since this\n+\t * command has a default operation mode, we have to treat this arg and\n+\t * all remaining args as args meant to that default operation mode.\n+\t * So we are done parsing.\n+\t */\n+\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n+\t\treturn PARSE_OPT_DONE;\n+\n+\terror(_(\"unknown subcommand: `%s'\"), arg);\n+\tusage_with_options(usagestr, options);\n }\n \n static void check_typos(const char *arg, const struct option *options)\n@@ -990,37 +1016,16 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\tif (*arg != '-' || !arg[1]) {\n \t\t\tif (parse_nodash_opt(ctx, arg, options) == 0)\n \t\t\t\tcontinue;\n+\n \t\t\tif (!ctx->has_subcommands) {\n \t\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n \t\t\t\t\treturn PARSE_OPT_NON_OPTION;\n \t\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n \t\t\t\tcontinue;\n-\t\t\t}\n-\t\t\tswitch (parse_subcommand(arg, options)) {\n-\t\t\tcase PARSE_OPT_SUBCOMMAND:\n-\t\t\t\treturn PARSE_OPT_SUBCOMMAND;\n-\t\t\tcase PARSE_OPT_UNKNOWN:\n-\t\t\t\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n-\t\t\t\t\t/*\n-\t\t\t\t\t * arg is neither a short or long\n-\t\t\t\t\t * option nor a subcommand.  Since\n-\t\t\t\t\t * this command has a default\n-\t\t\t\t\t * operation mode, we have to treat\n-\t\t\t\t\t * this arg and all remaining args\n-\t\t\t\t\t * as args meant to that default\n-\t\t\t\t\t * operation mode.\n-\t\t\t\t\t * So we are done parsing.\n-\t\t\t\t\t */\n-\t\t\t\t\treturn PARSE_OPT_DONE;\n-\t\t\t\terror(_(\"unknown subcommand: `%s'\"), arg);\n-\t\t\t\tusage_with_options(usagestr, options);\n-\t\t\tcase PARSE_OPT_COMPLETE:\n-\t\t\tcase PARSE_OPT_HELP:\n-\t\t\tcase PARSE_OPT_ERROR:\n-\t\t\tcase PARSE_OPT_DONE:\n-\t\t\tcase PARSE_OPT_NON_OPTION:\n-\t\t\t\t/* Impossible. */\n-\t\t\t\tBUG(\"parse_subcommand() cannot return these\");\n+\n+\t\t\t} else {\n+\t\t\t\treturn handle_subcommand(ctx, arg,\n+\t\t\t\t\t\t\t options, usagestr);\n \t\t\t}\n \t\t}\n \n-- \n2.53.0\n\n"},{"id":"538198","messageId":"SY0P300MB080180E17B471505A4150C08CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB08019805A8304105FA805EB1CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH 2/5] help: refactor command autocorrection handling","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-08T12:17:22Z","receivedAt":"2026-03-08T12:17:57Z","isPatch":true,"body":"Refactor autocorrect config into struct autocorr, with an explicit mode\nenum and a separate delay field. Move config parsing, TTY validation,\nand prompt handling into autocorrect.c.\n\nThis simplifies help.c and makes state handling for autocorrect explicit\nand easier to maintain.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n Makefile      |   1 +\n autocorrect.c |  92 +++++++++++++++++++++++++++++++++++++++++++\n autocorrect.h |  23 +++++++++++\n help.c        | 106 ++++++++++----------------------------------------\n 4 files changed, 136 insertions(+), 86 deletions(-)\n create mode 100644 autocorrect.c\n create mode 100644 autocorrect.h\n\ndiff --git a/Makefile b/Makefile\nindex f3264d0a37cc..6111631c2caa 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1098,6 +1098,7 @@ LIB_OBJS += archive-tar.o\n LIB_OBJS += archive-zip.o\n LIB_OBJS += archive.o\n LIB_OBJS += attr.o\n+LIB_OBJS += autocorrect.o\n LIB_OBJS += base85.o\n LIB_OBJS += bisect.o\n LIB_OBJS += blame.o\ndiff --git a/autocorrect.c b/autocorrect.c\nnew file mode 100644\nindex 000000000000..eaae01645910\n--- /dev/null\n+++ b/autocorrect.c\n@@ -0,0 +1,92 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n+#include \"git-compat-util.h\"\n+#include \"autocorrect.h\"\n+#include \"config.h\"\n+#include \"parse.h\"\n+#include \"strbuf.h\"\n+#include \"prompt.h\"\n+#include \"gettext.h\"\n+\n+static enum autocorr_mode parse_autocorr(const char *value)\n+{\n+\tswitch (git_parse_maybe_bool_text(value)) {\n+\t\tcase 1:\n+\t\t\treturn AUTOCORR_IMMEDIATELY;\n+\t\tcase 0:\n+\t\t\treturn AUTOCORR_HINTONLY;\n+\t\tdefault: /* other random text */\n+\t\t\tbreak;\n+\t}\n+\n+\tif (!strcmp(value, \"prompt\"))\n+\t\treturn AUTOCORR_PROMPT;\n+\telse if (!strcmp(value, \"never\"))\n+\t\treturn AUTOCORR_NEVER;\n+\telse if (!strcmp(value, \"immediate\"))\n+\t\treturn AUTOCORR_IMMEDIATELY;\n+\telse if (!strcmp(value, \"show\"))\n+\t\treturn AUTOCORR_HINTONLY;\n+\telse\n+\t\treturn AUTOCORR_DELAY;\n+}\n+\n+static int config_cb(const char *var, const char *value,\n+\t\t     const struct config_context *ctx, void *data)\n+{\n+\tstruct autocorr *conf = data;\n+\n+\tif (strcmp(var, \"help.autocorrect\"))\n+\t\treturn 0;\n+\n+\tconf->mode = parse_autocorr(value);\n+\n+\t/*\n+\t * Disable autocorrection prompt in a non-interactive session\n+\t */\n+\tif (conf->mode == AUTOCORR_PROMPT && (!isatty(0) || !isatty(2)))\n+\t\tconf->mode = AUTOCORR_NEVER;\n+\n+\tif (conf->mode == AUTOCORR_DELAY) {\n+\t\tconf->delay = git_config_int(var, value, ctx->kvi);\n+\n+\t\tif (conf->delay == 0)\n+\t\t\tconf->mode = AUTOCORR_HINTONLY;\n+\t\telse if (conf->delay <= 1)\n+\t\t\tconf->mode = AUTOCORR_IMMEDIATELY;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+void autocorr_resolve(struct autocorr *conf)\n+{\n+\tread_early_config(the_repository, config_cb, conf);\n+}\n+\n+void autocorr_prompt_or_delay(struct autocorr *conf, const char *assumed)\n+{\n+\tif (conf->mode == AUTOCORR_IMMEDIATELY) {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"Continuing under the assumption that you meant '%s'.\"),\n+\t\t\t   assumed);\n+\n+\t} else if (conf->mode == AUTOCORR_PROMPT) {\n+\t\tchar *answer;\n+\t\tstruct strbuf msg = STRBUF_INIT;\n+\n+\t\tstrbuf_addf(&msg, _(\"Run '%s' instead [y/N]? \"), assumed);\n+\t\tanswer = git_prompt(msg.buf, PROMPT_ECHO);\n+\t\tstrbuf_release(&msg);\n+\n+\t\tif (!(starts_with(answer, \"y\") || starts_with(answer, \"Y\")))\n+\t\t\texit(1);\n+\n+\t} else if (conf->mode == AUTOCORR_DELAY) {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"Continuing in %0.1f seconds, assuming that you meant '%s'.\"),\n+\t\t\t   (float)conf->delay / 10.0, assumed);\n+\n+\t\tsleep_millisec(conf->delay * 100);\n+\t}\n+}\ndiff --git a/autocorrect.h b/autocorrect.h\nnew file mode 100644\nindex 000000000000..ea21811a43f2\n--- /dev/null\n+++ b/autocorrect.h\n@@ -0,0 +1,23 @@\n+#ifndef AUTOCORRECT_H\n+#define AUTOCORRECT_H\n+\n+struct config_context;\n+\n+enum autocorr_mode {\n+\tAUTOCORR_HINTONLY,\n+\tAUTOCORR_NEVER,\n+\tAUTOCORR_PROMPT,\n+\tAUTOCORR_IMMEDIATELY,\n+\tAUTOCORR_DELAY,\n+};\n+\n+struct autocorr {\n+\tenum autocorr_mode mode;\n+\tint delay;\n+};\n+\n+void autocorr_resolve(struct autocorr *conf);\n+\n+void autocorr_prompt_or_delay(struct autocorr *conf, const char *assumed);\n+\n+#endif /* AUTOCORRECT_H */\ndiff --git a/help.c b/help.c\nindex 95f576c5c81d..5eec6a34de33 100644\n--- a/help.c\n+++ b/help.c\n@@ -22,6 +22,7 @@\n #include \"repository.h\"\n #include \"alias.h\"\n #include \"utf8.h\"\n+#include \"autocorrect.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -536,70 +537,23 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)\n \treturn 0;\n }\n \n-struct help_unknown_cmd_config {\n-\tint autocorrect;\n-\tstruct cmdnames aliases;\n-};\n-\n-#define AUTOCORRECT_SHOW (-4)\n-#define AUTOCORRECT_PROMPT (-3)\n-#define AUTOCORRECT_NEVER (-2)\n-#define AUTOCORRECT_IMMEDIATELY (-1)\n-\n-static int parse_autocorrect(const char *value)\n+static int resolve_aliases(const char *var, const char *value,\n+\t\t\t   const struct config_context *ctx, void *data)\n {\n-\tswitch (git_parse_maybe_bool_text(value)) {\n-\t\tcase 1:\n-\t\t\treturn AUTOCORRECT_IMMEDIATELY;\n-\t\tcase 0:\n-\t\t\treturn AUTOCORRECT_SHOW;\n-\t\tdefault: /* other random text */\n-\t\t\tbreak;\n-\t}\n-\n-\tif (!strcmp(value, \"prompt\"))\n-\t\treturn AUTOCORRECT_PROMPT;\n-\tif (!strcmp(value, \"never\"))\n-\t\treturn AUTOCORRECT_NEVER;\n-\tif (!strcmp(value, \"immediate\"))\n-\t\treturn AUTOCORRECT_IMMEDIATELY;\n-\tif (!strcmp(value, \"show\"))\n-\t\treturn AUTOCORRECT_SHOW;\n-\n-\treturn 0;\n-}\n-\n-static int git_unknown_cmd_config(const char *var, const char *value,\n-\t\t\t\t  const struct config_context *ctx,\n-\t\t\t\t  void *cb)\n-{\n-\tstruct help_unknown_cmd_config *cfg = cb;\n+\tstruct cmdnames *aliases = data;\n \tconst char *subsection, *key;\n \tsize_t subsection_len;\n \n-\tif (!strcmp(var, \"help.autocorrect\")) {\n-\t\tint v = parse_autocorrect(value);\n-\n-\t\tif (!v) {\n-\t\t\tv = git_config_int(var, value, ctx->kvi);\n-\t\t\tif (v < 0 || v == 1)\n-\t\t\t\tv = AUTOCORRECT_IMMEDIATELY;\n-\t\t}\n-\n-\t\tcfg->autocorrect = v;\n-\t}\n-\n-\t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n \t\t\t      &key)) {\n \t\tif (subsection) {\n \t\t\t/* [alias \"name\"] command = value */\n \t\t\tif (!strcmp(key, \"command\"))\n-\t\t\t\tadd_cmdname(&cfg->aliases, subsection,\n+\t\t\t\tadd_cmdname(aliases, subsection,\n \t\t\t\t\t    subsection_len);\n \t\t} else {\n \t\t\t/* alias.name = value */\n-\t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t\t\tadd_cmdname(aliases, key, strlen(key));\n \t\t}\n \t}\n \n@@ -636,28 +590,27 @@ static const char bad_interpreter_advice[] =\n \n char *help_unknown_cmd(const char *cmd)\n {\n-\tstruct help_unknown_cmd_config cfg = { 0 };\n+\tstruct cmdnames aliases = { 0 };\n+\tstruct autocorr autocorr = { 0 };\n+\n \tint i, n, best_similarity = 0;\n \tstruct cmdnames main_cmds = { 0 };\n \tstruct cmdnames other_cmds = { 0 };\n \tstruct cmdname_help *common_cmds;\n \n-\tread_early_config(the_repository, git_unknown_cmd_config, &cfg);\n+\tautocorr_resolve(&autocorr);\n \n-\t/*\n-\t * Disable autocorrection prompt in a non-interactive session\n-\t */\n-\tif ((cfg.autocorrect == AUTOCORRECT_PROMPT) && (!isatty(0) || !isatty(2)))\n-\t\tcfg.autocorrect = AUTOCORRECT_NEVER;\n+\t/* Also use aliases for command lookup */\n+\tread_early_config(the_repository, resolve_aliases, &aliases);\n \n-\tif (cfg.autocorrect == AUTOCORRECT_NEVER) {\n+\tif (autocorr.mode == AUTOCORR_NEVER) {\n \t\tfprintf_ln(stderr, _(\"git: '%s' is not a git command. See 'git --help'.\"), cmd);\n \t\texit(1);\n \t}\n \n \tload_command_list(\"git-\", &main_cmds, &other_cmds);\n \n-\tadd_cmd_list(&main_cmds, &cfg.aliases);\n+\tadd_cmd_list(&main_cmds, &aliases);\n \tadd_cmd_list(&main_cmds, &other_cmds);\n \tQSORT(main_cmds.names, main_cmds.cnt, cmdname_compare);\n \tuniq(&main_cmds);\n@@ -716,37 +669,18 @@ char *help_unknown_cmd(const char *cmd)\n \t\t     n++)\n \t\t\t; /* still counting */\n \t}\n-\tif (cfg.autocorrect && cfg.autocorrect != AUTOCORRECT_SHOW && n == 1 &&\n+\n+\tif (autocorr.mode != AUTOCORR_HINTONLY && n == 1 &&\n \t    SIMILAR_ENOUGH(best_similarity)) {\n \t\tchar *assumed = xstrdup(main_cmds.names[0]->name);\n \n \t\tfprintf_ln(stderr,\n-\t\t\t   _(\"WARNING: You called a Git command named '%s', \"\n-\t\t\t     \"which does not exist.\"),\n+\t\t\t   _(\"WARNING: You called a Git command named '%s', which does not exist.\"),\n \t\t\t   cmd);\n-\t\tif (cfg.autocorrect == AUTOCORRECT_IMMEDIATELY)\n-\t\t\tfprintf_ln(stderr,\n-\t\t\t\t   _(\"Continuing under the assumption that \"\n-\t\t\t\t     \"you meant '%s'.\"),\n-\t\t\t\t   assumed);\n-\t\telse if (cfg.autocorrect == AUTOCORRECT_PROMPT) {\n-\t\t\tchar *answer;\n-\t\t\tstruct strbuf msg = STRBUF_INIT;\n-\t\t\tstrbuf_addf(&msg, _(\"Run '%s' instead [y/N]? \"), assumed);\n-\t\t\tanswer = git_prompt(msg.buf, PROMPT_ECHO);\n-\t\t\tstrbuf_release(&msg);\n-\t\t\tif (!(starts_with(answer, \"y\") ||\n-\t\t\t      starts_with(answer, \"Y\")))\n-\t\t\t\texit(1);\n-\t\t} else {\n-\t\t\tfprintf_ln(stderr,\n-\t\t\t\t   _(\"Continuing in %0.1f seconds, \"\n-\t\t\t\t     \"assuming that you meant '%s'.\"),\n-\t\t\t\t   (float)cfg.autocorrect/10.0, assumed);\n-\t\t\tsleep_millisec(cfg.autocorrect * 100);\n-\t\t}\n \n-\t\tcmdnames_release(&cfg.aliases);\n+\t\tautocorr_prompt_or_delay(&autocorr, assumed);\n+\n+\t\tcmdnames_release(&aliases);\n \t\tcmdnames_release(&main_cmds);\n \t\tcmdnames_release(&other_cmds);\n \t\treturn assumed;\n-- \n2.53.0\n\n"},{"id":"538199","messageId":"SY0P300MB08010BFD00AA7C73503D8B92CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB08019805A8304105FA805EB1CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH 3/5] parseopt: autocorrect mistyped subcommands","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-08T12:17:23Z","receivedAt":"2026-03-08T12:17:58Z","isPatch":true,"body":"Enable autocorrection for mandatory subcommands parsed through\nparse-options.\n\nAUTOCORR_HINTONLY is equivalent to AUTOCORR_NEVER here, because builtins\nhave a limited number of subcommands. They won't become too large.\nDisplaying all subcommands via usage_with_options() is already good\nenough. This keeps the autocorrection handling simple, too.\n\nAlso, use a dynamic threshold for similar_enough(), which can yield more\naccurate typo correction results. Although subcommands are often short,\nthey can still vary across builtins. And a fixed threshold won't do\nbetter on both short and long subcommands at the same time.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n parse-options.c | 76 +++++++++++++++++++++++++++++++++++++++++++++++--\n 1 file changed, 74 insertions(+), 2 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex 6bb0c5697099..cbbb04b9997a 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -6,6 +6,8 @@\n #include \"strbuf.h\"\n #include \"string-list.h\"\n #include \"utf8.h\"\n+#include \"autocorrect.h\"\n+#include \"levenshtein.h\"\n \n static int disallow_abbreviated_options;\n \n@@ -621,6 +623,64 @@ static int parse_subcommand(const char *arg, const struct option *options)\n \treturn -1;\n }\n \n+static void find_subcommands(struct string_list *list,\n+\t\t\t     const struct option *options)\n+{\n+\tfor (; options->type != OPTION_END; options++) {\n+\t\tif (options->type == OPTION_SUBCOMMAND)\n+\t\t\tstring_list_append(list, options->long_name);\n+\t}\n+}\n+\n+static int similar_enough(const char *cmd, unsigned int dist)\n+{\n+\tsize_t len = strlen(cmd);\n+\tunsigned int threshold = len < 3 ? 1 : len < 6 ? 3 : 6;\n+\n+\treturn dist < threshold;\n+}\n+\n+static const char *autocorrect_subcmd(const char *cmd,\n+\t\t\t\t      struct string_list *cmds)\n+{\n+\tstruct autocorr autocorr = { 0 };\n+\n+\tautocorr_resolve(&autocorr);\n+\n+\tif (autocorr.mode == AUTOCORR_NEVER ||\n+\t    autocorr.mode == AUTOCORR_HINTONLY)\n+\t\treturn NULL;\n+\n+\tunsigned int min = -1;\n+\tunsigned int ties = 0;\n+\tstruct string_list_item *cand;\n+\tstruct string_list_item *best = NULL;\n+\n+\tfor_each_string_list_item(cand, cmds) {\n+\t\tunsigned int dist = levenshtein(cmd, cand->string, 0, 2, 1, 3);\n+\n+\t\tif (dist < min) {\n+\t\t\tmin = dist;\n+\t\t\tbest = cand;\n+\t\t\tties = 0;\n+\n+\t\t} else if (dist == min) {\n+\t\t\tties++;\n+\t\t}\n+\t}\n+\n+\tif (ties == 0 && similar_enough(cmd, min)) {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"WARNING: You called a subcommand named '%s', which does not exist.\"),\n+\t\t\t   cmd);\n+\n+\t\tautocorr_prompt_or_delay(&autocorr, best->string);\n+\t\treturn best->string;\n+\t}\n+\n+\treturn NULL;\n+}\n+\n static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \t\t\t\t\t       const char *arg,\n \t\t\t\t\t       const struct option *options,\n@@ -640,8 +700,20 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n \t\treturn PARSE_OPT_DONE;\n \n-\terror(_(\"unknown subcommand: `%s'\"), arg);\n-\tusage_with_options(usagestr, options);\n+\tstruct string_list cmds = STRING_LIST_INIT_NODUP;\n+\n+\tfind_subcommands(&cmds, options);\n+\n+\tconst char *cmd = autocorrect_subcmd(arg, &cmds);\n+\n+\tif (!cmd) {\n+\t\terror(_(\"unknown subcommand: `%s'\"), arg);\n+\t\tusage_with_options(usagestr, options);\n+\t}\n+\n+\tstring_list_clear(&cmds, 0);\n+\tparse_subcommand(cmd, options);\n+\treturn PARSE_OPT_SUBCOMMAND;\n }\n \n static void check_typos(const char *arg, const struct option *options)\n-- \n2.53.0\n\n"},{"id":"538200","messageId":"SY0P300MB08014A3AFE45F66951ADC8E7CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB08019805A8304105FA805EB1CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH 4/5] parseopt: enable subcommand autocorrect for remote and notes","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-08T12:17:24Z","receivedAt":"2026-03-08T12:18:00Z","isPatch":true,"body":"Add PARSE_OPT_SUBCOMMAND_AUTOCORR to enable autocorrection for\nsubcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL.\n\nUse it for git-remote and git-notes, so mistyped subcommands can be\nautomatically corrected, and builtin entry points no longer need\nto handle missing subcommand error path themselves.\n\nThis is safe for these two builtins, because they either resolve to a\nsingle subcommand or take no subcommand at all. This means that if the\nsubcommand parser encounters an unknown argument, it must be a mistyped\nsubcommand.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n builtin/notes.c  | 10 +++-------\n builtin/remote.c | 12 ++++--------\n parse-options.c  | 16 +++++++++-------\n parse-options.h  |  1 +\n 4 files changed, 17 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 9af602bdd7b4..087eb898a441 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -1149,14 +1149,10 @@ int cmd_notes(int argc,\n \n \trepo_config(the_repository, git_default_config, NULL);\n \targc = parse_options(argc, argv, prefix, options, git_notes_usage,\n-\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n-\tif (!fn) {\n-\t\tif (argc) {\n-\t\t\terror(_(\"unknown subcommand: `%s'\"), argv[0]);\n-\t\t\tusage_with_options(git_notes_usage, options);\n-\t\t}\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n+\t\t\t     PARSE_OPT_SUBCOMMAND_AUTOCORR);\n+\tif (!fn)\n \t\tfn = list;\n-\t}\n \n \tif (override_notes_ref) {\n \t\tstruct strbuf sb = STRBUF_INIT;\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex ace390c671d6..d1d6244a662a 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -1949,15 +1949,11 @@ int cmd_remote(int argc,\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, builtin_remote_usage,\n-\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n+\t\t\t     PARSE_OPT_SUBCOMMAND_AUTOCORR);\n \n-\tif (fn) {\n+\tif (fn)\n \t\treturn !!fn(argc, argv, prefix, repo);\n-\t} else {\n-\t\tif (argc) {\n-\t\t\terror(_(\"unknown subcommand: `%s'\"), argv[0]);\n-\t\t\tusage_with_options(builtin_remote_usage, options);\n-\t\t}\n+\telse\n \t\treturn !!show_all();\n-\t}\n }\ndiff --git a/parse-options.c b/parse-options.c\nindex cbbb04b9997a..b7c818e818fe 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -691,14 +691,16 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \tif (!err)\n \t\treturn PARSE_OPT_SUBCOMMAND;\n \n-\t/*\n-\t * arg is neither a short or long option nor a subcommand.  Since this\n-\t * command has a default operation mode, we have to treat this arg and\n-\t * all remaining args as args meant to that default operation mode.\n-\t * So we are done parsing.\n-\t */\n-\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n+\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL &&\n+\t    !(ctx->flags & PARSE_OPT_SUBCOMMAND_AUTOCORR)) {\n+\t\t/*\n+\t\t * arg is neither a short or long option nor a subcommand.\n+\t\t * Since this command has a default operation mode, we have to\n+\t\t * treat this arg and all remaining args as args meant to that\n+\t\t * default operation mode.  So we are done parsing.\n+\t\t */\n \t\treturn PARSE_OPT_DONE;\n+\t}\n \n \tstruct string_list cmds = STRING_LIST_INIT_NODUP;\n \ndiff --git a/parse-options.h b/parse-options.h\nindex 706de9729f6b..f29ac337893c 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -40,6 +40,7 @@ enum parse_opt_flags {\n \tPARSE_OPT_ONE_SHOT = 1 << 5,\n \tPARSE_OPT_SHELL_EVAL = 1 << 6,\n \tPARSE_OPT_SUBCOMMAND_OPTIONAL = 1 << 7,\n+\tPARSE_OPT_SUBCOMMAND_AUTOCORR = 1 << 8,\n };\n \n enum parse_opt_option_flags {\n-- \n2.53.0\n\n"},{"id":"538201","messageId":"SY0P300MB08015B9BA815B2C4F6CDC639CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB08019805A8304105FA805EB1CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH 5/5] help: add tests for subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-08T12:17:25Z","receivedAt":"2026-03-08T12:18:02Z","isPatch":true,"body":"These tests cover default behavior (help.autocorrect is unset), no\ncorrection, immediate correction, delayed correction, and rejection\nwhen the typo is too dissimilar.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n t/t9004-autocorrect-subcommand.sh | 49 +++++++++++++++++++++++++++++++\n 1 file changed, 49 insertions(+)\n create mode 100755 t/t9004-autocorrect-subcommand.sh\n\ndiff --git a/t/t9004-autocorrect-subcommand.sh b/t/t9004-autocorrect-subcommand.sh\nnew file mode 100755\nindex 000000000000..760760c8851a\n--- /dev/null\n+++ b/t/t9004-autocorrect-subcommand.sh\n@@ -0,0 +1,49 @@\n+#!/bin/sh\n+\n+test_description='subcommand auto-correction test\n+\n+Test autocorrection for subcommands with different\n+help.autocorrect mode.'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' \"\n+\techo '^error: unknown subcommand: ' >grep_unknown\n+\"\n+\n+test_expect_success 'default is not to autocorrect' '\n+\ttest_must_fail git worktree lsit 2>actual &&\n+\thead -n1 actual >first && test_grep -f grep_unknown first\n+'\n+\n+for mode in false no off 0 show never; do\n+\ttest_expect_success \"'$mode' disables autocorrection\" \"\n+\t\ttest_config help.autocorrect $mode &&\n+\n+\t\ttest_must_fail git worktree lsit 2>actual &&\n+\t\thead -n1 actual >first && test_grep -f grep_unknown first\n+\t\"\n+done\n+\n+for mode in -39 immediate 1; do\n+\ttest_expect_success \"autocorrect immediately with '$mode'\" - <<-EOT\n+\t\ttest_config help.autocorrect $mode &&\n+\n+\t\tgit worktree lsit 2>actual &&\n+\t\ttest_grep \"you meant 'list'\\.$\" actual\n+\tEOT\n+done\n+\n+test_expect_success 'delay path is executed' - <<-\\EOT\n+\ttest_config help.autocorrect 2 &&\n+\n+\tgit worktree lsit 2>actual &&\n+\ttest_grep '^Continuing in 0.2 seconds, ' actual\n+EOT\n+\n+test_expect_success 'deny if too dissimilar' - <<-\\EOT\n+\ttest_must_fail git remote rensnr 2>actual &&\n+\thead -n1 actual >first && test_grep -f grep_unknown first\n+EOT\n+\n+test_done\n-- \n2.53.0\n\n"},{"id":"538213","messageId":"xmqqo6ky9j4l.fsf@gitster.g","threadId":"65168","inReplyTo":"SY0P300MB08019805A8304105FA805EB1CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH 0/5] parseopt: add subcommand autocorrection","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-08T20:11:22Z","receivedAt":"2026-03-08T20:11:24Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> Git currently provides auto-correction for builtins and aliases, but\n> lacks this functionality for subcommands parsed via the parse-options\n> API. Subcommands are also commands, and typos will occur, too. Like:\n>\n> \tgit remote add-rul\n>\n> So, this series introduces subcommand auto-correction.\n>\n> Currently, builtins with mandatory subcommands enable autocorrection by\n> default. However, those using PARSE_OPT_SUBCOMMAND_OPTIONAL skip it to\n> avoid treating valid unknown arguments as mistyped subcommands.\n\nThis is a bit confusing as it describes the behavior introduced by\nthis series rather than the state of the codebase before these\npatches.  Since subcommand autocorrection doesn't exist yet, it\nwould be clearer to phrase this as describing your implementation\nchoices. Perhaps:\n\n    By default, this implementation enables autocorrection for\n    builtins with mandatory subcommands. However, for those using\n    PARSE_OPT_SUBCOMMAND_OPTIONAL, autocorrection is skipped to\n    avoid misinterpreting legitimate unknown arguments as mistyped\n    subcommands.\n\nor something, perhaps?\n"},{"id":"538216","messageId":"SY0P300MB080125A3AD82CEF7341D0863CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"xmqqo6ky9j4l.fsf@gitster.g","subject":"Re: [PATCH 0/5] parseopt: add subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-08T22:07:17Z","receivedAt":"2026-03-08T22:07:30Z","isPatch":true,"body":"On Sun, Mar 08, 2026 at 01:11:22PM -0700, Junio C Hamano wrote:\n> Jiamu Sun <39@barroit.sh> writes:\n> \n> > So, this series introduces subcommand auto-correction.\n> >\n> > Currently, builtins with mandatory subcommands enable autocorrection by\n> > default. However, those using PARSE_OPT_SUBCOMMAND_OPTIONAL skip it to\n> > avoid treating valid unknown arguments as mistyped subcommands.\n> \n> This is a bit confusing as it describes the behavior introduced by\n> this series rather than the state of the codebase before these\n> patches.  Since subcommand autocorrection doesn't exist yet, it\n> would be clearer to phrase this as describing your implementation\n> choices. Perhaps:\n> \n>     By default, this implementation enables autocorrection for\n>     builtins with mandatory subcommands. However, for those using\n>     PARSE_OPT_SUBCOMMAND_OPTIONAL, autocorrection is skipped to\n>     avoid misinterpreting legitimate unknown arguments as mistyped\n>     subcommands.\n> \n> or something, perhaps?\n\nRight, that's confusing. By \"currently\", I meant \"in this patch\".\n\nWill reword the cover letter in v2 to use your phrasing. Thanks!\n\n-- \nJiamu Sun <39@barroit.sh>\n"},{"id":"538218","messageId":"SY0P300MB08013E35DCA8FC31B0662125CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB08019805A8304105FA805EB1CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v2 0/5] parseopt: add subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-08T23:16:54Z","receivedAt":"2026-03-08T23:17:18Z","isPatch":true,"body":"Git currently provides auto-correction for builtins and aliases, but\nlacks this functionality for subcommands parsed via the parse-options\nAPI. Subcommands are also commands, and typos will occur, too. Like:\n\n\tgit remote add-rul\n\nSo, this series introduces subcommand auto-correction.\n\nBy default, this implementation enables autocorrection for builtins\nwith mandatory subcommands. However, for those using\nPARSE_OPT_SUBCOMMAND_OPTIONAL, autocorrection is skipped to avoid\nmisinterpreting legitimate unknown arguments as mistyped subcommands.\n\nTo allow builtins with optional subcommands to explicitly opt in,\nthis series adds the PARSE_OPT_SUBCOMMAND_AUTOCORR flag. This flag\nis subsequently enabled for git remote and git notes.\n\nAdditionally, the existing autocorrection logic is extracted from\nhelp.c so subcommand handling can reuse the same config parsing and\nprompt/delay logic.\n\nChanges in v2:\n  - Reworded the explanation of default autocorrection behavior.\n\nJiamu Sun (5):\n  parseopt: extract subcommand handling from parse_options_step()\n  help: refactor command autocorrection handling\n  parseopt: autocorrect mistyped subcommands\n  parseopt: enable subcommand autocorrect for remote and notes\n  help: add tests for subcommand autocorrection\n\n Makefile                          |   1 +\n autocorrect.c                     |  92 +++++++++++++++++++\n autocorrect.h                     |  23 +++++\n builtin/notes.c                   |  10 +-\n builtin/remote.c                  |  12 +--\n help.c                            | 106 ++++-----------------\n parse-options.c                   | 147 +++++++++++++++++++++++-------\n parse-options.h                   |   1 +\n t/t9004-autocorrect-subcommand.sh |  49 ++++++++++\n 9 files changed, 306 insertions(+), 135 deletions(-)\n create mode 100644 autocorrect.c\n create mode 100644 autocorrect.h\n create mode 100755 t/t9004-autocorrect-subcommand.sh\n\n\nbase-commit: 795c338de725e13bd361214c6b768019fc45a2c1\n-- \n2.53.0\n\n"},{"id":"538219","messageId":"SY0P300MB08017CA1F4E85F606B8FAE28CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB08013E35DCA8FC31B0662125CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v2 1/5] parseopt: extract subcommand handling from parse_options_step()","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-08T23:16:55Z","receivedAt":"2026-03-08T23:17:19Z","isPatch":true,"body":"Move the subcommand branch out of parse_options_step() into a new\nhandle_subcommand() helper. Also, make parse_subcommand() return a\nsimple success/failure status.\n\nThis removes the switch over impossible parse_opt_result values and\nmakes the non-option path easier to follow and maintain.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n parse-options.c | 75 ++++++++++++++++++++++++++-----------------------\n 1 file changed, 40 insertions(+), 35 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex c9cafc21b903..6bb0c5697099 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -605,17 +605,43 @@ static enum parse_opt_result parse_nodash_opt(struct parse_opt_ctx_t *p,\n \treturn PARSE_OPT_ERROR;\n }\n \n-static enum parse_opt_result parse_subcommand(const char *arg,\n-\t\t\t\t\t      const struct option *options)\n+static int parse_subcommand(const char *arg, const struct option *options)\n {\n-\tfor (; options->type != OPTION_END; options++)\n-\t\tif (options->type == OPTION_SUBCOMMAND &&\n-\t\t    !strcmp(options->long_name, arg)) {\n-\t\t\t*(parse_opt_subcommand_fn **)options->value = options->subcommand_fn;\n-\t\t\treturn PARSE_OPT_SUBCOMMAND;\n-\t\t}\n+\tfor (; options->type != OPTION_END; options++) {\n+\t\tif (options->type != OPTION_SUBCOMMAND ||\n+\t\t    strcmp(options->long_name, arg))\n+\t\t\tcontinue;\n \n-\treturn PARSE_OPT_UNKNOWN;\n+\t\tparse_opt_subcommand_fn **opt_val = options->value;\n+\t\t*opt_val = options->subcommand_fn;\n+\n+\t\treturn 0;\n+\t}\n+\n+\treturn -1;\n+}\n+\n+static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n+\t\t\t\t\t       const char *arg,\n+\t\t\t\t\t       const struct option *options,\n+\t\t\t\t\t       const char * const usagestr[])\n+{\n+\tint err = parse_subcommand(arg, options);\n+\n+\tif (!err)\n+\t\treturn PARSE_OPT_SUBCOMMAND;\n+\n+\t/*\n+\t * arg is neither a short or long option nor a subcommand.  Since this\n+\t * command has a default operation mode, we have to treat this arg and\n+\t * all remaining args as args meant to that default operation mode.\n+\t * So we are done parsing.\n+\t */\n+\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n+\t\treturn PARSE_OPT_DONE;\n+\n+\terror(_(\"unknown subcommand: `%s'\"), arg);\n+\tusage_with_options(usagestr, options);\n }\n \n static void check_typos(const char *arg, const struct option *options)\n@@ -990,37 +1016,16 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\tif (*arg != '-' || !arg[1]) {\n \t\t\tif (parse_nodash_opt(ctx, arg, options) == 0)\n \t\t\t\tcontinue;\n+\n \t\t\tif (!ctx->has_subcommands) {\n \t\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n \t\t\t\t\treturn PARSE_OPT_NON_OPTION;\n \t\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n \t\t\t\tcontinue;\n-\t\t\t}\n-\t\t\tswitch (parse_subcommand(arg, options)) {\n-\t\t\tcase PARSE_OPT_SUBCOMMAND:\n-\t\t\t\treturn PARSE_OPT_SUBCOMMAND;\n-\t\t\tcase PARSE_OPT_UNKNOWN:\n-\t\t\t\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n-\t\t\t\t\t/*\n-\t\t\t\t\t * arg is neither a short or long\n-\t\t\t\t\t * option nor a subcommand.  Since\n-\t\t\t\t\t * this command has a default\n-\t\t\t\t\t * operation mode, we have to treat\n-\t\t\t\t\t * this arg and all remaining args\n-\t\t\t\t\t * as args meant to that default\n-\t\t\t\t\t * operation mode.\n-\t\t\t\t\t * So we are done parsing.\n-\t\t\t\t\t */\n-\t\t\t\t\treturn PARSE_OPT_DONE;\n-\t\t\t\terror(_(\"unknown subcommand: `%s'\"), arg);\n-\t\t\t\tusage_with_options(usagestr, options);\n-\t\t\tcase PARSE_OPT_COMPLETE:\n-\t\t\tcase PARSE_OPT_HELP:\n-\t\t\tcase PARSE_OPT_ERROR:\n-\t\t\tcase PARSE_OPT_DONE:\n-\t\t\tcase PARSE_OPT_NON_OPTION:\n-\t\t\t\t/* Impossible. */\n-\t\t\t\tBUG(\"parse_subcommand() cannot return these\");\n+\n+\t\t\t} else {\n+\t\t\t\treturn handle_subcommand(ctx, arg,\n+\t\t\t\t\t\t\t options, usagestr);\n \t\t\t}\n \t\t}\n \n-- \n2.53.0\n\n"},{"id":"538220","messageId":"SY0P300MB0801DD4E14C17C5D199788B0CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB08013E35DCA8FC31B0662125CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v2 2/5] help: refactor command autocorrection handling","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-08T23:16:56Z","receivedAt":"2026-03-08T23:17:21Z","isPatch":true,"body":"Refactor autocorrect config into struct autocorr, with an explicit mode\nenum and a separate delay field. Move config parsing, TTY validation,\nand prompt handling into autocorrect.c.\n\nThis simplifies help.c and makes state handling for autocorrect explicit\nand easier to maintain.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n Makefile      |   1 +\n autocorrect.c |  92 +++++++++++++++++++++++++++++++++++++++++++\n autocorrect.h |  23 +++++++++++\n help.c        | 106 ++++++++++----------------------------------------\n 4 files changed, 136 insertions(+), 86 deletions(-)\n create mode 100644 autocorrect.c\n create mode 100644 autocorrect.h\n\ndiff --git a/Makefile b/Makefile\nindex f3264d0a37cc..6111631c2caa 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1098,6 +1098,7 @@ LIB_OBJS += archive-tar.o\n LIB_OBJS += archive-zip.o\n LIB_OBJS += archive.o\n LIB_OBJS += attr.o\n+LIB_OBJS += autocorrect.o\n LIB_OBJS += base85.o\n LIB_OBJS += bisect.o\n LIB_OBJS += blame.o\ndiff --git a/autocorrect.c b/autocorrect.c\nnew file mode 100644\nindex 000000000000..eaae01645910\n--- /dev/null\n+++ b/autocorrect.c\n@@ -0,0 +1,92 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n+#include \"git-compat-util.h\"\n+#include \"autocorrect.h\"\n+#include \"config.h\"\n+#include \"parse.h\"\n+#include \"strbuf.h\"\n+#include \"prompt.h\"\n+#include \"gettext.h\"\n+\n+static enum autocorr_mode parse_autocorr(const char *value)\n+{\n+\tswitch (git_parse_maybe_bool_text(value)) {\n+\t\tcase 1:\n+\t\t\treturn AUTOCORR_IMMEDIATELY;\n+\t\tcase 0:\n+\t\t\treturn AUTOCORR_HINTONLY;\n+\t\tdefault: /* other random text */\n+\t\t\tbreak;\n+\t}\n+\n+\tif (!strcmp(value, \"prompt\"))\n+\t\treturn AUTOCORR_PROMPT;\n+\telse if (!strcmp(value, \"never\"))\n+\t\treturn AUTOCORR_NEVER;\n+\telse if (!strcmp(value, \"immediate\"))\n+\t\treturn AUTOCORR_IMMEDIATELY;\n+\telse if (!strcmp(value, \"show\"))\n+\t\treturn AUTOCORR_HINTONLY;\n+\telse\n+\t\treturn AUTOCORR_DELAY;\n+}\n+\n+static int config_cb(const char *var, const char *value,\n+\t\t     const struct config_context *ctx, void *data)\n+{\n+\tstruct autocorr *conf = data;\n+\n+\tif (strcmp(var, \"help.autocorrect\"))\n+\t\treturn 0;\n+\n+\tconf->mode = parse_autocorr(value);\n+\n+\t/*\n+\t * Disable autocorrection prompt in a non-interactive session\n+\t */\n+\tif (conf->mode == AUTOCORR_PROMPT && (!isatty(0) || !isatty(2)))\n+\t\tconf->mode = AUTOCORR_NEVER;\n+\n+\tif (conf->mode == AUTOCORR_DELAY) {\n+\t\tconf->delay = git_config_int(var, value, ctx->kvi);\n+\n+\t\tif (conf->delay == 0)\n+\t\t\tconf->mode = AUTOCORR_HINTONLY;\n+\t\telse if (conf->delay <= 1)\n+\t\t\tconf->mode = AUTOCORR_IMMEDIATELY;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+void autocorr_resolve(struct autocorr *conf)\n+{\n+\tread_early_config(the_repository, config_cb, conf);\n+}\n+\n+void autocorr_prompt_or_delay(struct autocorr *conf, const char *assumed)\n+{\n+\tif (conf->mode == AUTOCORR_IMMEDIATELY) {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"Continuing under the assumption that you meant '%s'.\"),\n+\t\t\t   assumed);\n+\n+\t} else if (conf->mode == AUTOCORR_PROMPT) {\n+\t\tchar *answer;\n+\t\tstruct strbuf msg = STRBUF_INIT;\n+\n+\t\tstrbuf_addf(&msg, _(\"Run '%s' instead [y/N]? \"), assumed);\n+\t\tanswer = git_prompt(msg.buf, PROMPT_ECHO);\n+\t\tstrbuf_release(&msg);\n+\n+\t\tif (!(starts_with(answer, \"y\") || starts_with(answer, \"Y\")))\n+\t\t\texit(1);\n+\n+\t} else if (conf->mode == AUTOCORR_DELAY) {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"Continuing in %0.1f seconds, assuming that you meant '%s'.\"),\n+\t\t\t   (float)conf->delay / 10.0, assumed);\n+\n+\t\tsleep_millisec(conf->delay * 100);\n+\t}\n+}\ndiff --git a/autocorrect.h b/autocorrect.h\nnew file mode 100644\nindex 000000000000..ea21811a43f2\n--- /dev/null\n+++ b/autocorrect.h\n@@ -0,0 +1,23 @@\n+#ifndef AUTOCORRECT_H\n+#define AUTOCORRECT_H\n+\n+struct config_context;\n+\n+enum autocorr_mode {\n+\tAUTOCORR_HINTONLY,\n+\tAUTOCORR_NEVER,\n+\tAUTOCORR_PROMPT,\n+\tAUTOCORR_IMMEDIATELY,\n+\tAUTOCORR_DELAY,\n+};\n+\n+struct autocorr {\n+\tenum autocorr_mode mode;\n+\tint delay;\n+};\n+\n+void autocorr_resolve(struct autocorr *conf);\n+\n+void autocorr_prompt_or_delay(struct autocorr *conf, const char *assumed);\n+\n+#endif /* AUTOCORRECT_H */\ndiff --git a/help.c b/help.c\nindex 95f576c5c81d..5eec6a34de33 100644\n--- a/help.c\n+++ b/help.c\n@@ -22,6 +22,7 @@\n #include \"repository.h\"\n #include \"alias.h\"\n #include \"utf8.h\"\n+#include \"autocorrect.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -536,70 +537,23 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)\n \treturn 0;\n }\n \n-struct help_unknown_cmd_config {\n-\tint autocorrect;\n-\tstruct cmdnames aliases;\n-};\n-\n-#define AUTOCORRECT_SHOW (-4)\n-#define AUTOCORRECT_PROMPT (-3)\n-#define AUTOCORRECT_NEVER (-2)\n-#define AUTOCORRECT_IMMEDIATELY (-1)\n-\n-static int parse_autocorrect(const char *value)\n+static int resolve_aliases(const char *var, const char *value,\n+\t\t\t   const struct config_context *ctx, void *data)\n {\n-\tswitch (git_parse_maybe_bool_text(value)) {\n-\t\tcase 1:\n-\t\t\treturn AUTOCORRECT_IMMEDIATELY;\n-\t\tcase 0:\n-\t\t\treturn AUTOCORRECT_SHOW;\n-\t\tdefault: /* other random text */\n-\t\t\tbreak;\n-\t}\n-\n-\tif (!strcmp(value, \"prompt\"))\n-\t\treturn AUTOCORRECT_PROMPT;\n-\tif (!strcmp(value, \"never\"))\n-\t\treturn AUTOCORRECT_NEVER;\n-\tif (!strcmp(value, \"immediate\"))\n-\t\treturn AUTOCORRECT_IMMEDIATELY;\n-\tif (!strcmp(value, \"show\"))\n-\t\treturn AUTOCORRECT_SHOW;\n-\n-\treturn 0;\n-}\n-\n-static int git_unknown_cmd_config(const char *var, const char *value,\n-\t\t\t\t  const struct config_context *ctx,\n-\t\t\t\t  void *cb)\n-{\n-\tstruct help_unknown_cmd_config *cfg = cb;\n+\tstruct cmdnames *aliases = data;\n \tconst char *subsection, *key;\n \tsize_t subsection_len;\n \n-\tif (!strcmp(var, \"help.autocorrect\")) {\n-\t\tint v = parse_autocorrect(value);\n-\n-\t\tif (!v) {\n-\t\t\tv = git_config_int(var, value, ctx->kvi);\n-\t\t\tif (v < 0 || v == 1)\n-\t\t\t\tv = AUTOCORRECT_IMMEDIATELY;\n-\t\t}\n-\n-\t\tcfg->autocorrect = v;\n-\t}\n-\n-\t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n \t\t\t      &key)) {\n \t\tif (subsection) {\n \t\t\t/* [alias \"name\"] command = value */\n \t\t\tif (!strcmp(key, \"command\"))\n-\t\t\t\tadd_cmdname(&cfg->aliases, subsection,\n+\t\t\t\tadd_cmdname(aliases, subsection,\n \t\t\t\t\t    subsection_len);\n \t\t} else {\n \t\t\t/* alias.name = value */\n-\t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t\t\tadd_cmdname(aliases, key, strlen(key));\n \t\t}\n \t}\n \n@@ -636,28 +590,27 @@ static const char bad_interpreter_advice[] =\n \n char *help_unknown_cmd(const char *cmd)\n {\n-\tstruct help_unknown_cmd_config cfg = { 0 };\n+\tstruct cmdnames aliases = { 0 };\n+\tstruct autocorr autocorr = { 0 };\n+\n \tint i, n, best_similarity = 0;\n \tstruct cmdnames main_cmds = { 0 };\n \tstruct cmdnames other_cmds = { 0 };\n \tstruct cmdname_help *common_cmds;\n \n-\tread_early_config(the_repository, git_unknown_cmd_config, &cfg);\n+\tautocorr_resolve(&autocorr);\n \n-\t/*\n-\t * Disable autocorrection prompt in a non-interactive session\n-\t */\n-\tif ((cfg.autocorrect == AUTOCORRECT_PROMPT) && (!isatty(0) || !isatty(2)))\n-\t\tcfg.autocorrect = AUTOCORRECT_NEVER;\n+\t/* Also use aliases for command lookup */\n+\tread_early_config(the_repository, resolve_aliases, &aliases);\n \n-\tif (cfg.autocorrect == AUTOCORRECT_NEVER) {\n+\tif (autocorr.mode == AUTOCORR_NEVER) {\n \t\tfprintf_ln(stderr, _(\"git: '%s' is not a git command. See 'git --help'.\"), cmd);\n \t\texit(1);\n \t}\n \n \tload_command_list(\"git-\", &main_cmds, &other_cmds);\n \n-\tadd_cmd_list(&main_cmds, &cfg.aliases);\n+\tadd_cmd_list(&main_cmds, &aliases);\n \tadd_cmd_list(&main_cmds, &other_cmds);\n \tQSORT(main_cmds.names, main_cmds.cnt, cmdname_compare);\n \tuniq(&main_cmds);\n@@ -716,37 +669,18 @@ char *help_unknown_cmd(const char *cmd)\n \t\t     n++)\n \t\t\t; /* still counting */\n \t}\n-\tif (cfg.autocorrect && cfg.autocorrect != AUTOCORRECT_SHOW && n == 1 &&\n+\n+\tif (autocorr.mode != AUTOCORR_HINTONLY && n == 1 &&\n \t    SIMILAR_ENOUGH(best_similarity)) {\n \t\tchar *assumed = xstrdup(main_cmds.names[0]->name);\n \n \t\tfprintf_ln(stderr,\n-\t\t\t   _(\"WARNING: You called a Git command named '%s', \"\n-\t\t\t     \"which does not exist.\"),\n+\t\t\t   _(\"WARNING: You called a Git command named '%s', which does not exist.\"),\n \t\t\t   cmd);\n-\t\tif (cfg.autocorrect == AUTOCORRECT_IMMEDIATELY)\n-\t\t\tfprintf_ln(stderr,\n-\t\t\t\t   _(\"Continuing under the assumption that \"\n-\t\t\t\t     \"you meant '%s'.\"),\n-\t\t\t\t   assumed);\n-\t\telse if (cfg.autocorrect == AUTOCORRECT_PROMPT) {\n-\t\t\tchar *answer;\n-\t\t\tstruct strbuf msg = STRBUF_INIT;\n-\t\t\tstrbuf_addf(&msg, _(\"Run '%s' instead [y/N]? \"), assumed);\n-\t\t\tanswer = git_prompt(msg.buf, PROMPT_ECHO);\n-\t\t\tstrbuf_release(&msg);\n-\t\t\tif (!(starts_with(answer, \"y\") ||\n-\t\t\t      starts_with(answer, \"Y\")))\n-\t\t\t\texit(1);\n-\t\t} else {\n-\t\t\tfprintf_ln(stderr,\n-\t\t\t\t   _(\"Continuing in %0.1f seconds, \"\n-\t\t\t\t     \"assuming that you meant '%s'.\"),\n-\t\t\t\t   (float)cfg.autocorrect/10.0, assumed);\n-\t\t\tsleep_millisec(cfg.autocorrect * 100);\n-\t\t}\n \n-\t\tcmdnames_release(&cfg.aliases);\n+\t\tautocorr_prompt_or_delay(&autocorr, assumed);\n+\n+\t\tcmdnames_release(&aliases);\n \t\tcmdnames_release(&main_cmds);\n \t\tcmdnames_release(&other_cmds);\n \t\treturn assumed;\n-- \n2.53.0\n\n"},{"id":"538221","messageId":"SY0P300MB0801FB07C6D570E19243A99DCE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB08013E35DCA8FC31B0662125CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v2 3/5] parseopt: autocorrect mistyped subcommands","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-08T23:16:57Z","receivedAt":"2026-03-08T23:17:23Z","isPatch":true,"body":"Enable autocorrection for mandatory subcommands parsed through\nparse-options.\n\nAUTOCORR_HINTONLY is equivalent to AUTOCORR_NEVER here, because builtins\nhave a limited number of subcommands. They are currently small enough.\nTherefore, displaying all subcommands via usage_with_options() is\ngood enough. This keeps the autocorrection handling simple, too.\n\nAlso, use a dynamic threshold for similar_enough(), which can yield more\naccurate typo correction results. Although subcommands are often short,\nthey can still vary across builtins. And a fixed threshold won't do\nbetter on both short and long subcommands at the same time.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\nChanges in v2:\n  - Minor rewording in the commit message regarding AUTOCORR_HINTONLY.\n\n parse-options.c | 76 +++++++++++++++++++++++++++++++++++++++++++++++--\n 1 file changed, 74 insertions(+), 2 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex 6bb0c5697099..cbbb04b9997a 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -6,6 +6,8 @@\n #include \"strbuf.h\"\n #include \"string-list.h\"\n #include \"utf8.h\"\n+#include \"autocorrect.h\"\n+#include \"levenshtein.h\"\n \n static int disallow_abbreviated_options;\n \n@@ -621,6 +623,64 @@ static int parse_subcommand(const char *arg, const struct option *options)\n \treturn -1;\n }\n \n+static void find_subcommands(struct string_list *list,\n+\t\t\t     const struct option *options)\n+{\n+\tfor (; options->type != OPTION_END; options++) {\n+\t\tif (options->type == OPTION_SUBCOMMAND)\n+\t\t\tstring_list_append(list, options->long_name);\n+\t}\n+}\n+\n+static int similar_enough(const char *cmd, unsigned int dist)\n+{\n+\tsize_t len = strlen(cmd);\n+\tunsigned int threshold = len < 3 ? 1 : len < 6 ? 3 : 6;\n+\n+\treturn dist < threshold;\n+}\n+\n+static const char *autocorrect_subcmd(const char *cmd,\n+\t\t\t\t      struct string_list *cmds)\n+{\n+\tstruct autocorr autocorr = { 0 };\n+\n+\tautocorr_resolve(&autocorr);\n+\n+\tif (autocorr.mode == AUTOCORR_NEVER ||\n+\t    autocorr.mode == AUTOCORR_HINTONLY)\n+\t\treturn NULL;\n+\n+\tunsigned int min = -1;\n+\tunsigned int ties = 0;\n+\tstruct string_list_item *cand;\n+\tstruct string_list_item *best = NULL;\n+\n+\tfor_each_string_list_item(cand, cmds) {\n+\t\tunsigned int dist = levenshtein(cmd, cand->string, 0, 2, 1, 3);\n+\n+\t\tif (dist < min) {\n+\t\t\tmin = dist;\n+\t\t\tbest = cand;\n+\t\t\tties = 0;\n+\n+\t\t} else if (dist == min) {\n+\t\t\tties++;\n+\t\t}\n+\t}\n+\n+\tif (ties == 0 && similar_enough(cmd, min)) {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"WARNING: You called a subcommand named '%s', which does not exist.\"),\n+\t\t\t   cmd);\n+\n+\t\tautocorr_prompt_or_delay(&autocorr, best->string);\n+\t\treturn best->string;\n+\t}\n+\n+\treturn NULL;\n+}\n+\n static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \t\t\t\t\t       const char *arg,\n \t\t\t\t\t       const struct option *options,\n@@ -640,8 +700,20 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n \t\treturn PARSE_OPT_DONE;\n \n-\terror(_(\"unknown subcommand: `%s'\"), arg);\n-\tusage_with_options(usagestr, options);\n+\tstruct string_list cmds = STRING_LIST_INIT_NODUP;\n+\n+\tfind_subcommands(&cmds, options);\n+\n+\tconst char *cmd = autocorrect_subcmd(arg, &cmds);\n+\n+\tif (!cmd) {\n+\t\terror(_(\"unknown subcommand: `%s'\"), arg);\n+\t\tusage_with_options(usagestr, options);\n+\t}\n+\n+\tstring_list_clear(&cmds, 0);\n+\tparse_subcommand(cmd, options);\n+\treturn PARSE_OPT_SUBCOMMAND;\n }\n \n static void check_typos(const char *arg, const struct option *options)\n-- \n2.53.0\n\n"},{"id":"538222","messageId":"SY0P300MB08016D8AAA50C3DBB83FF794CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB08013E35DCA8FC31B0662125CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v2 4/5] parseopt: enable subcommand autocorrect for remote and notes","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-08T23:16:58Z","receivedAt":"2026-03-08T23:17:25Z","isPatch":true,"body":"Add PARSE_OPT_SUBCOMMAND_AUTOCORR to enable autocorrection for\nsubcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL.\n\nUse it for git-remote and git-notes, so mistyped subcommands can be\nautomatically corrected, and builtin entry points no longer need\nto handle missing subcommand error path themselves.\n\nThis is safe for these two builtins, because they either resolve to a\nsingle subcommand or take no subcommand at all. This means that if the\nsubcommand parser encounters an unknown argument, it must be a mistyped\nsubcommand.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n builtin/notes.c  | 10 +++-------\n builtin/remote.c | 12 ++++--------\n parse-options.c  | 16 +++++++++-------\n parse-options.h  |  1 +\n 4 files changed, 17 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 9af602bdd7b4..087eb898a441 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -1149,14 +1149,10 @@ int cmd_notes(int argc,\n \n \trepo_config(the_repository, git_default_config, NULL);\n \targc = parse_options(argc, argv, prefix, options, git_notes_usage,\n-\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n-\tif (!fn) {\n-\t\tif (argc) {\n-\t\t\terror(_(\"unknown subcommand: `%s'\"), argv[0]);\n-\t\t\tusage_with_options(git_notes_usage, options);\n-\t\t}\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n+\t\t\t     PARSE_OPT_SUBCOMMAND_AUTOCORR);\n+\tif (!fn)\n \t\tfn = list;\n-\t}\n \n \tif (override_notes_ref) {\n \t\tstruct strbuf sb = STRBUF_INIT;\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex ace390c671d6..d1d6244a662a 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -1949,15 +1949,11 @@ int cmd_remote(int argc,\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, builtin_remote_usage,\n-\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n+\t\t\t     PARSE_OPT_SUBCOMMAND_AUTOCORR);\n \n-\tif (fn) {\n+\tif (fn)\n \t\treturn !!fn(argc, argv, prefix, repo);\n-\t} else {\n-\t\tif (argc) {\n-\t\t\terror(_(\"unknown subcommand: `%s'\"), argv[0]);\n-\t\t\tusage_with_options(builtin_remote_usage, options);\n-\t\t}\n+\telse\n \t\treturn !!show_all();\n-\t}\n }\ndiff --git a/parse-options.c b/parse-options.c\nindex cbbb04b9997a..b7c818e818fe 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -691,14 +691,16 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \tif (!err)\n \t\treturn PARSE_OPT_SUBCOMMAND;\n \n-\t/*\n-\t * arg is neither a short or long option nor a subcommand.  Since this\n-\t * command has a default operation mode, we have to treat this arg and\n-\t * all remaining args as args meant to that default operation mode.\n-\t * So we are done parsing.\n-\t */\n-\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n+\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL &&\n+\t    !(ctx->flags & PARSE_OPT_SUBCOMMAND_AUTOCORR)) {\n+\t\t/*\n+\t\t * arg is neither a short or long option nor a subcommand.\n+\t\t * Since this command has a default operation mode, we have to\n+\t\t * treat this arg and all remaining args as args meant to that\n+\t\t * default operation mode.  So we are done parsing.\n+\t\t */\n \t\treturn PARSE_OPT_DONE;\n+\t}\n \n \tstruct string_list cmds = STRING_LIST_INIT_NODUP;\n \ndiff --git a/parse-options.h b/parse-options.h\nindex 706de9729f6b..f29ac337893c 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -40,6 +40,7 @@ enum parse_opt_flags {\n \tPARSE_OPT_ONE_SHOT = 1 << 5,\n \tPARSE_OPT_SHELL_EVAL = 1 << 6,\n \tPARSE_OPT_SUBCOMMAND_OPTIONAL = 1 << 7,\n+\tPARSE_OPT_SUBCOMMAND_AUTOCORR = 1 << 8,\n };\n \n enum parse_opt_option_flags {\n-- \n2.53.0\n\n"},{"id":"538223","messageId":"SY0P300MB080113CA2B4CF17AE6BEDD43CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB08013E35DCA8FC31B0662125CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v2 5/5] help: add tests for subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-08T23:16:59Z","receivedAt":"2026-03-08T23:17:28Z","isPatch":true,"body":"These tests cover default behavior (help.autocorrect is unset), no\ncorrection, immediate correction, delayed correction, and rejection\nwhen the typo is too dissimilar.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n t/t9004-autocorrect-subcommand.sh | 49 +++++++++++++++++++++++++++++++\n 1 file changed, 49 insertions(+)\n create mode 100755 t/t9004-autocorrect-subcommand.sh\n\ndiff --git a/t/t9004-autocorrect-subcommand.sh b/t/t9004-autocorrect-subcommand.sh\nnew file mode 100755\nindex 000000000000..760760c8851a\n--- /dev/null\n+++ b/t/t9004-autocorrect-subcommand.sh\n@@ -0,0 +1,49 @@\n+#!/bin/sh\n+\n+test_description='subcommand auto-correction test\n+\n+Test autocorrection for subcommands with different\n+help.autocorrect mode.'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' \"\n+\techo '^error: unknown subcommand: ' >grep_unknown\n+\"\n+\n+test_expect_success 'default is not to autocorrect' '\n+\ttest_must_fail git worktree lsit 2>actual &&\n+\thead -n1 actual >first && test_grep -f grep_unknown first\n+'\n+\n+for mode in false no off 0 show never; do\n+\ttest_expect_success \"'$mode' disables autocorrection\" \"\n+\t\ttest_config help.autocorrect $mode &&\n+\n+\t\ttest_must_fail git worktree lsit 2>actual &&\n+\t\thead -n1 actual >first && test_grep -f grep_unknown first\n+\t\"\n+done\n+\n+for mode in -39 immediate 1; do\n+\ttest_expect_success \"autocorrect immediately with '$mode'\" - <<-EOT\n+\t\ttest_config help.autocorrect $mode &&\n+\n+\t\tgit worktree lsit 2>actual &&\n+\t\ttest_grep \"you meant 'list'\\.$\" actual\n+\tEOT\n+done\n+\n+test_expect_success 'delay path is executed' - <<-\\EOT\n+\ttest_config help.autocorrect 2 &&\n+\n+\tgit worktree lsit 2>actual &&\n+\ttest_grep '^Continuing in 0.2 seconds, ' actual\n+EOT\n+\n+test_expect_success 'deny if too dissimilar' - <<-\\EOT\n+\ttest_must_fail git remote rensnr 2>actual &&\n+\thead -n1 actual >first && test_grep -f grep_unknown first\n+EOT\n+\n+test_done\n-- \n2.53.0\n\n"},{"id":"538224","messageId":"xmqq1phtao03.fsf@gitster.g","threadId":"65168","inReplyTo":"SY0P300MB0801422323C4C4185B9617A1CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH 1/5] parseopt: extract subcommand handling from parse_options_step()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-08T23:40:44Z","receivedAt":"2026-03-08T23:40:46Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> -static enum parse_opt_result parse_subcommand(const char *arg,\n> -\t\t\t\t\t      const struct option *options)\n\nSo, this function used to return either PARSE_OPT_SUBCOMMAND or\nPARSE_OPT_UNKNOWN, and the caller (in an \"switch ()\" statement in\nthe parse_options_step() we see below) expected only these two.  We\nnow instead ...\n\n> +static int parse_subcommand(const char *arg, const struct option *options)\n\n... make it return either 0 (success - we found a subcommand) or -1\n(failure - we did not).  The updated caller does use the return\nvalue in \"if ()\" condition to return early when we found a\nsubcommand successfully, which is a lot more straight-forward than\nthe original \"switch()\".  The original is even worse in that it\nenumerates other PARSE_OPT_* values that are possible at the time of\nwriting it, instead of catching everything else with \"default:\",\nwhich makes the switch() statement a maintenance burden.  Getting\nrid of that switch and clarifying that there are only two possible\noutcome from this function alone is a good enough justification to\nhave this clean-up patch.  Very well done.\n\n\nEditing the hunk to show only the postimage reveals that the new\nimplementation has CodingGuidelines violation ...\n\n>  {\n> +\tfor (; options->type != OPTION_END; options++) {\n> +\t\tif (options->type != OPTION_SUBCOMMAND ||\n> +\t\t    strcmp(options->long_name, arg))\n> +\t\t\tcontinue;\n>  \n> +\t\tparse_opt_subcommand_fn **opt_val = options->value;\n\n... here.  Move the variable definition up in the block introduced\nby the for(;;) statement, perhaps?\n\n> +\t\t*opt_val = options->subcommand_fn;\n> +\n> +\t\treturn 0;\n> +\t}\n> +\n> +\treturn -1;\n> +}\n> +\n\n\nAnd the part of the caller of parse_subcommand() that used to be the\nswitch() statement in the parse_options_step() is now extracted into\nyet another helper function here.\n\n> +static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n> +\t\t\t\t\t       const char *arg,\n> +\t\t\t\t\t       const struct option *options,\n> +\t\t\t\t\t       const char * const usagestr[])\n> +{\n> +\tint err = parse_subcommand(arg, options);\n> +\n> +\tif (!err)\n> +\t\treturn PARSE_OPT_SUBCOMMAND;\n> +\n> +\t/*\n> +\t * arg is neither a short or long option nor a subcommand.  Since this\n> +\t * command has a default operation mode, we have to treat this arg and\n> +\t * all remaining args as args meant to that default operation mode.\n> +\t * So we are done parsing.\n> +\t */\n\nThanks to being a small helper function, we lost two levels of\nindentation which made this comment a lot more readable with\nreflowing the lines ;-)\n\n> +\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n> +\t\treturn PARSE_OPT_DONE;\n> +\n> +\terror(_(\"unknown subcommand: `%s'\"), arg);\n> +\tusage_with_options(usagestr, options);\n>  }\n\nIt is clear that there is no unintended behaviour change around this\nhelper function and parse_subcommand().  Very nice.\n\n> @@ -990,37 +1016,16 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n>  \t\tif (*arg != '-' || !arg[1]) {\n>  \t\t\tif (parse_nodash_opt(ctx, arg, options) == 0)\n>  \t\t\t\tcontinue;\n> +\n>  \t\t\tif (!ctx->has_subcommands) {\n>  \t\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n>  \t\t\t\t\treturn PARSE_OPT_NON_OPTION;\n>  \t\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n>  \t\t\t\tcontinue;\n> +\n> +\t\t\t} else {\n> +\t\t\t\treturn handle_subcommand(ctx, arg,\n> +\t\t\t\t\t\t\t options, usagestr);\n>  \t\t\t}\n>  \t\t}\n\nEditing the hunk to only show the postimage shows us that the blank\nline at the end of the \"if ()\" block is funny.  Drop it.  Or even\nbetter, as the block either returns or continues anyway, lose the\n\"else\" block, perhaps?  Which will make the above read like so:\n\n\t\t\tif (!ctx->has_subcommands) {\n\t\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n                                \treturn PARSE_OPT_NON_OPTION;\n\t\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n\t\t\t\tcontinue;\n\t\t\t}\n\n                        return has_subcommands(ctx, arg, options, usagestr);\n\nor swapping the logic the other way around, i.e.,\n\n\t\t\tif (ctx->has_subcommands)\n\t\t\t\treturn has_subcommands(ctx, arg,\n\t\t\t\t\t\t       options, usagestr);\n\n                        if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n                                return PARSE_OPT_NON_OPTION;\n\n                        ctx->out[ctx->cpidx++] = ctx->argv[0];\n                        continue;\n\nmay make the result even easier to read, perhaps?\n"},{"id":"538226","messageId":"xmqqsea998vd.fsf@gitster.g","threadId":"65168","inReplyTo":"SY0P300MB080180E17B471505A4150C08CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH 2/5] help: refactor command autocorrection handling","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-08T23:52:54Z","receivedAt":"2026-03-08T23:52:56Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> Refactor autocorrect config into struct autocorr, with an explicit mode\n> enum and a separate delay field. Move config parsing, TTY validation,\n> and prompt handling into autocorrect.c.\n>\n> This simplifies help.c and makes state handling for autocorrect explicit\n> and easier to maintain.\n>\n> Signed-off-by: Jiamu Sun <39@barroit.sh>\n> ---\n>  Makefile      |   1 +\n>  autocorrect.c |  92 +++++++++++++++++++++++++++++++++++++++++++\n>  autocorrect.h |  23 +++++++++++\n>  help.c        | 106 ++++++++++----------------------------------------\n>  4 files changed, 136 insertions(+), 86 deletions(-)\n>  create mode 100644 autocorrect.c\n>  create mode 100644 autocorrect.h\n>\n> diff --git a/Makefile b/Makefile\n> index f3264d0a37cc..6111631c2caa 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -1098,6 +1098,7 @@ LIB_OBJS += archive-tar.o\n>  LIB_OBJS += archive-zip.o\n>  LIB_OBJS += archive.o\n>  LIB_OBJS += attr.o\n> +LIB_OBJS += autocorrect.o\n>  LIB_OBJS += base85.o\n>  LIB_OBJS += bisect.o\n>  LIB_OBJS += blame.o\n> diff --git a/autocorrect.c b/autocorrect.c\n> new file mode 100644\n> index 000000000000..eaae01645910\n> --- /dev/null\n> +++ b/autocorrect.c\n> @@ -0,0 +1,92 @@\n> +#define USE_THE_REPOSITORY_VARIABLE\n> +\n> +#include \"git-compat-util.h\"\n> +#include \"autocorrect.h\"\n> +#include \"config.h\"\n> +#include \"parse.h\"\n> +#include \"strbuf.h\"\n> +#include \"prompt.h\"\n> +#include \"gettext.h\"\n> +\n> +static enum autocorr_mode parse_autocorr(const char *value)\n> +{\n> +\tswitch (git_parse_maybe_bool_text(value)) {\n> +\t\tcase 1:\n> +\t\t\treturn AUTOCORR_IMMEDIATELY;\n> +\t\tcase 0:\n> +\t\t\treturn AUTOCORR_HINTONLY;\n> +\t\tdefault: /* other random text */\n> +\t\t\tbreak;\n> +\t}\n> +\n> +\tif (!strcmp(value, \"prompt\"))\n> +\t\treturn AUTOCORR_PROMPT;\n> +\telse if (!strcmp(value, \"never\"))\n> +\t\treturn AUTOCORR_NEVER;\n> +\telse if (!strcmp(value, \"immediate\"))\n> +\t\treturn AUTOCORR_IMMEDIATELY;\n> +\telse if (!strcmp(value, \"show\"))\n> +\t\treturn AUTOCORR_HINTONLY;\n> +\telse\n> +\t\treturn AUTOCORR_DELAY;\n> +}\n\nYou snuck in unnecessary \"style fixes\" to make bunch of \"if()return\"\ninto if/else if cascade.  Also what was AUTOCORRECT_SHOW is now\nreturned as AUTOCORR_HINTONLY.  There is no explanation on the\nreason behind these changes in the proposed log message, and hiding\nthese small changes in a code movement patch makes reviewing the\nseries harder than necessary.\n\nThe patch is doing too many things (namely, (1) code movement that\nwill make it later reusable as a side effect but has no semantic\nchanges in the current code, plus (2) change in style (like the one\nwe see here), semantics (possibly the difference in SHOW and\nHINTONLY we see here) and features, possibly including the renaming\nof AUTOCORRECT_* into AUTOCORR_*.)  Let's have \"restructure with\ncode movement and nothing else\", followed by \"other changes\n\nI'll stop here, and expect this step to be split into at least two\npatches to make it more readable before we can review it again.\n\nThanks.\n"},{"id":"538227","messageId":"xmqqo6kx98bi.fsf@gitster.g","threadId":"65168","inReplyTo":"SY0P300MB08010BFD00AA7C73503D8B92CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH 3/5] parseopt: autocorrect mistyped subcommands","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-09T00:04:49Z","receivedAt":"2026-03-09T00:04:52Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> +static const char *autocorrect_subcmd(const char *cmd,\n> +\t\t\t\t      struct string_list *cmds)\n> +{\n> +\tstruct autocorr autocorr = { 0 };\n> +\n> +\tautocorr_resolve(&autocorr);\n> +\n> +\tif (autocorr.mode == AUTOCORR_NEVER ||\n> +\t    autocorr.mode == AUTOCORR_HINTONLY)\n> +\t\treturn NULL;\n> +\n> +\tunsigned int min = -1;\n> +\tunsigned int ties = 0;\n> +\tstruct string_list_item *cand;\n> +\tstruct string_list_item *best = NULL;\n\nCodingGuidelines: -Wdeclaration-after-statement is here.\n\nUsing \"-1\" as \"the maximum value this unsigned type can represent\"\nis somewhat misleading.  Can't we avoid that?\n\n"},{"id":"538244","messageId":"SY0P300MB0801019DB36BC682AFDBF2C9CE79A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"xmqq1phtao03.fsf@gitster.g","subject":"Re: [PATCH 1/5] parseopt: extract subcommand handling from parse_options_step()","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-09T01:56:48Z","receivedAt":"2026-03-09T01:56:55Z","isPatch":true,"body":"On Sun, Mar 08, 2026 at 04:40:44PM -0700, Junio C Hamano wrote:\n> ... here.  Move the variable definition up in the block introduced\n> by the for(;;) statement, perhaps?\n> \n> > +\t\t*opt_val = options->subcommand_fn;\n> > +\n> > +\t\treturn 0;\n> > +\t}\n\nAgree. Actually, I forgot to check the CodingGuidelines.  Will fix the\ncoding style issue in v3.\n\n> Editing the hunk to only show the postimage shows us that the blank\n> line at the end of the \"if ()\" block is funny.  Drop it.  Or even\n> better, as the block either returns or continues anyway, lose the\n> \"else\" block, perhaps?  Which will make the above read like so:\n> \n> \t\t\tif (!ctx->has_subcommands) {\n> \t\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n>                                 \treturn PARSE_OPT_NON_OPTION;\n> \t\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n> \t\t\t\tcontinue;\n> \t\t\t}\n> \n>                         return has_subcommands(ctx, arg, options, usagestr);\n> \n> or swapping the logic the other way around, i.e.,\n> \n> \t\t\tif (ctx->has_subcommands)\n> \t\t\t\treturn has_subcommands(ctx, arg,\n> \t\t\t\t\t\t       options, usagestr);\n> \n>                         if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n>                                 return PARSE_OPT_NON_OPTION;\n> \n>                         ctx->out[ctx->cpidx++] = ctx->argv[0];\n>                         continue;\n> \n> may make the result even easier to read, perhaps?\n\nSwapping the logic works better here. Returning early in the\nctx->has_subcommands path lets the rest of the block assume\n!ctx->has_subcommands, so the extra check can go away. That makes\nthe code easier to read. Will do that.\n\n-- \nJiamu Sun <39@barroit.sh>\n"},{"id":"538245","messageId":"SY0P300MB080153161083DAA000A77035CE79A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"xmqqsea998vd.fsf@gitster.g","subject":"Re: [PATCH 2/5] help: refactor command autocorrection handling","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-09T02:06:55Z","receivedAt":"2026-03-09T02:07:02Z","isPatch":true,"body":"On Sun, Mar 08, 2026 at 04:52:54PM -0700, Junio C Hamano wrote:\n> You snuck in unnecessary \"style fixes\" to make bunch of \"if()return\"\n> into if/else if cascade.  Also what was AUTOCORRECT_SHOW is now\n> returned as AUTOCORR_HINTONLY.  There is no explanation on the\n> reason behind these changes in the proposed log message, and hiding\n> these small changes in a code movement patch makes reviewing the\n> series harder than necessary.\n> \n> The patch is doing too many things (namely, (1) code movement that\n> will make it later reusable as a side effect but has no semantic\n> changes in the current code, plus (2) change in style (like the one\n> we see here), semantics (possibly the difference in SHOW and\n> HINTONLY we see here) and features, possibly including the renaming\n> of AUTOCORRECT_* into AUTOCORR_*.)  Let's have \"restructure with\n> code movement and nothing else\", followed by \"other changes\n> \n> I'll stop here, and expect this step to be split into at least two\n> patches to make it more readable before we can review it again.\n> \n> Thanks.\n\nAh, I realized this patch is completely unreadable. You are right, the\ncode movement ended up hiding other changes entirely. Will split it to\nmake each change obvious.\n\n-- \nJiamu Sun <39@barroit.sh>\n"},{"id":"538246","messageId":"SY0P300MB0801336282FC2BB0FED45085CE79A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"xmqqo6kx98bi.fsf@gitster.g","subject":"Re: [PATCH 3/5] parseopt: autocorrect mistyped subcommands","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-09T02:11:45Z","receivedAt":"2026-03-09T02:11:52Z","isPatch":true,"body":"On Sun, Mar 08, 2026 at 05:04:49PM -0700, Junio C Hamano wrote:\n> > +\tunsigned int min = -1;\n> > +\tunsigned int ties = 0;\n> > +\tstruct string_list_item *cand;\n> > +\tstruct string_list_item *best = NULL;\n> \n> CodingGuidelines: -Wdeclaration-after-statement is here.\n\nWill fix that to follow CodingGuidelines.\n\n> Using \"-1\" as \"the maximum value this unsigned type can represent\"\n> is somewhat misleading.  Can't we avoid that?\n>\n\nYes, UINT_MAX is a better fit here.\n\n-- \nJiamu Sun <39@barroit.sh>\n"},{"id":"538382","messageId":"SY0P300MB0801C6F21C2D8F49892DF8E7CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB08013E35DCA8FC31B0662125CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v3 0/8] parseopt: add subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-10T11:40:59Z","receivedAt":"2026-03-10T11:41:21Z","isPatch":true,"body":"Git currently provides auto-correction for builtins and aliases, but\nlacks this functionality for subcommands parsed via the parse-options\nAPI. Subcommands are also commands, and typos will occur, too. Like:\n\n\tgit remote add-rul\n\nSo, this series introduces subcommand auto-correction.\n\nBy default, this implementation enables autocorrection for builtins\nwith mandatory subcommands. However, for those using\nPARSE_OPT_SUBCOMMAND_OPTIONAL, autocorrection is skipped to avoid\nmisinterpreting legitimate unknown arguments as mistyped subcommands.\n\nTo allow builtins with optional subcommands to explicitly opt in,\nthis series adds the PARSE_OPT_SUBCOMMAND_AUTOCORR flag. This flag\nis subsequently enabled for git-remote and git-notes.\n\nAdditionally, the existing autocorrection logic is extracted from\nhelp.c so subcommand handling can reuse the same config parsing and\nprompt/delay logic.\n\nSome split string literals are also combined so the full text is easier\nto grep for.\n\nChanges in v3:\n  - Align with the coding guildline\n\nChanges in v2:\n  - Reword the explanation of default autocorrection behavior\n\nJiamu Sun (8):\n  parseopt: extract subcommand handling from parse_options_step()\n  help: make autocorrect handling reusable\n  help: move tty check for autocorrection to autocorrect.c\n  autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINTONLY\n  autocorrect: provide config resolution API\n  parseopt: autocorrect mistyped subcommands\n  parseopt: enable subcommand autocorrection for git-remote and\n    git-notes\n  help: add tests for subcommand autocorrection\n\n Makefile                          |   1 +\n autocorrect.c                     |  89 +++++++++++++++++\n autocorrect.h                     |  21 ++++\n builtin/notes.c                   |  10 +-\n builtin/remote.c                  |  12 +--\n help.c                            | 104 ++++----------------\n parse-options.c                   | 156 ++++++++++++++++++++++--------\n parse-options.h                   |   1 +\n t/t9004-autocorrect-subcommand.sh |  51 ++++++++++\n 9 files changed, 305 insertions(+), 140 deletions(-)\n create mode 100644 autocorrect.c\n create mode 100644 autocorrect.h\n create mode 100755 t/t9004-autocorrect-subcommand.sh\n\n\nbase-commit: 795c338de725e13bd361214c6b768019fc45a2c1\n-- \n2.53.0\n\n"},{"id":"538383","messageId":"SY0P300MB080114A7548292AB4B60D817CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801C6F21C2D8F49892DF8E7CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step()","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-10T11:41:00Z","receivedAt":"2026-03-10T11:41:24Z","isPatch":true,"body":"Move the subcommand branch out of parse_options_step() into a new\nhandle_subcommand() helper. Also, make parse_subcommand() return a\nsimple success/failure status.\n\nThis removes the switch over impossible parse_opt_result values and\nmakes the non-option path easier to follow and maintain.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\nChanges in v3:\n  - Fix coding style issue\n\n parse-options.c | 86 ++++++++++++++++++++++++++-----------------------\n 1 file changed, 46 insertions(+), 40 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex c9cafc21b903..33f26d6b6179 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -605,17 +605,44 @@ static enum parse_opt_result parse_nodash_opt(struct parse_opt_ctx_t *p,\n \treturn PARSE_OPT_ERROR;\n }\n \n-static enum parse_opt_result parse_subcommand(const char *arg,\n-\t\t\t\t\t      const struct option *options)\n+static int parse_subcommand(const char *arg, const struct option *options)\n {\n-\tfor (; options->type != OPTION_END; options++)\n-\t\tif (options->type == OPTION_SUBCOMMAND &&\n-\t\t    !strcmp(options->long_name, arg)) {\n-\t\t\t*(parse_opt_subcommand_fn **)options->value = options->subcommand_fn;\n-\t\t\treturn PARSE_OPT_SUBCOMMAND;\n-\t\t}\n+\tfor (; options->type != OPTION_END; options++) {\n+\t\tparse_opt_subcommand_fn **opt_val;\n \n-\treturn PARSE_OPT_UNKNOWN;\n+\t\tif (options->type != OPTION_SUBCOMMAND ||\n+\t\t    strcmp(options->long_name, arg))\n+\t\t\tcontinue;\n+\n+\t\topt_val = options->value;\n+\t\t*opt_val = options->subcommand_fn;\n+\t\treturn 0;\n+\t}\n+\n+\treturn -1;\n+}\n+\n+static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n+\t\t\t\t\t       const char *arg,\n+\t\t\t\t\t       const struct option *options,\n+\t\t\t\t\t       const char * const usagestr[])\n+{\n+\tint err = parse_subcommand(arg, options);\n+\n+\tif (!err)\n+\t\treturn PARSE_OPT_SUBCOMMAND;\n+\n+\t/*\n+\t * arg is neither a short or long option nor a subcommand.  Since this\n+\t * command has a default operation mode, we have to treat this arg and\n+\t * all remaining args as args meant to that default operation mode.\n+\t * So we are done parsing.\n+\t */\n+\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n+\t\treturn PARSE_OPT_DONE;\n+\n+\terror(_(\"unknown subcommand: `%s'\"), arg);\n+\tusage_with_options(usagestr, options);\n }\n \n static void check_typos(const char *arg, const struct option *options)\n@@ -990,38 +1017,17 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\tif (*arg != '-' || !arg[1]) {\n \t\t\tif (parse_nodash_opt(ctx, arg, options) == 0)\n \t\t\t\tcontinue;\n-\t\t\tif (!ctx->has_subcommands) {\n-\t\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n-\t\t\t\t\treturn PARSE_OPT_NON_OPTION;\n-\t\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n-\t\t\t\tcontinue;\n-\t\t\t}\n-\t\t\tswitch (parse_subcommand(arg, options)) {\n-\t\t\tcase PARSE_OPT_SUBCOMMAND:\n-\t\t\t\treturn PARSE_OPT_SUBCOMMAND;\n-\t\t\tcase PARSE_OPT_UNKNOWN:\n-\t\t\t\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n-\t\t\t\t\t/*\n-\t\t\t\t\t * arg is neither a short or long\n-\t\t\t\t\t * option nor a subcommand.  Since\n-\t\t\t\t\t * this command has a default\n-\t\t\t\t\t * operation mode, we have to treat\n-\t\t\t\t\t * this arg and all remaining args\n-\t\t\t\t\t * as args meant to that default\n-\t\t\t\t\t * operation mode.\n-\t\t\t\t\t * So we are done parsing.\n-\t\t\t\t\t */\n-\t\t\t\t\treturn PARSE_OPT_DONE;\n-\t\t\t\terror(_(\"unknown subcommand: `%s'\"), arg);\n-\t\t\t\tusage_with_options(usagestr, options);\n-\t\t\tcase PARSE_OPT_COMPLETE:\n-\t\t\tcase PARSE_OPT_HELP:\n-\t\t\tcase PARSE_OPT_ERROR:\n-\t\t\tcase PARSE_OPT_DONE:\n-\t\t\tcase PARSE_OPT_NON_OPTION:\n-\t\t\t\t/* Impossible. */\n-\t\t\t\tBUG(\"parse_subcommand() cannot return these\");\n+\n+\t\t\tif (ctx->has_subcommands) {\n+\t\t\t\treturn handle_subcommand(ctx, arg, options,\n+\t\t\t\t\t\t\t usagestr);\n \t\t\t}\n+\n+\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n+\t\t\t\treturn PARSE_OPT_NON_OPTION;\n+\n+\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n+\t\t\tcontinue;\n \t\t}\n \n \t\t/* lone -h asks for help */\n-- \n2.53.0\n\n"},{"id":"538384","messageId":"SY0P300MB08017404C417D02058423130CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801C6F21C2D8F49892DF8E7CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v3 2/8] help: make autocorrect handling reusable","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-10T11:41:01Z","receivedAt":"2026-03-10T11:41:25Z","isPatch":true,"body":"Move config parsing and prompt/delay handling into autocorrect.c and\nexpose them in autocorrect.h. This makes autocorrect reusable regardless\nof which target links against it.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\nThis patch moves autocorrect handling out of help.c as a preparatory\nstep for subcommand autocorrection.\n\nThis is necessary because help.c uses load_builtin_commands(), which is\nprovided by git.o. Executables that do not link against git.o, such as\ngit-daemon, will hit a link error if they use symbols defined in help.o.\nA simple and clean fix is to make the relevant functions independent of\nhelp.c and move them to a dedicated file.\n\nChanges in v3:\n  - Split patch so diffs don't get hidden by code movement\n\n Makefile      |  1 +\n autocorrect.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++\n autocorrect.h | 16 ++++++++++++\n help.c        | 64 +++------------------------------------------\n 4 files changed, 93 insertions(+), 60 deletions(-)\n create mode 100644 autocorrect.c\n create mode 100644 autocorrect.h\n\ndiff --git a/Makefile b/Makefile\nindex f3264d0a37cc..6111631c2caa 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1098,6 +1098,7 @@ LIB_OBJS += archive-tar.o\n LIB_OBJS += archive-zip.o\n LIB_OBJS += archive.o\n LIB_OBJS += attr.o\n+LIB_OBJS += autocorrect.o\n LIB_OBJS += base85.o\n LIB_OBJS += bisect.o\n LIB_OBJS += blame.o\ndiff --git a/autocorrect.c b/autocorrect.c\nnew file mode 100644\nindex 000000000000..1037f032019b\n--- /dev/null\n+++ b/autocorrect.c\n@@ -0,0 +1,72 @@\n+#include \"git-compat-util.h\"\n+#include \"autocorrect.h\"\n+#include \"config.h\"\n+#include \"parse.h\"\n+#include \"strbuf.h\"\n+#include \"prompt.h\"\n+#include \"gettext.h\"\n+\n+static int parse_autocorrect(const char *value)\n+{\n+\tswitch (git_parse_maybe_bool_text(value)) {\n+\t\tcase 1:\n+\t\t\treturn AUTOCORRECT_IMMEDIATELY;\n+\t\tcase 0:\n+\t\t\treturn AUTOCORRECT_SHOW;\n+\t\tdefault: /* other random text */\n+\t\t\tbreak;\n+\t}\n+\n+\tif (!strcmp(value, \"prompt\"))\n+\t\treturn AUTOCORRECT_PROMPT;\n+\tif (!strcmp(value, \"never\"))\n+\t\treturn AUTOCORRECT_NEVER;\n+\tif (!strcmp(value, \"immediate\"))\n+\t\treturn AUTOCORRECT_IMMEDIATELY;\n+\tif (!strcmp(value, \"show\"))\n+\t\treturn AUTOCORRECT_SHOW;\n+\n+\treturn 0;\n+}\n+\n+void autocorr_resolve_config(const char *var, const char *value,\n+\t\t\t     const struct config_context *ctx, void *data)\n+{\n+\tint *out = data;\n+\n+\tif (!strcmp(var, \"help.autocorrect\")) {\n+\t\tint v = parse_autocorrect(value);\n+\n+\t\tif (!v) {\n+\t\t\tv = git_config_int(var, value, ctx->kvi);\n+\t\t\tif (v < 0 || v == 1)\n+\t\t\t\tv = AUTOCORRECT_IMMEDIATELY;\n+\t\t}\n+\n+\t\t*out = v;\n+\t}\n+}\n+\n+void autocorr_confirm(int autocorrect, const char *assumed)\n+{\n+\tif (autocorrect == AUTOCORRECT_IMMEDIATELY) {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"Continuing under the assumption that you meant '%s'.\"),\n+\t\t\t   assumed);\n+\t} else if (autocorrect == AUTOCORRECT_PROMPT) {\n+\t\tchar *answer;\n+\t\tstruct strbuf msg = STRBUF_INIT;\n+\n+\t\tstrbuf_addf(&msg, _(\"Run '%s' instead [y/N]? \"), assumed);\n+\t\tanswer = git_prompt(msg.buf, PROMPT_ECHO);\n+\t\tstrbuf_release(&msg);\n+\n+\t\tif (!(starts_with(answer, \"y\") || starts_with(answer, \"Y\")))\n+\t\t\texit(1);\n+\t} else {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"Continuing in %0.1f seconds, assuming that you meant '%s'.\"),\n+\t\t\t   (float)autocorrect / 10.0, assumed);\n+\t\tsleep_millisec(autocorrect * 100);\n+\t}\n+}\ndiff --git a/autocorrect.h b/autocorrect.h\nnew file mode 100644\nindex 000000000000..45609990c77e\n--- /dev/null\n+++ b/autocorrect.h\n@@ -0,0 +1,16 @@\n+#ifndef AUTOCORRECT_H\n+#define AUTOCORRECT_H\n+\n+#define AUTOCORRECT_SHOW (-4)\n+#define AUTOCORRECT_PROMPT (-3)\n+#define AUTOCORRECT_NEVER (-2)\n+#define AUTOCORRECT_IMMEDIATELY (-1)\n+\n+struct config_context;\n+\n+void autocorr_resolve_config(const char *var, const char *value,\n+\t\t\t     const struct config_context *ctx, void *data);\n+\n+void autocorr_confirm(int autocorr, const char *assumed);\n+\n+#endif /* AUTOCORRECT_H */\ndiff --git a/help.c b/help.c\nindex 95f576c5c81d..6be3ec9dfb12 100644\n--- a/help.c\n+++ b/help.c\n@@ -22,6 +22,7 @@\n #include \"repository.h\"\n #include \"alias.h\"\n #include \"utf8.h\"\n+#include \"autocorrect.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -541,34 +542,6 @@ struct help_unknown_cmd_config {\n \tstruct cmdnames aliases;\n };\n \n-#define AUTOCORRECT_SHOW (-4)\n-#define AUTOCORRECT_PROMPT (-3)\n-#define AUTOCORRECT_NEVER (-2)\n-#define AUTOCORRECT_IMMEDIATELY (-1)\n-\n-static int parse_autocorrect(const char *value)\n-{\n-\tswitch (git_parse_maybe_bool_text(value)) {\n-\t\tcase 1:\n-\t\t\treturn AUTOCORRECT_IMMEDIATELY;\n-\t\tcase 0:\n-\t\t\treturn AUTOCORRECT_SHOW;\n-\t\tdefault: /* other random text */\n-\t\t\tbreak;\n-\t}\n-\n-\tif (!strcmp(value, \"prompt\"))\n-\t\treturn AUTOCORRECT_PROMPT;\n-\tif (!strcmp(value, \"never\"))\n-\t\treturn AUTOCORRECT_NEVER;\n-\tif (!strcmp(value, \"immediate\"))\n-\t\treturn AUTOCORRECT_IMMEDIATELY;\n-\tif (!strcmp(value, \"show\"))\n-\t\treturn AUTOCORRECT_SHOW;\n-\n-\treturn 0;\n-}\n-\n static int git_unknown_cmd_config(const char *var, const char *value,\n \t\t\t\t  const struct config_context *ctx,\n \t\t\t\t  void *cb)\n@@ -577,17 +550,7 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \tconst char *subsection, *key;\n \tsize_t subsection_len;\n \n-\tif (!strcmp(var, \"help.autocorrect\")) {\n-\t\tint v = parse_autocorrect(value);\n-\n-\t\tif (!v) {\n-\t\t\tv = git_config_int(var, value, ctx->kvi);\n-\t\t\tif (v < 0 || v == 1)\n-\t\t\t\tv = AUTOCORRECT_IMMEDIATELY;\n-\t\t}\n-\n-\t\tcfg->autocorrect = v;\n-\t}\n+\tautocorr_resolve_config(var, value, ctx, &cfg->autocorrect);\n \n \t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n@@ -724,27 +687,8 @@ char *help_unknown_cmd(const char *cmd)\n \t\t\t   _(\"WARNING: You called a Git command named '%s', \"\n \t\t\t     \"which does not exist.\"),\n \t\t\t   cmd);\n-\t\tif (cfg.autocorrect == AUTOCORRECT_IMMEDIATELY)\n-\t\t\tfprintf_ln(stderr,\n-\t\t\t\t   _(\"Continuing under the assumption that \"\n-\t\t\t\t     \"you meant '%s'.\"),\n-\t\t\t\t   assumed);\n-\t\telse if (cfg.autocorrect == AUTOCORRECT_PROMPT) {\n-\t\t\tchar *answer;\n-\t\t\tstruct strbuf msg = STRBUF_INIT;\n-\t\t\tstrbuf_addf(&msg, _(\"Run '%s' instead [y/N]? \"), assumed);\n-\t\t\tanswer = git_prompt(msg.buf, PROMPT_ECHO);\n-\t\t\tstrbuf_release(&msg);\n-\t\t\tif (!(starts_with(answer, \"y\") ||\n-\t\t\t      starts_with(answer, \"Y\")))\n-\t\t\t\texit(1);\n-\t\t} else {\n-\t\t\tfprintf_ln(stderr,\n-\t\t\t\t   _(\"Continuing in %0.1f seconds, \"\n-\t\t\t\t     \"assuming that you meant '%s'.\"),\n-\t\t\t\t   (float)cfg.autocorrect/10.0, assumed);\n-\t\t\tsleep_millisec(cfg.autocorrect * 100);\n-\t\t}\n+\n+\t\tautocorr_confirm(cfg.autocorrect, assumed);\n \n \t\tcmdnames_release(&cfg.aliases);\n \t\tcmdnames_release(&main_cmds);\n-- \n2.53.0\n\n"},{"id":"538385","messageId":"SY0P300MB0801E98F538F5320E7B663F0CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801C6F21C2D8F49892DF8E7CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v3 3/8] help: move tty check for autocorrection to autocorrect.c","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-10T11:41:02Z","receivedAt":"2026-03-10T11:41:27Z","isPatch":true,"body":"TTY checking is the autocorrect config parser's responsibility. It must\nensure the parsed value is correct and reliable. Thus, move the check to\nautocorr_resolve_config().\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\nIn parse_autocorrect() I kept the if/else if cascade. In my opinion,\nthis is not a style change. It's a control-flow clarification, because\nthose returns describe different states of the mode. Chaining them\ntogether is better than leaving them discrete.\n\nAlso, I dropped the float cast in the delay calculation, which is\nredundant.\n\n autocorrect.c | 50 +++++++++++++++++++++++++++++---------------------\n autocorrect.h | 20 ++++++++++++++------\n help.c        | 17 ++++++-----------\n 3 files changed, 49 insertions(+), 38 deletions(-)\n\ndiff --git a/autocorrect.c b/autocorrect.c\nindex 1037f032019b..50d7f116d85d 100644\n--- a/autocorrect.c\n+++ b/autocorrect.c\n@@ -6,7 +6,7 @@\n #include \"prompt.h\"\n #include \"gettext.h\"\n \n-static int parse_autocorrect(const char *value)\n+static enum autocorr_mode parse_autocorrect(const char *value)\n {\n \tswitch (git_parse_maybe_bool_text(value)) {\n \t\tcase 1:\n@@ -19,41 +19,49 @@ static int parse_autocorrect(const char *value)\n \n \tif (!strcmp(value, \"prompt\"))\n \t\treturn AUTOCORRECT_PROMPT;\n-\tif (!strcmp(value, \"never\"))\n+\telse if (!strcmp(value, \"never\"))\n \t\treturn AUTOCORRECT_NEVER;\n-\tif (!strcmp(value, \"immediate\"))\n+\telse if (!strcmp(value, \"immediate\"))\n \t\treturn AUTOCORRECT_IMMEDIATELY;\n-\tif (!strcmp(value, \"show\"))\n+\telse if (!strcmp(value, \"show\"))\n \t\treturn AUTOCORRECT_SHOW;\n-\n-\treturn 0;\n+\telse\n+\t\treturn AUTOCORRECT_DELAY;\n }\n \n void autocorr_resolve_config(const char *var, const char *value,\n \t\t\t     const struct config_context *ctx, void *data)\n {\n-\tint *out = data;\n+\tstruct autocorr *conf = data;\n+\n+\tif (strcmp(var, \"help.autocorrect\"))\n+\t\treturn;\n+\n+\tconf->mode = parse_autocorrect(value);\n \n-\tif (!strcmp(var, \"help.autocorrect\")) {\n-\t\tint v = parse_autocorrect(value);\n+\t/*\n+\t * Disable autocorrection prompt in a non-interactive session.\n+\t */\n+\tif (conf->mode == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))\n+\t\tconf->mode = AUTOCORRECT_NEVER;\n \n-\t\tif (!v) {\n-\t\t\tv = git_config_int(var, value, ctx->kvi);\n-\t\t\tif (v < 0 || v == 1)\n-\t\t\t\tv = AUTOCORRECT_IMMEDIATELY;\n-\t\t}\n+\tif (conf->mode == AUTOCORRECT_DELAY) {\n+\t\tconf->delay = git_config_int(var, value, ctx->kvi);\n \n-\t\t*out = v;\n+\t\tif (!conf->delay)\n+\t\t\tconf->mode = AUTOCORRECT_SHOW;\n+\t\telse if (conf->delay <= 1)\n+\t\t\tconf->mode = AUTOCORRECT_IMMEDIATELY;\n \t}\n }\n \n-void autocorr_confirm(int autocorrect, const char *assumed)\n+void autocorr_confirm(struct autocorr *conf, const char *assumed)\n {\n-\tif (autocorrect == AUTOCORRECT_IMMEDIATELY) {\n+\tif (conf->mode == AUTOCORRECT_IMMEDIATELY) {\n \t\tfprintf_ln(stderr,\n \t\t\t   _(\"Continuing under the assumption that you meant '%s'.\"),\n \t\t\t   assumed);\n-\t} else if (autocorrect == AUTOCORRECT_PROMPT) {\n+\t} else if (conf->mode == AUTOCORRECT_PROMPT) {\n \t\tchar *answer;\n \t\tstruct strbuf msg = STRBUF_INIT;\n \n@@ -63,10 +71,10 @@ void autocorr_confirm(int autocorrect, const char *assumed)\n \n \t\tif (!(starts_with(answer, \"y\") || starts_with(answer, \"Y\")))\n \t\t\texit(1);\n-\t} else {\n+\t} else if (conf->mode == AUTOCORRECT_DELAY) {\n \t\tfprintf_ln(stderr,\n \t\t\t   _(\"Continuing in %0.1f seconds, assuming that you meant '%s'.\"),\n-\t\t\t   (float)autocorrect / 10.0, assumed);\n-\t\tsleep_millisec(autocorrect * 100);\n+\t\t\t   conf->delay / 10.0, assumed);\n+\t\tsleep_millisec(conf->delay * 100);\n \t}\n }\ndiff --git a/autocorrect.h b/autocorrect.h\nindex 45609990c77e..ce4a68379f2f 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -1,16 +1,24 @@\n #ifndef AUTOCORRECT_H\n #define AUTOCORRECT_H\n \n-#define AUTOCORRECT_SHOW (-4)\n-#define AUTOCORRECT_PROMPT (-3)\n-#define AUTOCORRECT_NEVER (-2)\n-#define AUTOCORRECT_IMMEDIATELY (-1)\n-\n struct config_context;\n \n+enum autocorr_mode {\n+\tAUTOCORRECT_SHOW,\n+\tAUTOCORRECT_NEVER,\n+\tAUTOCORRECT_PROMPT,\n+\tAUTOCORRECT_IMMEDIATELY,\n+\tAUTOCORRECT_DELAY,\n+};\n+\n+struct autocorr {\n+\tenum autocorr_mode mode;\n+\tint delay;\n+};\n+\n void autocorr_resolve_config(const char *var, const char *value,\n \t\t\t     const struct config_context *ctx, void *data);\n \n-void autocorr_confirm(int autocorr, const char *assumed);\n+void autocorr_confirm(struct autocorr *conf, const char *assumed);\n \n #endif /* AUTOCORRECT_H */\ndiff --git a/help.c b/help.c\nindex 6be3ec9dfb12..566d33299b9a 100644\n--- a/help.c\n+++ b/help.c\n@@ -538,7 +538,7 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)\n }\n \n struct help_unknown_cmd_config {\n-\tint autocorrect;\n+\tstruct autocorr autocorr;\n \tstruct cmdnames aliases;\n };\n \n@@ -550,7 +550,7 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \tconst char *subsection, *key;\n \tsize_t subsection_len;\n \n-\tautocorr_resolve_config(var, value, ctx, &cfg->autocorrect);\n+\tautocorr_resolve_config(var, value, ctx, &cfg->autocorr);\n \n \t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n@@ -607,13 +607,7 @@ char *help_unknown_cmd(const char *cmd)\n \n \tread_early_config(the_repository, git_unknown_cmd_config, &cfg);\n \n-\t/*\n-\t * Disable autocorrection prompt in a non-interactive session\n-\t */\n-\tif ((cfg.autocorrect == AUTOCORRECT_PROMPT) && (!isatty(0) || !isatty(2)))\n-\t\tcfg.autocorrect = AUTOCORRECT_NEVER;\n-\n-\tif (cfg.autocorrect == AUTOCORRECT_NEVER) {\n+\tif (cfg.autocorr.mode == AUTOCORRECT_NEVER) {\n \t\tfprintf_ln(stderr, _(\"git: '%s' is not a git command. See 'git --help'.\"), cmd);\n \t\texit(1);\n \t}\n@@ -679,7 +673,8 @@ char *help_unknown_cmd(const char *cmd)\n \t\t     n++)\n \t\t\t; /* still counting */\n \t}\n-\tif (cfg.autocorrect && cfg.autocorrect != AUTOCORRECT_SHOW && n == 1 &&\n+\n+\tif (cfg.autocorr.mode != AUTOCORRECT_SHOW && n == 1 &&\n \t    SIMILAR_ENOUGH(best_similarity)) {\n \t\tchar *assumed = xstrdup(main_cmds.names[0]->name);\n \n@@ -688,7 +683,7 @@ char *help_unknown_cmd(const char *cmd)\n \t\t\t     \"which does not exist.\"),\n \t\t\t   cmd);\n \n-\t\tautocorr_confirm(cfg.autocorrect, assumed);\n+\t\tautocorr_confirm(&cfg.autocorr, assumed);\n \n \t\tcmdnames_release(&cfg.aliases);\n \t\tcmdnames_release(&main_cmds);\n-- \n2.53.0\n\n"},{"id":"538386","messageId":"SY0P300MB080176719022D3D7C2C5D70FCE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801C6F21C2D8F49892DF8E7CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v3 4/8] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINTONLY","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-10T11:41:03Z","receivedAt":"2026-03-10T11:41:29Z","isPatch":true,"body":"AUTOCORRECT_SHOW is ambiguous. Its purpose is to show commands similar\nto the unknown one and take no other action. Rename it to fit the\nsemantics.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.c | 6 +++---\n autocorrect.h | 2 +-\n help.c        | 2 +-\n 3 files changed, 5 insertions(+), 5 deletions(-)\n\ndiff --git a/autocorrect.c b/autocorrect.c\nindex 50d7f116d85d..9c4b691fb003 100644\n--- a/autocorrect.c\n+++ b/autocorrect.c\n@@ -12,7 +12,7 @@ static enum autocorr_mode parse_autocorrect(const char *value)\n \t\tcase 1:\n \t\t\treturn AUTOCORRECT_IMMEDIATELY;\n \t\tcase 0:\n-\t\t\treturn AUTOCORRECT_SHOW;\n+\t\t\treturn AUTOCORRECT_HINTONLY;\n \t\tdefault: /* other random text */\n \t\t\tbreak;\n \t}\n@@ -24,7 +24,7 @@ static enum autocorr_mode parse_autocorrect(const char *value)\n \telse if (!strcmp(value, \"immediate\"))\n \t\treturn AUTOCORRECT_IMMEDIATELY;\n \telse if (!strcmp(value, \"show\"))\n-\t\treturn AUTOCORRECT_SHOW;\n+\t\treturn AUTOCORRECT_HINTONLY;\n \telse\n \t\treturn AUTOCORRECT_DELAY;\n }\n@@ -49,7 +49,7 @@ void autocorr_resolve_config(const char *var, const char *value,\n \t\tconf->delay = git_config_int(var, value, ctx->kvi);\n \n \t\tif (!conf->delay)\n-\t\t\tconf->mode = AUTOCORRECT_SHOW;\n+\t\t\tconf->mode = AUTOCORRECT_HINTONLY;\n \t\telse if (conf->delay <= 1)\n \t\t\tconf->mode = AUTOCORRECT_IMMEDIATELY;\n \t}\ndiff --git a/autocorrect.h b/autocorrect.h\nindex ce4a68379f2f..be4e3e8b2043 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -4,7 +4,7 @@\n struct config_context;\n \n enum autocorr_mode {\n-\tAUTOCORRECT_SHOW,\n+\tAUTOCORRECT_HINTONLY,\n \tAUTOCORRECT_NEVER,\n \tAUTOCORRECT_PROMPT,\n \tAUTOCORRECT_IMMEDIATELY,\ndiff --git a/help.c b/help.c\nindex 566d33299b9a..6158545e48a1 100644\n--- a/help.c\n+++ b/help.c\n@@ -674,7 +674,7 @@ char *help_unknown_cmd(const char *cmd)\n \t\t\t; /* still counting */\n \t}\n \n-\tif (cfg.autocorr.mode != AUTOCORRECT_SHOW && n == 1 &&\n+\tif (cfg.autocorr.mode != AUTOCORRECT_HINTONLY && n == 1 &&\n \t    SIMILAR_ENOUGH(best_similarity)) {\n \t\tchar *assumed = xstrdup(main_cmds.names[0]->name);\n \n-- \n2.53.0\n\n"},{"id":"538387","messageId":"SY0P300MB08013A8A073C935934E1B4D7CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801C6F21C2D8F49892DF8E7CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v3 5/8] autocorrect: provide config resolution API","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-10T11:41:04Z","receivedAt":"2026-03-10T11:41:31Z","isPatch":true,"body":"Add autocorr_resolve(). This resolves and populates the correct values\nfor autocorrect config.\n\nMake autocorrect config callback internal. The API is meant to provide\na high-level way to retrieve the config. Allowing access to the config\ncallback from outside violates that intent.\n\nAdditionally, in some cases, without access to the config callback, two\nconfig iterations cannot be merged into one, which can hurt performance.\nThis is fine, as the code path that calls autocorr_resolve() is cold.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.c | 15 ++++++++++++---\n autocorrect.h |  5 +----\n help.c        | 37 +++++++++++++++----------------------\n 3 files changed, 28 insertions(+), 29 deletions(-)\n\ndiff --git a/autocorrect.c b/autocorrect.c\nindex 9c4b691fb003..63fa331ef5e2 100644\n--- a/autocorrect.c\n+++ b/autocorrect.c\n@@ -1,3 +1,5 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n #include \"git-compat-util.h\"\n #include \"autocorrect.h\"\n #include \"config.h\"\n@@ -29,13 +31,13 @@ static enum autocorr_mode parse_autocorrect(const char *value)\n \t\treturn AUTOCORRECT_DELAY;\n }\n \n-void autocorr_resolve_config(const char *var, const char *value,\n-\t\t\t     const struct config_context *ctx, void *data)\n+static int resolve_autocorr(const char *var, const char *value,\n+\t\t\t    const struct config_context *ctx, void *data)\n {\n \tstruct autocorr *conf = data;\n \n \tif (strcmp(var, \"help.autocorrect\"))\n-\t\treturn;\n+\t\treturn 0;\n \n \tconf->mode = parse_autocorrect(value);\n \n@@ -53,6 +55,13 @@ void autocorr_resolve_config(const char *var, const char *value,\n \t\telse if (conf->delay <= 1)\n \t\t\tconf->mode = AUTOCORRECT_IMMEDIATELY;\n \t}\n+\n+\treturn 0;\n+}\n+\n+void autocorr_resolve(struct autocorr *conf)\n+{\n+\tread_early_config(the_repository, resolve_autocorr, conf);\n }\n \n void autocorr_confirm(struct autocorr *conf, const char *assumed)\ndiff --git a/autocorrect.h b/autocorrect.h\nindex be4e3e8b2043..5d82c49903be 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -1,8 +1,6 @@\n #ifndef AUTOCORRECT_H\n #define AUTOCORRECT_H\n \n-struct config_context;\n-\n enum autocorr_mode {\n \tAUTOCORRECT_HINTONLY,\n \tAUTOCORRECT_NEVER,\n@@ -16,8 +14,7 @@ struct autocorr {\n \tint delay;\n };\n \n-void autocorr_resolve_config(const char *var, const char *value,\n-\t\t\t     const struct config_context *ctx, void *data);\n+void autocorr_resolve(struct autocorr *conf);\n \n void autocorr_confirm(struct autocorr *conf, const char *assumed);\n \ndiff --git a/help.c b/help.c\nindex 6158545e48a1..4adb1fcf0726 100644\n--- a/help.c\n+++ b/help.c\n@@ -537,32 +537,24 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)\n \treturn 0;\n }\n \n-struct help_unknown_cmd_config {\n-\tstruct autocorr autocorr;\n-\tstruct cmdnames aliases;\n-};\n-\n-static int git_unknown_cmd_config(const char *var, const char *value,\n-\t\t\t\t  const struct config_context *ctx,\n-\t\t\t\t  void *cb)\n+static int resolve_aliases(const char *var, const char *value UNUSED,\n+\t\t\t   const struct config_context *ctx UNUSED, void *data)\n {\n-\tstruct help_unknown_cmd_config *cfg = cb;\n+\tstruct cmdnames *aliases = data;\n \tconst char *subsection, *key;\n \tsize_t subsection_len;\n \n-\tautocorr_resolve_config(var, value, ctx, &cfg->autocorr);\n-\n \t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n \t\t\t      &key)) {\n \t\tif (subsection) {\n \t\t\t/* [alias \"name\"] command = value */\n \t\t\tif (!strcmp(key, \"command\"))\n-\t\t\t\tadd_cmdname(&cfg->aliases, subsection,\n+\t\t\t\tadd_cmdname(aliases, subsection,\n \t\t\t\t\t    subsection_len);\n \t\t} else {\n \t\t\t/* alias.name = value */\n-\t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t\t\tadd_cmdname(aliases, key, strlen(key));\n \t\t}\n \t}\n \n@@ -599,22 +591,24 @@ static const char bad_interpreter_advice[] =\n \n char *help_unknown_cmd(const char *cmd)\n {\n-\tstruct help_unknown_cmd_config cfg = { 0 };\n+\tstruct cmdnames aliases = { 0 };\n+\tstruct autocorr autocorr = { 0 };\n \tint i, n, best_similarity = 0;\n \tstruct cmdnames main_cmds = { 0 };\n \tstruct cmdnames other_cmds = { 0 };\n \tstruct cmdname_help *common_cmds;\n \n-\tread_early_config(the_repository, git_unknown_cmd_config, &cfg);\n+\tautocorr_resolve(&autocorr);\n \n-\tif (cfg.autocorr.mode == AUTOCORRECT_NEVER) {\n+\tif (autocorr.mode == AUTOCORRECT_NEVER) {\n \t\tfprintf_ln(stderr, _(\"git: '%s' is not a git command. See 'git --help'.\"), cmd);\n \t\texit(1);\n \t}\n \n \tload_command_list(\"git-\", &main_cmds, &other_cmds);\n+\tread_early_config(the_repository, resolve_aliases, &aliases);\n \n-\tadd_cmd_list(&main_cmds, &cfg.aliases);\n+\tadd_cmd_list(&main_cmds, &aliases);\n \tadd_cmd_list(&main_cmds, &other_cmds);\n \tQSORT(main_cmds.names, main_cmds.cnt, cmdname_compare);\n \tuniq(&main_cmds);\n@@ -674,18 +668,17 @@ char *help_unknown_cmd(const char *cmd)\n \t\t\t; /* still counting */\n \t}\n \n-\tif (cfg.autocorr.mode != AUTOCORRECT_HINTONLY && n == 1 &&\n+\tif (autocorr.mode != AUTOCORRECT_HINTONLY && n == 1 &&\n \t    SIMILAR_ENOUGH(best_similarity)) {\n \t\tchar *assumed = xstrdup(main_cmds.names[0]->name);\n \n \t\tfprintf_ln(stderr,\n-\t\t\t   _(\"WARNING: You called a Git command named '%s', \"\n-\t\t\t     \"which does not exist.\"),\n+\t\t\t   _(\"WARNING: You called a Git command named '%s', which does not exist.\"),\n \t\t\t   cmd);\n \n-\t\tautocorr_confirm(&cfg.autocorr, assumed);\n+\t\tautocorr_confirm(&autocorr, assumed);\n \n-\t\tcmdnames_release(&cfg.aliases);\n+\t\tcmdnames_release(&aliases);\n \t\tcmdnames_release(&main_cmds);\n \t\tcmdnames_release(&other_cmds);\n \t\treturn assumed;\n-- \n2.53.0\n\n"},{"id":"538388","messageId":"SY0P300MB08011B31B360FC14F05418C1CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801C6F21C2D8F49892DF8E7CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v3 6/8] parseopt: autocorrect mistyped subcommands","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-10T11:41:05Z","receivedAt":"2026-03-10T11:41:32Z","isPatch":true,"body":"Try to autocorrect the mistyped mandatory subcommand before showing an\nerror and exiting. Subcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL\nare skipped.\n\nIn autocorrect_subcommand(), AUTOCORR_HINTONLY does the same as\nAUTOCORR_NEVER, because builtins have a limited number of subcommands.\nThose lists are currently not too large. Therefore, displaying all\nsubcommands via usage_with_options() is good enough here. This also\nkeeps the autocorrection handling simple.\n\nUse a dynamic threshold for similar_enough() to check if the result is\nusable. This can yield more accurate typo corrections. Even though\nsubcommands are often short, they can still vary across builtins. And in\nthe current implementation, a fixed threshold can't do better on both\nshort and long subcommands at the same time.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\nChanges in v3:\n  - Improve commit message\n  - Fix coding style issue\n\n parse-options.c | 76 +++++++++++++++++++++++++++++++++++++++++++++++--\n 1 file changed, 73 insertions(+), 3 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex 33f26d6b6179..227bc7499115 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -6,6 +6,8 @@\n #include \"strbuf.h\"\n #include \"string-list.h\"\n #include \"utf8.h\"\n+#include \"autocorrect.h\"\n+#include \"levenshtein.h\"\n \n static int disallow_abbreviated_options;\n \n@@ -622,13 +624,72 @@ static int parse_subcommand(const char *arg, const struct option *options)\n \treturn -1;\n }\n \n+static void find_subcommands(struct string_list *list,\n+\t\t\t     const struct option *options)\n+{\n+\tfor (; options->type != OPTION_END; options++) {\n+\t\tif (options->type == OPTION_SUBCOMMAND)\n+\t\t\tstring_list_append(list, options->long_name);\n+\t}\n+}\n+\n+static int similar_enough(const char *cmd, unsigned int dist)\n+{\n+\tsize_t len = strlen(cmd);\n+\tunsigned int threshold = len < 3 ? 1 : len < 6 ? 3 : 6;\n+\n+\treturn dist < threshold;\n+}\n+\n+static const char *autocorrect_subcommand(const char *cmd,\n+\t\t\t\t\t  struct string_list *cmds)\n+{\n+\tstruct autocorr autocorr = { 0 };\n+\tunsigned int min = UINT_MAX;\n+\tunsigned int ties = 0;\n+\tstruct string_list_item *cand;\n+\tstruct string_list_item *best = NULL;\n+\n+\tautocorr_resolve(&autocorr);\n+\n+\tif (autocorr.mode == AUTOCORRECT_NEVER ||\n+\t    autocorr.mode == AUTOCORRECT_HINTONLY)\n+\t\treturn NULL;\n+\n+\tfor_each_string_list_item(cand, cmds) {\n+\t\tunsigned int dist = levenshtein(cmd, cand->string, 0, 2, 1, 3);\n+\n+\t\tif (dist < min) {\n+\t\t\tmin = dist;\n+\t\t\tbest = cand;\n+\t\t\tties = 0;\n+\t\t} else if (dist == min) {\n+\t\t\tties++;\n+\t\t}\n+\t}\n+\n+\tif (!ties && similar_enough(cmd, min)) {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"WARNING: You called a subcommand named '%s', which does not exist.\"),\n+\t\t\t   cmd);\n+\n+\t\tautocorr_confirm(&autocorr, best->string);\n+\t\treturn best->string;\n+\t}\n+\n+\treturn NULL;\n+}\n+\n static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \t\t\t\t\t       const char *arg,\n \t\t\t\t\t       const struct option *options,\n \t\t\t\t\t       const char * const usagestr[])\n {\n-\tint err = parse_subcommand(arg, options);\n+\tint err;\n+\tconst char *assumed;\n+\tstruct string_list cmds = STRING_LIST_INIT_NODUP;\n \n+\terr = parse_subcommand(arg, options);\n \tif (!err)\n \t\treturn PARSE_OPT_SUBCOMMAND;\n \n@@ -641,8 +702,17 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n \t\treturn PARSE_OPT_DONE;\n \n-\terror(_(\"unknown subcommand: `%s'\"), arg);\n-\tusage_with_options(usagestr, options);\n+\tfind_subcommands(&cmds, options);\n+\tassumed = autocorrect_subcommand(arg, &cmds);\n+\n+\tif (!assumed) {\n+\t\terror(_(\"unknown subcommand: `%s'\"), arg);\n+\t\tusage_with_options(usagestr, options);\n+\t}\n+\n+\tstring_list_clear(&cmds, 0);\n+\tparse_subcommand(assumed, options);\n+\treturn PARSE_OPT_SUBCOMMAND;\n }\n \n static void check_typos(const char *arg, const struct option *options)\n-- \n2.53.0\n\n"},{"id":"538389","messageId":"SY0P300MB0801CB7831295EE02F733063CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801C6F21C2D8F49892DF8E7CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v3 7/8] parseopt: enable subcommand autocorrection for git-remote and git-notes","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-10T11:41:06Z","receivedAt":"2026-03-10T11:41:34Z","isPatch":true,"body":"Add PARSE_OPT_SUBCOMMAND_AUTOCORR to enable autocorrection for\nsubcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL.\n\nUse it for git-remote and git-notes, so mistyped subcommands can be\nautomatically corrected, and builtin entry points no longer need to\nhandle the unknown subcommand error path themselves.\n\nThis is safe for these two builtins, because they either resolve to a\nsingle subcommand or take no subcommand at all. This means that if the\nsubcommand parser encounters an unknown argument, it must be a mistyped\nsubcommand.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\nChanges in v3:\n  - Improve commit message\n\n builtin/notes.c  | 10 +++-------\n builtin/remote.c | 12 ++++--------\n parse-options.c  | 16 +++++++++-------\n parse-options.h  |  1 +\n 4 files changed, 17 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 9af602bdd7b4..087eb898a441 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -1149,14 +1149,10 @@ int cmd_notes(int argc,\n \n \trepo_config(the_repository, git_default_config, NULL);\n \targc = parse_options(argc, argv, prefix, options, git_notes_usage,\n-\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n-\tif (!fn) {\n-\t\tif (argc) {\n-\t\t\terror(_(\"unknown subcommand: `%s'\"), argv[0]);\n-\t\t\tusage_with_options(git_notes_usage, options);\n-\t\t}\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n+\t\t\t     PARSE_OPT_SUBCOMMAND_AUTOCORR);\n+\tif (!fn)\n \t\tfn = list;\n-\t}\n \n \tif (override_notes_ref) {\n \t\tstruct strbuf sb = STRBUF_INIT;\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex ace390c671d6..d1d6244a662a 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -1949,15 +1949,11 @@ int cmd_remote(int argc,\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, builtin_remote_usage,\n-\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n+\t\t\t     PARSE_OPT_SUBCOMMAND_AUTOCORR);\n \n-\tif (fn) {\n+\tif (fn)\n \t\treturn !!fn(argc, argv, prefix, repo);\n-\t} else {\n-\t\tif (argc) {\n-\t\t\terror(_(\"unknown subcommand: `%s'\"), argv[0]);\n-\t\t\tusage_with_options(builtin_remote_usage, options);\n-\t\t}\n+\telse\n \t\treturn !!show_all();\n-\t}\n }\ndiff --git a/parse-options.c b/parse-options.c\nindex 227bc7499115..6ac4bd30e23b 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -693,14 +693,16 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \tif (!err)\n \t\treturn PARSE_OPT_SUBCOMMAND;\n \n-\t/*\n-\t * arg is neither a short or long option nor a subcommand.  Since this\n-\t * command has a default operation mode, we have to treat this arg and\n-\t * all remaining args as args meant to that default operation mode.\n-\t * So we are done parsing.\n-\t */\n-\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n+\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL &&\n+\t    !(ctx->flags & PARSE_OPT_SUBCOMMAND_AUTOCORR)) {\n+\t\t/*\n+\t\t * arg is neither a short or long option nor a subcommand.\n+\t\t * Since this command has a default operation mode, we have to\n+\t\t * treat this arg and all remaining args as args meant to that\n+\t\t * default operation mode.  So we are done parsing.\n+\t\t */\n \t\treturn PARSE_OPT_DONE;\n+\t}\n \n \tfind_subcommands(&cmds, options);\n \tassumed = autocorrect_subcommand(arg, &cmds);\ndiff --git a/parse-options.h b/parse-options.h\nindex 706de9729f6b..f29ac337893c 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -40,6 +40,7 @@ enum parse_opt_flags {\n \tPARSE_OPT_ONE_SHOT = 1 << 5,\n \tPARSE_OPT_SHELL_EVAL = 1 << 6,\n \tPARSE_OPT_SUBCOMMAND_OPTIONAL = 1 << 7,\n+\tPARSE_OPT_SUBCOMMAND_AUTOCORR = 1 << 8,\n };\n \n enum parse_opt_option_flags {\n-- \n2.53.0\n\n"},{"id":"538390","messageId":"SY0P300MB080107A9E22F2C0959290BB0CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801C6F21C2D8F49892DF8E7CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v3 8/8] help: add tests for subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-10T11:41:07Z","receivedAt":"2026-03-10T11:41:36Z","isPatch":true,"body":"These tests cover default behavior (help.autocorrect is unset), no\ncorrection, immediate correction, delayed correction, and rejection\nwhen the typo is too dissimilar.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\nChanges in v3:\n  - Fix coding style issue\n\n t/t9004-autocorrect-subcommand.sh | 51 +++++++++++++++++++++++++++++++\n 1 file changed, 51 insertions(+)\n create mode 100755 t/t9004-autocorrect-subcommand.sh\n\ndiff --git a/t/t9004-autocorrect-subcommand.sh b/t/t9004-autocorrect-subcommand.sh\nnew file mode 100755\nindex 000000000000..d10031659b94\n--- /dev/null\n+++ b/t/t9004-autocorrect-subcommand.sh\n@@ -0,0 +1,51 @@\n+#!/bin/sh\n+\n+test_description='subcommand auto-correction test\n+\n+Test autocorrection for subcommands with different\n+help.autocorrect mode.'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' \"\n+\techo '^error: unknown subcommand: ' >grep_unknown\n+\"\n+\n+test_expect_success 'default is not to autocorrect' '\n+\ttest_must_fail git worktree lsit 2>actual &&\n+\thead -n1 actual >first && test_grep -f grep_unknown first\n+'\n+\n+for mode in false no off 0 show never\n+do\n+\ttest_expect_success \"'$mode' disables autocorrection\" \"\n+\t\ttest_config help.autocorrect $mode &&\n+\n+\t\ttest_must_fail git worktree lsit 2>actual &&\n+\t\thead -n1 actual >first && test_grep -f grep_unknown first\n+\t\"\n+done\n+\n+for mode in -39 immediate 1\n+do\n+\ttest_expect_success \"autocorrect immediately with '$mode'\" - <<-EOT\n+\t\ttest_config help.autocorrect $mode &&\n+\n+\t\tgit worktree lsit 2>actual &&\n+\t\ttest_grep \"you meant 'list'\\.$\" actual\n+\tEOT\n+done\n+\n+test_expect_success 'delay path is executed' - <<-\\EOT\n+\ttest_config help.autocorrect 2 &&\n+\n+\tgit worktree lsit 2>actual &&\n+\ttest_grep '^Continuing in 0.2 seconds, ' actual\n+EOT\n+\n+test_expect_success 'deny if too dissimilar' - <<-\\EOT\n+\ttest_must_fail git remote rensnr 2>actual &&\n+\thead -n1 actual >first && test_grep -f grep_unknown first\n+EOT\n+\n+test_done\n-- \n2.53.0\n\n"},{"id":"538413","messageId":"CAOLa=ZQ3eCky2rH_D-6=vwQ26TKW_dSO84+Z-WL2LFJ2rGVmqQ@mail.gmail.com","threadId":"65168","inReplyTo":"SY0P300MB080114A7548292AB4B60D817CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step()","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-03-10T12:46:12Z","receivedAt":"2026-03-10T12:46:14Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n[snip]\n\n> @@ -990,38 +1017,17 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n>  \t\tif (*arg != '-' || !arg[1]) {\n>  \t\t\tif (parse_nodash_opt(ctx, arg, options) == 0)\n>  \t\t\t\tcontinue;\n> -\t\t\tif (!ctx->has_subcommands) {\n> -\t\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n> -\t\t\t\t\treturn PARSE_OPT_NON_OPTION;\n> -\t\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n> -\t\t\t\tcontinue;\n> -\t\t\t}\n> -\t\t\tswitch (parse_subcommand(arg, options)) {\n> -\t\t\tcase PARSE_OPT_SUBCOMMAND:\n> -\t\t\t\treturn PARSE_OPT_SUBCOMMAND;\n> -\t\t\tcase PARSE_OPT_UNKNOWN:\n> -\t\t\t\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n> -\t\t\t\t\t/*\n> -\t\t\t\t\t * arg is neither a short or long\n> -\t\t\t\t\t * option nor a subcommand.  Since\n> -\t\t\t\t\t * this command has a default\n> -\t\t\t\t\t * operation mode, we have to treat\n> -\t\t\t\t\t * this arg and all remaining args\n> -\t\t\t\t\t * as args meant to that default\n> -\t\t\t\t\t * operation mode.\n> -\t\t\t\t\t * So we are done parsing.\n> -\t\t\t\t\t */\n> -\t\t\t\t\treturn PARSE_OPT_DONE;\n> -\t\t\t\terror(_(\"unknown subcommand: `%s'\"), arg);\n> -\t\t\t\tusage_with_options(usagestr, options);\n> -\t\t\tcase PARSE_OPT_COMPLETE:\n> -\t\t\tcase PARSE_OPT_HELP:\n> -\t\t\tcase PARSE_OPT_ERROR:\n> -\t\t\tcase PARSE_OPT_DONE:\n> -\t\t\tcase PARSE_OPT_NON_OPTION:\n> -\t\t\t\t/* Impossible. */\n> -\t\t\t\tBUG(\"parse_subcommand() cannot return these\");\n> +\n> +\t\t\tif (ctx->has_subcommands) {\n> +\t\t\t\treturn handle_subcommand(ctx, arg, options,\n> +\t\t\t\t\t\t\t usagestr);\n>  \t\t\t}\n> +\n\nNit: we try to avoid braces around single statement blocks.\n\n> +\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n> +\t\t\t\treturn PARSE_OPT_NON_OPTION;\n> +\n> +\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n> +\t\t\tcontinue;\n>  \t\t}\n>\n>  \t\t/* lone -h asks for help */\n> --\n> 2.53.0\n"},{"id":"538415","messageId":"CAOLa=ZQypadiSnU1hqHBUzzayGvvvv0z-X+-vNJRFgREmdB+qg@mail.gmail.com","threadId":"65168","inReplyTo":"SY0P300MB08017404C417D02058423130CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH v3 2/8] help: make autocorrect handling reusable","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-03-10T12:52:58Z","receivedAt":"2026-03-10T12:53:00Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> Move config parsing and prompt/delay handling into autocorrect.c and\n> expose them in autocorrect.h. This makes autocorrect reusable regardless\n> of which target links against it.\n>\n> Signed-off-by: Jiamu Sun <39@barroit.sh>\n> ---\n> This patch moves autocorrect handling out of help.c as a preparatory\n> step for subcommand autocorrection.\n>\n> This is necessary because help.c uses load_builtin_commands(), which is\n> provided by git.o. Executables that do not link against git.o, such as\n> git-daemon, will hit a link error if they use symbols defined in help.o.\n> A simple and clean fix is to make the relevant functions independent of\n> help.c and move them to a dedicated file.\n>\n> Changes in v3:\n>   - Split patch so diffs don't get hidden by code movement\n>\n>  Makefile      |  1 +\n>  autocorrect.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++\n>  autocorrect.h | 16 ++++++++++++\n>  help.c        | 64 +++------------------------------------------\n>  4 files changed, 93 insertions(+), 60 deletions(-)\n>  create mode 100644 autocorrect.c\n>  create mode 100644 autocorrect.h\n>\n\nThis should also be added to meson.build.\n\n[snip]\n\n> diff --git a/autocorrect.h b/autocorrect.h\n> new file mode 100644\n> index 000000000000..45609990c77e\n> --- /dev/null\n> +++ b/autocorrect.h\n> @@ -0,0 +1,16 @@\n> +#ifndef AUTOCORRECT_H\n> +#define AUTOCORRECT_H\n> +\n> +#define AUTOCORRECT_SHOW (-4)\n> +#define AUTOCORRECT_PROMPT (-3)\n> +#define AUTOCORRECT_NEVER (-2)\n> +#define AUTOCORRECT_IMMEDIATELY (-1)\n> +\n> +struct config_context;\n> +\n> +void autocorr_resolve_config(const char *var, const char *value,\n> +\t\t\t     const struct config_context *ctx, void *data);\n> +\n> +void autocorr_confirm(int autocorr, const char *assumed);\n> +\n\nWhy not use s/autocorr/autocorrect/ ? Also would be nice to add some\ndocumentation about each of the functions here.\n\n[snip]\n\nAlso got this from running `git-clang-format` on this commit. Generally\napplying changes while moving code makes it harder to review. But here\nthe changes are small enough that we could get away with it. I'll leave\nit to you.\n\ndiff --git a/autocorrect.c b/autocorrect.c\nindex 1037f03201..87351fd08f 100644\n--- a/autocorrect.c\n+++ b/autocorrect.c\n@@ -9,12 +9,12 @@\n static int parse_autocorrect(const char *value)\n {\n \tswitch (git_parse_maybe_bool_text(value)) {\n-\t\tcase 1:\n-\t\t\treturn AUTOCORRECT_IMMEDIATELY;\n-\t\tcase 0:\n-\t\t\treturn AUTOCORRECT_SHOW;\n-\t\tdefault: /* other random text */\n-\t\t\tbreak;\n+\tcase 1:\n+\t\treturn AUTOCORRECT_IMMEDIATELY;\n+\tcase 0:\n+\t\treturn AUTOCORRECT_SHOW;\n+\tdefault: /* other random text */\n+\t\tbreak;\n \t}\n\n \tif (!strcmp(value, \"prompt\"))\ndiff --git a/autocorrect.h b/autocorrect.h\nindex 45609990c7..38f1e73131 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -1,9 +1,9 @@\n #ifndef AUTOCORRECT_H\n #define AUTOCORRECT_H\n\n-#define AUTOCORRECT_SHOW (-4)\n-#define AUTOCORRECT_PROMPT (-3)\n-#define AUTOCORRECT_NEVER (-2)\n+#define AUTOCORRECT_SHOW\t(-4)\n+#define AUTOCORRECT_PROMPT\t(-3)\n+#define AUTOCORRECT_NEVER\t(-2)\n #define AUTOCORRECT_IMMEDIATELY (-1)\n\n struct config_context;\n"},{"id":"538441","messageId":"CAOLa=ZS=2msGg5d-6-LgVD6XSduYze46iWcT2X_dJDd=6AOOkA@mail.gmail.com","threadId":"65168","inReplyTo":"SY0P300MB0801E98F538F5320E7B663F0CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH v3 3/8] help: move tty check for autocorrection to autocorrect.c","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-03-10T14:06:17Z","receivedAt":"2026-03-10T14:06:20Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> TTY checking is the autocorrect config parser's responsibility. It must\n> ensure the parsed value is correct and reliable. Thus, move the check to\n> autocorr_resolve_config().\n>\n> Signed-off-by: Jiamu Sun <39@barroit.sh>\n> ---\n> In parse_autocorrect() I kept the if/else if cascade. In my opinion,\n> this is not a style change. It's a control-flow clarification, because\n> those returns describe different states of the mode. Chaining them\n> together is better than leaving them discrete.\n>\n> Also, I dropped the float cast in the delay calculation, which is\n> redundant.\n>\n>  autocorrect.c | 50 +++++++++++++++++++++++++++++---------------------\n>  autocorrect.h | 20 ++++++++++++++------\n>  help.c        | 17 ++++++-----------\n>  3 files changed, 49 insertions(+), 38 deletions(-)\n>\n> diff --git a/autocorrect.c b/autocorrect.c\n> index 1037f032019b..50d7f116d85d 100644\n> --- a/autocorrect.c\n> +++ b/autocorrect.c\n> @@ -6,7 +6,7 @@\n>  #include \"prompt.h\"\n>  #include \"gettext.h\"\n>\n> -static int parse_autocorrect(const char *value)\n> +static enum autocorr_mode parse_autocorrect(const char *value)\n>  {\n>  \tswitch (git_parse_maybe_bool_text(value)) {\n>  \t\tcase 1:\n> @@ -19,41 +19,49 @@ static int parse_autocorrect(const char *value)\n>\n>  \tif (!strcmp(value, \"prompt\"))\n>  \t\treturn AUTOCORRECT_PROMPT;\n> -\tif (!strcmp(value, \"never\"))\n> +\telse if (!strcmp(value, \"never\"))\n>  \t\treturn AUTOCORRECT_NEVER;\n> -\tif (!strcmp(value, \"immediate\"))\n> +\telse if (!strcmp(value, \"immediate\"))\n>  \t\treturn AUTOCORRECT_IMMEDIATELY;\n> -\tif (!strcmp(value, \"show\"))\n> +\telse if (!strcmp(value, \"show\"))\n>  \t\treturn AUTOCORRECT_SHOW;\n> -\n> -\treturn 0;\n> +\telse\n> +\t\treturn AUTOCORRECT_DELAY;\n>  }\n>\n\nOkay so since we introduce an enum we use that here.\n\n>  void autocorr_resolve_config(const char *var, const char *value,\n>  \t\t\t     const struct config_context *ctx, void *data)\n>  {\n> -\tint *out = data;\n> +\tstruct autocorr *conf = data;\n> +\n> +\tif (strcmp(var, \"help.autocorrect\"))\n> +\t\treturn;\n> +\n> +\tconf->mode = parse_autocorrect(value);\n>\n> -\tif (!strcmp(var, \"help.autocorrect\")) {\n> -\t\tint v = parse_autocorrect(value);\n> +\t/*\n> +\t * Disable autocorrection prompt in a non-interactive session.\n> +\t */\n> +\tif (conf->mode == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))\n> +\t\tconf->mode = AUTOCORRECT_NEVER;\n>\n> -\t\tif (!v) {\n> -\t\t\tv = git_config_int(var, value, ctx->kvi);\n> -\t\t\tif (v < 0 || v == 1)\n> -\t\t\t\tv = AUTOCORRECT_IMMEDIATELY;\n> -\t\t}\n> +\tif (conf->mode == AUTOCORRECT_DELAY) {\n> +\t\tconf->delay = git_config_int(var, value, ctx->kvi);\n>\n> -\t\t*out = v;\n> +\t\tif (!conf->delay)\n> +\t\t\tconf->mode = AUTOCORRECT_SHOW;\n> +\t\telse if (conf->delay <= 1)\n> +\t\t\tconf->mode = AUTOCORRECT_IMMEDIATELY;\n>  \t}\n>  }\n>\n> -void autocorr_confirm(int autocorrect, const char *assumed)\n> +void autocorr_confirm(struct autocorr *conf, const char *assumed)\n>  {\n> -\tif (autocorrect == AUTOCORRECT_IMMEDIATELY) {\n> +\tif (conf->mode == AUTOCORRECT_IMMEDIATELY) {\n>  \t\tfprintf_ln(stderr,\n>  \t\t\t   _(\"Continuing under the assumption that you meant '%s'.\"),\n>  \t\t\t   assumed);\n> -\t} else if (autocorrect == AUTOCORRECT_PROMPT) {\n> +\t} else if (conf->mode == AUTOCORRECT_PROMPT) {\n>  \t\tchar *answer;\n>  \t\tstruct strbuf msg = STRBUF_INIT;\n>\n> @@ -63,10 +71,10 @@ void autocorr_confirm(int autocorrect, const char *assumed)\n>\n>  \t\tif (!(starts_with(answer, \"y\") || starts_with(answer, \"Y\")))\n>  \t\t\texit(1);\n> -\t} else {\n> +\t} else if (conf->mode == AUTOCORRECT_DELAY) {\n>  \t\tfprintf_ln(stderr,\n>  \t\t\t   _(\"Continuing in %0.1f seconds, assuming that you meant '%s'.\"),\n> -\t\t\t   (float)autocorrect / 10.0, assumed);\n> -\t\tsleep_millisec(autocorrect * 100);\n> +\t\t\t   conf->delay / 10.0, assumed);\n> +\t\tsleep_millisec(conf->delay * 100);\n>  \t}\n>  }\n> diff --git a/autocorrect.h b/autocorrect.h\n> index 45609990c77e..ce4a68379f2f 100644\n> --- a/autocorrect.h\n> +++ b/autocorrect.h\n> @@ -1,16 +1,24 @@\n>  #ifndef AUTOCORRECT_H\n>  #define AUTOCORRECT_H\n>\n> -#define AUTOCORRECT_SHOW (-4)\n> -#define AUTOCORRECT_PROMPT (-3)\n> -#define AUTOCORRECT_NEVER (-2)\n> -#define AUTOCORRECT_IMMEDIATELY (-1)\n> -\n>  struct config_context;\n>\n> +enum autocorr_mode {\n> +\tAUTOCORRECT_SHOW,\n> +\tAUTOCORRECT_NEVER,\n> +\tAUTOCORRECT_PROMPT,\n> +\tAUTOCORRECT_IMMEDIATELY,\n> +\tAUTOCORRECT_DELAY,\n> +};\n> +\n> +struct autocorr {\n> +\tenum autocorr_mode mode;\n> +\tint delay;\n> +};\n> +\n\nI would say the naming doesn't indicate what it is used for. How about\n'autocorrect_config'?\n\n[snip]\n"},{"id":"538442","messageId":"CAOLa=ZTBFXZuzxm750w-xsT4e7C3+q6r+hcgyODRVZWjtCtbQA@mail.gmail.com","threadId":"65168","inReplyTo":"SY0P300MB080176719022D3D7C2C5D70FCE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH v3 4/8] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINTONLY","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-03-10T14:08:52Z","receivedAt":"2026-03-10T14:08:56Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> AUTOCORRECT_SHOW is ambiguous. Its purpose is to show commands similar\n> to the unknown one and take no other action. Rename it to fit the\n> semantics.\n>\n\nI'm not sure if AUTOCORRECT_HINTONLY is any better than\nAUTOCORRECT_SHOW. The latter indicates that we show the user something.\nDoesn't the former also mean the same?\n\nIf we do decide to go forward with AUTOCORRECT_HINTONLY, can we rename\nit to AUTOCORRECT_HINT_ONLY? It's easier to read that way.\n\n[snip]\n"},{"id":"538443","messageId":"CAOLa=ZSLRa1Jb4y=eu_jFeTH4id8CG1jjRGV9=1FKYat2JpQng@mail.gmail.com","threadId":"65168","inReplyTo":"SY0P300MB08013A8A073C935934E1B4D7CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH v3 5/8] autocorrect: provide config resolution API","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-03-10T14:15:52Z","receivedAt":"2026-03-10T14:15:56Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> Add autocorr_resolve(). This resolves and populates the correct values\n> for autocorrect config.\n>\n> Make autocorrect config callback internal. The API is meant to provide\n> a high-level way to retrieve the config. Allowing access to the config\n> callback from outside violates that intent.\n>\n> Additionally, in some cases, without access to the config callback, two\n> config iterations cannot be merged into one, which can hurt performance.\n> This is fine, as the code path that calls autocorr_resolve() is cold.\n>\n> Signed-off-by: Jiamu Sun <39@barroit.sh>\n> ---\n>  autocorrect.c | 15 ++++++++++++---\n>  autocorrect.h |  5 +----\n>  help.c        | 37 +++++++++++++++----------------------\n>  3 files changed, 28 insertions(+), 29 deletions(-)\n>\n> diff --git a/autocorrect.c b/autocorrect.c\n> index 9c4b691fb003..63fa331ef5e2 100644\n> --- a/autocorrect.c\n> +++ b/autocorrect.c\n> @@ -1,3 +1,5 @@\n> +#define USE_THE_REPOSITORY_VARIABLE\n> +\n>  #include \"git-compat-util.h\"\n>  #include \"autocorrect.h\"\n>  #include \"config.h\"\n> @@ -29,13 +31,13 @@ static enum autocorr_mode parse_autocorrect(const char *value)\n>  \t\treturn AUTOCORRECT_DELAY;\n>  }\n>\n> -void autocorr_resolve_config(const char *var, const char *value,\n> -\t\t\t     const struct config_context *ctx, void *data)\n> +static int resolve_autocorr(const char *var, const char *value,\n> +\t\t\t    const struct config_context *ctx, void *data)\n\nSo we made this an internal, the return type was changed as\n`read_early_config()` expects that. Okay\n\n[snip]\n\nThe rest of the code makes sense too\n"},{"id":"538514","messageId":"xmqqcy1btpht.fsf@gitster.g","threadId":"65168","inReplyTo":"CAOLa=ZQypadiSnU1hqHBUzzayGvvvv0z-X+-vNJRFgREmdB+qg@mail.gmail.com","subject":"Re: [PATCH v3 2/8] help: make autocorrect handling reusable","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-10T20:10:22Z","receivedAt":"2026-03-10T20:10:25Z","isPatch":true,"body":"Karthik Nayak <karthik.188@gmail.com> writes:\n\n>> +#define AUTOCORRECT_SHOW (-4)\n>> +#define AUTOCORRECT_PROMPT (-3)\n>> +#define AUTOCORRECT_NEVER (-2)\n>> +#define AUTOCORRECT_IMMEDIATELY (-1)\n>> +\n>> +struct config_context;\n>> +\n>> +void autocorr_resolve_config(const char *var, const char *value,\n>> +\t\t\t     const struct config_context *ctx, void *data);\n>> +\n>> +void autocorr_confirm(int autocorr, const char *assumed);\n>> +\n>\n> Why not use s/autocorr/autocorrect/ ? Also would be nice to add some\n> documentation about each of the functions here.\n\nGood.\n\n>\n> [snip]\n>\n> Also got this from running `git-clang-format` on this commit. Generally\n> applying changes while moving code makes it harder to review. But here\n> the changes are small enough that we could get away with it. I'll leave\n> it to you.\n\nNo, you cannot leave it to the author.  \n\nLeave it to CodingGuidelines; case and switch would start at the\nsame column.\n\n> diff --git a/autocorrect.c b/autocorrect.c\n> index 1037f03201..87351fd08f 100644\n> --- a/autocorrect.c\n> +++ b/autocorrect.c\n> @@ -9,12 +9,12 @@\n>  static int parse_autocorrect(const char *value)\n>  {\n>  \tswitch (git_parse_maybe_bool_text(value)) {\n> -\t\tcase 1:\n> -\t\t\treturn AUTOCORRECT_IMMEDIATELY;\n> -\t\tcase 0:\n> -\t\t\treturn AUTOCORRECT_SHOW;\n> -\t\tdefault: /* other random text */\n> -\t\t\tbreak;\n> +\tcase 1:\n> +\t\treturn AUTOCORRECT_IMMEDIATELY;\n> +\tcase 0:\n> +\t\treturn AUTOCORRECT_SHOW;\n> +\tdefault: /* other random text */\n> +\t\tbreak;\n>  \t}\n>\n>  \tif (!strcmp(value, \"prompt\"))\n> diff --git a/autocorrect.h b/autocorrect.h\n> index 45609990c7..38f1e73131 100644\n> --- a/autocorrect.h\n> +++ b/autocorrect.h\n> @@ -1,9 +1,9 @@\n>  #ifndef AUTOCORRECT_H\n>  #define AUTOCORRECT_H\n>\n> -#define AUTOCORRECT_SHOW (-4)\n> -#define AUTOCORRECT_PROMPT (-3)\n> -#define AUTOCORRECT_NEVER (-2)\n> +#define AUTOCORRECT_SHOW\t(-4)\n> +#define AUTOCORRECT_PROMPT\t(-3)\n> +#define AUTOCORRECT_NEVER\t(-2)\n>  #define AUTOCORRECT_IMMEDIATELY (-1)\n>\n>  struct config_context;\n"},{"id":"538519","messageId":"xmqq7brjtp7c.fsf@gitster.g","threadId":"65168","inReplyTo":"SY0P300MB08011B31B360FC14F05418C1CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH v3 6/8] parseopt: autocorrect mistyped subcommands","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-10T20:16:39Z","receivedAt":"2026-03-10T20:16:41Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> +static int similar_enough(const char *cmd, unsigned int dist)\n> +{\n> +\tsize_t len = strlen(cmd);\n> +\tunsigned int threshold = len < 3 ? 1 : len < 6 ? 3 : 6;\n> +\n> +\treturn dist < threshold;\n> +}\n\nThere should be some explanation on the reason why this is very\ndifferent from SIMILAR_ENOUGH used in help.c for main commands,\nespecially given that the levenshtein() call here uses identical\nweight parameters (0,2,1,3) as used by the call there.\n\n> +static const char *autocorrect_subcommand(const char *cmd,\n> + ...\n> +\tfor_each_string_list_item(cand, cmds) {\n> +\t\tunsigned int dist = levenshtein(cmd, cand->string, 0, 2, 1, 3);\n> +\n> +\t\tif (dist < min) {\n> +\t\t\tmin = dist;\n> +\t\t\tbest = cand;\n> +\t\t\tties = 0;\n> +\t\t} else if (dist == min) {\n> +\t\t\tties++;\n> +\t\t}\n> +\t}\n> +\n> +\tif (!ties && similar_enough(cmd, min)) {\n"},{"id":"538541","messageId":"SY0P300MB0801AE08F2AE4C0EAA274A68CE47A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"CAOLa=ZQ3eCky2rH_D-6=vwQ26TKW_dSO84+Z-WL2LFJ2rGVmqQ@mail.gmail.com","subject":"Re: [PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step()","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-11T01:49:40Z","receivedAt":"2026-03-11T01:49:56Z","isPatch":true,"body":"On Tue, Mar 10, 2026 at 05:46:12AM -0700, Karthik Nayak wrote:\n> > +\t\t\tif (ctx->has_subcommands) {\n> > +\t\t\t\treturn handle_subcommand(ctx, arg, options,\n> > +\t\t\t\t\t\t\t usagestr);\n> >  \t\t\t}\n> > +\n> \n> Nit: we try to avoid braces around single statement blocks.\n\nI'm not sure if we should drop the braces in this case. I mean, the\nstatement is indeed a single one, but it spans multiple lines. Keeping\nthe braces improves readability. Also, CodingGuidelines says: \"When the\nstatement extends over a few lines\" use braces. So I think we should\nkeep those braces?\n\n-- \nJiamu Sun <39@barroit.sh>\n          <sunjiamu@outlook.com>\n"},{"id":"538542","messageId":"SY0P300MB0801E77F7C2D725AA3F02316CE47A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"CAOLa=ZQypadiSnU1hqHBUzzayGvvvv0z-X+-vNJRFgREmdB+qg@mail.gmail.com","subject":"Re: [PATCH v3 2/8] help: make autocorrect handling reusable","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-11T01:58:28Z","receivedAt":"2026-03-11T01:58:36Z","isPatch":true,"body":"On Tue, Mar 10, 2026 at 05:52:58AM -0700, Karthik Nayak wrote:\n> >  autocorrect.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++\n> >  autocorrect.h | 16 ++++++++++++\n> >  help.c        | 64 +++------------------------------------------\n> >  4 files changed, 93 insertions(+), 60 deletions(-)\n> >  create mode 100644 autocorrect.c\n> >  create mode 100644 autocorrect.h\n> >\n> \n> This should also be added to meson.build.\n\nSure, and if I missed anything else, please tell me, thanks.\n\n-- \nJiamu Sun <39@barroit.sh>\n          <sunjiamu@outlook.com>\n"},{"id":"538543","messageId":"SY0P300MB0801952EB445C6B196EEEFEFCE47A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"xmqqcy1btpht.fsf@gitster.g","subject":"Re: [PATCH v3 2/8] help: make autocorrect handling reusable","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-11T02:05:41Z","receivedAt":"2026-03-11T02:05:48Z","isPatch":true,"body":"On Tue, Mar 10, 2026 at 01:10:22PM -0700, Junio C Hamano wrote:\n> > Why not use s/autocorr/autocorrect/ ? Also would be nice to add some\n> > documentation about each of the functions here.\n> \n> Good.\n\nUsed autocorr just because it's shorter than autocorrect. Will rename it\nand add docs in v4.\n\n> > Also got this from running `git-clang-format` on this commit. Generally\n> > applying changes while moving code makes it harder to review. But here\n> > the changes are small enough that we could get away with it. I'll leave\n> > it to you.\n> \n> No, you cannot leave it to the author.  \n> \n> Leave it to CodingGuidelines; case and switch would start at the\n> same column.\n\nWas just copy-pasted as-is. Will fix.\n\n-- \nJiamu Sun <39@barroit.sh>\n          <sunjiamu@outlook.com>\n"},{"id":"538544","messageId":"SY0P300MB08013EBB2DC457AF040977D3CE47A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"CAOLa=ZS=2msGg5d-6-LgVD6XSduYze46iWcT2X_dJDd=6AOOkA@mail.gmail.com","subject":"Re: [PATCH v3 3/8] help: move tty check for autocorrection to autocorrect.c","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-11T02:16:09Z","receivedAt":"2026-03-11T02:16:17Z","isPatch":true,"body":"On Tue, Mar 10, 2026 at 07:06:17AM -0700, Karthik Nayak wrote:\n> > +struct autocorr {\n> > +\tenum autocorr_mode mode;\n> > +\tint delay;\n> > +};\n> > +\n> \n> I would say the naming doesn't indicate what it is used for. How about\n> 'autocorrect_config'?\n\nUsing autocorrect_config is best for semantics. The only problem is that\nit's too long.  Since currently, this struct contains all fields needed\nby our autocorrect_*, using \"autocorrect\" to describe it is enough,\nperhaps?\n\n-- \nJiamu Sun <39@barroit.sh>\n          <sunjiamu@outlook.com>\n"},{"id":"538545","messageId":"SY0P300MB08013A8975797924A3C908FFCE47A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"CAOLa=ZTBFXZuzxm750w-xsT4e7C3+q6r+hcgyODRVZWjtCtbQA@mail.gmail.com","subject":"Re: [PATCH v3 4/8] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINTONLY","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-11T02:46:51Z","receivedAt":"2026-03-11T02:47:00Z","isPatch":true,"body":"On Tue, Mar 10, 2026 at 07:08:52AM -0700, Karthik Nayak wrote:\n> \n> [snip]\n> \n> I'm not sure if AUTOCORRECT_HINTONLY is any better than\n> AUTOCORRECT_SHOW. The latter indicates that we show the user something.\n> Doesn't the former also mean the same?\n\nYou are right, it indicates that we show the user something.  But show\nwhat?  A prompt can also contain a message that acts as a \"show\", and I\nwas quite confused when I first saw this part.  On the other hand,\nHINTONLY has a clear intent, which is \"hint\".\n\n> If we do decide to go forward with AUTOCORRECT_HINTONLY, can we rename\n> it to AUTOCORRECT_HINT_ONLY? It's easier to read that way.\n\nAUTOCORRECT_HINT_ONLY breaks the current naming pattern of the mode\nenum.  Maybe we can use \"AUTOCORRECT_HINT\", as the \"only\" intent is\nclear with other enum items.\n\n-- \nJiamu Sun <39@barroit.sh>\n          <sunjiamu@outlook.com>\n"},{"id":"538546","messageId":"SY0P300MB0801DA185098623A3729B9F8CE47A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"xmqq7brjtp7c.fsf@gitster.g","subject":"Re: [PATCH v3 6/8] parseopt: autocorrect mistyped subcommands","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-11T02:48:41Z","receivedAt":"2026-03-11T02:48:47Z","isPatch":true,"body":"On Tue, Mar 10, 2026 at 01:16:39PM -0700, Junio C Hamano wrote:\n> > +\tunsigned int threshold = len < 3 ? 1 : len < 6 ? 3 : 6;\n> > +\n> > +\treturn dist < threshold;\n> > +}\n> \n> There should be some explanation on the reason why this is very\n> different from SIMILAR_ENOUGH used in help.c for main commands,\n> especially given that the levenshtein() call here uses identical\n> weight parameters (0,2,1,3) as used by the call there.\n\nWill add a comment to explain it.\n\n-- \nJiamu Sun <39@barroit.sh>\n          <sunjiamu@outlook.com>\n"},{"id":"538549","messageId":"xmqq4imnro8s.fsf@gitster.g","threadId":"65168","inReplyTo":"SY0P300MB0801AE08F2AE4C0EAA274A68CE47A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-11T04:20:19Z","receivedAt":"2026-03-11T04:20:22Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> On Tue, Mar 10, 2026 at 05:46:12AM -0700, Karthik Nayak wrote:\n>> > +\t\t\tif (ctx->has_subcommands) {\n>> > +\t\t\t\treturn handle_subcommand(ctx, arg, options,\n>> > +\t\t\t\t\t\t\t usagestr);\n>> >  \t\t\t}\n>> > +\n>> \n>> Nit: we try to avoid braces around single statement blocks.\n>\n> I'm not sure if we should drop the braces in this case.\n\nYou should.\n\nYou can tell that it is a single statement immediately after seeing\nthe beginning of the line, which says \"return\".  It does not matter\nhow many lines the function call that follows \"return\" spans.\n\n"},{"id":"538550","messageId":"xmqq3427ro8h.fsf@gitster.g","threadId":"65168","inReplyTo":"SY0P300MB0801AE08F2AE4C0EAA274A68CE47A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-11T04:20:30Z","receivedAt":"2026-03-11T04:20:32Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> On Tue, Mar 10, 2026 at 05:46:12AM -0700, Karthik Nayak wrote:\n>> > +\t\t\tif (ctx->has_subcommands) {\n>> > +\t\t\t\treturn handle_subcommand(ctx, arg, options,\n>> > +\t\t\t\t\t\t\t usagestr);\n>> >  \t\t\t}\n>> > +\n>> \n>> Nit: we try to avoid braces around single statement blocks.\n>\n> I'm not sure if we should drop the braces in this case.\n\nYou should.\n\nYou can tell that it is a single statement immediately after seeing\nthe beginning of the line, which says \"return\".  It does not matter\nhow many lines the function call that follows \"return\" spans.\n\n"},{"id":"538551","messageId":"xmqqy0jzq9js.fsf@gitster.g","threadId":"65168","inReplyTo":"SY0P300MB080107A9E22F2C0959290BB0CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH v3 8/8] help: add tests for subcommand autocorrection","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-11T04:23:03Z","receivedAt":"2026-03-11T04:23:06Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> These tests cover default behavior (help.autocorrect is unset), no\n> correction, immediate correction, delayed correction, and rejection\n> when the typo is too dissimilar.\n>\n> Signed-off-by: Jiamu Sun <39@barroit.sh>\n> ---\n> Changes in v3:\n>   - Fix coding style issue\n>\n>  t/t9004-autocorrect-subcommand.sh | 51 +++++++++++++++++++++++++++++++\n>  1 file changed, 51 insertions(+)\n>  create mode 100755 t/t9004-autocorrect-subcommand.sh\n\nt/meson.build needs to be told about this file, or you'll break CI,\nlike this:\n\n    https://github.com/git/git/actions/runs/22929006339/job/66546202060\n"},{"id":"538554","messageId":"SY0P300MB080155275E53D8C11279DCE4CE47A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"xmqqy0jzq9js.fsf@gitster.g","subject":"Re: [PATCH v3 8/8] help: add tests for subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-11T06:10:06Z","receivedAt":"2026-03-11T06:10:18Z","isPatch":true,"body":"On Tue, Mar 10, 2026 at 09:23:03PM -0700, Junio C Hamano wrote:\n> t/meson.build needs to be told about this file, or you'll break CI,\n> like this:\n> \n>     https://github.com/git/git/actions/runs/22929006339/job/66546202060\n\nWill update and verify the meson build. Thanks for the catch.\n\n-- \nJiamu Sun <39@barroit.sh>\n          <sunjiamu@outlook.com>\n"},{"id":"538555","messageId":"SY0P300MB08016CECDD1CE84BC3CE9240CE47A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"xmqq3427ro8h.fsf@gitster.g","subject":"Re: [PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step()","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-11T06:12:07Z","receivedAt":"2026-03-11T06:12:15Z","isPatch":true,"body":"On Tue, Mar 10, 2026 at 09:20:30PM -0700, Junio C Hamano wrote:\n> You can tell that it is a single statement immediately after seeing\n> the beginning of the line, which says \"return\".  It does not matter\n> how many lines the function call that follows \"return\" spans.\n\nMakes sense, will drop it.\n\n-- \nJiamu Sun <39@barroit.sh>\n          <sunjiamu@outlook.com>\n"},{"id":"538622","messageId":"7caf4a0f-f11c-4a4f-864a-933142311a68@nvidia.com","threadId":"65168","inReplyTo":"SY0P300MB08015B9BA815B2C4F6CDC639CE78A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH 5/5] help: add tests for subcommand autocorrection","fromName":"Aaron Plattner","fromEmail":"aplattner@nvidia.com","sentAt":"2026-03-11T17:01:55Z","receivedAt":"2026-03-11T17:02:35Z","isPatch":true,"body":"On 3/8/26 5:17 AM, Jiamu Sun wrote:\n> These tests cover default behavior (help.autocorrect is unset), no\n> correction, immediate correction, delayed correction, and rejection\n> when the typo is too dissimilar.\n> \n> Signed-off-by: Jiamu Sun <39@barroit.sh>\n> ---\n>   t/t9004-autocorrect-subcommand.sh | 49 +++++++++++++++++++++++++++++++\n>   1 file changed, 49 insertions(+)\n>   create mode 100755 t/t9004-autocorrect-subcommand.sh\n\nAdding this causes meson setup to fail:\n\nt/meson.build:1193:6: ERROR: Problem encountered: Test files found, but \nnot configured:\n\n  - t9004-autocorrect-subcommand.sh\n\n\nI think you just need to add it to meson.build:\n\ndiff --git a/t/meson.build b/t/meson.build\nindex f66a73f8a0..bf0503d705 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -973,6 +973,7 @@ integration_tests = [\n    't9001-send-email.sh',\n    't9002-column.sh',\n    't9003-help-autocorrect.sh',\n+  't9004-autocorrect-subcommand.sh',\n    't9100-git-svn-basic.sh',\n    't9101-git-svn-props.sh',\n    't9102-git-svn-deep-rmdir.sh',\n\n-- Aaron\n\n> \n> diff --git a/t/t9004-autocorrect-subcommand.sh b/t/t9004-autocorrect-subcommand.sh\n> new file mode 100755\n> index 000000000000..760760c8851a\n> --- /dev/null\n> +++ b/t/t9004-autocorrect-subcommand.sh\n> @@ -0,0 +1,49 @@\n> +#!/bin/sh\n> +\n> +test_description='subcommand auto-correction test\n> +\n> +Test autocorrection for subcommands with different\n> +help.autocorrect mode.'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'setup' \"\n> +\techo '^error: unknown subcommand: ' >grep_unknown\n> +\"\n> +\n> +test_expect_success 'default is not to autocorrect' '\n> +\ttest_must_fail git worktree lsit 2>actual &&\n> +\thead -n1 actual >first && test_grep -f grep_unknown first\n> +'\n> +\n> +for mode in false no off 0 show never; do\n> +\ttest_expect_success \"'$mode' disables autocorrection\" \"\n> +\t\ttest_config help.autocorrect $mode &&\n> +\n> +\t\ttest_must_fail git worktree lsit 2>actual &&\n> +\t\thead -n1 actual >first && test_grep -f grep_unknown first\n> +\t\"\n> +done\n> +\n> +for mode in -39 immediate 1; do\n> +\ttest_expect_success \"autocorrect immediately with '$mode'\" - <<-EOT\n> +\t\ttest_config help.autocorrect $mode &&\n> +\n> +\t\tgit worktree lsit 2>actual &&\n> +\t\ttest_grep \"you meant 'list'\\.$\" actual\n> +\tEOT\n> +done\n> +\n> +test_expect_success 'delay path is executed' - <<-\\EOT\n> +\ttest_config help.autocorrect 2 &&\n> +\n> +\tgit worktree lsit 2>actual &&\n> +\ttest_grep '^Continuing in 0.2 seconds, ' actual\n> +EOT\n> +\n> +test_expect_success 'deny if too dissimilar' - <<-\\EOT\n> +\ttest_must_fail git remote rensnr 2>actual &&\n> +\thead -n1 actual >first && test_grep -f grep_unknown first\n> +EOT\n> +\n> +test_done\n\n"},{"id":"538697","messageId":"SY0P300MB0801600E6ACF38D7BC715D66CE47A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"7caf4a0f-f11c-4a4f-864a-933142311a68@nvidia.com","subject":"Re: [PATCH 5/5] help: add tests for subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-11T22:45:18Z","receivedAt":"2026-03-11T22:45:28Z","isPatch":true,"body":"On Wed, Mar 11, 2026 at 10:01:55AM -0700, Aaron Plattner wrote:\n> t/meson.build:1193:6: ERROR: Problem encountered: Test files found, but not\n> configured:\n> \n>  - t9004-autocorrect-subcommand.sh\n> \n> \n> I think you just need to add it to meson.build:\n\nWill add it, thanks!\n\n-- \nJiamu Sun <39@barroit.sh>\n          <sunjiamu@outlook.com>\n"},{"id":"538699","messageId":"SY0P300MB0801C9B110080DA6DE9827BFCE47A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801DA185098623A3729B9F8CE47A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH v3 6/8] parseopt: autocorrect mistyped subcommands","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-11T23:26:11Z","receivedAt":"2026-03-11T23:26:20Z","isPatch":true,"body":"On Wed, Mar 11, 2026 at 11:48:41AM +0900, Jiamu Sun wrote:\n> > There should be some explanation on the reason why this is very\n> > different from SIMILAR_ENOUGH used in help.c for main commands,\n> > especially given that the levenshtein() call here uses identical\n> > weight parameters (0,2,1,3) as used by the call there.\n> \n> Will add a comment to explain it.\n\n\t/* skip and count prefix matches */\n\tfor (n = 0; n < main_cmds.cnt && !main_cmds.names[n]->len; n++)\n\t\t; /* still counting */\n\n\tif (main_cmds.cnt <= n) {\n\t\t/* prefix matches with everything? that is too ambiguous */\n\t\tbest_similarity = SIMILARITY_FLOOR + 1;\n\t} else {\n\t\t/* count all the most similar ones */\n\t\tfor (best_similarity = main_cmds.names[n++]->len;\n\t\t     (n < main_cmds.cnt &&\n\t\t      best_similarity == main_cmds.names[n]->len);\n\t\t     n++)\n\t\t\t; /* still counting */\n\t}\n\n\tif (autocorrect.mode != AUTOCORRECT_HINTONLY && n == 1 &&\n\nIf I read the code correctly, for the prefix matched case, the similar\ncommand finding in help.c skips all prefix matched strings and increases\n\"n\". Inside the \"else\", since that \"n++\", the \"n\" will always be greater\nthan one, thus no correction happens, e.g., it doesn't autocorrect\n\"commi\" to \"commit\". Do you know if this behavior is by design or\nsomething else?\n"},{"id":"538701","messageId":"SY0P300MB0801E747D883046C5D030D22CE44A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"CAOLa=ZS=2msGg5d-6-LgVD6XSduYze46iWcT2X_dJDd=6AOOkA@mail.gmail.com","subject":"Re: [PATCH v3 3/8] help: move tty check for autocorrection to autocorrect.c","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-12T00:10:20Z","receivedAt":"2026-03-12T00:10:30Z","isPatch":true,"body":"On Tue, Mar 10, 2026 at 07:06:17AM -0700, Karthik Nayak wrote:\n> > +\t\treturn AUTOCORRECT_DELAY;\n> >  }\n> >\n> \n> Okay so since we introduce an enum we use that here.\n\nAh actually, I messed up this patch. There was a commit that moved\nautocorrect config into a struct and introduced the mode enum. But\nsomehow it's gone. I'll split this patch into two.\n\n-- \nJiamu Sun <39@barroit.sh>\n          <sunjiamu@outlook.com>\n"},{"id":"538712","messageId":"xmqq4imlkc04.fsf@gitster.g","threadId":"65168","inReplyTo":"SY0P300MB0801C9B110080DA6DE9827BFCE47A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH v3 6/8] parseopt: autocorrect mistyped subcommands","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-12T02:38:51Z","receivedAt":"2026-03-12T02:38:55Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> \"commi\" to \"commit\". Do you know if this behavior is by design or\n> something else?\n\nI don't, but others who have their hand in the code may.  \"git\nblame\" or \"git shortlog --no-merges\" may be good tools to use to\nfind out who they are.\n\nThanks.\n\n"},{"id":"538715","messageId":"SY0P300MB08019AC7A54CBB4031646F70CE44A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"xmqq4imlkc04.fsf@gitster.g","subject":"Re: [PATCH v3 6/8] parseopt: autocorrect mistyped subcommands","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-12T04:53:51Z","receivedAt":"2026-03-12T04:54:01Z","isPatch":true,"body":"On Wed, Mar 11, 2026 at 07:38:51PM -0700, Junio C Hamano wrote:\n> I don't, but others who have their hand in the code may.  \"git\n> blame\" or \"git shortlog --no-merges\" may be good tools to use to\n> find out who they are.\n\nThanks for the pointer!\n\n+Cc Erik Faye-Lund <kusmabite@gmail.com>\n\nThe reason I asked is that I'm documenting similar_enough() for\nsubcommand autocorrection in this series. It allows \"lis\" to correct to\n\"list\", but avoids \"ad\" to \"add\".\n\nI noticed that the command autocorrection ignores prefix-matched strings\nentirely (e.g., not correcting \"commi\" to \"commit\", as introduced in\ncommit 6612b9e471a3). I wanted to understand the rationale behind this\nto see if subcommands should align with that design.\n\nThanks.\n\n-- \nJiamu Sun <39@barroit.sh>\n          <sunjiamu@outlook.com>\n"},{"id":"538746","messageId":"xmqqy0jxi30z.fsf@gitster.g","threadId":"65168","inReplyTo":"SY0P300MB0801600E6ACF38D7BC715D66CE47A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH 5/5] help: add tests for subcommand autocorrection","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-12T13:35:40Z","receivedAt":"2026-03-12T13:35:43Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> On Wed, Mar 11, 2026 at 10:01:55AM -0700, Aaron Plattner wrote:\n>> t/meson.build:1193:6: ERROR: Problem encountered: Test files found, but not\n>> configured:\n>> \n>>  - t9004-autocorrect-subcommand.sh\n>> \n>> \n>> I think you just need to add it to meson.build:\n>\n> Will add it, thanks!\n\nIn the meantime, I have the following as a fixup patch on top of the\nseries.  The 'seen' branch with the topic still seems to be failing\nwith meson build in my environment, but I didn't have time to dig\nfurther.\n\ndiff --git a/meson.build b/meson.build\nindex 4b536e0124..0429e80a5c 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -283,6 +283,7 @@ libgit_sources = [\n   'archive-zip.c',\n   'archive.c',\n   'attr.c',\n+  'autocorrect.c',\n   'base85.c',\n   'bisect.c',\n   'blame.c',\ndiff --git a/t/meson.build b/t/meson.build\nindex f66a73f8a0..bf0503d705 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -973,6 +973,7 @@ integration_tests = [\n   't9001-send-email.sh',\n   't9002-column.sh',\n   't9003-help-autocorrect.sh',\n+  't9004-autocorrect-subcommand.sh',\n   't9100-git-svn-basic.sh',\n   't9101-git-svn-props.sh',\n   't9102-git-svn-deep-rmdir.sh',\n"},{"id":"539116","messageId":"SY0P300MB080186A23FB9582AD793F0D1CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801C6F21C2D8F49892DF8E7CE46A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v4 00/10] parseopt: add subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-16T15:36:13Z","receivedAt":"2026-03-16T15:36:37Z","isPatch":true,"body":"Git currently provides auto-correction for builtins and aliases, but\nlacks this functionality for subcommands parsed via the parse-options\nAPI. Subcommands are also commands, and typos will occur, too. Like:\n\n\tgit remote add-rul\n\nSo, this series introduces subcommand auto-correction.\n\nBy default, this implementation enables autocorrection for builtins\nwith mandatory subcommands. However, for those using\nPARSE_OPT_SUBCOMMAND_OPTIONAL, autocorrection is skipped to avoid\nmisinterpreting legitimate unknown arguments as mistyped subcommands.\n\nTo allow builtins with optional subcommands to explicitly opt in,\nthis series adds the PARSE_OPT_SUBCOMMAND_AUTOCORR flag, and enables\nit for git-remote and git-notes.\n\nAdditionally, the existing autocorrection logic is extracted from\nhelp.c so subcommand handling can reuse the same config parsing and\nprompt/delay logic.\n\nSome split string literals are also combined so the full text is easier\nto grep for.\n\nChanges in v4:\n  - Add missing files to Meson build\n  - Change API prefix from autocorr to autocorrect\n  - Split the commit that moves tty code\n  - Add API documentation\n  - Use standard Damerau-Levenshtein distance and common practice\n    fuzziness thresholds\n  - Rename AUTOCORRECT_HINTONLY to AUTOCORRECT_HINT\n  - Change commit subject prefix for tests from \"help:\" to \"parseopt:\"\n  - Fix coding style issues\n\nNote on the autocorrection thresholds:\nIn v3, there was a discussion about why specific thresholds were chosen.\nWhile attempting to document the legacy magic penalties (0, 2, 1, 3), I\nrealized those weights exist in a system where prefix matches are\ncompletely ignored (due to a historical side-effect). Applying them to\nsubcommands (which do evaluate prefix matches) makes the tolerance\noverly permissive. Therefore, v4 abandons the legacy weights in favor of\na standard Damerau-Levenshtein distance and common practice length-based\nthresholds.\n\nChanges in v3:\n  - Align with the coding guildline\n  - Split patch so diffs don't get hidden by code movement\n  - Improve commit messages\n\nChanges in v2:\n  - Reword the explanation of default autocorrection behavior\n\nJiamu Sun (10):\n  parseopt: extract subcommand handling from parse_options_step()\n  help: make autocorrect handling reusable\n  help: move tty check for autocorrection to autocorrect.c\n  autocorrect: use mode and delay instead of magic numbers\n  autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINT\n  autocorrect: provide config resolution API\n  parseopt: autocorrect mistyped subcommands\n  parseopt: enable subcommand autocorrection for git-remote and\n    git-notes\n  parseopt: add tests for subcommand autocorrection\n  doc: document autocorrect API\n\n Makefile                          |   1 +\n autocorrect.c                     |  89 ++++++++++++++++\n autocorrect.h                     |  32 ++++++\n builtin/notes.c                   |  10 +-\n builtin/remote.c                  |  12 +--\n help.c                            | 107 ++++----------------\n meson.build                       |   1 +\n parse-options.c                   | 162 ++++++++++++++++++++++--------\n parse-options.h                   |   1 +\n t/meson.build                     |   1 +\n t/t9004-autocorrect-subcommand.sh |  51 ++++++++++\n 11 files changed, 325 insertions(+), 142 deletions(-)\n create mode 100644 autocorrect.c\n create mode 100644 autocorrect.h\n create mode 100755 t/t9004-autocorrect-subcommand.sh\n\n\nbase-commit: 795c338de725e13bd361214c6b768019fc45a2c1\n-- \n2.53.0\n\n"},{"id":"539117","messageId":"SY0P300MB080125630943A294F4E96482CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB080186A23FB9582AD793F0D1CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v4 01/10] parseopt: extract subcommand handling from parse_options_step()","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-16T15:36:14Z","receivedAt":"2026-03-16T15:36:39Z","isPatch":true,"body":"Move the subcommand branch out of parse_options_step() into a new\nhandle_subcommand() helper. Also, make parse_subcommand() return a\nsimple success/failure status.\n\nThis removes the switch over impossible parse_opt_result values and\nmakes the non-option path easier to follow and maintain.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n parse-options.c | 87 ++++++++++++++++++++++++++-----------------------\n 1 file changed, 46 insertions(+), 41 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex c9cafc21b903..02a4f00919f6 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -605,17 +605,44 @@ static enum parse_opt_result parse_nodash_opt(struct parse_opt_ctx_t *p,\n \treturn PARSE_OPT_ERROR;\n }\n \n-static enum parse_opt_result parse_subcommand(const char *arg,\n-\t\t\t\t\t      const struct option *options)\n+static int parse_subcommand(const char *arg, const struct option *options)\n {\n-\tfor (; options->type != OPTION_END; options++)\n-\t\tif (options->type == OPTION_SUBCOMMAND &&\n-\t\t    !strcmp(options->long_name, arg)) {\n-\t\t\t*(parse_opt_subcommand_fn **)options->value = options->subcommand_fn;\n-\t\t\treturn PARSE_OPT_SUBCOMMAND;\n-\t\t}\n+\tfor (; options->type != OPTION_END; options++) {\n+\t\tparse_opt_subcommand_fn **opt_val;\n \n-\treturn PARSE_OPT_UNKNOWN;\n+\t\tif (options->type != OPTION_SUBCOMMAND ||\n+\t\t    strcmp(options->long_name, arg))\n+\t\t\tcontinue;\n+\n+\t\topt_val = options->value;\n+\t\t*opt_val = options->subcommand_fn;\n+\t\treturn 0;\n+\t}\n+\n+\treturn -1;\n+}\n+\n+static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n+\t\t\t\t\t       const char *arg,\n+\t\t\t\t\t       const struct option *options,\n+\t\t\t\t\t       const char * const usagestr[])\n+{\n+\tint err = parse_subcommand(arg, options);\n+\n+\tif (!err)\n+\t\treturn PARSE_OPT_SUBCOMMAND;\n+\n+\t/*\n+\t * arg is neither a short or long option nor a subcommand.  Since this\n+\t * command has a default operation mode, we have to treat this arg and\n+\t * all remaining args as args meant to that default operation mode.\n+\t * So we are done parsing.\n+\t */\n+\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n+\t\treturn PARSE_OPT_DONE;\n+\n+\terror(_(\"unknown subcommand: `%s'\"), arg);\n+\tusage_with_options(usagestr, options);\n }\n \n static void check_typos(const char *arg, const struct option *options)\n@@ -990,38 +1017,16 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\tif (*arg != '-' || !arg[1]) {\n \t\t\tif (parse_nodash_opt(ctx, arg, options) == 0)\n \t\t\t\tcontinue;\n-\t\t\tif (!ctx->has_subcommands) {\n-\t\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n-\t\t\t\t\treturn PARSE_OPT_NON_OPTION;\n-\t\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n-\t\t\t\tcontinue;\n-\t\t\t}\n-\t\t\tswitch (parse_subcommand(arg, options)) {\n-\t\t\tcase PARSE_OPT_SUBCOMMAND:\n-\t\t\t\treturn PARSE_OPT_SUBCOMMAND;\n-\t\t\tcase PARSE_OPT_UNKNOWN:\n-\t\t\t\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n-\t\t\t\t\t/*\n-\t\t\t\t\t * arg is neither a short or long\n-\t\t\t\t\t * option nor a subcommand.  Since\n-\t\t\t\t\t * this command has a default\n-\t\t\t\t\t * operation mode, we have to treat\n-\t\t\t\t\t * this arg and all remaining args\n-\t\t\t\t\t * as args meant to that default\n-\t\t\t\t\t * operation mode.\n-\t\t\t\t\t * So we are done parsing.\n-\t\t\t\t\t */\n-\t\t\t\t\treturn PARSE_OPT_DONE;\n-\t\t\t\terror(_(\"unknown subcommand: `%s'\"), arg);\n-\t\t\t\tusage_with_options(usagestr, options);\n-\t\t\tcase PARSE_OPT_COMPLETE:\n-\t\t\tcase PARSE_OPT_HELP:\n-\t\t\tcase PARSE_OPT_ERROR:\n-\t\t\tcase PARSE_OPT_DONE:\n-\t\t\tcase PARSE_OPT_NON_OPTION:\n-\t\t\t\t/* Impossible. */\n-\t\t\t\tBUG(\"parse_subcommand() cannot return these\");\n-\t\t\t}\n+\n+\t\t\tif (ctx->has_subcommands)\n+\t\t\t\treturn handle_subcommand(ctx, arg, options,\n+\t\t\t\t\t\t\t usagestr);\n+\n+\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n+\t\t\t\treturn PARSE_OPT_NON_OPTION;\n+\n+\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n+\t\t\tcontinue;\n \t\t}\n \n \t\t/* lone -h asks for help */\n-- \n2.53.0\n\n"},{"id":"539118","messageId":"SY0P300MB0801AF3C254CD460EE2A1997CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB080186A23FB9582AD793F0D1CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v4 02/10] help: make autocorrect handling reusable","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-16T15:36:15Z","receivedAt":"2026-03-16T15:36:42Z","isPatch":true,"body":"Move config parsing and prompt/delay handling into autocorrect.c and\nexpose them in autocorrect.h. This makes autocorrect reusable regardless\nof which target links against it.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n Makefile      |  1 +\n autocorrect.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++\n autocorrect.h | 16 ++++++++++++\n help.c        | 64 +++------------------------------------------\n meson.build   |  1 +\n 5 files changed, 94 insertions(+), 60 deletions(-)\n create mode 100644 autocorrect.c\n create mode 100644 autocorrect.h\n\ndiff --git a/Makefile b/Makefile\nindex f3264d0a37cc..6111631c2caa 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1098,6 +1098,7 @@ LIB_OBJS += archive-tar.o\n LIB_OBJS += archive-zip.o\n LIB_OBJS += archive.o\n LIB_OBJS += attr.o\n+LIB_OBJS += autocorrect.o\n LIB_OBJS += base85.o\n LIB_OBJS += bisect.o\n LIB_OBJS += blame.o\ndiff --git a/autocorrect.c b/autocorrect.c\nnew file mode 100644\nindex 000000000000..97145d3a53ce\n--- /dev/null\n+++ b/autocorrect.c\n@@ -0,0 +1,72 @@\n+#include \"git-compat-util.h\"\n+#include \"autocorrect.h\"\n+#include \"config.h\"\n+#include \"parse.h\"\n+#include \"strbuf.h\"\n+#include \"prompt.h\"\n+#include \"gettext.h\"\n+\n+static int parse_autocorrect(const char *value)\n+{\n+\tswitch (git_parse_maybe_bool_text(value)) {\n+\tcase 1:\n+\t\treturn AUTOCORRECT_IMMEDIATELY;\n+\tcase 0:\n+\t\treturn AUTOCORRECT_SHOW;\n+\tdefault: /* other random text */\n+\t\tbreak;\n+\t}\n+\n+\tif (!strcmp(value, \"prompt\"))\n+\t\treturn AUTOCORRECT_PROMPT;\n+\tif (!strcmp(value, \"never\"))\n+\t\treturn AUTOCORRECT_NEVER;\n+\tif (!strcmp(value, \"immediate\"))\n+\t\treturn AUTOCORRECT_IMMEDIATELY;\n+\tif (!strcmp(value, \"show\"))\n+\t\treturn AUTOCORRECT_SHOW;\n+\n+\treturn 0;\n+}\n+\n+void autocorrect_resolve_config(const char *var, const char *value,\n+\t\t\t\tconst struct config_context *ctx, void *data)\n+{\n+\tint *out = data;\n+\n+\tif (!strcmp(var, \"help.autocorrect\")) {\n+\t\tint v = parse_autocorrect(value);\n+\n+\t\tif (!v) {\n+\t\t\tv = git_config_int(var, value, ctx->kvi);\n+\t\t\tif (v < 0 || v == 1)\n+\t\t\t\tv = AUTOCORRECT_IMMEDIATELY;\n+\t\t}\n+\n+\t\t*out = v;\n+\t}\n+}\n+\n+void autocorrect_confirm(int autocorrect, const char *assumed)\n+{\n+\tif (autocorrect == AUTOCORRECT_IMMEDIATELY) {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"Continuing under the assumption that you meant '%s'.\"),\n+\t\t\t   assumed);\n+\t} else if (autocorrect == AUTOCORRECT_PROMPT) {\n+\t\tchar *answer;\n+\t\tstruct strbuf msg = STRBUF_INIT;\n+\n+\t\tstrbuf_addf(&msg, _(\"Run '%s' instead [y/N]? \"), assumed);\n+\t\tanswer = git_prompt(msg.buf, PROMPT_ECHO);\n+\t\tstrbuf_release(&msg);\n+\n+\t\tif (!(starts_with(answer, \"y\") || starts_with(answer, \"Y\")))\n+\t\t\texit(1);\n+\t} else {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"Continuing in %0.1f seconds, assuming that you meant '%s'.\"),\n+\t\t\t   (float)autocorrect / 10.0, assumed);\n+\t\tsleep_millisec(autocorrect * 100);\n+\t}\n+}\ndiff --git a/autocorrect.h b/autocorrect.h\nnew file mode 100644\nindex 000000000000..f5fadf9d9605\n--- /dev/null\n+++ b/autocorrect.h\n@@ -0,0 +1,16 @@\n+#ifndef AUTOCORRECT_H\n+#define AUTOCORRECT_H\n+\n+#define AUTOCORRECT_SHOW (-4)\n+#define AUTOCORRECT_PROMPT (-3)\n+#define AUTOCORRECT_NEVER (-2)\n+#define AUTOCORRECT_IMMEDIATELY (-1)\n+\n+struct config_context;\n+\n+void autocorrect_resolve_config(const char *var, const char *value,\n+\t\t\t\tconst struct config_context *ctx, void *data);\n+\n+void autocorrect_confirm(int autocorrect, const char *assumed);\n+\n+#endif /* AUTOCORRECT_H */\ndiff --git a/help.c b/help.c\nindex 95f576c5c81d..4acb6ca585ff 100644\n--- a/help.c\n+++ b/help.c\n@@ -22,6 +22,7 @@\n #include \"repository.h\"\n #include \"alias.h\"\n #include \"utf8.h\"\n+#include \"autocorrect.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -541,34 +542,6 @@ struct help_unknown_cmd_config {\n \tstruct cmdnames aliases;\n };\n \n-#define AUTOCORRECT_SHOW (-4)\n-#define AUTOCORRECT_PROMPT (-3)\n-#define AUTOCORRECT_NEVER (-2)\n-#define AUTOCORRECT_IMMEDIATELY (-1)\n-\n-static int parse_autocorrect(const char *value)\n-{\n-\tswitch (git_parse_maybe_bool_text(value)) {\n-\t\tcase 1:\n-\t\t\treturn AUTOCORRECT_IMMEDIATELY;\n-\t\tcase 0:\n-\t\t\treturn AUTOCORRECT_SHOW;\n-\t\tdefault: /* other random text */\n-\t\t\tbreak;\n-\t}\n-\n-\tif (!strcmp(value, \"prompt\"))\n-\t\treturn AUTOCORRECT_PROMPT;\n-\tif (!strcmp(value, \"never\"))\n-\t\treturn AUTOCORRECT_NEVER;\n-\tif (!strcmp(value, \"immediate\"))\n-\t\treturn AUTOCORRECT_IMMEDIATELY;\n-\tif (!strcmp(value, \"show\"))\n-\t\treturn AUTOCORRECT_SHOW;\n-\n-\treturn 0;\n-}\n-\n static int git_unknown_cmd_config(const char *var, const char *value,\n \t\t\t\t  const struct config_context *ctx,\n \t\t\t\t  void *cb)\n@@ -577,17 +550,7 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \tconst char *subsection, *key;\n \tsize_t subsection_len;\n \n-\tif (!strcmp(var, \"help.autocorrect\")) {\n-\t\tint v = parse_autocorrect(value);\n-\n-\t\tif (!v) {\n-\t\t\tv = git_config_int(var, value, ctx->kvi);\n-\t\t\tif (v < 0 || v == 1)\n-\t\t\t\tv = AUTOCORRECT_IMMEDIATELY;\n-\t\t}\n-\n-\t\tcfg->autocorrect = v;\n-\t}\n+\tautocorrect_resolve_config(var, value, ctx, &cfg->autocorrect);\n \n \t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n@@ -724,27 +687,8 @@ char *help_unknown_cmd(const char *cmd)\n \t\t\t   _(\"WARNING: You called a Git command named '%s', \"\n \t\t\t     \"which does not exist.\"),\n \t\t\t   cmd);\n-\t\tif (cfg.autocorrect == AUTOCORRECT_IMMEDIATELY)\n-\t\t\tfprintf_ln(stderr,\n-\t\t\t\t   _(\"Continuing under the assumption that \"\n-\t\t\t\t     \"you meant '%s'.\"),\n-\t\t\t\t   assumed);\n-\t\telse if (cfg.autocorrect == AUTOCORRECT_PROMPT) {\n-\t\t\tchar *answer;\n-\t\t\tstruct strbuf msg = STRBUF_INIT;\n-\t\t\tstrbuf_addf(&msg, _(\"Run '%s' instead [y/N]? \"), assumed);\n-\t\t\tanswer = git_prompt(msg.buf, PROMPT_ECHO);\n-\t\t\tstrbuf_release(&msg);\n-\t\t\tif (!(starts_with(answer, \"y\") ||\n-\t\t\t      starts_with(answer, \"Y\")))\n-\t\t\t\texit(1);\n-\t\t} else {\n-\t\t\tfprintf_ln(stderr,\n-\t\t\t\t   _(\"Continuing in %0.1f seconds, \"\n-\t\t\t\t     \"assuming that you meant '%s'.\"),\n-\t\t\t\t   (float)cfg.autocorrect/10.0, assumed);\n-\t\t\tsleep_millisec(cfg.autocorrect * 100);\n-\t\t}\n+\n+\t\tautocorrect_confirm(cfg.autocorrect, assumed);\n \n \t\tcmdnames_release(&cfg.aliases);\n \t\tcmdnames_release(&main_cmds);\ndiff --git a/meson.build b/meson.build\nindex 4b536e012481..0429e80a5c96 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -283,6 +283,7 @@ libgit_sources = [\n   'archive-zip.c',\n   'archive.c',\n   'attr.c',\n+  'autocorrect.c',\n   'base85.c',\n   'bisect.c',\n   'blame.c',\n-- \n2.53.0\n\n"},{"id":"539119","messageId":"SY0P300MB0801450C65264D1ED270C50DCE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB080186A23FB9582AD793F0D1CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v4 03/10] help: move tty check for autocorrection to autocorrect.c","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-16T15:36:16Z","receivedAt":"2026-03-16T15:36:44Z","isPatch":true,"body":"TTY checking is the autocorrect config parser's responsibility. It must\nensure the parsed value is correct and reliable. Thus, move the check to\nautocorrect_resolve_config().\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.c | 24 ++++++++++++++++--------\n help.c        |  6 ------\n 2 files changed, 16 insertions(+), 14 deletions(-)\n\ndiff --git a/autocorrect.c b/autocorrect.c\nindex 97145d3a53ce..887d2396da44 100644\n--- a/autocorrect.c\n+++ b/autocorrect.c\n@@ -33,18 +33,26 @@ void autocorrect_resolve_config(const char *var, const char *value,\n \t\t\t\tconst struct config_context *ctx, void *data)\n {\n \tint *out = data;\n+\tint parsed;\n \n-\tif (!strcmp(var, \"help.autocorrect\")) {\n-\t\tint v = parse_autocorrect(value);\n+\tif (strcmp(var, \"help.autocorrect\"))\n+\t\treturn;\n \n-\t\tif (!v) {\n-\t\t\tv = git_config_int(var, value, ctx->kvi);\n-\t\t\tif (v < 0 || v == 1)\n-\t\t\t\tv = AUTOCORRECT_IMMEDIATELY;\n-\t\t}\n+\tparsed = parse_autocorrect(value);\n \n-\t\t*out = v;\n+\t/*\n+\t * Disable autocorrection prompt in a non-interactive session\n+\t */\n+\tif (parsed == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))\n+\t\tparsed = AUTOCORRECT_NEVER;\n+\n+\tif (!parsed) {\n+\t\tparsed = git_config_int(var, value, ctx->kvi);\n+\t\tif (parsed < 0 || parsed == 1)\n+\t\t\tparsed = AUTOCORRECT_IMMEDIATELY;\n \t}\n+\n+\t*out = parsed;\n }\n \n void autocorrect_confirm(int autocorrect, const char *assumed)\ndiff --git a/help.c b/help.c\nindex 4acb6ca585ff..983057970e7c 100644\n--- a/help.c\n+++ b/help.c\n@@ -607,12 +607,6 @@ char *help_unknown_cmd(const char *cmd)\n \n \tread_early_config(the_repository, git_unknown_cmd_config, &cfg);\n \n-\t/*\n-\t * Disable autocorrection prompt in a non-interactive session\n-\t */\n-\tif ((cfg.autocorrect == AUTOCORRECT_PROMPT) && (!isatty(0) || !isatty(2)))\n-\t\tcfg.autocorrect = AUTOCORRECT_NEVER;\n-\n \tif (cfg.autocorrect == AUTOCORRECT_NEVER) {\n \t\tfprintf_ln(stderr, _(\"git: '%s' is not a git command. See 'git --help'.\"), cmd);\n \t\texit(1);\n-- \n2.53.0\n\n"},{"id":"539120","messageId":"SY0P300MB0801890FF5D48DE132C37699CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB080186A23FB9582AD793F0D1CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v4 04/10] autocorrect: use mode and delay instead of magic numbers","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-16T15:36:17Z","receivedAt":"2026-03-16T15:36:46Z","isPatch":true,"body":"Drop magic numbers and describe autocorrect config with a mode enum and\nan integer delay. This reduces errors when mutating config values and\nmakes the values easier to access.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.c | 46 +++++++++++++++++++++++-----------------------\n autocorrect.h | 20 ++++++++++++++------\n help.c        |  9 +++++----\n 3 files changed, 42 insertions(+), 33 deletions(-)\n\ndiff --git a/autocorrect.c b/autocorrect.c\nindex 887d2396da44..2484546fc731 100644\n--- a/autocorrect.c\n+++ b/autocorrect.c\n@@ -6,7 +6,7 @@\n #include \"prompt.h\"\n #include \"gettext.h\"\n \n-static int parse_autocorrect(const char *value)\n+static enum autocorrect_mode parse_autocorrect(const char *value)\n {\n \tswitch (git_parse_maybe_bool_text(value)) {\n \tcase 1:\n@@ -19,49 +19,49 @@ static int parse_autocorrect(const char *value)\n \n \tif (!strcmp(value, \"prompt\"))\n \t\treturn AUTOCORRECT_PROMPT;\n-\tif (!strcmp(value, \"never\"))\n+\telse if (!strcmp(value, \"never\"))\n \t\treturn AUTOCORRECT_NEVER;\n-\tif (!strcmp(value, \"immediate\"))\n+\telse if (!strcmp(value, \"immediate\"))\n \t\treturn AUTOCORRECT_IMMEDIATELY;\n-\tif (!strcmp(value, \"show\"))\n+\telse if (!strcmp(value, \"show\"))\n \t\treturn AUTOCORRECT_SHOW;\n-\n-\treturn 0;\n+\telse\n+\t\treturn AUTOCORRECT_DELAY;\n }\n \n void autocorrect_resolve_config(const char *var, const char *value,\n \t\t\t\tconst struct config_context *ctx, void *data)\n {\n-\tint *out = data;\n-\tint parsed;\n+\tstruct autocorrect *conf = data;\n \n \tif (strcmp(var, \"help.autocorrect\"))\n \t\treturn;\n \n-\tparsed = parse_autocorrect(value);\n+\tconf->mode = parse_autocorrect(value);\n \n \t/*\n \t * Disable autocorrection prompt in a non-interactive session\n \t */\n-\tif (parsed == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))\n-\t\tparsed = AUTOCORRECT_NEVER;\n+\tif (conf->mode == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))\n+\t\tconf->mode = AUTOCORRECT_NEVER;\n \n-\tif (!parsed) {\n-\t\tparsed = git_config_int(var, value, ctx->kvi);\n-\t\tif (parsed < 0 || parsed == 1)\n-\t\t\tparsed = AUTOCORRECT_IMMEDIATELY;\n-\t}\n+\tif (conf->mode == AUTOCORRECT_DELAY) {\n+\t\tconf->delay = git_config_int(var, value, ctx->kvi);\n \n-\t*out = parsed;\n+\t\tif (!conf->delay)\n+\t\t\tconf->mode = AUTOCORRECT_SHOW;\n+\t\telse if (conf->delay < 0 || conf->delay == 1)\n+\t\t\tconf->mode = AUTOCORRECT_IMMEDIATELY;\n+\t}\n }\n \n-void autocorrect_confirm(int autocorrect, const char *assumed)\n+void autocorrect_confirm(struct autocorrect *conf, const char *assumed)\n {\n-\tif (autocorrect == AUTOCORRECT_IMMEDIATELY) {\n+\tif (conf->mode == AUTOCORRECT_IMMEDIATELY) {\n \t\tfprintf_ln(stderr,\n \t\t\t   _(\"Continuing under the assumption that you meant '%s'.\"),\n \t\t\t   assumed);\n-\t} else if (autocorrect == AUTOCORRECT_PROMPT) {\n+\t} else if (conf->mode == AUTOCORRECT_PROMPT) {\n \t\tchar *answer;\n \t\tstruct strbuf msg = STRBUF_INIT;\n \n@@ -71,10 +71,10 @@ void autocorrect_confirm(int autocorrect, const char *assumed)\n \n \t\tif (!(starts_with(answer, \"y\") || starts_with(answer, \"Y\")))\n \t\t\texit(1);\n-\t} else {\n+\t} else if (conf->mode == AUTOCORRECT_DELAY) {\n \t\tfprintf_ln(stderr,\n \t\t\t   _(\"Continuing in %0.1f seconds, assuming that you meant '%s'.\"),\n-\t\t\t   (float)autocorrect / 10.0, assumed);\n-\t\tsleep_millisec(autocorrect * 100);\n+\t\t\t   conf->delay / 10.0, assumed);\n+\t\tsleep_millisec(conf->delay * 100);\n \t}\n }\ndiff --git a/autocorrect.h b/autocorrect.h\nindex f5fadf9d9605..5506a36f11a7 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -1,16 +1,24 @@\n #ifndef AUTOCORRECT_H\n #define AUTOCORRECT_H\n \n-#define AUTOCORRECT_SHOW (-4)\n-#define AUTOCORRECT_PROMPT (-3)\n-#define AUTOCORRECT_NEVER (-2)\n-#define AUTOCORRECT_IMMEDIATELY (-1)\n-\n struct config_context;\n \n+enum autocorrect_mode {\n+\tAUTOCORRECT_SHOW,\n+\tAUTOCORRECT_NEVER,\n+\tAUTOCORRECT_PROMPT,\n+\tAUTOCORRECT_IMMEDIATELY,\n+\tAUTOCORRECT_DELAY,\n+};\n+\n+struct autocorrect {\n+\tenum autocorrect_mode mode;\n+\tint delay;\n+};\n+\n void autocorrect_resolve_config(const char *var, const char *value,\n \t\t\t\tconst struct config_context *ctx, void *data);\n \n-void autocorrect_confirm(int autocorrect, const char *assumed);\n+void autocorrect_confirm(struct autocorrect *conf, const char *assumed);\n \n #endif /* AUTOCORRECT_H */\ndiff --git a/help.c b/help.c\nindex 983057970e7c..a89ac5aced99 100644\n--- a/help.c\n+++ b/help.c\n@@ -538,7 +538,7 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)\n }\n \n struct help_unknown_cmd_config {\n-\tint autocorrect;\n+\tstruct autocorrect autocorrect;\n \tstruct cmdnames aliases;\n };\n \n@@ -607,7 +607,7 @@ char *help_unknown_cmd(const char *cmd)\n \n \tread_early_config(the_repository, git_unknown_cmd_config, &cfg);\n \n-\tif (cfg.autocorrect == AUTOCORRECT_NEVER) {\n+\tif (cfg.autocorrect.mode == AUTOCORRECT_NEVER) {\n \t\tfprintf_ln(stderr, _(\"git: '%s' is not a git command. See 'git --help'.\"), cmd);\n \t\texit(1);\n \t}\n@@ -673,7 +673,8 @@ char *help_unknown_cmd(const char *cmd)\n \t\t     n++)\n \t\t\t; /* still counting */\n \t}\n-\tif (cfg.autocorrect && cfg.autocorrect != AUTOCORRECT_SHOW && n == 1 &&\n+\n+\tif (cfg.autocorrect.mode != AUTOCORRECT_SHOW && n == 1 &&\n \t    SIMILAR_ENOUGH(best_similarity)) {\n \t\tchar *assumed = xstrdup(main_cmds.names[0]->name);\n \n@@ -682,7 +683,7 @@ char *help_unknown_cmd(const char *cmd)\n \t\t\t     \"which does not exist.\"),\n \t\t\t   cmd);\n \n-\t\tautocorrect_confirm(cfg.autocorrect, assumed);\n+\t\tautocorrect_confirm(&cfg.autocorrect, assumed);\n \n \t\tcmdnames_release(&cfg.aliases);\n \t\tcmdnames_release(&main_cmds);\n-- \n2.53.0\n\n"},{"id":"539121","messageId":"SY0P300MB0801BE42AB3C2C483E9976B3CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB080186A23FB9582AD793F0D1CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v4 05/10] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINT","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-16T15:36:18Z","receivedAt":"2026-03-16T15:36:47Z","isPatch":true,"body":"AUTOCORRECT_SHOW is ambiguous. Its purpose is to show commands similar\nto the unknown one and take no other action. Rename it to fit the\nsemantics.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.c | 6 +++---\n autocorrect.h | 2 +-\n help.c        | 2 +-\n 3 files changed, 5 insertions(+), 5 deletions(-)\n\ndiff --git a/autocorrect.c b/autocorrect.c\nindex 2484546fc731..de0fa282c934 100644\n--- a/autocorrect.c\n+++ b/autocorrect.c\n@@ -12,7 +12,7 @@ static enum autocorrect_mode parse_autocorrect(const char *value)\n \tcase 1:\n \t\treturn AUTOCORRECT_IMMEDIATELY;\n \tcase 0:\n-\t\treturn AUTOCORRECT_SHOW;\n+\t\treturn AUTOCORRECT_HINT;\n \tdefault: /* other random text */\n \t\tbreak;\n \t}\n@@ -24,7 +24,7 @@ static enum autocorrect_mode parse_autocorrect(const char *value)\n \telse if (!strcmp(value, \"immediate\"))\n \t\treturn AUTOCORRECT_IMMEDIATELY;\n \telse if (!strcmp(value, \"show\"))\n-\t\treturn AUTOCORRECT_SHOW;\n+\t\treturn AUTOCORRECT_HINT;\n \telse\n \t\treturn AUTOCORRECT_DELAY;\n }\n@@ -49,7 +49,7 @@ void autocorrect_resolve_config(const char *var, const char *value,\n \t\tconf->delay = git_config_int(var, value, ctx->kvi);\n \n \t\tif (!conf->delay)\n-\t\t\tconf->mode = AUTOCORRECT_SHOW;\n+\t\t\tconf->mode = AUTOCORRECT_HINT;\n \t\telse if (conf->delay < 0 || conf->delay == 1)\n \t\t\tconf->mode = AUTOCORRECT_IMMEDIATELY;\n \t}\ndiff --git a/autocorrect.h b/autocorrect.h\nindex 5506a36f11a7..328807242c15 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -4,7 +4,7 @@\n struct config_context;\n \n enum autocorrect_mode {\n-\tAUTOCORRECT_SHOW,\n+\tAUTOCORRECT_HINT,\n \tAUTOCORRECT_NEVER,\n \tAUTOCORRECT_PROMPT,\n \tAUTOCORRECT_IMMEDIATELY,\ndiff --git a/help.c b/help.c\nindex a89ac5aced99..2d441ded3f14 100644\n--- a/help.c\n+++ b/help.c\n@@ -674,7 +674,7 @@ char *help_unknown_cmd(const char *cmd)\n \t\t\t; /* still counting */\n \t}\n \n-\tif (cfg.autocorrect.mode != AUTOCORRECT_SHOW && n == 1 &&\n+\tif (cfg.autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&\n \t    SIMILAR_ENOUGH(best_similarity)) {\n \t\tchar *assumed = xstrdup(main_cmds.names[0]->name);\n \n-- \n2.53.0\n\n"},{"id":"539122","messageId":"SY0P300MB080164A69A247148EDA79F38CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB080186A23FB9582AD793F0D1CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v4 06/10] autocorrect: provide config resolution API","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-16T15:36:19Z","receivedAt":"2026-03-16T15:36:49Z","isPatch":true,"body":"Add autocorrect_resolve(). This resolves and populates the correct\nvalues for autocorrect config.\n\nMake autocorrect config callback internal. The API is meant to provide\na high-level way to retrieve the config. Allowing access to the config\ncallback from outside violates that intent.\n\nAdditionally, in some cases, without access to the config callback, two\nconfig iterations cannot be merged into one, which can hurt performance.\nThis is fine, as the code path that calls autocorrect_resolve() is cold.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.c | 15 ++++++++++++---\n autocorrect.h |  5 +----\n help.c        | 40 +++++++++++++++++-----------------------\n 3 files changed, 30 insertions(+), 30 deletions(-)\n\ndiff --git a/autocorrect.c b/autocorrect.c\nindex de0fa282c934..b2ee9f51e8c0 100644\n--- a/autocorrect.c\n+++ b/autocorrect.c\n@@ -1,3 +1,5 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n #include \"git-compat-util.h\"\n #include \"autocorrect.h\"\n #include \"config.h\"\n@@ -29,13 +31,13 @@ static enum autocorrect_mode parse_autocorrect(const char *value)\n \t\treturn AUTOCORRECT_DELAY;\n }\n \n-void autocorrect_resolve_config(const char *var, const char *value,\n-\t\t\t\tconst struct config_context *ctx, void *data)\n+static int resolve_autocorrect(const char *var, const char *value,\n+\t\t\t       const struct config_context *ctx, void *data)\n {\n \tstruct autocorrect *conf = data;\n \n \tif (strcmp(var, \"help.autocorrect\"))\n-\t\treturn;\n+\t\treturn 0;\n \n \tconf->mode = parse_autocorrect(value);\n \n@@ -53,6 +55,13 @@ void autocorrect_resolve_config(const char *var, const char *value,\n \t\telse if (conf->delay < 0 || conf->delay == 1)\n \t\t\tconf->mode = AUTOCORRECT_IMMEDIATELY;\n \t}\n+\n+\treturn 0;\n+}\n+\n+void autocorrect_resolve(struct autocorrect *conf)\n+{\n+\tread_early_config(the_repository, resolve_autocorrect, conf);\n }\n \n void autocorrect_confirm(struct autocorrect *conf, const char *assumed)\ndiff --git a/autocorrect.h b/autocorrect.h\nindex 328807242c15..0d3e819262ed 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -1,8 +1,6 @@\n #ifndef AUTOCORRECT_H\n #define AUTOCORRECT_H\n \n-struct config_context;\n-\n enum autocorrect_mode {\n \tAUTOCORRECT_HINT,\n \tAUTOCORRECT_NEVER,\n@@ -16,8 +14,7 @@ struct autocorrect {\n \tint delay;\n };\n \n-void autocorrect_resolve_config(const char *var, const char *value,\n-\t\t\t\tconst struct config_context *ctx, void *data);\n+void autocorrect_resolve(struct autocorrect *conf);\n \n void autocorrect_confirm(struct autocorrect *conf, const char *assumed);\n \ndiff --git a/help.c b/help.c\nindex 2d441ded3f14..81efdb13d4a3 100644\n--- a/help.c\n+++ b/help.c\n@@ -537,32 +537,23 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)\n \treturn 0;\n }\n \n-struct help_unknown_cmd_config {\n-\tstruct autocorrect autocorrect;\n-\tstruct cmdnames aliases;\n-};\n-\n-static int git_unknown_cmd_config(const char *var, const char *value,\n-\t\t\t\t  const struct config_context *ctx,\n-\t\t\t\t  void *cb)\n+static int resolve_aliases(const char *var, const char *value UNUSED,\n+\t\t\t   const struct config_context *ctx UNUSED, void *data)\n {\n-\tstruct help_unknown_cmd_config *cfg = cb;\n+\tstruct cmdnames *aliases = data;\n \tconst char *subsection, *key;\n \tsize_t subsection_len;\n \n-\tautocorrect_resolve_config(var, value, ctx, &cfg->autocorrect);\n-\n-\t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n \t\t\t      &key)) {\n \t\tif (subsection) {\n \t\t\t/* [alias \"name\"] command = value */\n \t\t\tif (!strcmp(key, \"command\"))\n-\t\t\t\tadd_cmdname(&cfg->aliases, subsection,\n+\t\t\t\tadd_cmdname(aliases, subsection,\n \t\t\t\t\t    subsection_len);\n \t\t} else {\n \t\t\t/* alias.name = value */\n-\t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t\t\tadd_cmdname(aliases, key, strlen(key));\n \t\t}\n \t}\n \n@@ -599,22 +590,26 @@ static const char bad_interpreter_advice[] =\n \n char *help_unknown_cmd(const char *cmd)\n {\n-\tstruct help_unknown_cmd_config cfg = { 0 };\n+\tstruct cmdnames aliases = { 0 };\n+\tstruct autocorrect autocorrect = { 0 };\n \tint i, n, best_similarity = 0;\n \tstruct cmdnames main_cmds = { 0 };\n \tstruct cmdnames other_cmds = { 0 };\n \tstruct cmdname_help *common_cmds;\n \n-\tread_early_config(the_repository, git_unknown_cmd_config, &cfg);\n+\tautocorrect_resolve(&autocorrect);\n \n-\tif (cfg.autocorrect.mode == AUTOCORRECT_NEVER) {\n+\tif (autocorrect.mode == AUTOCORRECT_NEVER) {\n \t\tfprintf_ln(stderr, _(\"git: '%s' is not a git command. See 'git --help'.\"), cmd);\n \t\texit(1);\n \t}\n \n \tload_command_list(\"git-\", &main_cmds, &other_cmds);\n \n-\tadd_cmd_list(&main_cmds, &cfg.aliases);\n+\t/* Also use aliases for command lookup */\n+\tread_early_config(the_repository, resolve_aliases, &aliases);\n+\n+\tadd_cmd_list(&main_cmds, &aliases);\n \tadd_cmd_list(&main_cmds, &other_cmds);\n \tQSORT(main_cmds.names, main_cmds.cnt, cmdname_compare);\n \tuniq(&main_cmds);\n@@ -674,18 +669,17 @@ char *help_unknown_cmd(const char *cmd)\n \t\t\t; /* still counting */\n \t}\n \n-\tif (cfg.autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&\n+\tif (autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&\n \t    SIMILAR_ENOUGH(best_similarity)) {\n \t\tchar *assumed = xstrdup(main_cmds.names[0]->name);\n \n \t\tfprintf_ln(stderr,\n-\t\t\t   _(\"WARNING: You called a Git command named '%s', \"\n-\t\t\t     \"which does not exist.\"),\n+\t\t\t   _(\"WARNING: You called a Git command named '%s', which does not exist.\"),\n \t\t\t   cmd);\n \n-\t\tautocorrect_confirm(&cfg.autocorrect, assumed);\n+\t\tautocorrect_confirm(&autocorrect, assumed);\n \n-\t\tcmdnames_release(&cfg.aliases);\n+\t\tcmdnames_release(&aliases);\n \t\tcmdnames_release(&main_cmds);\n \t\tcmdnames_release(&other_cmds);\n \t\treturn assumed;\n-- \n2.53.0\n\n"},{"id":"539123","messageId":"SY0P300MB0801E3F75349DFDF98C221F6CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB080186A23FB9582AD793F0D1CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v4 07/10] parseopt: autocorrect mistyped subcommands","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-16T15:36:20Z","receivedAt":"2026-03-16T15:36:51Z","isPatch":true,"body":"Try to autocorrect the mistyped mandatory subcommand before showing an\nerror and exiting. Subcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL\nare skipped.\n\nUse standard Damerau-Levenshtein distance (weights 1, 1, 1, 1) to\nestablish a predictable, mathematically sound baseline.\n\nScale the allowed edit distance based on input length to prevent\nfalse positives on short commands, following common practice for\nfuzziness thresholds (e.g., Elasticsearch's AUTO fuzziness):\n  - Length 0-2: 0 edits allowed\n  - Length 3-5: 1 edit allowed\n  - Length 6+:  2 edits allowed\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n parse-options.c | 81 +++++++++++++++++++++++++++++++++++++++++++++++--\n 1 file changed, 78 insertions(+), 3 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex 02a4f00919f6..1f1b72762790 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -6,6 +6,8 @@\n #include \"strbuf.h\"\n #include \"string-list.h\"\n #include \"utf8.h\"\n+#include \"autocorrect.h\"\n+#include \"levenshtein.h\"\n \n static int disallow_abbreviated_options;\n \n@@ -622,13 +624,77 @@ static int parse_subcommand(const char *arg, const struct option *options)\n \treturn -1;\n }\n \n+static void find_subcommands(struct string_list *list,\n+\t\t\t     const struct option *options)\n+{\n+\tfor (; options->type != OPTION_END; options++) {\n+\t\tif (options->type == OPTION_SUBCOMMAND)\n+\t\t\tstring_list_append(list, options->long_name);\n+\t}\n+}\n+\n+static int similar_enough(const char *cmd, unsigned int edit)\n+{\n+\tsize_t len = strlen(cmd);\n+\tunsigned int allowed = len < 3 ? 0 : len < 6 ? 1 : 2;\n+\n+\treturn edit <= allowed;\n+}\n+\n+static const char *autocorrect_subcommand(const char *cmd,\n+\t\t\t\t\t  struct string_list *cmds)\n+{\n+\tstruct autocorrect autocorrect = { 0 };\n+\tunsigned int min = UINT_MAX;\n+\tunsigned int ties = 0;\n+\tstruct string_list_item *cand;\n+\tstruct string_list_item *best = NULL;\n+\n+\tautocorrect_resolve(&autocorrect);\n+\n+\t/*\n+\t * Builtin subcommands are small enough that printing them all via\n+\t * usage_with_options() is sufficient. Therefore, AUTOCORRECT_HINT\n+\t * acts like AUTOCORRECT_NEVER.\n+\t */\n+\tif (autocorrect.mode == AUTOCORRECT_HINT ||\n+\t    autocorrect.mode == AUTOCORRECT_NEVER)\n+\t\treturn NULL;\n+\n+\tfor_each_string_list_item(cand, cmds) {\n+\t\tunsigned int edit = levenshtein(cmd, cand->string, 1, 1, 1, 1);\n+\n+\t\tif (edit < min) {\n+\t\t\tmin = edit;\n+\t\t\tbest = cand;\n+\t\t\tties = 0;\n+\t\t} else if (edit == min) {\n+\t\t\tties++;\n+\t\t}\n+\t}\n+\n+\tif (!ties && similar_enough(cmd, min)) {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"WARNING: You called a subcommand named '%s', which does not exist.\"),\n+\t\t\t   cmd);\n+\n+\t\tautocorrect_confirm(&autocorrect, best->string);\n+\t\treturn best->string;\n+\t}\n+\n+\treturn NULL;\n+}\n+\n static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \t\t\t\t\t       const char *arg,\n \t\t\t\t\t       const struct option *options,\n \t\t\t\t\t       const char * const usagestr[])\n {\n-\tint err = parse_subcommand(arg, options);\n+\tint err;\n+\tconst char *assumed;\n+\tstruct string_list cmds = STRING_LIST_INIT_NODUP;\n \n+\terr = parse_subcommand(arg, options);\n \tif (!err)\n \t\treturn PARSE_OPT_SUBCOMMAND;\n \n@@ -641,8 +707,17 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n \t\treturn PARSE_OPT_DONE;\n \n-\terror(_(\"unknown subcommand: `%s'\"), arg);\n-\tusage_with_options(usagestr, options);\n+\tfind_subcommands(&cmds, options);\n+\tassumed = autocorrect_subcommand(arg, &cmds);\n+\n+\tif (!assumed) {\n+\t\terror(_(\"unknown subcommand: `%s'\"), arg);\n+\t\tusage_with_options(usagestr, options);\n+\t}\n+\n+\tstring_list_clear(&cmds, 0);\n+\tparse_subcommand(assumed, options);\n+\treturn PARSE_OPT_SUBCOMMAND;\n }\n \n static void check_typos(const char *arg, const struct option *options)\n-- \n2.53.0\n\n"},{"id":"539124","messageId":"SY0P300MB08013C6876A8683EC0EE69A0CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB080186A23FB9582AD793F0D1CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v4 08/10] parseopt: enable subcommand autocorrection for git-remote and git-notes","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-16T15:36:21Z","receivedAt":"2026-03-16T15:36:52Z","isPatch":true,"body":"Add PARSE_OPT_SUBCOMMAND_AUTOCORR to enable autocorrection for\nsubcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL.\n\nUse it for git-remote and git-notes, so mistyped subcommands can be\nautomatically corrected, and builtin entry points no longer need to\nhandle the unknown subcommand error path themselves.\n\nThis is safe for these two builtins, because they either resolve to a\nsingle subcommand or take no subcommand at all. This means that if the\nsubcommand parser encounters an unknown argument, it must be a mistyped\nsubcommand.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n builtin/notes.c  | 10 +++-------\n builtin/remote.c | 12 ++++--------\n parse-options.c  | 16 +++++++++-------\n parse-options.h  |  1 +\n 4 files changed, 17 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 9af602bdd7b4..087eb898a441 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -1149,14 +1149,10 @@ int cmd_notes(int argc,\n \n \trepo_config(the_repository, git_default_config, NULL);\n \targc = parse_options(argc, argv, prefix, options, git_notes_usage,\n-\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n-\tif (!fn) {\n-\t\tif (argc) {\n-\t\t\terror(_(\"unknown subcommand: `%s'\"), argv[0]);\n-\t\t\tusage_with_options(git_notes_usage, options);\n-\t\t}\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n+\t\t\t     PARSE_OPT_SUBCOMMAND_AUTOCORR);\n+\tif (!fn)\n \t\tfn = list;\n-\t}\n \n \tif (override_notes_ref) {\n \t\tstruct strbuf sb = STRBUF_INIT;\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex ace390c671d6..d1d6244a662a 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -1949,15 +1949,11 @@ int cmd_remote(int argc,\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, builtin_remote_usage,\n-\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n+\t\t\t     PARSE_OPT_SUBCOMMAND_AUTOCORR);\n \n-\tif (fn) {\n+\tif (fn)\n \t\treturn !!fn(argc, argv, prefix, repo);\n-\t} else {\n-\t\tif (argc) {\n-\t\t\terror(_(\"unknown subcommand: `%s'\"), argv[0]);\n-\t\t\tusage_with_options(builtin_remote_usage, options);\n-\t\t}\n+\telse\n \t\treturn !!show_all();\n-\t}\n }\ndiff --git a/parse-options.c b/parse-options.c\nindex 1f1b72762790..0b84061a3811 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -698,14 +698,16 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \tif (!err)\n \t\treturn PARSE_OPT_SUBCOMMAND;\n \n-\t/*\n-\t * arg is neither a short or long option nor a subcommand.  Since this\n-\t * command has a default operation mode, we have to treat this arg and\n-\t * all remaining args as args meant to that default operation mode.\n-\t * So we are done parsing.\n-\t */\n-\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n+\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL &&\n+\t    !(ctx->flags & PARSE_OPT_SUBCOMMAND_AUTOCORR)) {\n+\t\t/*\n+\t\t * arg is neither a short or long option nor a subcommand.\n+\t\t * Since this command has a default operation mode, we have to\n+\t\t * treat this arg and all remaining args as args meant to that\n+\t\t * default operation mode.  So we are done parsing.\n+\t\t */\n \t\treturn PARSE_OPT_DONE;\n+\t}\n \n \tfind_subcommands(&cmds, options);\n \tassumed = autocorrect_subcommand(arg, &cmds);\ndiff --git a/parse-options.h b/parse-options.h\nindex 706de9729f6b..f29ac337893c 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -40,6 +40,7 @@ enum parse_opt_flags {\n \tPARSE_OPT_ONE_SHOT = 1 << 5,\n \tPARSE_OPT_SHELL_EVAL = 1 << 6,\n \tPARSE_OPT_SUBCOMMAND_OPTIONAL = 1 << 7,\n+\tPARSE_OPT_SUBCOMMAND_AUTOCORR = 1 << 8,\n };\n \n enum parse_opt_option_flags {\n-- \n2.53.0\n\n"},{"id":"539125","messageId":"SY0P300MB0801E27A59F6CC1894167806CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB080186A23FB9582AD793F0D1CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v4 09/10] parseopt: add tests for subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-16T15:36:22Z","receivedAt":"2026-03-16T15:36:54Z","isPatch":true,"body":"These tests cover default behavior (help.autocorrect is unset), no\ncorrection, immediate correction, delayed correction, and rejection\nwhen the typo is too dissimilar.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n t/meson.build                     |  1 +\n t/t9004-autocorrect-subcommand.sh | 51 +++++++++++++++++++++++++++++++\n 2 files changed, 52 insertions(+)\n create mode 100755 t/t9004-autocorrect-subcommand.sh\n\ndiff --git a/t/meson.build b/t/meson.build\nindex 106c68df3d01..311be82af346 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -972,6 +972,7 @@ integration_tests = [\n   't9001-send-email.sh',\n   't9002-column.sh',\n   't9003-help-autocorrect.sh',\n+  't9004-autocorrect-subcommand.sh',\n   't9100-git-svn-basic.sh',\n   't9101-git-svn-props.sh',\n   't9102-git-svn-deep-rmdir.sh',\ndiff --git a/t/t9004-autocorrect-subcommand.sh b/t/t9004-autocorrect-subcommand.sh\nnew file mode 100755\nindex 000000000000..d10031659b94\n--- /dev/null\n+++ b/t/t9004-autocorrect-subcommand.sh\n@@ -0,0 +1,51 @@\n+#!/bin/sh\n+\n+test_description='subcommand auto-correction test\n+\n+Test autocorrection for subcommands with different\n+help.autocorrect mode.'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' \"\n+\techo '^error: unknown subcommand: ' >grep_unknown\n+\"\n+\n+test_expect_success 'default is not to autocorrect' '\n+\ttest_must_fail git worktree lsit 2>actual &&\n+\thead -n1 actual >first && test_grep -f grep_unknown first\n+'\n+\n+for mode in false no off 0 show never\n+do\n+\ttest_expect_success \"'$mode' disables autocorrection\" \"\n+\t\ttest_config help.autocorrect $mode &&\n+\n+\t\ttest_must_fail git worktree lsit 2>actual &&\n+\t\thead -n1 actual >first && test_grep -f grep_unknown first\n+\t\"\n+done\n+\n+for mode in -39 immediate 1\n+do\n+\ttest_expect_success \"autocorrect immediately with '$mode'\" - <<-EOT\n+\t\ttest_config help.autocorrect $mode &&\n+\n+\t\tgit worktree lsit 2>actual &&\n+\t\ttest_grep \"you meant 'list'\\.$\" actual\n+\tEOT\n+done\n+\n+test_expect_success 'delay path is executed' - <<-\\EOT\n+\ttest_config help.autocorrect 2 &&\n+\n+\tgit worktree lsit 2>actual &&\n+\ttest_grep '^Continuing in 0.2 seconds, ' actual\n+EOT\n+\n+test_expect_success 'deny if too dissimilar' - <<-\\EOT\n+\ttest_must_fail git remote rensnr 2>actual &&\n+\thead -n1 actual >first && test_grep -f grep_unknown first\n+EOT\n+\n+test_done\n-- \n2.53.0\n\n"},{"id":"539126","messageId":"SY0P300MB0801D52DC6F8E7F37E3A85A7CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB080186A23FB9582AD793F0D1CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v4 10/10] doc: document autocorrect API","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-16T15:36:23Z","receivedAt":"2026-03-16T15:36:55Z","isPatch":true,"body":"Explain behaviors for autocorrect_resolve(), autocorrect_confirm(), and\nstruct autocorrect.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.h | 11 +++++++++++\n 1 file changed, 11 insertions(+)\n\ndiff --git a/autocorrect.h b/autocorrect.h\nindex 0d3e819262ed..bfa3ba20a4fb 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -9,13 +9,24 @@ enum autocorrect_mode {\n \tAUTOCORRECT_DELAY,\n };\n \n+/**\n+ * `mode` indicates which action will be performed by autocorrect_confirm().\n+ * `delay` is the timeout before autocorrect_confirm() returns, in tenths of a\n+ * second. Use it only with AUTOCORRECT_DELAY.\n+ */\n struct autocorrect {\n \tenum autocorrect_mode mode;\n \tint delay;\n };\n \n+/**\n+ * Resolve the autocorrect configuration into `conf`.\n+ */\n void autocorrect_resolve(struct autocorrect *conf);\n \n+/**\n+ * Interact with the user in different ways depending on `conf->mode`.\n+ */\n void autocorrect_confirm(struct autocorrect *conf, const char *assumed);\n \n #endif /* AUTOCORRECT_H */\n-- \n2.53.0\n\n"},{"id":"539147","messageId":"xmqqse9zo93p.fsf@gitster.g","threadId":"65168","inReplyTo":"SY0P300MB0801E3F75349DFDF98C221F6CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH v4 07/10] parseopt: autocorrect mistyped subcommands","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-16T19:41:30Z","receivedAt":"2026-03-16T19:41:33Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> Try to autocorrect the mistyped mandatory subcommand before showing an\n> error and exiting. Subcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL\n> are skipped.\n>\n> Use standard Damerau-Levenshtein distance (weights 1, 1, 1, 1) to\n> establish a predictable, mathematically sound baseline.\n>\n> Scale the allowed edit distance based on input length to prevent\n> false positives on short commands, following common practice for\n> fuzziness thresholds (e.g., Elasticsearch's AUTO fuzziness):\n>   - Length 0-2: 0 edits allowed\n>   - Length 3-5: 1 edit allowed\n>   - Length 6+:  2 edits allowed\n\nIs there a reason why this needs to differ from the settings for the\ntypo detection/fixes for main commands?  Would the same reasoning\napply to both, and if not why not?\n\nI would have expected that we would just emulate what we already do\nto the main commands, and later with experience with the subcommand\ntypo detection/fixes, would tweak the parameters either only to the\nsubcommand part or to the both with justifications.\n\n> +\t/*\n> +\t * Builtin subcommands are small enough that printing them all via\n> +\t * usage_with_options() is sufficient. Therefore, AUTOCORRECT_HINT\n> +\t * acts like AUTOCORRECT_NEVER.\n> +\t */\n\nSorry, but I am a bit confused with this reference to \"Builtin\nsubcommands\".  Are there subcommands that are not built-in?\n\n"},{"id":"539200","messageId":"ME0P300MB08117F31CDFE8D038C3E0557CE41A@ME0P300MB0811.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"xmqqse9zo93p.fsf@gitster.g","subject":"Re: [PATCH v4 07/10] parseopt: autocorrect mistyped subcommands","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-03-17T03:21:42Z","receivedAt":"2026-03-17T03:21:59Z","isPatch":true,"body":"On Mon, Mar 16, 2026 at 12:41:30PM -0700, Junio C Hamano wrote:\n> Is there a reason why this needs to differ from the settings for the\n> typo detection/fixes for main commands?  Would the same reasoning\n> apply to both, and if not why not?\n> \n> I would have expected that we would just emulate what we already do\n> to the main commands, and later with experience with the subcommand\n> typo detection/fixes, would tweak the parameters either only to the\n> subcommand part or to the both with justifications.\n\nI initially wanted to emulate the existing behavior. However, I noticed\na huge difference in how prefix-matched commands are handled, which\naffects the autocorrection behavior. And we can only use the same\nparameters if we do the exact same thing on prefix match handling in\nsubcommand autocorrection.\n\nI looked through the old mailing list. If I didn't miss anything, the\npatch that introduced this behavior (e.g., not correcting \"statu\" to\n\"status\") was only trying to fix a UX issue, where the suggestion output\ndidn't include prefix-matched commands. In that thread, they didn't\nmention that this would also change the typo detection behavior, and no\none discussed this side effect. I treat it as an accident.\n\nBecause of this, I was confused about whether I should copy this\nbehavior. I chose not to.\n\nHowever, if we want the main commands and subcommands to act the same, I\ncan do that and try to move the logic to autocorrect.c so both places\nshare the exact same typo detection.\n\nDo you want me to do this?\n\n> > +\t/*\n> > +\t * Builtin subcommands are small enough that printing them all via\n> > +\t * usage_with_options() is sufficient. Therefore, AUTOCORRECT_HINT\n> > +\t * acts like AUTOCORRECT_NEVER.\n> > +\t */\n> \n> Sorry, but I am a bit confused with this reference to \"Builtin\n> subcommands\".  Are there subcommands that are not built-in?\n\nNo, that's just poor wording. Will fix it.\n\n-- \nJiamu Sun <39@barroit.sh>\n          <sunjiamu@outlook.com>\n"},{"id":"541683","messageId":"xmqqse8w6rd7.fsf@gitster.g","threadId":"65168","inReplyTo":"ME0P300MB08117F31CDFE8D038C3E0557CE41A@ME0P300MB0811.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH v4 07/10] parseopt: autocorrect mistyped subcommands","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-15T17:55:00Z","receivedAt":"2026-04-15T17:55:05Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n>> I would have expected that we would just emulate what we already do\n>> to the main commands, and later with experience with the subcommand\n>> typo detection/fixes, would tweak the parameters either only to the\n>> subcommand part or to the both with justifications.\n> ...\n> Because of this, I was confused about whether I should copy this\n> behavior. I chose not to.\n>\n> However, if we want the main commands and subcommands to act the same, I\n> can do that and try to move the logic to autocorrect.c so both places\n> share the exact same typo detection.\n>\n> Do you want me to do this?\n\nEither do that (which is probably conceptually simpler), or explain\nin the proposed log message why they have to be different.  My gut\nfeeling is that you should start from identical settings that has\nbeen used for years, and then in a separate topic propose to improve\nthe parameters to improve the behaviour for both main commands and\nsub commands at the same time, but that is largely because there\nweren't any explanation why subcommands correction should behave\ndifferently from main commands (\"Correction for main commands\nbehaves strangely and I do not want to inherit it\" is not a good\nexplanation as it invites a natural question \"if you have a better\nbehaviour you can use for subcommand correction, can't it be used\nalso for main commands?  If not, why not?\").\n\n"},{"id":"541706","messageId":"SY0P300MB080153D97E8B1E90846C8FC3CE232@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"xmqqse8w6rd7.fsf@gitster.g","subject":"Re: [PATCH v4 07/10] parseopt: autocorrect mistyped subcommands","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-16T02:24:56Z","receivedAt":"2026-04-16T02:25:05Z","isPatch":true,"body":"On Wed, Apr 15, 2026 at 10:55:00AM -0700, Junio C Hamano wrote: \n> Either do that (which is probably conceptually simpler), or explain\n> in the proposed log message why they have to be different.  My gut\n> feeling is that you should start from identical settings that has\n> been used for years, and then in a separate topic propose to improve\n> the parameters to improve the behaviour for both main commands and\n> sub commands at the same time, but that is largely because there\n\nAgreed. I will change the subcommand autocorrection behavior to be the\nsame as the main command in the next revision.\n\n> weren't any explanation why subcommands correction should behave\n> differently from main commands (\"Correction for main commands\n> behaves strangely and I do not want to inherit it\" is not a good\n> explanation as it invites a natural question \"if you have a better\n> behaviour you can use for subcommand correction, can't it be used\n> also for main commands?  If not, why not?\").\n> \n\nCurrently, command autocorrection skips all prefix matched commands.\nIn my mind, this shouldn't happen because \"branc\" can indeed be\nautocorrected to \"branch\". I studied the old thread, but didn't find\nany explanation that expands on why it's doing that. Instead of\nskipping that, applying the autocorrect handling to that case would\nbe better, maybe? But for this patch, I'll keep them the same.\n\n-- \nJiamu Sun <39@barroit.sh>\n          <sunjiamu@outlook.com>\n"},{"id":"542114","messageId":"SY0P300MB0801AE56F740AD087D22B35ACE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB080186A23FB9582AD793F0D1CE40A@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v5 00/10] parseopt: add subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-22T12:18:00Z","receivedAt":"2026-04-22T12:19:49Z","isPatch":true,"body":"Git currently provides auto-correction for builtins and aliases, but\nlacks this functionality for subcommands parsed via the parse-options\nAPI. Subcommands are also commands, and typos will occur, too. Like:\n\n\tgit remote add-rul\n\nSo, this series introduces subcommand auto-correction.\n\nBy default, this implementation enables autocorrection for builtins\nwith mandatory subcommands. However, for those using\nPARSE_OPT_SUBCOMMAND_OPTIONAL, autocorrection is skipped to avoid\nmisinterpreting legitimate unknown arguments as mistyped subcommands.\n\nTo allow builtins with optional subcommands to explicitly opt in,\nthis series adds the PARSE_OPT_SUBCOMMAND_AUTOCORRECT flag, and enables\nit for git-remote and git-notes.\n\nAdditionally, the existing autocorrection logic is extracted from\nhelp.c so subcommand handling can reuse the same config parsing and\nprompt/delay logic.\n\nSome string literals are also combined so the full text is easier to\ngrep for.\n\nChanges in v5:\n  - Make subcommand autocorrection behave the same as command\n    autocorrection\n  - Rename PARSE_OPT_SUBCOMMAND_AUTOCORR to\n    PARSE_OPT_SUBCOMMAND_AUTOCORRECT\n  - Adjust subcommand autocorrection tests to fit new behavior\n\nChanges in v4:\n  - Add missing files to Meson build\n  - Change API prefix from autocorr to autocorrect\n  - Split the commit that moves tty code\n  - Add API documentation\n  - Use standard Damerau-Levenshtein distance and common practice\n    fuzziness thresholds\n  - Rename AUTOCORRECT_HINTONLY to AUTOCORRECT_HINT\n  - Change commit subject prefix for tests from \"help:\" to \"parseopt:\"\n  - Fix coding style issues\n\nChanges in v3:\n  - Align with the coding guildline\n  - Split patch so diffs don't get hidden by code movement\n  - Improve commit messages\n\nChanges in v2:\n  - Reword the explanation of default autocorrection behavior\n\nJiamu Sun (10):\n  parseopt: extract subcommand handling from parse_options_step()\n  help: make autocorrect handling reusable\n  help: move tty check for autocorrection to autocorrect.c\n  autocorrect: use mode and delay instead of magic numbers\n  autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINT\n  autocorrect: provide config resolution API\n  parseopt: autocorrect mistyped subcommands\n  parseopt: enable subcommand autocorrection for git-remote and\n    git-notes\n  parseopt: add tests for subcommand autocorrection\n  doc: document autocorrect API\n\n Makefile                          |   1 +\n autocorrect.c                     |  89 +++++++++++++++\n autocorrect.h                     |  36 ++++++\n builtin/notes.c                   |  10 +-\n builtin/remote.c                  |  12 +-\n help.c                            | 120 ++++----------------\n meson.build                       |   1 +\n parse-options.c                   | 183 +++++++++++++++++++++++-------\n parse-options.h                   |   1 +\n t/meson.build                     |   1 +\n t/t9004-autocorrect-subcommand.sh |  58 ++++++++++\n 11 files changed, 361 insertions(+), 151 deletions(-)\n create mode 100644 autocorrect.c\n create mode 100644 autocorrect.h\n create mode 100755 t/t9004-autocorrect-subcommand.sh\n\n\nbase-commit: f65aba1e87db64413b6d1ed5ae5a45b5a84a0997\n-- \n2.53.0\n\n"},{"id":"542115","messageId":"SY0P300MB0801DC1AC241760564952863CE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801AE56F740AD087D22B35ACE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v5 02/10] help: make autocorrect handling reusable","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-22T12:18:02Z","receivedAt":"2026-04-22T12:19:51Z","isPatch":true,"body":"Move config parsing and prompt/delay handling into autocorrect.c and\nexpose them in autocorrect.h. This makes autocorrect reusable regardless\nof which target links against it.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n Makefile      |  1 +\n autocorrect.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++\n autocorrect.h | 16 ++++++++++++\n help.c        | 64 +++------------------------------------------\n meson.build   |  1 +\n 5 files changed, 94 insertions(+), 60 deletions(-)\n create mode 100644 autocorrect.c\n create mode 100644 autocorrect.h\n\ndiff --git a/Makefile b/Makefile\nindex cedc234173e3..ced8a9b5abb4 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1100,6 +1100,7 @@ LIB_OBJS += archive-tar.o\n LIB_OBJS += archive-zip.o\n LIB_OBJS += archive.o\n LIB_OBJS += attr.o\n+LIB_OBJS += autocorrect.o\n LIB_OBJS += base85.o\n LIB_OBJS += bisect.o\n LIB_OBJS += blame.o\ndiff --git a/autocorrect.c b/autocorrect.c\nnew file mode 100644\nindex 000000000000..97145d3a53ce\n--- /dev/null\n+++ b/autocorrect.c\n@@ -0,0 +1,72 @@\n+#include \"git-compat-util.h\"\n+#include \"autocorrect.h\"\n+#include \"config.h\"\n+#include \"parse.h\"\n+#include \"strbuf.h\"\n+#include \"prompt.h\"\n+#include \"gettext.h\"\n+\n+static int parse_autocorrect(const char *value)\n+{\n+\tswitch (git_parse_maybe_bool_text(value)) {\n+\tcase 1:\n+\t\treturn AUTOCORRECT_IMMEDIATELY;\n+\tcase 0:\n+\t\treturn AUTOCORRECT_SHOW;\n+\tdefault: /* other random text */\n+\t\tbreak;\n+\t}\n+\n+\tif (!strcmp(value, \"prompt\"))\n+\t\treturn AUTOCORRECT_PROMPT;\n+\tif (!strcmp(value, \"never\"))\n+\t\treturn AUTOCORRECT_NEVER;\n+\tif (!strcmp(value, \"immediate\"))\n+\t\treturn AUTOCORRECT_IMMEDIATELY;\n+\tif (!strcmp(value, \"show\"))\n+\t\treturn AUTOCORRECT_SHOW;\n+\n+\treturn 0;\n+}\n+\n+void autocorrect_resolve_config(const char *var, const char *value,\n+\t\t\t\tconst struct config_context *ctx, void *data)\n+{\n+\tint *out = data;\n+\n+\tif (!strcmp(var, \"help.autocorrect\")) {\n+\t\tint v = parse_autocorrect(value);\n+\n+\t\tif (!v) {\n+\t\t\tv = git_config_int(var, value, ctx->kvi);\n+\t\t\tif (v < 0 || v == 1)\n+\t\t\t\tv = AUTOCORRECT_IMMEDIATELY;\n+\t\t}\n+\n+\t\t*out = v;\n+\t}\n+}\n+\n+void autocorrect_confirm(int autocorrect, const char *assumed)\n+{\n+\tif (autocorrect == AUTOCORRECT_IMMEDIATELY) {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"Continuing under the assumption that you meant '%s'.\"),\n+\t\t\t   assumed);\n+\t} else if (autocorrect == AUTOCORRECT_PROMPT) {\n+\t\tchar *answer;\n+\t\tstruct strbuf msg = STRBUF_INIT;\n+\n+\t\tstrbuf_addf(&msg, _(\"Run '%s' instead [y/N]? \"), assumed);\n+\t\tanswer = git_prompt(msg.buf, PROMPT_ECHO);\n+\t\tstrbuf_release(&msg);\n+\n+\t\tif (!(starts_with(answer, \"y\") || starts_with(answer, \"Y\")))\n+\t\t\texit(1);\n+\t} else {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"Continuing in %0.1f seconds, assuming that you meant '%s'.\"),\n+\t\t\t   (float)autocorrect / 10.0, assumed);\n+\t\tsleep_millisec(autocorrect * 100);\n+\t}\n+}\ndiff --git a/autocorrect.h b/autocorrect.h\nnew file mode 100644\nindex 000000000000..f5fadf9d9605\n--- /dev/null\n+++ b/autocorrect.h\n@@ -0,0 +1,16 @@\n+#ifndef AUTOCORRECT_H\n+#define AUTOCORRECT_H\n+\n+#define AUTOCORRECT_SHOW (-4)\n+#define AUTOCORRECT_PROMPT (-3)\n+#define AUTOCORRECT_NEVER (-2)\n+#define AUTOCORRECT_IMMEDIATELY (-1)\n+\n+struct config_context;\n+\n+void autocorrect_resolve_config(const char *var, const char *value,\n+\t\t\t\tconst struct config_context *ctx, void *data);\n+\n+void autocorrect_confirm(int autocorrect, const char *assumed);\n+\n+#endif /* AUTOCORRECT_H */\ndiff --git a/help.c b/help.c\nindex 3e59d07c370b..ab619ed43c7a 100644\n--- a/help.c\n+++ b/help.c\n@@ -22,6 +22,7 @@\n #include \"repository.h\"\n #include \"alias.h\"\n #include \"utf8.h\"\n+#include \"autocorrect.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -541,34 +542,6 @@ struct help_unknown_cmd_config {\n \tstruct cmdnames aliases;\n };\n \n-#define AUTOCORRECT_SHOW (-4)\n-#define AUTOCORRECT_PROMPT (-3)\n-#define AUTOCORRECT_NEVER (-2)\n-#define AUTOCORRECT_IMMEDIATELY (-1)\n-\n-static int parse_autocorrect(const char *value)\n-{\n-\tswitch (git_parse_maybe_bool_text(value)) {\n-\t\tcase 1:\n-\t\t\treturn AUTOCORRECT_IMMEDIATELY;\n-\t\tcase 0:\n-\t\t\treturn AUTOCORRECT_SHOW;\n-\t\tdefault: /* other random text */\n-\t\t\tbreak;\n-\t}\n-\n-\tif (!strcmp(value, \"prompt\"))\n-\t\treturn AUTOCORRECT_PROMPT;\n-\tif (!strcmp(value, \"never\"))\n-\t\treturn AUTOCORRECT_NEVER;\n-\tif (!strcmp(value, \"immediate\"))\n-\t\treturn AUTOCORRECT_IMMEDIATELY;\n-\tif (!strcmp(value, \"show\"))\n-\t\treturn AUTOCORRECT_SHOW;\n-\n-\treturn 0;\n-}\n-\n static int git_unknown_cmd_config(const char *var, const char *value,\n \t\t\t\t  const struct config_context *ctx,\n \t\t\t\t  void *cb)\n@@ -577,17 +550,7 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \tconst char *subsection, *key;\n \tsize_t subsection_len;\n \n-\tif (!strcmp(var, \"help.autocorrect\")) {\n-\t\tint v = parse_autocorrect(value);\n-\n-\t\tif (!v) {\n-\t\t\tv = git_config_int(var, value, ctx->kvi);\n-\t\t\tif (v < 0 || v == 1)\n-\t\t\t\tv = AUTOCORRECT_IMMEDIATELY;\n-\t\t}\n-\n-\t\tcfg->autocorrect = v;\n-\t}\n+\tautocorrect_resolve_config(var, value, ctx, &cfg->autocorrect);\n \n \t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n@@ -724,27 +687,8 @@ char *help_unknown_cmd(const char *cmd)\n \t\t\t   _(\"WARNING: You called a Git command named '%s', \"\n \t\t\t     \"which does not exist.\"),\n \t\t\t   cmd);\n-\t\tif (cfg.autocorrect == AUTOCORRECT_IMMEDIATELY)\n-\t\t\tfprintf_ln(stderr,\n-\t\t\t\t   _(\"Continuing under the assumption that \"\n-\t\t\t\t     \"you meant '%s'.\"),\n-\t\t\t\t   assumed);\n-\t\telse if (cfg.autocorrect == AUTOCORRECT_PROMPT) {\n-\t\t\tchar *answer;\n-\t\t\tstruct strbuf msg = STRBUF_INIT;\n-\t\t\tstrbuf_addf(&msg, _(\"Run '%s' instead [y/N]? \"), assumed);\n-\t\t\tanswer = git_prompt(msg.buf, PROMPT_ECHO);\n-\t\t\tstrbuf_release(&msg);\n-\t\t\tif (!(starts_with(answer, \"y\") ||\n-\t\t\t      starts_with(answer, \"Y\")))\n-\t\t\t\texit(1);\n-\t\t} else {\n-\t\t\tfprintf_ln(stderr,\n-\t\t\t\t   _(\"Continuing in %0.1f seconds, \"\n-\t\t\t\t     \"assuming that you meant '%s'.\"),\n-\t\t\t\t   (float)cfg.autocorrect/10.0, assumed);\n-\t\t\tsleep_millisec(cfg.autocorrect * 100);\n-\t\t}\n+\n+\t\tautocorrect_confirm(cfg.autocorrect, assumed);\n \n \t\tcmdnames_release(&cfg.aliases);\n \t\tcmdnames_release(&main_cmds);\ndiff --git a/meson.build b/meson.build\nindex 11488623bfd8..be20d507826d 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -290,6 +290,7 @@ libgit_sources = [\n   'archive-zip.c',\n   'archive.c',\n   'attr.c',\n+  'autocorrect.c',\n   'base85.c',\n   'bisect.c',\n   'blame.c',\n-- \n2.53.0\n\n"},{"id":"542116","messageId":"SY0P300MB0801E48CB82BBAD8946BA37BCE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801AE56F740AD087D22B35ACE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v5 01/10] parseopt: extract subcommand handling from parse_options_step()","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-22T12:18:01Z","receivedAt":"2026-04-22T12:19:51Z","isPatch":true,"body":"Move the subcommand branch out of parse_options_step() into a new\nhandle_subcommand() helper. Also, make parse_subcommand() return a\nsimple success/failure status.\n\nThis removes the switch over impossible parse_opt_result values and\nmakes the non-option path easier to follow and maintain.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n parse-options.c | 87 ++++++++++++++++++++++++++-----------------------\n 1 file changed, 46 insertions(+), 41 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex a676da86f5d6..803ce2ba4443 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -606,17 +606,44 @@ static enum parse_opt_result parse_nodash_opt(struct parse_opt_ctx_t *p,\n \treturn PARSE_OPT_ERROR;\n }\n \n-static enum parse_opt_result parse_subcommand(const char *arg,\n-\t\t\t\t\t      const struct option *options)\n+static int parse_subcommand(const char *arg, const struct option *options)\n {\n-\tfor (; options->type != OPTION_END; options++)\n-\t\tif (options->type == OPTION_SUBCOMMAND &&\n-\t\t    !strcmp(options->long_name, arg)) {\n-\t\t\t*(parse_opt_subcommand_fn **)options->value = options->subcommand_fn;\n-\t\t\treturn PARSE_OPT_SUBCOMMAND;\n-\t\t}\n+\tfor (; options->type != OPTION_END; options++) {\n+\t\tparse_opt_subcommand_fn **opt_val;\n \n-\treturn PARSE_OPT_UNKNOWN;\n+\t\tif (options->type != OPTION_SUBCOMMAND ||\n+\t\t    strcmp(options->long_name, arg))\n+\t\t\tcontinue;\n+\n+\t\topt_val = options->value;\n+\t\t*opt_val = options->subcommand_fn;\n+\t\treturn 0;\n+\t}\n+\n+\treturn -1;\n+}\n+\n+static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n+\t\t\t\t\t       const char *arg,\n+\t\t\t\t\t       const struct option *options,\n+\t\t\t\t\t       const char * const usagestr[])\n+{\n+\tint err = parse_subcommand(arg, options);\n+\n+\tif (!err)\n+\t\treturn PARSE_OPT_SUBCOMMAND;\n+\n+\t/*\n+\t * arg is neither a short or long option nor a subcommand.  Since this\n+\t * command has a default operation mode, we have to treat this arg and\n+\t * all remaining args as args meant to that default operation mode.\n+\t * So we are done parsing.\n+\t */\n+\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n+\t\treturn PARSE_OPT_DONE;\n+\n+\terror(_(\"unknown subcommand: `%s'\"), arg);\n+\tusage_with_options(usagestr, options);\n }\n \n static void check_typos(const char *arg, const struct option *options)\n@@ -1011,38 +1038,16 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\tif (*arg != '-' || !arg[1]) {\n \t\t\tif (parse_nodash_opt(ctx, arg, options) == 0)\n \t\t\t\tcontinue;\n-\t\t\tif (!ctx->has_subcommands) {\n-\t\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n-\t\t\t\t\treturn PARSE_OPT_NON_OPTION;\n-\t\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n-\t\t\t\tcontinue;\n-\t\t\t}\n-\t\t\tswitch (parse_subcommand(arg, options)) {\n-\t\t\tcase PARSE_OPT_SUBCOMMAND:\n-\t\t\t\treturn PARSE_OPT_SUBCOMMAND;\n-\t\t\tcase PARSE_OPT_UNKNOWN:\n-\t\t\t\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n-\t\t\t\t\t/*\n-\t\t\t\t\t * arg is neither a short or long\n-\t\t\t\t\t * option nor a subcommand.  Since\n-\t\t\t\t\t * this command has a default\n-\t\t\t\t\t * operation mode, we have to treat\n-\t\t\t\t\t * this arg and all remaining args\n-\t\t\t\t\t * as args meant to that default\n-\t\t\t\t\t * operation mode.\n-\t\t\t\t\t * So we are done parsing.\n-\t\t\t\t\t */\n-\t\t\t\t\treturn PARSE_OPT_DONE;\n-\t\t\t\terror(_(\"unknown subcommand: `%s'\"), arg);\n-\t\t\t\tusage_with_options(usagestr, options);\n-\t\t\tcase PARSE_OPT_COMPLETE:\n-\t\t\tcase PARSE_OPT_HELP:\n-\t\t\tcase PARSE_OPT_ERROR:\n-\t\t\tcase PARSE_OPT_DONE:\n-\t\t\tcase PARSE_OPT_NON_OPTION:\n-\t\t\t\t/* Impossible. */\n-\t\t\t\tBUG(\"parse_subcommand() cannot return these\");\n-\t\t\t}\n+\n+\t\t\tif (ctx->has_subcommands)\n+\t\t\t\treturn handle_subcommand(ctx, arg, options,\n+\t\t\t\t\t\t\t usagestr);\n+\n+\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n+\t\t\t\treturn PARSE_OPT_NON_OPTION;\n+\n+\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n+\t\t\tcontinue;\n \t\t}\n \n \t\t/* lone -h asks for help */\n-- \n2.53.0\n\n"},{"id":"542117","messageId":"SY0P300MB0801D0D27D3304F6D0CE2425CE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801AE56F740AD087D22B35ACE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v5 03/10] help: move tty check for autocorrection to autocorrect.c","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-22T12:18:03Z","receivedAt":"2026-04-22T12:19:53Z","isPatch":true,"body":"TTY checking is the autocorrect config parser's responsibility. It must\nensure the parsed value is correct and reliable. Thus, move the check to\nautocorrect_resolve_config().\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.c | 24 ++++++++++++++++--------\n help.c        |  6 ------\n 2 files changed, 16 insertions(+), 14 deletions(-)\n\ndiff --git a/autocorrect.c b/autocorrect.c\nindex 97145d3a53ce..887d2396da44 100644\n--- a/autocorrect.c\n+++ b/autocorrect.c\n@@ -33,18 +33,26 @@ void autocorrect_resolve_config(const char *var, const char *value,\n \t\t\t\tconst struct config_context *ctx, void *data)\n {\n \tint *out = data;\n+\tint parsed;\n \n-\tif (!strcmp(var, \"help.autocorrect\")) {\n-\t\tint v = parse_autocorrect(value);\n+\tif (strcmp(var, \"help.autocorrect\"))\n+\t\treturn;\n \n-\t\tif (!v) {\n-\t\t\tv = git_config_int(var, value, ctx->kvi);\n-\t\t\tif (v < 0 || v == 1)\n-\t\t\t\tv = AUTOCORRECT_IMMEDIATELY;\n-\t\t}\n+\tparsed = parse_autocorrect(value);\n \n-\t\t*out = v;\n+\t/*\n+\t * Disable autocorrection prompt in a non-interactive session\n+\t */\n+\tif (parsed == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))\n+\t\tparsed = AUTOCORRECT_NEVER;\n+\n+\tif (!parsed) {\n+\t\tparsed = git_config_int(var, value, ctx->kvi);\n+\t\tif (parsed < 0 || parsed == 1)\n+\t\t\tparsed = AUTOCORRECT_IMMEDIATELY;\n \t}\n+\n+\t*out = parsed;\n }\n \n void autocorrect_confirm(int autocorrect, const char *assumed)\ndiff --git a/help.c b/help.c\nindex ab619ed43c7a..d2b29715817e 100644\n--- a/help.c\n+++ b/help.c\n@@ -607,12 +607,6 @@ char *help_unknown_cmd(const char *cmd)\n \n \tread_early_config(the_repository, git_unknown_cmd_config, &cfg);\n \n-\t/*\n-\t * Disable autocorrection prompt in a non-interactive session\n-\t */\n-\tif ((cfg.autocorrect == AUTOCORRECT_PROMPT) && (!isatty(0) || !isatty(2)))\n-\t\tcfg.autocorrect = AUTOCORRECT_NEVER;\n-\n \tif (cfg.autocorrect == AUTOCORRECT_NEVER) {\n \t\tfprintf_ln(stderr, _(\"git: '%s' is not a git command. See 'git --help'.\"), cmd);\n \t\texit(1);\n-- \n2.53.0\n\n"},{"id":"542118","messageId":"SY0P300MB08015E4816BE4B04727A4D44CE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801AE56F740AD087D22B35ACE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v5 04/10] autocorrect: use mode and delay instead of magic numbers","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-22T12:18:04Z","receivedAt":"2026-04-22T12:19:55Z","isPatch":true,"body":"Drop magic numbers and describe autocorrect config with a mode enum and\nan integer delay. This reduces errors when mutating config values and\nmakes the values easier to access.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.c | 46 +++++++++++++++++++++++-----------------------\n autocorrect.h | 20 ++++++++++++++------\n help.c        |  9 +++++----\n 3 files changed, 42 insertions(+), 33 deletions(-)\n\ndiff --git a/autocorrect.c b/autocorrect.c\nindex 887d2396da44..2484546fc731 100644\n--- a/autocorrect.c\n+++ b/autocorrect.c\n@@ -6,7 +6,7 @@\n #include \"prompt.h\"\n #include \"gettext.h\"\n \n-static int parse_autocorrect(const char *value)\n+static enum autocorrect_mode parse_autocorrect(const char *value)\n {\n \tswitch (git_parse_maybe_bool_text(value)) {\n \tcase 1:\n@@ -19,49 +19,49 @@ static int parse_autocorrect(const char *value)\n \n \tif (!strcmp(value, \"prompt\"))\n \t\treturn AUTOCORRECT_PROMPT;\n-\tif (!strcmp(value, \"never\"))\n+\telse if (!strcmp(value, \"never\"))\n \t\treturn AUTOCORRECT_NEVER;\n-\tif (!strcmp(value, \"immediate\"))\n+\telse if (!strcmp(value, \"immediate\"))\n \t\treturn AUTOCORRECT_IMMEDIATELY;\n-\tif (!strcmp(value, \"show\"))\n+\telse if (!strcmp(value, \"show\"))\n \t\treturn AUTOCORRECT_SHOW;\n-\n-\treturn 0;\n+\telse\n+\t\treturn AUTOCORRECT_DELAY;\n }\n \n void autocorrect_resolve_config(const char *var, const char *value,\n \t\t\t\tconst struct config_context *ctx, void *data)\n {\n-\tint *out = data;\n-\tint parsed;\n+\tstruct autocorrect *conf = data;\n \n \tif (strcmp(var, \"help.autocorrect\"))\n \t\treturn;\n \n-\tparsed = parse_autocorrect(value);\n+\tconf->mode = parse_autocorrect(value);\n \n \t/*\n \t * Disable autocorrection prompt in a non-interactive session\n \t */\n-\tif (parsed == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))\n-\t\tparsed = AUTOCORRECT_NEVER;\n+\tif (conf->mode == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))\n+\t\tconf->mode = AUTOCORRECT_NEVER;\n \n-\tif (!parsed) {\n-\t\tparsed = git_config_int(var, value, ctx->kvi);\n-\t\tif (parsed < 0 || parsed == 1)\n-\t\t\tparsed = AUTOCORRECT_IMMEDIATELY;\n-\t}\n+\tif (conf->mode == AUTOCORRECT_DELAY) {\n+\t\tconf->delay = git_config_int(var, value, ctx->kvi);\n \n-\t*out = parsed;\n+\t\tif (!conf->delay)\n+\t\t\tconf->mode = AUTOCORRECT_SHOW;\n+\t\telse if (conf->delay < 0 || conf->delay == 1)\n+\t\t\tconf->mode = AUTOCORRECT_IMMEDIATELY;\n+\t}\n }\n \n-void autocorrect_confirm(int autocorrect, const char *assumed)\n+void autocorrect_confirm(struct autocorrect *conf, const char *assumed)\n {\n-\tif (autocorrect == AUTOCORRECT_IMMEDIATELY) {\n+\tif (conf->mode == AUTOCORRECT_IMMEDIATELY) {\n \t\tfprintf_ln(stderr,\n \t\t\t   _(\"Continuing under the assumption that you meant '%s'.\"),\n \t\t\t   assumed);\n-\t} else if (autocorrect == AUTOCORRECT_PROMPT) {\n+\t} else if (conf->mode == AUTOCORRECT_PROMPT) {\n \t\tchar *answer;\n \t\tstruct strbuf msg = STRBUF_INIT;\n \n@@ -71,10 +71,10 @@ void autocorrect_confirm(int autocorrect, const char *assumed)\n \n \t\tif (!(starts_with(answer, \"y\") || starts_with(answer, \"Y\")))\n \t\t\texit(1);\n-\t} else {\n+\t} else if (conf->mode == AUTOCORRECT_DELAY) {\n \t\tfprintf_ln(stderr,\n \t\t\t   _(\"Continuing in %0.1f seconds, assuming that you meant '%s'.\"),\n-\t\t\t   (float)autocorrect / 10.0, assumed);\n-\t\tsleep_millisec(autocorrect * 100);\n+\t\t\t   conf->delay / 10.0, assumed);\n+\t\tsleep_millisec(conf->delay * 100);\n \t}\n }\ndiff --git a/autocorrect.h b/autocorrect.h\nindex f5fadf9d9605..5506a36f11a7 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -1,16 +1,24 @@\n #ifndef AUTOCORRECT_H\n #define AUTOCORRECT_H\n \n-#define AUTOCORRECT_SHOW (-4)\n-#define AUTOCORRECT_PROMPT (-3)\n-#define AUTOCORRECT_NEVER (-2)\n-#define AUTOCORRECT_IMMEDIATELY (-1)\n-\n struct config_context;\n \n+enum autocorrect_mode {\n+\tAUTOCORRECT_SHOW,\n+\tAUTOCORRECT_NEVER,\n+\tAUTOCORRECT_PROMPT,\n+\tAUTOCORRECT_IMMEDIATELY,\n+\tAUTOCORRECT_DELAY,\n+};\n+\n+struct autocorrect {\n+\tenum autocorrect_mode mode;\n+\tint delay;\n+};\n+\n void autocorrect_resolve_config(const char *var, const char *value,\n \t\t\t\tconst struct config_context *ctx, void *data);\n \n-void autocorrect_confirm(int autocorrect, const char *assumed);\n+void autocorrect_confirm(struct autocorrect *conf, const char *assumed);\n \n #endif /* AUTOCORRECT_H */\ndiff --git a/help.c b/help.c\nindex d2b29715817e..353596c17d82 100644\n--- a/help.c\n+++ b/help.c\n@@ -538,7 +538,7 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)\n }\n \n struct help_unknown_cmd_config {\n-\tint autocorrect;\n+\tstruct autocorrect autocorrect;\n \tstruct cmdnames aliases;\n };\n \n@@ -607,7 +607,7 @@ char *help_unknown_cmd(const char *cmd)\n \n \tread_early_config(the_repository, git_unknown_cmd_config, &cfg);\n \n-\tif (cfg.autocorrect == AUTOCORRECT_NEVER) {\n+\tif (cfg.autocorrect.mode == AUTOCORRECT_NEVER) {\n \t\tfprintf_ln(stderr, _(\"git: '%s' is not a git command. See 'git --help'.\"), cmd);\n \t\texit(1);\n \t}\n@@ -673,7 +673,8 @@ char *help_unknown_cmd(const char *cmd)\n \t\t     n++)\n \t\t\t; /* still counting */\n \t}\n-\tif (cfg.autocorrect && cfg.autocorrect != AUTOCORRECT_SHOW && n == 1 &&\n+\n+\tif (cfg.autocorrect.mode != AUTOCORRECT_SHOW && n == 1 &&\n \t    SIMILAR_ENOUGH(best_similarity)) {\n \t\tchar *assumed = xstrdup(main_cmds.names[0]->name);\n \n@@ -682,7 +683,7 @@ char *help_unknown_cmd(const char *cmd)\n \t\t\t     \"which does not exist.\"),\n \t\t\t   cmd);\n \n-\t\tautocorrect_confirm(cfg.autocorrect, assumed);\n+\t\tautocorrect_confirm(&cfg.autocorrect, assumed);\n \n \t\tcmdnames_release(&cfg.aliases);\n \t\tcmdnames_release(&main_cmds);\n-- \n2.53.0\n\n"},{"id":"542119","messageId":"SY0P300MB0801893D2DD10EDEB682D722CE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801AE56F740AD087D22B35ACE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v5 05/10] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINT","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-22T12:18:05Z","receivedAt":"2026-04-22T12:19:57Z","isPatch":true,"body":"AUTOCORRECT_SHOW is ambiguous. Its purpose is to show commands similar\nto the unknown one and take no other action. Rename it to fit the\nsemantics.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.c | 6 +++---\n autocorrect.h | 2 +-\n help.c        | 2 +-\n 3 files changed, 5 insertions(+), 5 deletions(-)\n\ndiff --git a/autocorrect.c b/autocorrect.c\nindex 2484546fc731..de0fa282c934 100644\n--- a/autocorrect.c\n+++ b/autocorrect.c\n@@ -12,7 +12,7 @@ static enum autocorrect_mode parse_autocorrect(const char *value)\n \tcase 1:\n \t\treturn AUTOCORRECT_IMMEDIATELY;\n \tcase 0:\n-\t\treturn AUTOCORRECT_SHOW;\n+\t\treturn AUTOCORRECT_HINT;\n \tdefault: /* other random text */\n \t\tbreak;\n \t}\n@@ -24,7 +24,7 @@ static enum autocorrect_mode parse_autocorrect(const char *value)\n \telse if (!strcmp(value, \"immediate\"))\n \t\treturn AUTOCORRECT_IMMEDIATELY;\n \telse if (!strcmp(value, \"show\"))\n-\t\treturn AUTOCORRECT_SHOW;\n+\t\treturn AUTOCORRECT_HINT;\n \telse\n \t\treturn AUTOCORRECT_DELAY;\n }\n@@ -49,7 +49,7 @@ void autocorrect_resolve_config(const char *var, const char *value,\n \t\tconf->delay = git_config_int(var, value, ctx->kvi);\n \n \t\tif (!conf->delay)\n-\t\t\tconf->mode = AUTOCORRECT_SHOW;\n+\t\t\tconf->mode = AUTOCORRECT_HINT;\n \t\telse if (conf->delay < 0 || conf->delay == 1)\n \t\t\tconf->mode = AUTOCORRECT_IMMEDIATELY;\n \t}\ndiff --git a/autocorrect.h b/autocorrect.h\nindex 5506a36f11a7..328807242c15 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -4,7 +4,7 @@\n struct config_context;\n \n enum autocorrect_mode {\n-\tAUTOCORRECT_SHOW,\n+\tAUTOCORRECT_HINT,\n \tAUTOCORRECT_NEVER,\n \tAUTOCORRECT_PROMPT,\n \tAUTOCORRECT_IMMEDIATELY,\ndiff --git a/help.c b/help.c\nindex 353596c17d82..c7dab8395ee2 100644\n--- a/help.c\n+++ b/help.c\n@@ -674,7 +674,7 @@ char *help_unknown_cmd(const char *cmd)\n \t\t\t; /* still counting */\n \t}\n \n-\tif (cfg.autocorrect.mode != AUTOCORRECT_SHOW && n == 1 &&\n+\tif (cfg.autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&\n \t    SIMILAR_ENOUGH(best_similarity)) {\n \t\tchar *assumed = xstrdup(main_cmds.names[0]->name);\n \n-- \n2.53.0\n\n"},{"id":"542120","messageId":"SY0P300MB0801D329C871F2F277526CD7CE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801AE56F740AD087D22B35ACE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v5 06/10] autocorrect: provide config resolution API","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-22T12:18:06Z","receivedAt":"2026-04-22T12:19:58Z","isPatch":true,"body":"Add autocorrect_resolve(). This resolves and populates the correct\nvalues for autocorrect config.\n\nMake autocorrect config callback internal. The API is meant to provide\na high-level way to retrieve the config. Allowing access to the config\ncallback from outside violates that intent.\n\nAdditionally, in some cases, without access to the config callback, two\nconfig iterations cannot be merged into one, which can hurt performance.\nThis is fine, as the code path that calls autocorrect_resolve() is cold.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.c | 15 ++++++++++++---\n autocorrect.h |  5 +----\n help.c        | 40 +++++++++++++++++-----------------------\n 3 files changed, 30 insertions(+), 30 deletions(-)\n\ndiff --git a/autocorrect.c b/autocorrect.c\nindex de0fa282c934..b2ee9f51e8c0 100644\n--- a/autocorrect.c\n+++ b/autocorrect.c\n@@ -1,3 +1,5 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n #include \"git-compat-util.h\"\n #include \"autocorrect.h\"\n #include \"config.h\"\n@@ -29,13 +31,13 @@ static enum autocorrect_mode parse_autocorrect(const char *value)\n \t\treturn AUTOCORRECT_DELAY;\n }\n \n-void autocorrect_resolve_config(const char *var, const char *value,\n-\t\t\t\tconst struct config_context *ctx, void *data)\n+static int resolve_autocorrect(const char *var, const char *value,\n+\t\t\t       const struct config_context *ctx, void *data)\n {\n \tstruct autocorrect *conf = data;\n \n \tif (strcmp(var, \"help.autocorrect\"))\n-\t\treturn;\n+\t\treturn 0;\n \n \tconf->mode = parse_autocorrect(value);\n \n@@ -53,6 +55,13 @@ void autocorrect_resolve_config(const char *var, const char *value,\n \t\telse if (conf->delay < 0 || conf->delay == 1)\n \t\t\tconf->mode = AUTOCORRECT_IMMEDIATELY;\n \t}\n+\n+\treturn 0;\n+}\n+\n+void autocorrect_resolve(struct autocorrect *conf)\n+{\n+\tread_early_config(the_repository, resolve_autocorrect, conf);\n }\n \n void autocorrect_confirm(struct autocorrect *conf, const char *assumed)\ndiff --git a/autocorrect.h b/autocorrect.h\nindex 328807242c15..0d3e819262ed 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -1,8 +1,6 @@\n #ifndef AUTOCORRECT_H\n #define AUTOCORRECT_H\n \n-struct config_context;\n-\n enum autocorrect_mode {\n \tAUTOCORRECT_HINT,\n \tAUTOCORRECT_NEVER,\n@@ -16,8 +14,7 @@ struct autocorrect {\n \tint delay;\n };\n \n-void autocorrect_resolve_config(const char *var, const char *value,\n-\t\t\t\tconst struct config_context *ctx, void *data);\n+void autocorrect_resolve(struct autocorrect *conf);\n \n void autocorrect_confirm(struct autocorrect *conf, const char *assumed);\n \ndiff --git a/help.c b/help.c\nindex c7dab8395ee2..0fff43545cc1 100644\n--- a/help.c\n+++ b/help.c\n@@ -537,32 +537,23 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)\n \treturn 0;\n }\n \n-struct help_unknown_cmd_config {\n-\tstruct autocorrect autocorrect;\n-\tstruct cmdnames aliases;\n-};\n-\n-static int git_unknown_cmd_config(const char *var, const char *value,\n-\t\t\t\t  const struct config_context *ctx,\n-\t\t\t\t  void *cb)\n+static int resolve_aliases(const char *var, const char *value UNUSED,\n+\t\t\t   const struct config_context *ctx UNUSED, void *data)\n {\n-\tstruct help_unknown_cmd_config *cfg = cb;\n+\tstruct cmdnames *aliases = data;\n \tconst char *subsection, *key;\n \tsize_t subsection_len;\n \n-\tautocorrect_resolve_config(var, value, ctx, &cfg->autocorrect);\n-\n-\t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n \t\t\t      &key)) {\n \t\tif (subsection) {\n \t\t\t/* [alias \"name\"] command = value */\n \t\t\tif (!strcmp(key, \"command\"))\n-\t\t\t\tadd_cmdname(&cfg->aliases, subsection,\n+\t\t\t\tadd_cmdname(aliases, subsection,\n \t\t\t\t\t    subsection_len);\n \t\t} else {\n \t\t\t/* alias.name = value */\n-\t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t\t\tadd_cmdname(aliases, key, strlen(key));\n \t\t}\n \t}\n \n@@ -599,22 +590,26 @@ static const char bad_interpreter_advice[] =\n \n char *help_unknown_cmd(const char *cmd)\n {\n-\tstruct help_unknown_cmd_config cfg = { 0 };\n+\tstruct cmdnames aliases = { 0 };\n+\tstruct autocorrect autocorrect = { 0 };\n \tint i, n, best_similarity = 0;\n \tstruct cmdnames main_cmds = { 0 };\n \tstruct cmdnames other_cmds = { 0 };\n \tstruct cmdname_help *common_cmds;\n \n-\tread_early_config(the_repository, git_unknown_cmd_config, &cfg);\n+\tautocorrect_resolve(&autocorrect);\n \n-\tif (cfg.autocorrect.mode == AUTOCORRECT_NEVER) {\n+\tif (autocorrect.mode == AUTOCORRECT_NEVER) {\n \t\tfprintf_ln(stderr, _(\"git: '%s' is not a git command. See 'git --help'.\"), cmd);\n \t\texit(1);\n \t}\n \n \tload_command_list(\"git-\", &main_cmds, &other_cmds);\n \n-\tadd_cmd_list(&main_cmds, &cfg.aliases);\n+\t/* Also use aliases for command lookup */\n+\tread_early_config(the_repository, resolve_aliases, &aliases);\n+\n+\tadd_cmd_list(&main_cmds, &aliases);\n \tadd_cmd_list(&main_cmds, &other_cmds);\n \tQSORT(main_cmds.names, main_cmds.cnt, cmdname_compare);\n \tuniq(&main_cmds);\n@@ -674,18 +669,17 @@ char *help_unknown_cmd(const char *cmd)\n \t\t\t; /* still counting */\n \t}\n \n-\tif (cfg.autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&\n+\tif (autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&\n \t    SIMILAR_ENOUGH(best_similarity)) {\n \t\tchar *assumed = xstrdup(main_cmds.names[0]->name);\n \n \t\tfprintf_ln(stderr,\n-\t\t\t   _(\"WARNING: You called a Git command named '%s', \"\n-\t\t\t     \"which does not exist.\"),\n+\t\t\t   _(\"WARNING: You called a Git command named '%s', which does not exist.\"),\n \t\t\t   cmd);\n \n-\t\tautocorrect_confirm(&cfg.autocorrect, assumed);\n+\t\tautocorrect_confirm(&autocorrect, assumed);\n \n-\t\tcmdnames_release(&cfg.aliases);\n+\t\tcmdnames_release(&aliases);\n \t\tcmdnames_release(&main_cmds);\n \t\tcmdnames_release(&other_cmds);\n \t\treturn assumed;\n-- \n2.53.0\n\n"},{"id":"542121","messageId":"SY0P300MB080136CB238AAA21CB36153DCE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801AE56F740AD087D22B35ACE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v5 07/10] parseopt: autocorrect mistyped subcommands","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-22T12:18:07Z","receivedAt":"2026-04-22T12:20:00Z","isPatch":true,"body":"Try to autocorrect the mistyped mandatory subcommand before showing an\nerror and exiting. Subcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL\nare skipped.\n\nThe subcommand autocorrection behaves the same as the command\nautocorrection.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.h   |   4 ++\n help.c          |  13 ++----\n parse-options.c | 102 ++++++++++++++++++++++++++++++++++++++++++++++--\n 3 files changed, 107 insertions(+), 12 deletions(-)\n\ndiff --git a/autocorrect.h b/autocorrect.h\nindex 0d3e819262ed..14ee7c4548d3 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -1,6 +1,10 @@\n #ifndef AUTOCORRECT_H\n #define AUTOCORRECT_H\n \n+/* An empirically derived magic number */\n+#define AUTOCORRECT_SIMILARITY_FLOOR 7\n+#define AUTOCORRECT_SIMILAR_ENOUGH(x) ((x) < AUTOCORRECT_SIMILARITY_FLOOR)\n+\n enum autocorrect_mode {\n \tAUTOCORRECT_HINT,\n \tAUTOCORRECT_NEVER,\ndiff --git a/help.c b/help.c\nindex 0fff43545cc1..ecc5673b7243 100644\n--- a/help.c\n+++ b/help.c\n@@ -580,10 +580,6 @@ static void add_cmd_list(struct cmdnames *cmds, struct cmdnames *old)\n \told->cnt = 0;\n }\n \n-/* An empirically derived magic number */\n-#define SIMILARITY_FLOOR 7\n-#define SIMILAR_ENOUGH(x) ((x) < SIMILARITY_FLOOR)\n-\n static const char bad_interpreter_advice[] =\n \tN_(\"'%s' appears to be a git command, but we were not\\n\"\n \t\"able to execute it. Maybe git-%s is broken?\");\n@@ -659,7 +655,7 @@ char *help_unknown_cmd(const char *cmd)\n \n \tif (main_cmds.cnt <= n) {\n \t\t/* prefix matches with everything? that is too ambiguous */\n-\t\tbest_similarity = SIMILARITY_FLOOR + 1;\n+\t\tbest_similarity = AUTOCORRECT_SIMILARITY_FLOOR + 1;\n \t} else {\n \t\t/* count all the most similar ones */\n \t\tfor (best_similarity = main_cmds.names[n++]->len;\n@@ -670,7 +666,7 @@ char *help_unknown_cmd(const char *cmd)\n \t}\n \n \tif (autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&\n-\t    SIMILAR_ENOUGH(best_similarity)) {\n+\t    AUTOCORRECT_SIMILAR_ENOUGH(best_similarity)) {\n \t\tchar *assumed = xstrdup(main_cmds.names[0]->name);\n \n \t\tfprintf_ln(stderr,\n@@ -687,11 +683,10 @@ char *help_unknown_cmd(const char *cmd)\n \n \tfprintf_ln(stderr, _(\"git: '%s' is not a git command. See 'git --help'.\"), cmd);\n \n-\tif (SIMILAR_ENOUGH(best_similarity)) {\n+\tif (AUTOCORRECT_SIMILAR_ENOUGH(best_similarity)) {\n \t\tfprintf_ln(stderr,\n \t\t\t   Q_(\"\\nThe most similar command is\",\n-\t\t\t      \"\\nThe most similar commands are\",\n-\t\t\t   n));\n+\t\t\t      \"\\nThe most similar commands are\", n));\n \n \t\tfor (i = 0; i < n; i++)\n \t\t\tfprintf(stderr, \"\\t%s\\n\", main_cmds.names[i]->name);\ndiff --git a/parse-options.c b/parse-options.c\nindex 803ce2ba4443..a488f9a41df8 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -7,6 +7,8 @@\n #include \"string-list.h\"\n #include \"strmap.h\"\n #include \"utf8.h\"\n+#include \"autocorrect.h\"\n+#include \"levenshtein.h\"\n \n static int disallow_abbreviated_options;\n \n@@ -623,13 +625,98 @@ static int parse_subcommand(const char *arg, const struct option *options)\n \treturn -1;\n }\n \n+static void find_subcommands(struct string_list *list,\n+\t\t\t     const struct option *options)\n+{\n+\tfor (; options->type != OPTION_END; options++) {\n+\t\tif (options->type == OPTION_SUBCOMMAND)\n+\t\t\tstring_list_append(list, options->long_name);\n+\t}\n+}\n+\n+static int levenshtein_compare(const void *p1, const void *p2)\n+{\n+\tconst struct string_list_item *i1 = p1, *i2 = p2;\n+\tconst char *s1 = i1->string, *s2 = i2->string;\n+\tint l1 = (intptr_t)i1->util;\n+\tint l2 = (intptr_t)i2->util;\n+\n+\treturn l1 != l2 ? l1 - l2 : strcmp(s1, s2);\n+}\n+\n+static const char *autocorrect_subcommand(const char *cmd,\n+\t\t\t\t\t  struct string_list *cmds)\n+{\n+\tstruct autocorrect autocorrect = { 0 };\n+\tunsigned int n = 0, best = 0;\n+\tstruct string_list_item *cand;\n+\n+\tautocorrect_resolve(&autocorrect);\n+\n+\tif (autocorrect.mode == AUTOCORRECT_NEVER)\n+\t\treturn NULL;\n+\n+\tfor_each_string_list_item(cand, cmds) {\n+\t\tif (starts_with(cand->string, cmd)) {\n+\t\t\tcand->util = 0;\n+\t\t} else {\n+\t\t\tint edit = levenshtein(cmd, cand->string,\n+\t\t\t\t\t       0, 2, 1, 3) + 1;\n+\n+\t\t\tcand->util = (void *)(intptr_t)edit;\n+\t\t}\n+\t}\n+\n+\tQSORT(cmds->items, cmds->nr, levenshtein_compare);\n+\n+\t/* Match help.c:help_unknown_cmd */\n+\tfor (; n < cmds->nr && !cmds->items[n].util; n++);\n+\n+\tif (n == cmds->nr)\n+\t\t/* prefix matches with every subcommands */\n+\t\tbest = AUTOCORRECT_SIMILARITY_FLOOR + 1;\n+\telse\n+\t\tfor (best = (intptr_t)cmds->items[n++].util;\n+\t\t     (n < cmds->nr && best == (intptr_t)cmds->items[n].util);\n+\t\t     n++);\n+\n+\tif (autocorrect.mode != AUTOCORRECT_HINT &&  n == 1 &&\n+\t    AUTOCORRECT_SIMILAR_ENOUGH(best)) {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"WARNING: You called a subcommand named '%s', which does not exist.\"),\n+\t\t\t   cmd);\n+\n+\t\tautocorrect_confirm(&autocorrect, cmds->items[0].string);\n+\t\treturn cmds->items[0].string;\n+\t}\n+\n+\tif (AUTOCORRECT_SIMILAR_ENOUGH(best)) {\n+\t\terror(_(\"'%s' is not a subcommand.\"), cmd);\n+\n+\t\tfprintf_ln(stderr,\n+\t\t\t   Q_(\"\\nThe most similar subcommand is\",\n+\t\t\t      \"\\nThe most similar subcommands are\",\n+\t\t\t   n));\n+\n+\t\tfor (unsigned int i = 0; i < n; i++)\n+\t\t\tfprintf(stderr, \"\\t%s\\n\", cmds->items[i].string);\n+\n+\t\texit(1);\n+\t}\n+\n+\treturn NULL;\n+}\n+\n static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \t\t\t\t\t       const char *arg,\n \t\t\t\t\t       const struct option *options,\n \t\t\t\t\t       const char * const usagestr[])\n {\n-\tint err = parse_subcommand(arg, options);\n+\tint err;\n+\tconst char *assumed;\n+\tstruct string_list cmds = STRING_LIST_INIT_NODUP;\n \n+\terr = parse_subcommand(arg, options);\n \tif (!err)\n \t\treturn PARSE_OPT_SUBCOMMAND;\n \n@@ -642,8 +729,17 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n \t\treturn PARSE_OPT_DONE;\n \n-\terror(_(\"unknown subcommand: `%s'\"), arg);\n-\tusage_with_options(usagestr, options);\n+\tfind_subcommands(&cmds, options);\n+\tassumed = autocorrect_subcommand(arg, &cmds);\n+\n+\tif (!assumed) {\n+\t\terror(_(\"unknown subcommand: `%s'\"), arg);\n+\t\tusage_with_options(usagestr, options);\n+\t}\n+\n+\tstring_list_clear(&cmds, 0);\n+\tparse_subcommand(assumed, options);\n+\treturn PARSE_OPT_SUBCOMMAND;\n }\n \n static void check_typos(const char *arg, const struct option *options)\n-- \n2.53.0\n\n"},{"id":"542122","messageId":"SY0P300MB08013857F222BCCC22870ADCCE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801AE56F740AD087D22B35ACE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v5 08/10] parseopt: enable subcommand autocorrection for git-remote and git-notes","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-22T12:18:08Z","receivedAt":"2026-04-22T12:20:02Z","isPatch":true,"body":"Add PARSE_OPT_SUBCOMMAND_AUTOCORRECT to enable autocorrection for\nsubcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL.\n\nApply this to git-remote and git-notes, so mistyped subcommands can be\nautomatically corrected, and builtin entry points no longer need to\nhandle the unknown subcommand error path themselves.\n\nThis is safe. Both builtins either resolve to a single subcommand or\ntake no subcommand at all, meaning any unknown argument encountered by\nthe parser must be a mistyped subcommand.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n builtin/notes.c  | 10 +++-------\n builtin/remote.c | 12 ++++--------\n parse-options.c  | 16 +++++++++-------\n parse-options.h  |  1 +\n 4 files changed, 17 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 9af602bdd7b4..f9bf350df4e8 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -1149,14 +1149,10 @@ int cmd_notes(int argc,\n \n \trepo_config(the_repository, git_default_config, NULL);\n \targc = parse_options(argc, argv, prefix, options, git_notes_usage,\n-\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n-\tif (!fn) {\n-\t\tif (argc) {\n-\t\t\terror(_(\"unknown subcommand: `%s'\"), argv[0]);\n-\t\t\tusage_with_options(git_notes_usage, options);\n-\t\t}\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n+\t\t\t     PARSE_OPT_SUBCOMMAND_AUTOCORRECT);\n+\tif (!fn)\n \t\tfn = list;\n-\t}\n \n \tif (override_notes_ref) {\n \t\tstruct strbuf sb = STRBUF_INIT;\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex de989ea3ba96..6a78ab8f4cd2 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -1953,15 +1953,11 @@ int cmd_remote(int argc,\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, builtin_remote_usage,\n-\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n+\t\t\t     PARSE_OPT_SUBCOMMAND_AUTOCORRECT);\n \n-\tif (fn) {\n+\tif (fn)\n \t\treturn !!fn(argc, argv, prefix, repo);\n-\t} else {\n-\t\tif (argc) {\n-\t\t\terror(_(\"unknown subcommand: `%s'\"), argv[0]);\n-\t\t\tusage_with_options(builtin_remote_usage, options);\n-\t\t}\n+\telse\n \t\treturn !!show_all();\n-\t}\n }\ndiff --git a/parse-options.c b/parse-options.c\nindex a488f9a41df8..820683e982c1 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -720,14 +720,16 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \tif (!err)\n \t\treturn PARSE_OPT_SUBCOMMAND;\n \n-\t/*\n-\t * arg is neither a short or long option nor a subcommand.  Since this\n-\t * command has a default operation mode, we have to treat this arg and\n-\t * all remaining args as args meant to that default operation mode.\n-\t * So we are done parsing.\n-\t */\n-\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n+\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL &&\n+\t    !(ctx->flags & PARSE_OPT_SUBCOMMAND_AUTOCORRECT)) {\n+\t\t/*\n+\t\t * arg is neither a short or long option nor a subcommand.\n+\t\t * Since this command has a default operation mode, we have to\n+\t\t * treat this arg and all remaining args as args meant to that\n+\t\t * default operation mode.  So we are done parsing.\n+\t\t */\n \t\treturn PARSE_OPT_DONE;\n+\t}\n \n \tfind_subcommands(&cmds, options);\n \tassumed = autocorrect_subcommand(arg, &cmds);\ndiff --git a/parse-options.h b/parse-options.h\nindex 706de9729f6b..e5fd4da4055b 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -40,6 +40,7 @@ enum parse_opt_flags {\n \tPARSE_OPT_ONE_SHOT = 1 << 5,\n \tPARSE_OPT_SHELL_EVAL = 1 << 6,\n \tPARSE_OPT_SUBCOMMAND_OPTIONAL = 1 << 7,\n+\tPARSE_OPT_SUBCOMMAND_AUTOCORRECT = 1 << 8,\n };\n \n enum parse_opt_option_flags {\n-- \n2.53.0\n\n"},{"id":"542123","messageId":"SY0P300MB08018E1D69CECC5CD4098BABCE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801AE56F740AD087D22B35ACE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v5 09/10] parseopt: add tests for subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-22T12:18:09Z","receivedAt":"2026-04-22T12:20:04Z","isPatch":true,"body":"These tests cover default behavior (help.autocorrect is unset), no\ncorrection, immediate correction, delayed correction, and rejection\nwhen the typo is too dissimilar.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n t/meson.build                     |  1 +\n t/t9004-autocorrect-subcommand.sh | 58 +++++++++++++++++++++++++++++++\n 2 files changed, 59 insertions(+)\n create mode 100755 t/t9004-autocorrect-subcommand.sh\n\ndiff --git a/t/meson.build b/t/meson.build\nindex 7528e5cda5fe..871cd7ab0a08 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -980,6 +980,7 @@ integration_tests = [\n   't9001-send-email.sh',\n   't9002-column.sh',\n   't9003-help-autocorrect.sh',\n+  't9004-autocorrect-subcommand.sh',\n   't9100-git-svn-basic.sh',\n   't9101-git-svn-props.sh',\n   't9102-git-svn-deep-rmdir.sh',\ndiff --git a/t/t9004-autocorrect-subcommand.sh b/t/t9004-autocorrect-subcommand.sh\nnew file mode 100755\nindex 000000000000..09d281bd105b\n--- /dev/null\n+++ b/t/t9004-autocorrect-subcommand.sh\n@@ -0,0 +1,58 @@\n+#!/bin/sh\n+\n+test_description='subcommand auto-correction test\n+\n+Test autocorrection for subcommands with different\n+help.autocorrect mode.'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' \"\n+\techo '^error: unknown subcommand: ' >grep_unknown\n+\"\n+\n+test_expect_success 'default is show hint only' '\n+\ttest_must_fail git worktree lsit 2>actual &&\n+\ttest_grep \"most similar subcommand\" actual\n+'\n+\n+test_expect_success \"'never' disables autocorrection\" '\n+\ttest_config help.autocorrect never &&\n+\n+\ttest_must_fail git worktree lsit 2>actual &&\n+\thead -n1 actual >first && test_grep -f grep_unknown first\n+'\n+\n+for mode in false no off 0 show\n+do\n+\ttest_expect_success \"'$mode' disables autocorrection and shows hints\" \"\n+\t\ttest_config help.autocorrect $mode &&\n+\n+\t\ttest_must_fail git worktree lsit 2>actual &&\n+\t\ttest_grep 'most similar subcommand' actual\n+\t\"\n+done\n+\n+for mode in -39 immediate 1\n+do\n+\ttest_expect_success \"autocorrect immediately with '$mode'\" - <<-EOT\n+\t\ttest_config help.autocorrect $mode &&\n+\n+\t\tgit worktree lsit 2>actual &&\n+\t\ttest_grep \"you meant 'list'\\.$\" actual\n+\tEOT\n+done\n+\n+test_expect_success 'delay path is executed' - <<-\\EOT\n+\ttest_config help.autocorrect 2 &&\n+\n+\tgit worktree lsit 2>actual &&\n+\ttest_grep '^Continuing in 0.2 seconds, ' actual\n+EOT\n+\n+test_expect_success 'deny if too dissimilar' - <<-\\EOT\n+\ttest_must_fail git remote rensnr 2>actual &&\n+\thead -n1 actual >first && test_grep -f grep_unknown first\n+EOT\n+\n+test_done\n-- \n2.53.0\n\n"},{"id":"542124","messageId":"SY0P300MB0801D8D5DCD61BE70C05395BCE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801AE56F740AD087D22B35ACE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v5 10/10] doc: document autocorrect API","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-22T12:18:10Z","receivedAt":"2026-04-22T12:20:06Z","isPatch":true,"body":"Explain behaviors for autocorrect_resolve(), autocorrect_confirm(), and\nstruct autocorrect.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.h | 11 +++++++++++\n 1 file changed, 11 insertions(+)\n\ndiff --git a/autocorrect.h b/autocorrect.h\nindex 14ee7c4548d3..5bb67cf6debd 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -13,13 +13,24 @@ enum autocorrect_mode {\n \tAUTOCORRECT_DELAY,\n };\n \n+/**\n+ * `mode` indicates which action will be performed by autocorrect_confirm().\n+ * `delay` is the timeout before autocorrect_confirm() returns, in tenths of a\n+ * second. Use it only with AUTOCORRECT_DELAY.\n+ */\n struct autocorrect {\n \tenum autocorrect_mode mode;\n \tint delay;\n };\n \n+/**\n+ * Resolve the autocorrect configuration into `conf`.\n+ */\n void autocorrect_resolve(struct autocorrect *conf);\n \n+/**\n+ * Interact with the user in different ways depending on `conf->mode`.\n+ */\n void autocorrect_confirm(struct autocorrect *conf, const char *assumed);\n \n #endif /* AUTOCORRECT_H */\n-- \n2.53.0\n\n"},{"id":"542164","messageId":"xmqqwlxyo7w6.fsf@gitster.g","threadId":"65168","inReplyTo":"SY0P300MB0801AE56F740AD087D22B35ACE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH v5 00/10] parseopt: add subcommand autocorrection","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-23T00:08:41Z","receivedAt":"2026-04-23T00:08:43Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> Git currently provides auto-correction for builtins and aliases, but\n> lacks this functionality for subcommands parsed via the parse-options\n> API. Subcommands are also commands, and typos will occur, too. Like:\n>\n> \tgit remote add-rul\n>\n> So, this series introduces subcommand auto-correction.\n> ...\n> base-commit: f65aba1e87db64413b6d1ed5ae5a45b5a84a0997\n\nWhen the series is applied on top of Git 2.54, or on top of f65aba1e\n(codeql: bump actions/cache from 4 to 5, 2026-04-13), the result\ndoes not pass its self test, at least for me.\n\nIt seems that the exit status of some error path (subcommand parse\nfailure, presumably) has changed unexpectedly?  As I am mostly\noffline for this and next week, I didn't dig any further.\n\nThanks.\n\n\nTest Summary Report\n-------------------\nt0040-parse-options.sh (Wstat: 256 (exited 1) Tests: 94 Failed: 1)\n  Failed test:  73\n  Non-zero exit status: 1\nt7900-maintenance.sh  (Wstat: 256 (exited 1) Tests: 72 Failed: 1)\n  Failed test:  1\n  Non-zero exit status: 1\n\n"},{"id":"542165","messageId":"SY0P300MB08012E039329CC8EF7C3EB92CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"xmqqwlxyo7w6.fsf@gitster.g","subject":"Re: [PATCH v5 00/10] parseopt: add subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-23T01:14:13Z","receivedAt":"2026-04-23T01:14:25Z","isPatch":true,"body":"On Wed, Apr 22, 2026 at 05:08:41PM -0700, Junio C Hamano wrote:\n> It seems that the exit status of some error path (subcommand parse\n> failure, presumably) has changed unexpectedly?  As I am mostly\n> offline for this and next week, I didn't dig any further.\n\nMy fault, forgot to adjust these tests. Since subcommand parser learned\nto autocorrect misspelled names,\n\n> t0040-parse-options.sh (Wstat: 256 (exited 1) Tests: 94 Failed: 1)\n\n\ttest-tool parse-subcommand cmd subcmd-o\n\nin \"subcommand - subcommands cannot be abbreviated\" test now shows\nsimilar subcommands.\n\n> t7900-maintenance.sh  (Wstat: 256 (exited 1) Tests: 72 Failed: 1)\n\n\tgit maintenance barf\n\nin \"help text\" test now corrects \"barf\" to \"start\".\n\nAfter fixing these two, all tests pass.\n\n----\nAll tests successful.\nFiles=1043, Tests=33202, 465 wallclock secs\n( 7.02 usr  2.23 sys + 1244.78 cusr 1642.22 csys = 2896.25 CPU)\nResult: PASS\n\nAlso, I found most error paths exit with 129 in parse-options.c, but\nthe similar subcommand hint exits with 1. That's inconsistent, will\nchange the exit code to 129.\n\n-- \nJiamu Sun <39@barroit.sh>\n          <sunjiamu@outlook.com>\n"},{"id":"542166","messageId":"SY0P300MB0801677A2A1E0FD38D06A841CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801AE56F740AD087D22B35ACE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v6 00/10] parseopt: add subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-23T01:37:50Z","receivedAt":"2026-04-23T01:38:16Z","isPatch":true,"body":"Git currently provides auto-correction for builtins and aliases, but\nlacks this functionality for subcommands parsed via the parse-options\nAPI. Subcommands are also commands, and typos will occur, too. Like:\n\n\tgit remote add-rul\n\nSo, this series introduces subcommand auto-correction.\n\nBy default, this implementation enables autocorrection for builtins\nwith mandatory subcommands. However, for those using\nPARSE_OPT_SUBCOMMAND_OPTIONAL, autocorrection is skipped to avoid\nmisinterpreting legitimate unknown arguments as mistyped subcommands.\n\nTo allow builtins with optional subcommands to explicitly opt in,\nthis series adds the PARSE_OPT_SUBCOMMAND_AUTOCORRECT flag, and enables\nit for git-remote and git-notes.\n\nAdditionally, the existing autocorrection logic is extracted from\nhelp.c so subcommand handling can reuse the same config parsing and\nprompt/delay logic.\n\nSome string literals are also combined so the full text is easier to\ngrep for.\n\nChanges in v6:\n  - Adjust existing tests to fit subcommand autocorrection behavior\n  - Change the similar subcommand hint exit code to 129\n\nChanges in v5:\n  - Make subcommand autocorrection behave the same as command\n    autocorrection\n  - Rename PARSE_OPT_SUBCOMMAND_AUTOCORR to\n    PARSE_OPT_SUBCOMMAND_AUTOCORRECT\n  - Adjust subcommand autocorrection tests to fit new behavior\n\nChanges in v4:\n  - Add missing files to Meson build\n  - Change API prefix from autocorr to autocorrect\n  - Split the commit that moves tty code\n  - Add API documentation\n  - Use standard Damerau-Levenshtein distance and common practice\n    fuzziness thresholds\n  - Rename AUTOCORRECT_HINTONLY to AUTOCORRECT_HINT\n  - Change commit subject prefix for tests from \"help:\" to \"parseopt:\"\n  - Fix coding style issues\n\nChanges in v3:\n  - Align with the coding guildline\n  - Split patch so diffs don't get hidden by code movement\n  - Improve commit messages\n\nChanges in v2:\n  - Reword the explanation of default autocorrection behavior\n\nJiamu Sun (10):\n  parseopt: extract subcommand handling from parse_options_step()\n  help: make autocorrect handling reusable\n  help: move tty check for autocorrection to autocorrect.c\n  autocorrect: use mode and delay instead of magic numbers\n  autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINT\n  autocorrect: provide config resolution API\n  parseopt: autocorrect mistyped subcommands\n  parseopt: enable subcommand autocorrection for git-remote and\n    git-notes\n  parseopt: add tests for subcommand autocorrection\n  doc: document autocorrect API\n\n Makefile                          |   1 +\n autocorrect.c                     |  89 +++++++++++++++\n autocorrect.h                     |  36 ++++++\n builtin/notes.c                   |  10 +-\n builtin/remote.c                  |  12 +-\n help.c                            | 120 ++++----------------\n meson.build                       |   1 +\n parse-options.c                   | 183 +++++++++++++++++++++++-------\n parse-options.h                   |   1 +\n t/meson.build                     |   1 +\n t/t0040-parse-options.sh          |   5 +-\n t/t7900-maintenance.sh            |   4 +-\n t/t9004-autocorrect-subcommand.sh |  58 ++++++++++\n 13 files changed, 366 insertions(+), 155 deletions(-)\n create mode 100644 autocorrect.c\n create mode 100644 autocorrect.h\n create mode 100755 t/t9004-autocorrect-subcommand.sh\n\n\nbase-commit: f65aba1e87db64413b6d1ed5ae5a45b5a84a0997\n-- \n2.54.0\n\n"},{"id":"542167","messageId":"SY0P300MB08016A54D415C87EED8EEEA8CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801677A2A1E0FD38D06A841CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v6 01/10] parseopt: extract subcommand handling from parse_options_step()","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-23T01:37:51Z","receivedAt":"2026-04-23T01:38:18Z","isPatch":true,"body":"Move the subcommand branch out of parse_options_step() into a new\nhandle_subcommand() helper. Also, make parse_subcommand() return a\nsimple success/failure status.\n\nThis removes the switch over impossible parse_opt_result values and\nmakes the non-option path easier to follow and maintain.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n parse-options.c | 87 ++++++++++++++++++++++++++-----------------------\n 1 file changed, 46 insertions(+), 41 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex a676da86f5d6..803ce2ba4443 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -606,17 +606,44 @@ static enum parse_opt_result parse_nodash_opt(struct parse_opt_ctx_t *p,\n \treturn PARSE_OPT_ERROR;\n }\n \n-static enum parse_opt_result parse_subcommand(const char *arg,\n-\t\t\t\t\t      const struct option *options)\n+static int parse_subcommand(const char *arg, const struct option *options)\n {\n-\tfor (; options->type != OPTION_END; options++)\n-\t\tif (options->type == OPTION_SUBCOMMAND &&\n-\t\t    !strcmp(options->long_name, arg)) {\n-\t\t\t*(parse_opt_subcommand_fn **)options->value = options->subcommand_fn;\n-\t\t\treturn PARSE_OPT_SUBCOMMAND;\n-\t\t}\n+\tfor (; options->type != OPTION_END; options++) {\n+\t\tparse_opt_subcommand_fn **opt_val;\n \n-\treturn PARSE_OPT_UNKNOWN;\n+\t\tif (options->type != OPTION_SUBCOMMAND ||\n+\t\t    strcmp(options->long_name, arg))\n+\t\t\tcontinue;\n+\n+\t\topt_val = options->value;\n+\t\t*opt_val = options->subcommand_fn;\n+\t\treturn 0;\n+\t}\n+\n+\treturn -1;\n+}\n+\n+static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n+\t\t\t\t\t       const char *arg,\n+\t\t\t\t\t       const struct option *options,\n+\t\t\t\t\t       const char * const usagestr[])\n+{\n+\tint err = parse_subcommand(arg, options);\n+\n+\tif (!err)\n+\t\treturn PARSE_OPT_SUBCOMMAND;\n+\n+\t/*\n+\t * arg is neither a short or long option nor a subcommand.  Since this\n+\t * command has a default operation mode, we have to treat this arg and\n+\t * all remaining args as args meant to that default operation mode.\n+\t * So we are done parsing.\n+\t */\n+\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n+\t\treturn PARSE_OPT_DONE;\n+\n+\terror(_(\"unknown subcommand: `%s'\"), arg);\n+\tusage_with_options(usagestr, options);\n }\n \n static void check_typos(const char *arg, const struct option *options)\n@@ -1011,38 +1038,16 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\tif (*arg != '-' || !arg[1]) {\n \t\t\tif (parse_nodash_opt(ctx, arg, options) == 0)\n \t\t\t\tcontinue;\n-\t\t\tif (!ctx->has_subcommands) {\n-\t\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n-\t\t\t\t\treturn PARSE_OPT_NON_OPTION;\n-\t\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n-\t\t\t\tcontinue;\n-\t\t\t}\n-\t\t\tswitch (parse_subcommand(arg, options)) {\n-\t\t\tcase PARSE_OPT_SUBCOMMAND:\n-\t\t\t\treturn PARSE_OPT_SUBCOMMAND;\n-\t\t\tcase PARSE_OPT_UNKNOWN:\n-\t\t\t\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n-\t\t\t\t\t/*\n-\t\t\t\t\t * arg is neither a short or long\n-\t\t\t\t\t * option nor a subcommand.  Since\n-\t\t\t\t\t * this command has a default\n-\t\t\t\t\t * operation mode, we have to treat\n-\t\t\t\t\t * this arg and all remaining args\n-\t\t\t\t\t * as args meant to that default\n-\t\t\t\t\t * operation mode.\n-\t\t\t\t\t * So we are done parsing.\n-\t\t\t\t\t */\n-\t\t\t\t\treturn PARSE_OPT_DONE;\n-\t\t\t\terror(_(\"unknown subcommand: `%s'\"), arg);\n-\t\t\t\tusage_with_options(usagestr, options);\n-\t\t\tcase PARSE_OPT_COMPLETE:\n-\t\t\tcase PARSE_OPT_HELP:\n-\t\t\tcase PARSE_OPT_ERROR:\n-\t\t\tcase PARSE_OPT_DONE:\n-\t\t\tcase PARSE_OPT_NON_OPTION:\n-\t\t\t\t/* Impossible. */\n-\t\t\t\tBUG(\"parse_subcommand() cannot return these\");\n-\t\t\t}\n+\n+\t\t\tif (ctx->has_subcommands)\n+\t\t\t\treturn handle_subcommand(ctx, arg, options,\n+\t\t\t\t\t\t\t usagestr);\n+\n+\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n+\t\t\t\treturn PARSE_OPT_NON_OPTION;\n+\n+\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n+\t\t\tcontinue;\n \t\t}\n \n \t\t/* lone -h asks for help */\n-- \n2.54.0\n\n"},{"id":"542168","messageId":"SY0P300MB080119BC1519FDE5DCD62088CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801677A2A1E0FD38D06A841CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v6 02/10] help: make autocorrect handling reusable","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-23T01:37:52Z","receivedAt":"2026-04-23T01:38:22Z","isPatch":true,"body":"Move config parsing and prompt/delay handling into autocorrect.c and\nexpose them in autocorrect.h. This makes autocorrect reusable regardless\nof which target links against it.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n Makefile      |  1 +\n autocorrect.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++\n autocorrect.h | 16 ++++++++++++\n help.c        | 64 +++------------------------------------------\n meson.build   |  1 +\n 5 files changed, 94 insertions(+), 60 deletions(-)\n create mode 100644 autocorrect.c\n create mode 100644 autocorrect.h\n\ndiff --git a/Makefile b/Makefile\nindex cedc234173e3..ced8a9b5abb4 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1100,6 +1100,7 @@ LIB_OBJS += archive-tar.o\n LIB_OBJS += archive-zip.o\n LIB_OBJS += archive.o\n LIB_OBJS += attr.o\n+LIB_OBJS += autocorrect.o\n LIB_OBJS += base85.o\n LIB_OBJS += bisect.o\n LIB_OBJS += blame.o\ndiff --git a/autocorrect.c b/autocorrect.c\nnew file mode 100644\nindex 000000000000..97145d3a53ce\n--- /dev/null\n+++ b/autocorrect.c\n@@ -0,0 +1,72 @@\n+#include \"git-compat-util.h\"\n+#include \"autocorrect.h\"\n+#include \"config.h\"\n+#include \"parse.h\"\n+#include \"strbuf.h\"\n+#include \"prompt.h\"\n+#include \"gettext.h\"\n+\n+static int parse_autocorrect(const char *value)\n+{\n+\tswitch (git_parse_maybe_bool_text(value)) {\n+\tcase 1:\n+\t\treturn AUTOCORRECT_IMMEDIATELY;\n+\tcase 0:\n+\t\treturn AUTOCORRECT_SHOW;\n+\tdefault: /* other random text */\n+\t\tbreak;\n+\t}\n+\n+\tif (!strcmp(value, \"prompt\"))\n+\t\treturn AUTOCORRECT_PROMPT;\n+\tif (!strcmp(value, \"never\"))\n+\t\treturn AUTOCORRECT_NEVER;\n+\tif (!strcmp(value, \"immediate\"))\n+\t\treturn AUTOCORRECT_IMMEDIATELY;\n+\tif (!strcmp(value, \"show\"))\n+\t\treturn AUTOCORRECT_SHOW;\n+\n+\treturn 0;\n+}\n+\n+void autocorrect_resolve_config(const char *var, const char *value,\n+\t\t\t\tconst struct config_context *ctx, void *data)\n+{\n+\tint *out = data;\n+\n+\tif (!strcmp(var, \"help.autocorrect\")) {\n+\t\tint v = parse_autocorrect(value);\n+\n+\t\tif (!v) {\n+\t\t\tv = git_config_int(var, value, ctx->kvi);\n+\t\t\tif (v < 0 || v == 1)\n+\t\t\t\tv = AUTOCORRECT_IMMEDIATELY;\n+\t\t}\n+\n+\t\t*out = v;\n+\t}\n+}\n+\n+void autocorrect_confirm(int autocorrect, const char *assumed)\n+{\n+\tif (autocorrect == AUTOCORRECT_IMMEDIATELY) {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"Continuing under the assumption that you meant '%s'.\"),\n+\t\t\t   assumed);\n+\t} else if (autocorrect == AUTOCORRECT_PROMPT) {\n+\t\tchar *answer;\n+\t\tstruct strbuf msg = STRBUF_INIT;\n+\n+\t\tstrbuf_addf(&msg, _(\"Run '%s' instead [y/N]? \"), assumed);\n+\t\tanswer = git_prompt(msg.buf, PROMPT_ECHO);\n+\t\tstrbuf_release(&msg);\n+\n+\t\tif (!(starts_with(answer, \"y\") || starts_with(answer, \"Y\")))\n+\t\t\texit(1);\n+\t} else {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"Continuing in %0.1f seconds, assuming that you meant '%s'.\"),\n+\t\t\t   (float)autocorrect / 10.0, assumed);\n+\t\tsleep_millisec(autocorrect * 100);\n+\t}\n+}\ndiff --git a/autocorrect.h b/autocorrect.h\nnew file mode 100644\nindex 000000000000..f5fadf9d9605\n--- /dev/null\n+++ b/autocorrect.h\n@@ -0,0 +1,16 @@\n+#ifndef AUTOCORRECT_H\n+#define AUTOCORRECT_H\n+\n+#define AUTOCORRECT_SHOW (-4)\n+#define AUTOCORRECT_PROMPT (-3)\n+#define AUTOCORRECT_NEVER (-2)\n+#define AUTOCORRECT_IMMEDIATELY (-1)\n+\n+struct config_context;\n+\n+void autocorrect_resolve_config(const char *var, const char *value,\n+\t\t\t\tconst struct config_context *ctx, void *data);\n+\n+void autocorrect_confirm(int autocorrect, const char *assumed);\n+\n+#endif /* AUTOCORRECT_H */\ndiff --git a/help.c b/help.c\nindex 3e59d07c370b..ab619ed43c7a 100644\n--- a/help.c\n+++ b/help.c\n@@ -22,6 +22,7 @@\n #include \"repository.h\"\n #include \"alias.h\"\n #include \"utf8.h\"\n+#include \"autocorrect.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -541,34 +542,6 @@ struct help_unknown_cmd_config {\n \tstruct cmdnames aliases;\n };\n \n-#define AUTOCORRECT_SHOW (-4)\n-#define AUTOCORRECT_PROMPT (-3)\n-#define AUTOCORRECT_NEVER (-2)\n-#define AUTOCORRECT_IMMEDIATELY (-1)\n-\n-static int parse_autocorrect(const char *value)\n-{\n-\tswitch (git_parse_maybe_bool_text(value)) {\n-\t\tcase 1:\n-\t\t\treturn AUTOCORRECT_IMMEDIATELY;\n-\t\tcase 0:\n-\t\t\treturn AUTOCORRECT_SHOW;\n-\t\tdefault: /* other random text */\n-\t\t\tbreak;\n-\t}\n-\n-\tif (!strcmp(value, \"prompt\"))\n-\t\treturn AUTOCORRECT_PROMPT;\n-\tif (!strcmp(value, \"never\"))\n-\t\treturn AUTOCORRECT_NEVER;\n-\tif (!strcmp(value, \"immediate\"))\n-\t\treturn AUTOCORRECT_IMMEDIATELY;\n-\tif (!strcmp(value, \"show\"))\n-\t\treturn AUTOCORRECT_SHOW;\n-\n-\treturn 0;\n-}\n-\n static int git_unknown_cmd_config(const char *var, const char *value,\n \t\t\t\t  const struct config_context *ctx,\n \t\t\t\t  void *cb)\n@@ -577,17 +550,7 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \tconst char *subsection, *key;\n \tsize_t subsection_len;\n \n-\tif (!strcmp(var, \"help.autocorrect\")) {\n-\t\tint v = parse_autocorrect(value);\n-\n-\t\tif (!v) {\n-\t\t\tv = git_config_int(var, value, ctx->kvi);\n-\t\t\tif (v < 0 || v == 1)\n-\t\t\t\tv = AUTOCORRECT_IMMEDIATELY;\n-\t\t}\n-\n-\t\tcfg->autocorrect = v;\n-\t}\n+\tautocorrect_resolve_config(var, value, ctx, &cfg->autocorrect);\n \n \t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n@@ -724,27 +687,8 @@ char *help_unknown_cmd(const char *cmd)\n \t\t\t   _(\"WARNING: You called a Git command named '%s', \"\n \t\t\t     \"which does not exist.\"),\n \t\t\t   cmd);\n-\t\tif (cfg.autocorrect == AUTOCORRECT_IMMEDIATELY)\n-\t\t\tfprintf_ln(stderr,\n-\t\t\t\t   _(\"Continuing under the assumption that \"\n-\t\t\t\t     \"you meant '%s'.\"),\n-\t\t\t\t   assumed);\n-\t\telse if (cfg.autocorrect == AUTOCORRECT_PROMPT) {\n-\t\t\tchar *answer;\n-\t\t\tstruct strbuf msg = STRBUF_INIT;\n-\t\t\tstrbuf_addf(&msg, _(\"Run '%s' instead [y/N]? \"), assumed);\n-\t\t\tanswer = git_prompt(msg.buf, PROMPT_ECHO);\n-\t\t\tstrbuf_release(&msg);\n-\t\t\tif (!(starts_with(answer, \"y\") ||\n-\t\t\t      starts_with(answer, \"Y\")))\n-\t\t\t\texit(1);\n-\t\t} else {\n-\t\t\tfprintf_ln(stderr,\n-\t\t\t\t   _(\"Continuing in %0.1f seconds, \"\n-\t\t\t\t     \"assuming that you meant '%s'.\"),\n-\t\t\t\t   (float)cfg.autocorrect/10.0, assumed);\n-\t\t\tsleep_millisec(cfg.autocorrect * 100);\n-\t\t}\n+\n+\t\tautocorrect_confirm(cfg.autocorrect, assumed);\n \n \t\tcmdnames_release(&cfg.aliases);\n \t\tcmdnames_release(&main_cmds);\ndiff --git a/meson.build b/meson.build\nindex 11488623bfd8..be20d507826d 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -290,6 +290,7 @@ libgit_sources = [\n   'archive-zip.c',\n   'archive.c',\n   'attr.c',\n+  'autocorrect.c',\n   'base85.c',\n   'bisect.c',\n   'blame.c',\n-- \n2.54.0\n\n"},{"id":"542169","messageId":"SY0P300MB0801870630EC7F15000596D7CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801677A2A1E0FD38D06A841CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v6 03/10] help: move tty check for autocorrection to autocorrect.c","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-23T01:37:53Z","receivedAt":"2026-04-23T01:38:24Z","isPatch":true,"body":"TTY checking is the autocorrect config parser's responsibility. It must\nensure the parsed value is correct and reliable. Thus, move the check to\nautocorrect_resolve_config().\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.c | 24 ++++++++++++++++--------\n help.c        |  6 ------\n 2 files changed, 16 insertions(+), 14 deletions(-)\n\ndiff --git a/autocorrect.c b/autocorrect.c\nindex 97145d3a53ce..887d2396da44 100644\n--- a/autocorrect.c\n+++ b/autocorrect.c\n@@ -33,18 +33,26 @@ void autocorrect_resolve_config(const char *var, const char *value,\n \t\t\t\tconst struct config_context *ctx, void *data)\n {\n \tint *out = data;\n+\tint parsed;\n \n-\tif (!strcmp(var, \"help.autocorrect\")) {\n-\t\tint v = parse_autocorrect(value);\n+\tif (strcmp(var, \"help.autocorrect\"))\n+\t\treturn;\n \n-\t\tif (!v) {\n-\t\t\tv = git_config_int(var, value, ctx->kvi);\n-\t\t\tif (v < 0 || v == 1)\n-\t\t\t\tv = AUTOCORRECT_IMMEDIATELY;\n-\t\t}\n+\tparsed = parse_autocorrect(value);\n \n-\t\t*out = v;\n+\t/*\n+\t * Disable autocorrection prompt in a non-interactive session\n+\t */\n+\tif (parsed == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))\n+\t\tparsed = AUTOCORRECT_NEVER;\n+\n+\tif (!parsed) {\n+\t\tparsed = git_config_int(var, value, ctx->kvi);\n+\t\tif (parsed < 0 || parsed == 1)\n+\t\t\tparsed = AUTOCORRECT_IMMEDIATELY;\n \t}\n+\n+\t*out = parsed;\n }\n \n void autocorrect_confirm(int autocorrect, const char *assumed)\ndiff --git a/help.c b/help.c\nindex ab619ed43c7a..d2b29715817e 100644\n--- a/help.c\n+++ b/help.c\n@@ -607,12 +607,6 @@ char *help_unknown_cmd(const char *cmd)\n \n \tread_early_config(the_repository, git_unknown_cmd_config, &cfg);\n \n-\t/*\n-\t * Disable autocorrection prompt in a non-interactive session\n-\t */\n-\tif ((cfg.autocorrect == AUTOCORRECT_PROMPT) && (!isatty(0) || !isatty(2)))\n-\t\tcfg.autocorrect = AUTOCORRECT_NEVER;\n-\n \tif (cfg.autocorrect == AUTOCORRECT_NEVER) {\n \t\tfprintf_ln(stderr, _(\"git: '%s' is not a git command. See 'git --help'.\"), cmd);\n \t\texit(1);\n-- \n2.54.0\n\n"},{"id":"542170","messageId":"SY0P300MB0801FD1292D97ACCAB153374CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801677A2A1E0FD38D06A841CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v6 04/10] autocorrect: use mode and delay instead of magic numbers","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-23T01:37:54Z","receivedAt":"2026-04-23T01:38:29Z","isPatch":true,"body":"Drop magic numbers and describe autocorrect config with a mode enum and\nan integer delay. This reduces errors when mutating config values and\nmakes the values easier to access.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.c | 46 +++++++++++++++++++++++-----------------------\n autocorrect.h | 20 ++++++++++++++------\n help.c        |  9 +++++----\n 3 files changed, 42 insertions(+), 33 deletions(-)\n\ndiff --git a/autocorrect.c b/autocorrect.c\nindex 887d2396da44..2484546fc731 100644\n--- a/autocorrect.c\n+++ b/autocorrect.c\n@@ -6,7 +6,7 @@\n #include \"prompt.h\"\n #include \"gettext.h\"\n \n-static int parse_autocorrect(const char *value)\n+static enum autocorrect_mode parse_autocorrect(const char *value)\n {\n \tswitch (git_parse_maybe_bool_text(value)) {\n \tcase 1:\n@@ -19,49 +19,49 @@ static int parse_autocorrect(const char *value)\n \n \tif (!strcmp(value, \"prompt\"))\n \t\treturn AUTOCORRECT_PROMPT;\n-\tif (!strcmp(value, \"never\"))\n+\telse if (!strcmp(value, \"never\"))\n \t\treturn AUTOCORRECT_NEVER;\n-\tif (!strcmp(value, \"immediate\"))\n+\telse if (!strcmp(value, \"immediate\"))\n \t\treturn AUTOCORRECT_IMMEDIATELY;\n-\tif (!strcmp(value, \"show\"))\n+\telse if (!strcmp(value, \"show\"))\n \t\treturn AUTOCORRECT_SHOW;\n-\n-\treturn 0;\n+\telse\n+\t\treturn AUTOCORRECT_DELAY;\n }\n \n void autocorrect_resolve_config(const char *var, const char *value,\n \t\t\t\tconst struct config_context *ctx, void *data)\n {\n-\tint *out = data;\n-\tint parsed;\n+\tstruct autocorrect *conf = data;\n \n \tif (strcmp(var, \"help.autocorrect\"))\n \t\treturn;\n \n-\tparsed = parse_autocorrect(value);\n+\tconf->mode = parse_autocorrect(value);\n \n \t/*\n \t * Disable autocorrection prompt in a non-interactive session\n \t */\n-\tif (parsed == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))\n-\t\tparsed = AUTOCORRECT_NEVER;\n+\tif (conf->mode == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))\n+\t\tconf->mode = AUTOCORRECT_NEVER;\n \n-\tif (!parsed) {\n-\t\tparsed = git_config_int(var, value, ctx->kvi);\n-\t\tif (parsed < 0 || parsed == 1)\n-\t\t\tparsed = AUTOCORRECT_IMMEDIATELY;\n-\t}\n+\tif (conf->mode == AUTOCORRECT_DELAY) {\n+\t\tconf->delay = git_config_int(var, value, ctx->kvi);\n \n-\t*out = parsed;\n+\t\tif (!conf->delay)\n+\t\t\tconf->mode = AUTOCORRECT_SHOW;\n+\t\telse if (conf->delay < 0 || conf->delay == 1)\n+\t\t\tconf->mode = AUTOCORRECT_IMMEDIATELY;\n+\t}\n }\n \n-void autocorrect_confirm(int autocorrect, const char *assumed)\n+void autocorrect_confirm(struct autocorrect *conf, const char *assumed)\n {\n-\tif (autocorrect == AUTOCORRECT_IMMEDIATELY) {\n+\tif (conf->mode == AUTOCORRECT_IMMEDIATELY) {\n \t\tfprintf_ln(stderr,\n \t\t\t   _(\"Continuing under the assumption that you meant '%s'.\"),\n \t\t\t   assumed);\n-\t} else if (autocorrect == AUTOCORRECT_PROMPT) {\n+\t} else if (conf->mode == AUTOCORRECT_PROMPT) {\n \t\tchar *answer;\n \t\tstruct strbuf msg = STRBUF_INIT;\n \n@@ -71,10 +71,10 @@ void autocorrect_confirm(int autocorrect, const char *assumed)\n \n \t\tif (!(starts_with(answer, \"y\") || starts_with(answer, \"Y\")))\n \t\t\texit(1);\n-\t} else {\n+\t} else if (conf->mode == AUTOCORRECT_DELAY) {\n \t\tfprintf_ln(stderr,\n \t\t\t   _(\"Continuing in %0.1f seconds, assuming that you meant '%s'.\"),\n-\t\t\t   (float)autocorrect / 10.0, assumed);\n-\t\tsleep_millisec(autocorrect * 100);\n+\t\t\t   conf->delay / 10.0, assumed);\n+\t\tsleep_millisec(conf->delay * 100);\n \t}\n }\ndiff --git a/autocorrect.h b/autocorrect.h\nindex f5fadf9d9605..5506a36f11a7 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -1,16 +1,24 @@\n #ifndef AUTOCORRECT_H\n #define AUTOCORRECT_H\n \n-#define AUTOCORRECT_SHOW (-4)\n-#define AUTOCORRECT_PROMPT (-3)\n-#define AUTOCORRECT_NEVER (-2)\n-#define AUTOCORRECT_IMMEDIATELY (-1)\n-\n struct config_context;\n \n+enum autocorrect_mode {\n+\tAUTOCORRECT_SHOW,\n+\tAUTOCORRECT_NEVER,\n+\tAUTOCORRECT_PROMPT,\n+\tAUTOCORRECT_IMMEDIATELY,\n+\tAUTOCORRECT_DELAY,\n+};\n+\n+struct autocorrect {\n+\tenum autocorrect_mode mode;\n+\tint delay;\n+};\n+\n void autocorrect_resolve_config(const char *var, const char *value,\n \t\t\t\tconst struct config_context *ctx, void *data);\n \n-void autocorrect_confirm(int autocorrect, const char *assumed);\n+void autocorrect_confirm(struct autocorrect *conf, const char *assumed);\n \n #endif /* AUTOCORRECT_H */\ndiff --git a/help.c b/help.c\nindex d2b29715817e..353596c17d82 100644\n--- a/help.c\n+++ b/help.c\n@@ -538,7 +538,7 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)\n }\n \n struct help_unknown_cmd_config {\n-\tint autocorrect;\n+\tstruct autocorrect autocorrect;\n \tstruct cmdnames aliases;\n };\n \n@@ -607,7 +607,7 @@ char *help_unknown_cmd(const char *cmd)\n \n \tread_early_config(the_repository, git_unknown_cmd_config, &cfg);\n \n-\tif (cfg.autocorrect == AUTOCORRECT_NEVER) {\n+\tif (cfg.autocorrect.mode == AUTOCORRECT_NEVER) {\n \t\tfprintf_ln(stderr, _(\"git: '%s' is not a git command. See 'git --help'.\"), cmd);\n \t\texit(1);\n \t}\n@@ -673,7 +673,8 @@ char *help_unknown_cmd(const char *cmd)\n \t\t     n++)\n \t\t\t; /* still counting */\n \t}\n-\tif (cfg.autocorrect && cfg.autocorrect != AUTOCORRECT_SHOW && n == 1 &&\n+\n+\tif (cfg.autocorrect.mode != AUTOCORRECT_SHOW && n == 1 &&\n \t    SIMILAR_ENOUGH(best_similarity)) {\n \t\tchar *assumed = xstrdup(main_cmds.names[0]->name);\n \n@@ -682,7 +683,7 @@ char *help_unknown_cmd(const char *cmd)\n \t\t\t     \"which does not exist.\"),\n \t\t\t   cmd);\n \n-\t\tautocorrect_confirm(cfg.autocorrect, assumed);\n+\t\tautocorrect_confirm(&cfg.autocorrect, assumed);\n \n \t\tcmdnames_release(&cfg.aliases);\n \t\tcmdnames_release(&main_cmds);\n-- \n2.54.0\n\n"},{"id":"542171","messageId":"SY0P300MB0801C8300663FF8204A2AFA3CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801677A2A1E0FD38D06A841CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v6 06/10] autocorrect: provide config resolution API","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-23T01:37:56Z","receivedAt":"2026-04-23T01:38:32Z","isPatch":true,"body":"Add autocorrect_resolve(). This resolves and populates the correct\nvalues for autocorrect config.\n\nMake autocorrect config callback internal. The API is meant to provide\na high-level way to retrieve the config. Allowing access to the config\ncallback from outside violates that intent.\n\nAdditionally, in some cases, without access to the config callback, two\nconfig iterations cannot be merged into one, which can hurt performance.\nThis is fine, as the code path that calls autocorrect_resolve() is cold.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.c | 15 ++++++++++++---\n autocorrect.h |  5 +----\n help.c        | 40 +++++++++++++++++-----------------------\n 3 files changed, 30 insertions(+), 30 deletions(-)\n\ndiff --git a/autocorrect.c b/autocorrect.c\nindex de0fa282c934..b2ee9f51e8c0 100644\n--- a/autocorrect.c\n+++ b/autocorrect.c\n@@ -1,3 +1,5 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n #include \"git-compat-util.h\"\n #include \"autocorrect.h\"\n #include \"config.h\"\n@@ -29,13 +31,13 @@ static enum autocorrect_mode parse_autocorrect(const char *value)\n \t\treturn AUTOCORRECT_DELAY;\n }\n \n-void autocorrect_resolve_config(const char *var, const char *value,\n-\t\t\t\tconst struct config_context *ctx, void *data)\n+static int resolve_autocorrect(const char *var, const char *value,\n+\t\t\t       const struct config_context *ctx, void *data)\n {\n \tstruct autocorrect *conf = data;\n \n \tif (strcmp(var, \"help.autocorrect\"))\n-\t\treturn;\n+\t\treturn 0;\n \n \tconf->mode = parse_autocorrect(value);\n \n@@ -53,6 +55,13 @@ void autocorrect_resolve_config(const char *var, const char *value,\n \t\telse if (conf->delay < 0 || conf->delay == 1)\n \t\t\tconf->mode = AUTOCORRECT_IMMEDIATELY;\n \t}\n+\n+\treturn 0;\n+}\n+\n+void autocorrect_resolve(struct autocorrect *conf)\n+{\n+\tread_early_config(the_repository, resolve_autocorrect, conf);\n }\n \n void autocorrect_confirm(struct autocorrect *conf, const char *assumed)\ndiff --git a/autocorrect.h b/autocorrect.h\nindex 328807242c15..0d3e819262ed 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -1,8 +1,6 @@\n #ifndef AUTOCORRECT_H\n #define AUTOCORRECT_H\n \n-struct config_context;\n-\n enum autocorrect_mode {\n \tAUTOCORRECT_HINT,\n \tAUTOCORRECT_NEVER,\n@@ -16,8 +14,7 @@ struct autocorrect {\n \tint delay;\n };\n \n-void autocorrect_resolve_config(const char *var, const char *value,\n-\t\t\t\tconst struct config_context *ctx, void *data);\n+void autocorrect_resolve(struct autocorrect *conf);\n \n void autocorrect_confirm(struct autocorrect *conf, const char *assumed);\n \ndiff --git a/help.c b/help.c\nindex c7dab8395ee2..0fff43545cc1 100644\n--- a/help.c\n+++ b/help.c\n@@ -537,32 +537,23 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)\n \treturn 0;\n }\n \n-struct help_unknown_cmd_config {\n-\tstruct autocorrect autocorrect;\n-\tstruct cmdnames aliases;\n-};\n-\n-static int git_unknown_cmd_config(const char *var, const char *value,\n-\t\t\t\t  const struct config_context *ctx,\n-\t\t\t\t  void *cb)\n+static int resolve_aliases(const char *var, const char *value UNUSED,\n+\t\t\t   const struct config_context *ctx UNUSED, void *data)\n {\n-\tstruct help_unknown_cmd_config *cfg = cb;\n+\tstruct cmdnames *aliases = data;\n \tconst char *subsection, *key;\n \tsize_t subsection_len;\n \n-\tautocorrect_resolve_config(var, value, ctx, &cfg->autocorrect);\n-\n-\t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n \t\t\t      &key)) {\n \t\tif (subsection) {\n \t\t\t/* [alias \"name\"] command = value */\n \t\t\tif (!strcmp(key, \"command\"))\n-\t\t\t\tadd_cmdname(&cfg->aliases, subsection,\n+\t\t\t\tadd_cmdname(aliases, subsection,\n \t\t\t\t\t    subsection_len);\n \t\t} else {\n \t\t\t/* alias.name = value */\n-\t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t\t\tadd_cmdname(aliases, key, strlen(key));\n \t\t}\n \t}\n \n@@ -599,22 +590,26 @@ static const char bad_interpreter_advice[] =\n \n char *help_unknown_cmd(const char *cmd)\n {\n-\tstruct help_unknown_cmd_config cfg = { 0 };\n+\tstruct cmdnames aliases = { 0 };\n+\tstruct autocorrect autocorrect = { 0 };\n \tint i, n, best_similarity = 0;\n \tstruct cmdnames main_cmds = { 0 };\n \tstruct cmdnames other_cmds = { 0 };\n \tstruct cmdname_help *common_cmds;\n \n-\tread_early_config(the_repository, git_unknown_cmd_config, &cfg);\n+\tautocorrect_resolve(&autocorrect);\n \n-\tif (cfg.autocorrect.mode == AUTOCORRECT_NEVER) {\n+\tif (autocorrect.mode == AUTOCORRECT_NEVER) {\n \t\tfprintf_ln(stderr, _(\"git: '%s' is not a git command. See 'git --help'.\"), cmd);\n \t\texit(1);\n \t}\n \n \tload_command_list(\"git-\", &main_cmds, &other_cmds);\n \n-\tadd_cmd_list(&main_cmds, &cfg.aliases);\n+\t/* Also use aliases for command lookup */\n+\tread_early_config(the_repository, resolve_aliases, &aliases);\n+\n+\tadd_cmd_list(&main_cmds, &aliases);\n \tadd_cmd_list(&main_cmds, &other_cmds);\n \tQSORT(main_cmds.names, main_cmds.cnt, cmdname_compare);\n \tuniq(&main_cmds);\n@@ -674,18 +669,17 @@ char *help_unknown_cmd(const char *cmd)\n \t\t\t; /* still counting */\n \t}\n \n-\tif (cfg.autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&\n+\tif (autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&\n \t    SIMILAR_ENOUGH(best_similarity)) {\n \t\tchar *assumed = xstrdup(main_cmds.names[0]->name);\n \n \t\tfprintf_ln(stderr,\n-\t\t\t   _(\"WARNING: You called a Git command named '%s', \"\n-\t\t\t     \"which does not exist.\"),\n+\t\t\t   _(\"WARNING: You called a Git command named '%s', which does not exist.\"),\n \t\t\t   cmd);\n \n-\t\tautocorrect_confirm(&cfg.autocorrect, assumed);\n+\t\tautocorrect_confirm(&autocorrect, assumed);\n \n-\t\tcmdnames_release(&cfg.aliases);\n+\t\tcmdnames_release(&aliases);\n \t\tcmdnames_release(&main_cmds);\n \t\tcmdnames_release(&other_cmds);\n \t\treturn assumed;\n-- \n2.54.0\n\n"},{"id":"542172","messageId":"SY0P300MB080173C8CA87881A110E19FBCE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801677A2A1E0FD38D06A841CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v6 10/10] doc: document autocorrect API","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-23T01:38:00Z","receivedAt":"2026-04-23T01:38:39Z","isPatch":true,"body":"Explain behaviors for autocorrect_resolve(), autocorrect_confirm(), and\nstruct autocorrect.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.h | 11 +++++++++++\n 1 file changed, 11 insertions(+)\n\ndiff --git a/autocorrect.h b/autocorrect.h\nindex 14ee7c4548d3..5bb67cf6debd 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -13,13 +13,24 @@ enum autocorrect_mode {\n \tAUTOCORRECT_DELAY,\n };\n \n+/**\n+ * `mode` indicates which action will be performed by autocorrect_confirm().\n+ * `delay` is the timeout before autocorrect_confirm() returns, in tenths of a\n+ * second. Use it only with AUTOCORRECT_DELAY.\n+ */\n struct autocorrect {\n \tenum autocorrect_mode mode;\n \tint delay;\n };\n \n+/**\n+ * Resolve the autocorrect configuration into `conf`.\n+ */\n void autocorrect_resolve(struct autocorrect *conf);\n \n+/**\n+ * Interact with the user in different ways depending on `conf->mode`.\n+ */\n void autocorrect_confirm(struct autocorrect *conf, const char *assumed);\n \n #endif /* AUTOCORRECT_H */\n-- \n2.54.0\n\n"},{"id":"542173","messageId":"SY0P300MB080173E3DC433F8E4D215C7ACE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801677A2A1E0FD38D06A841CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v6 05/10] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINT","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-23T01:37:55Z","receivedAt":"2026-04-23T01:38:40Z","isPatch":true,"body":"AUTOCORRECT_SHOW is ambiguous. Its purpose is to show commands similar\nto the unknown one and take no other action. Rename it to fit the\nsemantics.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.c | 6 +++---\n autocorrect.h | 2 +-\n help.c        | 2 +-\n 3 files changed, 5 insertions(+), 5 deletions(-)\n\ndiff --git a/autocorrect.c b/autocorrect.c\nindex 2484546fc731..de0fa282c934 100644\n--- a/autocorrect.c\n+++ b/autocorrect.c\n@@ -12,7 +12,7 @@ static enum autocorrect_mode parse_autocorrect(const char *value)\n \tcase 1:\n \t\treturn AUTOCORRECT_IMMEDIATELY;\n \tcase 0:\n-\t\treturn AUTOCORRECT_SHOW;\n+\t\treturn AUTOCORRECT_HINT;\n \tdefault: /* other random text */\n \t\tbreak;\n \t}\n@@ -24,7 +24,7 @@ static enum autocorrect_mode parse_autocorrect(const char *value)\n \telse if (!strcmp(value, \"immediate\"))\n \t\treturn AUTOCORRECT_IMMEDIATELY;\n \telse if (!strcmp(value, \"show\"))\n-\t\treturn AUTOCORRECT_SHOW;\n+\t\treturn AUTOCORRECT_HINT;\n \telse\n \t\treturn AUTOCORRECT_DELAY;\n }\n@@ -49,7 +49,7 @@ void autocorrect_resolve_config(const char *var, const char *value,\n \t\tconf->delay = git_config_int(var, value, ctx->kvi);\n \n \t\tif (!conf->delay)\n-\t\t\tconf->mode = AUTOCORRECT_SHOW;\n+\t\t\tconf->mode = AUTOCORRECT_HINT;\n \t\telse if (conf->delay < 0 || conf->delay == 1)\n \t\t\tconf->mode = AUTOCORRECT_IMMEDIATELY;\n \t}\ndiff --git a/autocorrect.h b/autocorrect.h\nindex 5506a36f11a7..328807242c15 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -4,7 +4,7 @@\n struct config_context;\n \n enum autocorrect_mode {\n-\tAUTOCORRECT_SHOW,\n+\tAUTOCORRECT_HINT,\n \tAUTOCORRECT_NEVER,\n \tAUTOCORRECT_PROMPT,\n \tAUTOCORRECT_IMMEDIATELY,\ndiff --git a/help.c b/help.c\nindex 353596c17d82..c7dab8395ee2 100644\n--- a/help.c\n+++ b/help.c\n@@ -674,7 +674,7 @@ char *help_unknown_cmd(const char *cmd)\n \t\t\t; /* still counting */\n \t}\n \n-\tif (cfg.autocorrect.mode != AUTOCORRECT_SHOW && n == 1 &&\n+\tif (cfg.autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&\n \t    SIMILAR_ENOUGH(best_similarity)) {\n \t\tchar *assumed = xstrdup(main_cmds.names[0]->name);\n \n-- \n2.54.0\n\n"},{"id":"542174","messageId":"SY0P300MB0801EB4A5AB42DAC9607F819CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801677A2A1E0FD38D06A841CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v6 07/10] parseopt: autocorrect mistyped subcommands","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-23T01:37:57Z","receivedAt":"2026-04-23T01:38:54Z","isPatch":true,"body":"Try to autocorrect the mistyped mandatory subcommand before showing an\nerror and exiting. Subcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL\nare skipped.\n\nThe subcommand autocorrection behaves the same as the command\nautocorrection.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n autocorrect.h            |   4 ++\n help.c                   |  13 ++---\n parse-options.c          | 102 +++++++++++++++++++++++++++++++++++++--\n t/t0040-parse-options.sh |   5 +-\n t/t7900-maintenance.sh   |   4 +-\n 5 files changed, 112 insertions(+), 16 deletions(-)\n\ndiff --git a/autocorrect.h b/autocorrect.h\nindex 0d3e819262ed..14ee7c4548d3 100644\n--- a/autocorrect.h\n+++ b/autocorrect.h\n@@ -1,6 +1,10 @@\n #ifndef AUTOCORRECT_H\n #define AUTOCORRECT_H\n \n+/* An empirically derived magic number */\n+#define AUTOCORRECT_SIMILARITY_FLOOR 7\n+#define AUTOCORRECT_SIMILAR_ENOUGH(x) ((x) < AUTOCORRECT_SIMILARITY_FLOOR)\n+\n enum autocorrect_mode {\n \tAUTOCORRECT_HINT,\n \tAUTOCORRECT_NEVER,\ndiff --git a/help.c b/help.c\nindex 0fff43545cc1..ecc5673b7243 100644\n--- a/help.c\n+++ b/help.c\n@@ -580,10 +580,6 @@ static void add_cmd_list(struct cmdnames *cmds, struct cmdnames *old)\n \told->cnt = 0;\n }\n \n-/* An empirically derived magic number */\n-#define SIMILARITY_FLOOR 7\n-#define SIMILAR_ENOUGH(x) ((x) < SIMILARITY_FLOOR)\n-\n static const char bad_interpreter_advice[] =\n \tN_(\"'%s' appears to be a git command, but we were not\\n\"\n \t\"able to execute it. Maybe git-%s is broken?\");\n@@ -659,7 +655,7 @@ char *help_unknown_cmd(const char *cmd)\n \n \tif (main_cmds.cnt <= n) {\n \t\t/* prefix matches with everything? that is too ambiguous */\n-\t\tbest_similarity = SIMILARITY_FLOOR + 1;\n+\t\tbest_similarity = AUTOCORRECT_SIMILARITY_FLOOR + 1;\n \t} else {\n \t\t/* count all the most similar ones */\n \t\tfor (best_similarity = main_cmds.names[n++]->len;\n@@ -670,7 +666,7 @@ char *help_unknown_cmd(const char *cmd)\n \t}\n \n \tif (autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&\n-\t    SIMILAR_ENOUGH(best_similarity)) {\n+\t    AUTOCORRECT_SIMILAR_ENOUGH(best_similarity)) {\n \t\tchar *assumed = xstrdup(main_cmds.names[0]->name);\n \n \t\tfprintf_ln(stderr,\n@@ -687,11 +683,10 @@ char *help_unknown_cmd(const char *cmd)\n \n \tfprintf_ln(stderr, _(\"git: '%s' is not a git command. See 'git --help'.\"), cmd);\n \n-\tif (SIMILAR_ENOUGH(best_similarity)) {\n+\tif (AUTOCORRECT_SIMILAR_ENOUGH(best_similarity)) {\n \t\tfprintf_ln(stderr,\n \t\t\t   Q_(\"\\nThe most similar command is\",\n-\t\t\t      \"\\nThe most similar commands are\",\n-\t\t\t   n));\n+\t\t\t      \"\\nThe most similar commands are\", n));\n \n \t\tfor (i = 0; i < n; i++)\n \t\t\tfprintf(stderr, \"\\t%s\\n\", main_cmds.names[i]->name);\ndiff --git a/parse-options.c b/parse-options.c\nindex 803ce2ba4443..4370d9c623e4 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -7,6 +7,8 @@\n #include \"string-list.h\"\n #include \"strmap.h\"\n #include \"utf8.h\"\n+#include \"autocorrect.h\"\n+#include \"levenshtein.h\"\n \n static int disallow_abbreviated_options;\n \n@@ -623,13 +625,98 @@ static int parse_subcommand(const char *arg, const struct option *options)\n \treturn -1;\n }\n \n+static void find_subcommands(struct string_list *list,\n+\t\t\t     const struct option *options)\n+{\n+\tfor (; options->type != OPTION_END; options++) {\n+\t\tif (options->type == OPTION_SUBCOMMAND)\n+\t\t\tstring_list_append(list, options->long_name);\n+\t}\n+}\n+\n+static int levenshtein_compare(const void *p1, const void *p2)\n+{\n+\tconst struct string_list_item *i1 = p1, *i2 = p2;\n+\tconst char *s1 = i1->string, *s2 = i2->string;\n+\tint l1 = (intptr_t)i1->util;\n+\tint l2 = (intptr_t)i2->util;\n+\n+\treturn l1 != l2 ? l1 - l2 : strcmp(s1, s2);\n+}\n+\n+static const char *autocorrect_subcommand(const char *cmd,\n+\t\t\t\t\t  struct string_list *cmds)\n+{\n+\tstruct autocorrect autocorrect = { 0 };\n+\tunsigned int n = 0, best = 0;\n+\tstruct string_list_item *cand;\n+\n+\tautocorrect_resolve(&autocorrect);\n+\n+\tif (autocorrect.mode == AUTOCORRECT_NEVER)\n+\t\treturn NULL;\n+\n+\tfor_each_string_list_item(cand, cmds) {\n+\t\tif (starts_with(cand->string, cmd)) {\n+\t\t\tcand->util = 0;\n+\t\t} else {\n+\t\t\tint edit = levenshtein(cmd, cand->string,\n+\t\t\t\t\t       0, 2, 1, 3) + 1;\n+\n+\t\t\tcand->util = (void *)(intptr_t)edit;\n+\t\t}\n+\t}\n+\n+\tQSORT(cmds->items, cmds->nr, levenshtein_compare);\n+\n+\t/* Match help.c:help_unknown_cmd */\n+\tfor (; n < cmds->nr && !cmds->items[n].util; n++);\n+\n+\tif (n == cmds->nr)\n+\t\t/* prefix matches with every subcommands */\n+\t\tbest = AUTOCORRECT_SIMILARITY_FLOOR + 1;\n+\telse\n+\t\tfor (best = (intptr_t)cmds->items[n++].util;\n+\t\t     (n < cmds->nr && best == (intptr_t)cmds->items[n].util);\n+\t\t     n++);\n+\n+\tif (autocorrect.mode != AUTOCORRECT_HINT &&  n == 1 &&\n+\t    AUTOCORRECT_SIMILAR_ENOUGH(best)) {\n+\t\tfprintf_ln(stderr,\n+\t\t\t   _(\"WARNING: You called a subcommand named '%s', which does not exist.\"),\n+\t\t\t   cmd);\n+\n+\t\tautocorrect_confirm(&autocorrect, cmds->items[0].string);\n+\t\treturn cmds->items[0].string;\n+\t}\n+\n+\tif (AUTOCORRECT_SIMILAR_ENOUGH(best)) {\n+\t\terror(_(\"'%s' is not a subcommand.\"), cmd);\n+\n+\t\tfprintf_ln(stderr,\n+\t\t\t   Q_(\"\\nThe most similar subcommand is\",\n+\t\t\t      \"\\nThe most similar subcommands are\",\n+\t\t\t   n));\n+\n+\t\tfor (unsigned int i = 0; i < n; i++)\n+\t\t\tfprintf(stderr, \"\\t%s\\n\", cmds->items[i].string);\n+\n+\t\texit(129);\n+\t}\n+\n+\treturn NULL;\n+}\n+\n static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \t\t\t\t\t       const char *arg,\n \t\t\t\t\t       const struct option *options,\n \t\t\t\t\t       const char * const usagestr[])\n {\n-\tint err = parse_subcommand(arg, options);\n+\tint err;\n+\tconst char *assumed;\n+\tstruct string_list cmds = STRING_LIST_INIT_NODUP;\n \n+\terr = parse_subcommand(arg, options);\n \tif (!err)\n \t\treturn PARSE_OPT_SUBCOMMAND;\n \n@@ -642,8 +729,17 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n \t\treturn PARSE_OPT_DONE;\n \n-\terror(_(\"unknown subcommand: `%s'\"), arg);\n-\tusage_with_options(usagestr, options);\n+\tfind_subcommands(&cmds, options);\n+\tassumed = autocorrect_subcommand(arg, &cmds);\n+\n+\tif (!assumed) {\n+\t\terror(_(\"unknown subcommand: `%s'\"), arg);\n+\t\tusage_with_options(usagestr, options);\n+\t}\n+\n+\tstring_list_clear(&cmds, 0);\n+\tparse_subcommand(assumed, options);\n+\treturn PARSE_OPT_SUBCOMMAND;\n }\n \n static void check_typos(const char *arg, const struct option *options)\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex ca55ea8228c3..2a2fef1e17dc 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -632,8 +632,9 @@ test_expect_success 'subcommand - unknown subcommand shows error and usage' '\n \n test_expect_success 'subcommand - subcommands cannot be abbreviated' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd subcmd-o 2>err &&\n-\tgrep \"^error: unknown subcommand: \\`subcmd-o$SQ$\" err &&\n-\tgrep ^usage: err\n+\tgrep \"^The most similar subcommands are$\" err &&\n+\tgrep \"subcmd-one$\" err &&\n+\tgrep \"subcmd-two$\" err\n '\n \n test_expect_success 'subcommand - no negated subcommands' '\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 4700beacc182..7174b9932829 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -37,8 +37,8 @@ test_systemd_analyze_verify () {\n test_expect_success 'help text' '\n \ttest_expect_code 129 git maintenance -h >actual &&\n \ttest_grep \"usage: git maintenance <subcommand>\" actual &&\n-\ttest_expect_code 129 git maintenance barf 2>err &&\n-\ttest_grep \"unknown subcommand: \\`barf'\\''\" err &&\n+\ttest_expect_code 129 git maintenance abarf 2>err &&\n+\ttest_grep \"unknown subcommand: \\`abarf'\\''\" err &&\n \ttest_grep \"usage: git maintenance\" err &&\n \ttest_expect_code 129 git maintenance 2>err &&\n \ttest_grep \"error: need a subcommand\" err &&\n-- \n2.54.0\n\n"},{"id":"542175","messageId":"SY0P300MB08014BED919401CACCE547FDCE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801677A2A1E0FD38D06A841CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v6 08/10] parseopt: enable subcommand autocorrection for git-remote and git-notes","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-23T01:37:58Z","receivedAt":"2026-04-23T01:39:05Z","isPatch":true,"body":"Add PARSE_OPT_SUBCOMMAND_AUTOCORRECT to enable autocorrection for\nsubcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL.\n\nApply this to git-remote and git-notes, so mistyped subcommands can be\nautomatically corrected, and builtin entry points no longer need to\nhandle the unknown subcommand error path themselves.\n\nThis is safe. Both builtins either resolve to a single subcommand or\ntake no subcommand at all, meaning any unknown argument encountered by\nthe parser must be a mistyped subcommand.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n builtin/notes.c  | 10 +++-------\n builtin/remote.c | 12 ++++--------\n parse-options.c  | 16 +++++++++-------\n parse-options.h  |  1 +\n 4 files changed, 17 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 9af602bdd7b4..f9bf350df4e8 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -1149,14 +1149,10 @@ int cmd_notes(int argc,\n \n \trepo_config(the_repository, git_default_config, NULL);\n \targc = parse_options(argc, argv, prefix, options, git_notes_usage,\n-\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n-\tif (!fn) {\n-\t\tif (argc) {\n-\t\t\terror(_(\"unknown subcommand: `%s'\"), argv[0]);\n-\t\t\tusage_with_options(git_notes_usage, options);\n-\t\t}\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n+\t\t\t     PARSE_OPT_SUBCOMMAND_AUTOCORRECT);\n+\tif (!fn)\n \t\tfn = list;\n-\t}\n \n \tif (override_notes_ref) {\n \t\tstruct strbuf sb = STRBUF_INIT;\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex de989ea3ba96..6a78ab8f4cd2 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -1953,15 +1953,11 @@ int cmd_remote(int argc,\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, builtin_remote_usage,\n-\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n+\t\t\t     PARSE_OPT_SUBCOMMAND_AUTOCORRECT);\n \n-\tif (fn) {\n+\tif (fn)\n \t\treturn !!fn(argc, argv, prefix, repo);\n-\t} else {\n-\t\tif (argc) {\n-\t\t\terror(_(\"unknown subcommand: `%s'\"), argv[0]);\n-\t\t\tusage_with_options(builtin_remote_usage, options);\n-\t\t}\n+\telse\n \t\treturn !!show_all();\n-\t}\n }\ndiff --git a/parse-options.c b/parse-options.c\nindex 4370d9c623e4..d60e7bd3c977 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -720,14 +720,16 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,\n \tif (!err)\n \t\treturn PARSE_OPT_SUBCOMMAND;\n \n-\t/*\n-\t * arg is neither a short or long option nor a subcommand.  Since this\n-\t * command has a default operation mode, we have to treat this arg and\n-\t * all remaining args as args meant to that default operation mode.\n-\t * So we are done parsing.\n-\t */\n-\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n+\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL &&\n+\t    !(ctx->flags & PARSE_OPT_SUBCOMMAND_AUTOCORRECT)) {\n+\t\t/*\n+\t\t * arg is neither a short or long option nor a subcommand.\n+\t\t * Since this command has a default operation mode, we have to\n+\t\t * treat this arg and all remaining args as args meant to that\n+\t\t * default operation mode.  So we are done parsing.\n+\t\t */\n \t\treturn PARSE_OPT_DONE;\n+\t}\n \n \tfind_subcommands(&cmds, options);\n \tassumed = autocorrect_subcommand(arg, &cmds);\ndiff --git a/parse-options.h b/parse-options.h\nindex 706de9729f6b..e5fd4da4055b 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -40,6 +40,7 @@ enum parse_opt_flags {\n \tPARSE_OPT_ONE_SHOT = 1 << 5,\n \tPARSE_OPT_SHELL_EVAL = 1 << 6,\n \tPARSE_OPT_SUBCOMMAND_OPTIONAL = 1 << 7,\n+\tPARSE_OPT_SUBCOMMAND_AUTOCORRECT = 1 << 8,\n };\n \n enum parse_opt_option_flags {\n-- \n2.54.0\n\n"},{"id":"542176","messageId":"SY0P300MB08017300D45E29F36F4B2408CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"SY0P300MB0801677A2A1E0FD38D06A841CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"[PATCH v6 09/10] parseopt: add tests for subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-04-23T01:37:59Z","receivedAt":"2026-04-23T01:39:20Z","isPatch":true,"body":"These tests cover default behavior (help.autocorrect is unset), no\ncorrection, immediate correction, delayed correction, and rejection\nwhen the typo is too dissimilar.\n\nSigned-off-by: Jiamu Sun <39@barroit.sh>\n---\n t/meson.build                     |  1 +\n t/t9004-autocorrect-subcommand.sh | 58 +++++++++++++++++++++++++++++++\n 2 files changed, 59 insertions(+)\n create mode 100755 t/t9004-autocorrect-subcommand.sh\n\ndiff --git a/t/meson.build b/t/meson.build\nindex 7528e5cda5fe..871cd7ab0a08 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -980,6 +980,7 @@ integration_tests = [\n   't9001-send-email.sh',\n   't9002-column.sh',\n   't9003-help-autocorrect.sh',\n+  't9004-autocorrect-subcommand.sh',\n   't9100-git-svn-basic.sh',\n   't9101-git-svn-props.sh',\n   't9102-git-svn-deep-rmdir.sh',\ndiff --git a/t/t9004-autocorrect-subcommand.sh b/t/t9004-autocorrect-subcommand.sh\nnew file mode 100755\nindex 000000000000..09d281bd105b\n--- /dev/null\n+++ b/t/t9004-autocorrect-subcommand.sh\n@@ -0,0 +1,58 @@\n+#!/bin/sh\n+\n+test_description='subcommand auto-correction test\n+\n+Test autocorrection for subcommands with different\n+help.autocorrect mode.'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' \"\n+\techo '^error: unknown subcommand: ' >grep_unknown\n+\"\n+\n+test_expect_success 'default is show hint only' '\n+\ttest_must_fail git worktree lsit 2>actual &&\n+\ttest_grep \"most similar subcommand\" actual\n+'\n+\n+test_expect_success \"'never' disables autocorrection\" '\n+\ttest_config help.autocorrect never &&\n+\n+\ttest_must_fail git worktree lsit 2>actual &&\n+\thead -n1 actual >first && test_grep -f grep_unknown first\n+'\n+\n+for mode in false no off 0 show\n+do\n+\ttest_expect_success \"'$mode' disables autocorrection and shows hints\" \"\n+\t\ttest_config help.autocorrect $mode &&\n+\n+\t\ttest_must_fail git worktree lsit 2>actual &&\n+\t\ttest_grep 'most similar subcommand' actual\n+\t\"\n+done\n+\n+for mode in -39 immediate 1\n+do\n+\ttest_expect_success \"autocorrect immediately with '$mode'\" - <<-EOT\n+\t\ttest_config help.autocorrect $mode &&\n+\n+\t\tgit worktree lsit 2>actual &&\n+\t\ttest_grep \"you meant 'list'\\.$\" actual\n+\tEOT\n+done\n+\n+test_expect_success 'delay path is executed' - <<-\\EOT\n+\ttest_config help.autocorrect 2 &&\n+\n+\tgit worktree lsit 2>actual &&\n+\ttest_grep '^Continuing in 0.2 seconds, ' actual\n+EOT\n+\n+test_expect_success 'deny if too dissimilar' - <<-\\EOT\n+\ttest_must_fail git remote rensnr 2>actual &&\n+\thead -n1 actual >first && test_grep -f grep_unknown first\n+EOT\n+\n+test_done\n-- \n2.54.0\n\n"},{"id":"543006","messageId":"xmqqcxz2tzpr.fsf@gitster.g","threadId":"65168","inReplyTo":"SY0P300MB0801677A2A1E0FD38D06A841CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH v6 00/10] parseopt: add subcommand autocorrection","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-11T03:03:12Z","receivedAt":"2026-05-11T03:03:15Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> Git currently provides auto-correction for builtins and aliases, but\n> lacks this functionality for subcommands parsed via the parse-options\n> API. Subcommands are also commands, and typos will occur, too. Like:\n>\n> \tgit remote add-rul\n>\n> So, this series introduces subcommand auto-correction.\n>\n> By default, this implementation enables autocorrection for builtins\n> with mandatory subcommands. However, for those using\n> PARSE_OPT_SUBCOMMAND_OPTIONAL, autocorrection is skipped to avoid\n> misinterpreting legitimate unknown arguments as mistyped subcommands.\n>\n> To allow builtins with optional subcommands to explicitly opt in,\n> this series adds the PARSE_OPT_SUBCOMMAND_AUTOCORRECT flag, and enables\n> it for git-remote and git-notes.\n>\n> Additionally, the existing autocorrection logic is extracted from\n> help.c so subcommand handling can reuse the same config parsing and\n> prompt/delay logic.\n>\n> Some string literals are also combined so the full text is easier to\n> grep for.\n>\n> Changes in v6:\n>   - Adjust existing tests to fit subcommand autocorrection behavior\n>   - Change the similar subcommand hint exit code to 129\n\nI've been carrying the following fix on top of these series since\nApr 23 when the topic was merged to 'seen'.  Can you fix these up at\nthe source, so that we can move forward with this topic?\n\nThanks.\n\n\ndiff --git a/parse-options.c b/parse-options.c\nindex a1258134df..1a5feab5dd 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -647,7 +647,8 @@ static const char *autocorrect_subcommand(const char *cmd,\n \t\t\t\t\t  struct string_list *cmds)\n {\n \tstruct autocorrect autocorrect = { 0 };\n-\tunsigned int n = 0, best = 0;\n+\tunsigned int n = 0;\n+\tint best = 0;\n \tstruct string_list_item *cand;\n \n \tautocorrect_resolve(&autocorrect);\n@@ -657,7 +658,7 @@ static const char *autocorrect_subcommand(const char *cmd,\n \n \tfor_each_string_list_item(cand, cmds) {\n \t\tif (starts_with(cand->string, cmd)) {\n-\t\t\tcand->util = 0;\n+\t\t\tcand->util = NULL;\n \t\t} else {\n \t\t\tint edit = levenshtein(cmd, cand->string,\n \t\t\t\t\t       0, 2, 1, 3) + 1;\n-- \n2.54.0-170-g88022b8681\n\n"},{"id":"543402","messageId":"SY0P300MB0801E50FCB7EB2F45CD15208CE042@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","threadId":"65168","inReplyTo":"xmqqcxz2tzpr.fsf@gitster.g","subject":"Re: [PATCH v6 00/10] parseopt: add subcommand autocorrection","fromName":"Jiamu Sun","fromEmail":"39@barroit.sh","sentAt":"2026-05-15T14:34:22Z","receivedAt":"2026-05-15T14:34:34Z","isPatch":true,"body":"On Mon, May 11, 2026 at 12:03:12PM +0900, Junio C Hamano wrote:\n> I've been carrying the following fix on top of these series since\n> Apr 23 when the topic was merged to 'seen'.  Can you fix these up at\n> the source, so that we can move forward with this topic?\n> \n> Thanks.\n\nSorry for the delay. This email didn't reach my inbox.\n\nBy the time I saw this fix, I had already sent v6. Should I resend v6\nwith this fix squashed in, or bump to v7?\n\n-- \nJiamu Sun <39@barroit.sh>\n          <sunjiamu@outlook.com>\n"},{"id":"544829","messageId":"xmqq33yzd9yf.fsf@gitster.g","threadId":"65168","inReplyTo":"SY0P300MB0801E50FCB7EB2F45CD15208CE042@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM","subject":"Re: [PATCH v6 00/10] parseopt: add subcommand autocorrection","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-06T14:23:20Z","receivedAt":"2026-06-06T14:23:24Z","isPatch":true,"body":"Jiamu Sun <39@barroit.sh> writes:\n\n> On Mon, May 11, 2026 at 12:03:12PM +0900, Junio C Hamano wrote:\n>> I've been carrying the following fix on top of these series since\n>> Apr 23 when the topic was merged to 'seen'.  Can you fix these up at\n>> the source, so that we can move forward with this topic?\n>> \n>> Thanks.\n>\n> Sorry for the delay. This email didn't reach my inbox.\n>\n> By the time I saw this fix, I had already sent v6. Should I resend v6\n> with this fix squashed in, or bump to v7?\n\nSorry for the delay; this exchange fell through the cracks, and then\nI no longer recall exactly what the \"fix\" was.  If your v6 still\nlacks the \"fix\" I gave (sorry but I do not remember what it was, and\nI am away from my desk), then please do incorporate and send an\nupdated one as v7.  Hopefully it would be the final edition, unless\nthere are other issues outstanding.\n\nThanks.\n"}]}