* [PATCH 0/5] parseopt: add subcommand autocorrection
@ 2026-03-08 12:17 Jiamu Sun
2026-03-08 12:17 ` [PATCH 1/5] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
` (6 more replies)
0 siblings, 7 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-08 12:17 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
Git currently provides auto-correction for builtins and aliases, but
lacks this functionality for subcommands parsed via the parse-options
API. Subcommands are also commands, and typos will occur, too. Like:
git remote add-rul
So, this series introduces subcommand auto-correction.
Currently, builtins with mandatory subcommands enable autocorrection by
default. However, those using PARSE_OPT_SUBCOMMAND_OPTIONAL skip it to
avoid treating valid unknown arguments as mistyped subcommands.
This series adds PARSE_OPT_SUBCOMMAND_AUTOCORR, allowing commands with
optional subcommands to explicitly opt in to autocorrection. It then
enables this flag for git-remote and git-notes.
Additionally, it extracts the existing autocorrection logic from help.c
so subcommand handling can reuse the same config parsing and
prompt/delay logic.
Jiamu Sun (5):
parseopt: extract subcommand handling from parse_options_step()
help: refactor command autocorrection handling
parseopt: autocorrect mistyped subcommands
parseopt: enable subcommand autocorrect for remote and notes
help: add tests for subcommand autocorrection
Makefile | 1 +
autocorrect.c | 92 +++++++++++++++++++
autocorrect.h | 23 +++++
builtin/notes.c | 10 +-
builtin/remote.c | 12 +--
help.c | 106 ++++-----------------
parse-options.c | 147 +++++++++++++++++++++++-------
parse-options.h | 1 +
t/t9004-autocorrect-subcommand.sh | 49 ++++++++++
9 files changed, 306 insertions(+), 135 deletions(-)
create mode 100644 autocorrect.c
create mode 100644 autocorrect.h
create mode 100755 t/t9004-autocorrect-subcommand.sh
base-commit: 795c338de725e13bd361214c6b768019fc45a2c1
--
2.53.0
^ permalink raw reply [flat|nested] 67+ messages in thread
* [PATCH 1/5] parseopt: extract subcommand handling from parse_options_step()
2026-03-08 12:17 [PATCH 0/5] parseopt: add subcommand autocorrection Jiamu Sun
@ 2026-03-08 12:17 ` Jiamu Sun
2026-03-08 23:40 ` Junio C Hamano
2026-03-08 12:17 ` [PATCH 2/5] help: refactor command autocorrection handling Jiamu Sun
` (5 subsequent siblings)
6 siblings, 1 reply; 67+ messages in thread
From: Jiamu Sun @ 2026-03-08 12:17 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
Move the subcommand branch out of parse_options_step() into a new
handle_subcommand() helper. Also, make parse_subcommand() return a
simple success/failure status.
This removes the switch over impossible parse_opt_result values and
makes the non-option path easier to follow and maintain.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
parse-options.c | 75 ++++++++++++++++++++++++++-----------------------
1 file changed, 40 insertions(+), 35 deletions(-)
diff --git a/parse-options.c b/parse-options.c
index c9cafc21b903..6bb0c5697099 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -605,17 +605,43 @@ static enum parse_opt_result parse_nodash_opt(struct parse_opt_ctx_t *p,
return PARSE_OPT_ERROR;
}
-static enum parse_opt_result parse_subcommand(const char *arg,
- const struct option *options)
+static int parse_subcommand(const char *arg, const struct option *options)
{
- for (; options->type != OPTION_END; options++)
- if (options->type == OPTION_SUBCOMMAND &&
- !strcmp(options->long_name, arg)) {
- *(parse_opt_subcommand_fn **)options->value = options->subcommand_fn;
- return PARSE_OPT_SUBCOMMAND;
- }
+ for (; options->type != OPTION_END; options++) {
+ if (options->type != OPTION_SUBCOMMAND ||
+ strcmp(options->long_name, arg))
+ continue;
- return PARSE_OPT_UNKNOWN;
+ parse_opt_subcommand_fn **opt_val = options->value;
+ *opt_val = options->subcommand_fn;
+
+ return 0;
+ }
+
+ return -1;
+}
+
+static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
+ const char *arg,
+ const struct option *options,
+ const char * const usagestr[])
+{
+ int err = parse_subcommand(arg, options);
+
+ if (!err)
+ return PARSE_OPT_SUBCOMMAND;
+
+ /*
+ * arg is neither a short or long option nor a subcommand. Since this
+ * command has a default operation mode, we have to treat this arg and
+ * all remaining args as args meant to that default operation mode.
+ * So we are done parsing.
+ */
+ if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
+ return PARSE_OPT_DONE;
+
+ error(_("unknown subcommand: `%s'"), arg);
+ usage_with_options(usagestr, options);
}
static void check_typos(const char *arg, const struct option *options)
@@ -990,37 +1016,16 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,
if (*arg != '-' || !arg[1]) {
if (parse_nodash_opt(ctx, arg, options) == 0)
continue;
+
if (!ctx->has_subcommands) {
if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)
return PARSE_OPT_NON_OPTION;
ctx->out[ctx->cpidx++] = ctx->argv[0];
continue;
- }
- switch (parse_subcommand(arg, options)) {
- case PARSE_OPT_SUBCOMMAND:
- return PARSE_OPT_SUBCOMMAND;
- case PARSE_OPT_UNKNOWN:
- if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
- /*
- * arg is neither a short or long
- * option nor a subcommand. Since
- * this command has a default
- * operation mode, we have to treat
- * this arg and all remaining args
- * as args meant to that default
- * operation mode.
- * So we are done parsing.
- */
- return PARSE_OPT_DONE;
- error(_("unknown subcommand: `%s'"), arg);
- usage_with_options(usagestr, options);
- case PARSE_OPT_COMPLETE:
- case PARSE_OPT_HELP:
- case PARSE_OPT_ERROR:
- case PARSE_OPT_DONE:
- case PARSE_OPT_NON_OPTION:
- /* Impossible. */
- BUG("parse_subcommand() cannot return these");
+
+ } else {
+ return handle_subcommand(ctx, arg,
+ options, usagestr);
}
}
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH 2/5] help: refactor command autocorrection handling
2026-03-08 12:17 [PATCH 0/5] parseopt: add subcommand autocorrection Jiamu Sun
2026-03-08 12:17 ` [PATCH 1/5] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
@ 2026-03-08 12:17 ` Jiamu Sun
2026-03-08 23:52 ` Junio C Hamano
2026-03-08 12:17 ` [PATCH 3/5] parseopt: autocorrect mistyped subcommands Jiamu Sun
` (4 subsequent siblings)
6 siblings, 1 reply; 67+ messages in thread
From: Jiamu Sun @ 2026-03-08 12:17 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
Refactor autocorrect config into struct autocorr, with an explicit mode
enum and a separate delay field. Move config parsing, TTY validation,
and prompt handling into autocorrect.c.
This simplifies help.c and makes state handling for autocorrect explicit
and easier to maintain.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
Makefile | 1 +
autocorrect.c | 92 +++++++++++++++++++++++++++++++++++++++++++
autocorrect.h | 23 +++++++++++
help.c | 106 ++++++++++----------------------------------------
4 files changed, 136 insertions(+), 86 deletions(-)
create mode 100644 autocorrect.c
create mode 100644 autocorrect.h
diff --git a/Makefile b/Makefile
index f3264d0a37cc..6111631c2caa 100644
--- a/Makefile
+++ b/Makefile
@@ -1098,6 +1098,7 @@ LIB_OBJS += archive-tar.o
LIB_OBJS += archive-zip.o
LIB_OBJS += archive.o
LIB_OBJS += attr.o
+LIB_OBJS += autocorrect.o
LIB_OBJS += base85.o
LIB_OBJS += bisect.o
LIB_OBJS += blame.o
diff --git a/autocorrect.c b/autocorrect.c
new file mode 100644
index 000000000000..eaae01645910
--- /dev/null
+++ b/autocorrect.c
@@ -0,0 +1,92 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "git-compat-util.h"
+#include "autocorrect.h"
+#include "config.h"
+#include "parse.h"
+#include "strbuf.h"
+#include "prompt.h"
+#include "gettext.h"
+
+static enum autocorr_mode parse_autocorr(const char *value)
+{
+ switch (git_parse_maybe_bool_text(value)) {
+ case 1:
+ return AUTOCORR_IMMEDIATELY;
+ case 0:
+ return AUTOCORR_HINTONLY;
+ default: /* other random text */
+ break;
+ }
+
+ if (!strcmp(value, "prompt"))
+ return AUTOCORR_PROMPT;
+ else if (!strcmp(value, "never"))
+ return AUTOCORR_NEVER;
+ else if (!strcmp(value, "immediate"))
+ return AUTOCORR_IMMEDIATELY;
+ else if (!strcmp(value, "show"))
+ return AUTOCORR_HINTONLY;
+ else
+ return AUTOCORR_DELAY;
+}
+
+static int config_cb(const char *var, const char *value,
+ const struct config_context *ctx, void *data)
+{
+ struct autocorr *conf = data;
+
+ if (strcmp(var, "help.autocorrect"))
+ return 0;
+
+ conf->mode = parse_autocorr(value);
+
+ /*
+ * Disable autocorrection prompt in a non-interactive session
+ */
+ if (conf->mode == AUTOCORR_PROMPT && (!isatty(0) || !isatty(2)))
+ conf->mode = AUTOCORR_NEVER;
+
+ if (conf->mode == AUTOCORR_DELAY) {
+ conf->delay = git_config_int(var, value, ctx->kvi);
+
+ if (conf->delay == 0)
+ conf->mode = AUTOCORR_HINTONLY;
+ else if (conf->delay <= 1)
+ conf->mode = AUTOCORR_IMMEDIATELY;
+ }
+
+ return 0;
+}
+
+void autocorr_resolve(struct autocorr *conf)
+{
+ read_early_config(the_repository, config_cb, conf);
+}
+
+void autocorr_prompt_or_delay(struct autocorr *conf, const char *assumed)
+{
+ if (conf->mode == AUTOCORR_IMMEDIATELY) {
+ fprintf_ln(stderr,
+ _("Continuing under the assumption that you meant '%s'."),
+ assumed);
+
+ } else if (conf->mode == AUTOCORR_PROMPT) {
+ char *answer;
+ struct strbuf msg = STRBUF_INIT;
+
+ strbuf_addf(&msg, _("Run '%s' instead [y/N]? "), assumed);
+ answer = git_prompt(msg.buf, PROMPT_ECHO);
+ strbuf_release(&msg);
+
+ if (!(starts_with(answer, "y") || starts_with(answer, "Y")))
+ exit(1);
+
+ } else if (conf->mode == AUTOCORR_DELAY) {
+ fprintf_ln(stderr,
+ _("Continuing in %0.1f seconds, assuming that you meant '%s'."),
+ (float)conf->delay / 10.0, assumed);
+
+ sleep_millisec(conf->delay * 100);
+ }
+}
diff --git a/autocorrect.h b/autocorrect.h
new file mode 100644
index 000000000000..ea21811a43f2
--- /dev/null
+++ b/autocorrect.h
@@ -0,0 +1,23 @@
+#ifndef AUTOCORRECT_H
+#define AUTOCORRECT_H
+
+struct config_context;
+
+enum autocorr_mode {
+ AUTOCORR_HINTONLY,
+ AUTOCORR_NEVER,
+ AUTOCORR_PROMPT,
+ AUTOCORR_IMMEDIATELY,
+ AUTOCORR_DELAY,
+};
+
+struct autocorr {
+ enum autocorr_mode mode;
+ int delay;
+};
+
+void autocorr_resolve(struct autocorr *conf);
+
+void autocorr_prompt_or_delay(struct autocorr *conf, const char *assumed);
+
+#endif /* AUTOCORRECT_H */
diff --git a/help.c b/help.c
index 95f576c5c81d..5eec6a34de33 100644
--- a/help.c
+++ b/help.c
@@ -22,6 +22,7 @@
#include "repository.h"
#include "alias.h"
#include "utf8.h"
+#include "autocorrect.h"
#ifndef NO_CURL
#include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -536,70 +537,23 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)
return 0;
}
-struct help_unknown_cmd_config {
- int autocorrect;
- struct cmdnames aliases;
-};
-
-#define AUTOCORRECT_SHOW (-4)
-#define AUTOCORRECT_PROMPT (-3)
-#define AUTOCORRECT_NEVER (-2)
-#define AUTOCORRECT_IMMEDIATELY (-1)
-
-static int parse_autocorrect(const char *value)
+static int resolve_aliases(const char *var, const char *value,
+ const struct config_context *ctx, void *data)
{
- switch (git_parse_maybe_bool_text(value)) {
- case 1:
- return AUTOCORRECT_IMMEDIATELY;
- case 0:
- return AUTOCORRECT_SHOW;
- default: /* other random text */
- break;
- }
-
- if (!strcmp(value, "prompt"))
- return AUTOCORRECT_PROMPT;
- if (!strcmp(value, "never"))
- return AUTOCORRECT_NEVER;
- if (!strcmp(value, "immediate"))
- return AUTOCORRECT_IMMEDIATELY;
- if (!strcmp(value, "show"))
- return AUTOCORRECT_SHOW;
-
- return 0;
-}
-
-static int git_unknown_cmd_config(const char *var, const char *value,
- const struct config_context *ctx,
- void *cb)
-{
- struct help_unknown_cmd_config *cfg = cb;
+ struct cmdnames *aliases = data;
const char *subsection, *key;
size_t subsection_len;
- if (!strcmp(var, "help.autocorrect")) {
- int v = parse_autocorrect(value);
-
- if (!v) {
- v = git_config_int(var, value, ctx->kvi);
- if (v < 0 || v == 1)
- v = AUTOCORRECT_IMMEDIATELY;
- }
-
- cfg->autocorrect = v;
- }
-
- /* Also use aliases for command lookup */
if (!parse_config_key(var, "alias", &subsection, &subsection_len,
&key)) {
if (subsection) {
/* [alias "name"] command = value */
if (!strcmp(key, "command"))
- add_cmdname(&cfg->aliases, subsection,
+ add_cmdname(aliases, subsection,
subsection_len);
} else {
/* alias.name = value */
- add_cmdname(&cfg->aliases, key, strlen(key));
+ add_cmdname(aliases, key, strlen(key));
}
}
@@ -636,28 +590,27 @@ static const char bad_interpreter_advice[] =
char *help_unknown_cmd(const char *cmd)
{
- struct help_unknown_cmd_config cfg = { 0 };
+ struct cmdnames aliases = { 0 };
+ struct autocorr autocorr = { 0 };
+
int i, n, best_similarity = 0;
struct cmdnames main_cmds = { 0 };
struct cmdnames other_cmds = { 0 };
struct cmdname_help *common_cmds;
- read_early_config(the_repository, git_unknown_cmd_config, &cfg);
+ autocorr_resolve(&autocorr);
- /*
- * Disable autocorrection prompt in a non-interactive session
- */
- if ((cfg.autocorrect == AUTOCORRECT_PROMPT) && (!isatty(0) || !isatty(2)))
- cfg.autocorrect = AUTOCORRECT_NEVER;
+ /* Also use aliases for command lookup */
+ read_early_config(the_repository, resolve_aliases, &aliases);
- if (cfg.autocorrect == AUTOCORRECT_NEVER) {
+ if (autocorr.mode == AUTOCORR_NEVER) {
fprintf_ln(stderr, _("git: '%s' is not a git command. See 'git --help'."), cmd);
exit(1);
}
load_command_list("git-", &main_cmds, &other_cmds);
- add_cmd_list(&main_cmds, &cfg.aliases);
+ add_cmd_list(&main_cmds, &aliases);
add_cmd_list(&main_cmds, &other_cmds);
QSORT(main_cmds.names, main_cmds.cnt, cmdname_compare);
uniq(&main_cmds);
@@ -716,37 +669,18 @@ char *help_unknown_cmd(const char *cmd)
n++)
; /* still counting */
}
- if (cfg.autocorrect && cfg.autocorrect != AUTOCORRECT_SHOW && n == 1 &&
+
+ if (autocorr.mode != AUTOCORR_HINTONLY && n == 1 &&
SIMILAR_ENOUGH(best_similarity)) {
char *assumed = xstrdup(main_cmds.names[0]->name);
fprintf_ln(stderr,
- _("WARNING: You called a Git command named '%s', "
- "which does not exist."),
+ _("WARNING: You called a Git command named '%s', which does not exist."),
cmd);
- if (cfg.autocorrect == AUTOCORRECT_IMMEDIATELY)
- fprintf_ln(stderr,
- _("Continuing under the assumption that "
- "you meant '%s'."),
- assumed);
- else if (cfg.autocorrect == AUTOCORRECT_PROMPT) {
- char *answer;
- struct strbuf msg = STRBUF_INIT;
- strbuf_addf(&msg, _("Run '%s' instead [y/N]? "), assumed);
- answer = git_prompt(msg.buf, PROMPT_ECHO);
- strbuf_release(&msg);
- if (!(starts_with(answer, "y") ||
- starts_with(answer, "Y")))
- exit(1);
- } else {
- fprintf_ln(stderr,
- _("Continuing in %0.1f seconds, "
- "assuming that you meant '%s'."),
- (float)cfg.autocorrect/10.0, assumed);
- sleep_millisec(cfg.autocorrect * 100);
- }
- cmdnames_release(&cfg.aliases);
+ autocorr_prompt_or_delay(&autocorr, assumed);
+
+ cmdnames_release(&aliases);
cmdnames_release(&main_cmds);
cmdnames_release(&other_cmds);
return assumed;
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH 3/5] parseopt: autocorrect mistyped subcommands
2026-03-08 12:17 [PATCH 0/5] parseopt: add subcommand autocorrection Jiamu Sun
2026-03-08 12:17 ` [PATCH 1/5] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
2026-03-08 12:17 ` [PATCH 2/5] help: refactor command autocorrection handling Jiamu Sun
@ 2026-03-08 12:17 ` Jiamu Sun
2026-03-09 0:04 ` Junio C Hamano
2026-03-08 12:17 ` [PATCH 4/5] parseopt: enable subcommand autocorrect for remote and notes Jiamu Sun
` (3 subsequent siblings)
6 siblings, 1 reply; 67+ messages in thread
From: Jiamu Sun @ 2026-03-08 12:17 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
Enable autocorrection for mandatory subcommands parsed through
parse-options.
AUTOCORR_HINTONLY is equivalent to AUTOCORR_NEVER here, because builtins
have a limited number of subcommands. They won't become too large.
Displaying all subcommands via usage_with_options() is already good
enough. This keeps the autocorrection handling simple, too.
Also, use a dynamic threshold for similar_enough(), which can yield more
accurate typo correction results. Although subcommands are often short,
they can still vary across builtins. And a fixed threshold won't do
better on both short and long subcommands at the same time.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
parse-options.c | 76 +++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 74 insertions(+), 2 deletions(-)
diff --git a/parse-options.c b/parse-options.c
index 6bb0c5697099..cbbb04b9997a 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -6,6 +6,8 @@
#include "strbuf.h"
#include "string-list.h"
#include "utf8.h"
+#include "autocorrect.h"
+#include "levenshtein.h"
static int disallow_abbreviated_options;
@@ -621,6 +623,64 @@ static int parse_subcommand(const char *arg, const struct option *options)
return -1;
}
+static void find_subcommands(struct string_list *list,
+ const struct option *options)
+{
+ for (; options->type != OPTION_END; options++) {
+ if (options->type == OPTION_SUBCOMMAND)
+ string_list_append(list, options->long_name);
+ }
+}
+
+static int similar_enough(const char *cmd, unsigned int dist)
+{
+ size_t len = strlen(cmd);
+ unsigned int threshold = len < 3 ? 1 : len < 6 ? 3 : 6;
+
+ return dist < threshold;
+}
+
+static const char *autocorrect_subcmd(const char *cmd,
+ struct string_list *cmds)
+{
+ struct autocorr autocorr = { 0 };
+
+ autocorr_resolve(&autocorr);
+
+ if (autocorr.mode == AUTOCORR_NEVER ||
+ autocorr.mode == AUTOCORR_HINTONLY)
+ return NULL;
+
+ unsigned int min = -1;
+ unsigned int ties = 0;
+ struct string_list_item *cand;
+ struct string_list_item *best = NULL;
+
+ for_each_string_list_item(cand, cmds) {
+ unsigned int dist = levenshtein(cmd, cand->string, 0, 2, 1, 3);
+
+ if (dist < min) {
+ min = dist;
+ best = cand;
+ ties = 0;
+
+ } else if (dist == min) {
+ ties++;
+ }
+ }
+
+ if (ties == 0 && similar_enough(cmd, min)) {
+ fprintf_ln(stderr,
+ _("WARNING: You called a subcommand named '%s', which does not exist."),
+ cmd);
+
+ autocorr_prompt_or_delay(&autocorr, best->string);
+ return best->string;
+ }
+
+ return NULL;
+}
+
static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
const char *arg,
const struct option *options,
@@ -640,8 +700,20 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
return PARSE_OPT_DONE;
- error(_("unknown subcommand: `%s'"), arg);
- usage_with_options(usagestr, options);
+ struct string_list cmds = STRING_LIST_INIT_NODUP;
+
+ find_subcommands(&cmds, options);
+
+ const char *cmd = autocorrect_subcmd(arg, &cmds);
+
+ if (!cmd) {
+ error(_("unknown subcommand: `%s'"), arg);
+ usage_with_options(usagestr, options);
+ }
+
+ string_list_clear(&cmds, 0);
+ parse_subcommand(cmd, options);
+ return PARSE_OPT_SUBCOMMAND;
}
static void check_typos(const char *arg, const struct option *options)
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH 4/5] parseopt: enable subcommand autocorrect for remote and notes
2026-03-08 12:17 [PATCH 0/5] parseopt: add subcommand autocorrection Jiamu Sun
` (2 preceding siblings ...)
2026-03-08 12:17 ` [PATCH 3/5] parseopt: autocorrect mistyped subcommands Jiamu Sun
@ 2026-03-08 12:17 ` Jiamu Sun
2026-03-08 12:17 ` [PATCH 5/5] help: add tests for subcommand autocorrection Jiamu Sun
` (2 subsequent siblings)
6 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-08 12:17 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
Add PARSE_OPT_SUBCOMMAND_AUTOCORR to enable autocorrection for
subcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL.
Use it for git-remote and git-notes, so mistyped subcommands can be
automatically corrected, and builtin entry points no longer need
to handle missing subcommand error path themselves.
This is safe for these two builtins, because they either resolve to a
single subcommand or take no subcommand at all. This means that if the
subcommand parser encounters an unknown argument, it must be a mistyped
subcommand.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
builtin/notes.c | 10 +++-------
builtin/remote.c | 12 ++++--------
parse-options.c | 16 +++++++++-------
parse-options.h | 1 +
4 files changed, 17 insertions(+), 22 deletions(-)
diff --git a/builtin/notes.c b/builtin/notes.c
index 9af602bdd7b4..087eb898a441 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -1149,14 +1149,10 @@ int cmd_notes(int argc,
repo_config(the_repository, git_default_config, NULL);
argc = parse_options(argc, argv, prefix, options, git_notes_usage,
- PARSE_OPT_SUBCOMMAND_OPTIONAL);
- if (!fn) {
- if (argc) {
- error(_("unknown subcommand: `%s'"), argv[0]);
- usage_with_options(git_notes_usage, options);
- }
+ PARSE_OPT_SUBCOMMAND_OPTIONAL |
+ PARSE_OPT_SUBCOMMAND_AUTOCORR);
+ if (!fn)
fn = list;
- }
if (override_notes_ref) {
struct strbuf sb = STRBUF_INIT;
diff --git a/builtin/remote.c b/builtin/remote.c
index ace390c671d6..d1d6244a662a 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1949,15 +1949,11 @@ int cmd_remote(int argc,
};
argc = parse_options(argc, argv, prefix, options, builtin_remote_usage,
- PARSE_OPT_SUBCOMMAND_OPTIONAL);
+ PARSE_OPT_SUBCOMMAND_OPTIONAL |
+ PARSE_OPT_SUBCOMMAND_AUTOCORR);
- if (fn) {
+ if (fn)
return !!fn(argc, argv, prefix, repo);
- } else {
- if (argc) {
- error(_("unknown subcommand: `%s'"), argv[0]);
- usage_with_options(builtin_remote_usage, options);
- }
+ else
return !!show_all();
- }
}
diff --git a/parse-options.c b/parse-options.c
index cbbb04b9997a..b7c818e818fe 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -691,14 +691,16 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
if (!err)
return PARSE_OPT_SUBCOMMAND;
- /*
- * arg is neither a short or long option nor a subcommand. Since this
- * command has a default operation mode, we have to treat this arg and
- * all remaining args as args meant to that default operation mode.
- * So we are done parsing.
- */
- if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
+ if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL &&
+ !(ctx->flags & PARSE_OPT_SUBCOMMAND_AUTOCORR)) {
+ /*
+ * arg is neither a short or long option nor a subcommand.
+ * Since this command has a default operation mode, we have to
+ * treat this arg and all remaining args as args meant to that
+ * default operation mode. So we are done parsing.
+ */
return PARSE_OPT_DONE;
+ }
struct string_list cmds = STRING_LIST_INIT_NODUP;
diff --git a/parse-options.h b/parse-options.h
index 706de9729f6b..f29ac337893c 100644
--- a/parse-options.h
+++ b/parse-options.h
@@ -40,6 +40,7 @@ enum parse_opt_flags {
PARSE_OPT_ONE_SHOT = 1 << 5,
PARSE_OPT_SHELL_EVAL = 1 << 6,
PARSE_OPT_SUBCOMMAND_OPTIONAL = 1 << 7,
+ PARSE_OPT_SUBCOMMAND_AUTOCORR = 1 << 8,
};
enum parse_opt_option_flags {
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH 5/5] help: add tests for subcommand autocorrection
2026-03-08 12:17 [PATCH 0/5] parseopt: add subcommand autocorrection Jiamu Sun
` (3 preceding siblings ...)
2026-03-08 12:17 ` [PATCH 4/5] parseopt: enable subcommand autocorrect for remote and notes Jiamu Sun
@ 2026-03-08 12:17 ` Jiamu Sun
2026-03-11 17:01 ` Aaron Plattner
2026-03-08 20:11 ` [PATCH 0/5] parseopt: add " Junio C Hamano
2026-03-08 23:16 ` [PATCH v2 " Jiamu Sun
6 siblings, 1 reply; 67+ messages in thread
From: Jiamu Sun @ 2026-03-08 12:17 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
These tests cover default behavior (help.autocorrect is unset), no
correction, immediate correction, delayed correction, and rejection
when the typo is too dissimilar.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
t/t9004-autocorrect-subcommand.sh | 49 +++++++++++++++++++++++++++++++
1 file changed, 49 insertions(+)
create mode 100755 t/t9004-autocorrect-subcommand.sh
diff --git a/t/t9004-autocorrect-subcommand.sh b/t/t9004-autocorrect-subcommand.sh
new file mode 100755
index 000000000000..760760c8851a
--- /dev/null
+++ b/t/t9004-autocorrect-subcommand.sh
@@ -0,0 +1,49 @@
+#!/bin/sh
+
+test_description='subcommand auto-correction test
+
+Test autocorrection for subcommands with different
+help.autocorrect mode.'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' "
+ echo '^error: unknown subcommand: ' >grep_unknown
+"
+
+test_expect_success 'default is not to autocorrect' '
+ test_must_fail git worktree lsit 2>actual &&
+ head -n1 actual >first && test_grep -f grep_unknown first
+'
+
+for mode in false no off 0 show never; do
+ test_expect_success "'$mode' disables autocorrection" "
+ test_config help.autocorrect $mode &&
+
+ test_must_fail git worktree lsit 2>actual &&
+ head -n1 actual >first && test_grep -f grep_unknown first
+ "
+done
+
+for mode in -39 immediate 1; do
+ test_expect_success "autocorrect immediately with '$mode'" - <<-EOT
+ test_config help.autocorrect $mode &&
+
+ git worktree lsit 2>actual &&
+ test_grep "you meant 'list'\.$" actual
+ EOT
+done
+
+test_expect_success 'delay path is executed' - <<-\EOT
+ test_config help.autocorrect 2 &&
+
+ git worktree lsit 2>actual &&
+ test_grep '^Continuing in 0.2 seconds, ' actual
+EOT
+
+test_expect_success 'deny if too dissimilar' - <<-\EOT
+ test_must_fail git remote rensnr 2>actual &&
+ head -n1 actual >first && test_grep -f grep_unknown first
+EOT
+
+test_done
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* Re: [PATCH 0/5] parseopt: add subcommand autocorrection
2026-03-08 12:17 [PATCH 0/5] parseopt: add subcommand autocorrection Jiamu Sun
` (4 preceding siblings ...)
2026-03-08 12:17 ` [PATCH 5/5] help: add tests for subcommand autocorrection Jiamu Sun
@ 2026-03-08 20:11 ` Junio C Hamano
2026-03-08 22:07 ` Jiamu Sun
2026-03-08 23:16 ` [PATCH v2 " Jiamu Sun
6 siblings, 1 reply; 67+ messages in thread
From: Junio C Hamano @ 2026-03-08 20:11 UTC (permalink / raw)
To: Jiamu Sun; +Cc: git
Jiamu Sun <39@barroit.sh> writes:
> Git currently provides auto-correction for builtins and aliases, but
> lacks this functionality for subcommands parsed via the parse-options
> API. Subcommands are also commands, and typos will occur, too. Like:
>
> git remote add-rul
>
> So, this series introduces subcommand auto-correction.
>
> Currently, builtins with mandatory subcommands enable autocorrection by
> default. However, those using PARSE_OPT_SUBCOMMAND_OPTIONAL skip it to
> avoid treating valid unknown arguments as mistyped subcommands.
This is a bit confusing as it describes the behavior introduced by
this series rather than the state of the codebase before these
patches. Since subcommand autocorrection doesn't exist yet, it
would be clearer to phrase this as describing your implementation
choices. Perhaps:
By default, this implementation enables autocorrection for
builtins with mandatory subcommands. However, for those using
PARSE_OPT_SUBCOMMAND_OPTIONAL, autocorrection is skipped to
avoid misinterpreting legitimate unknown arguments as mistyped
subcommands.
or something, perhaps?
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH 0/5] parseopt: add subcommand autocorrection
2026-03-08 20:11 ` [PATCH 0/5] parseopt: add " Junio C Hamano
@ 2026-03-08 22:07 ` Jiamu Sun
0 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-08 22:07 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
On Sun, Mar 08, 2026 at 01:11:22PM -0700, Junio C Hamano wrote:
> Jiamu Sun <39@barroit.sh> writes:
>
> > So, this series introduces subcommand auto-correction.
> >
> > Currently, builtins with mandatory subcommands enable autocorrection by
> > default. However, those using PARSE_OPT_SUBCOMMAND_OPTIONAL skip it to
> > avoid treating valid unknown arguments as mistyped subcommands.
>
> This is a bit confusing as it describes the behavior introduced by
> this series rather than the state of the codebase before these
> patches. Since subcommand autocorrection doesn't exist yet, it
> would be clearer to phrase this as describing your implementation
> choices. Perhaps:
>
> By default, this implementation enables autocorrection for
> builtins with mandatory subcommands. However, for those using
> PARSE_OPT_SUBCOMMAND_OPTIONAL, autocorrection is skipped to
> avoid misinterpreting legitimate unknown arguments as mistyped
> subcommands.
>
> or something, perhaps?
Right, that's confusing. By "currently", I meant "in this patch".
Will reword the cover letter in v2 to use your phrasing. Thanks!
--
Jiamu Sun <39@barroit.sh>
^ permalink raw reply [flat|nested] 67+ messages in thread
* [PATCH v2 0/5] parseopt: add subcommand autocorrection
2026-03-08 12:17 [PATCH 0/5] parseopt: add subcommand autocorrection Jiamu Sun
` (5 preceding siblings ...)
2026-03-08 20:11 ` [PATCH 0/5] parseopt: add " Junio C Hamano
@ 2026-03-08 23:16 ` Jiamu Sun
2026-03-08 23:16 ` [PATCH v2 1/5] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
` (5 more replies)
6 siblings, 6 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-08 23:16 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
Git currently provides auto-correction for builtins and aliases, but
lacks this functionality for subcommands parsed via the parse-options
API. Subcommands are also commands, and typos will occur, too. Like:
git remote add-rul
So, this series introduces subcommand auto-correction.
By default, this implementation enables autocorrection for builtins
with mandatory subcommands. However, for those using
PARSE_OPT_SUBCOMMAND_OPTIONAL, autocorrection is skipped to avoid
misinterpreting legitimate unknown arguments as mistyped subcommands.
To allow builtins with optional subcommands to explicitly opt in,
this series adds the PARSE_OPT_SUBCOMMAND_AUTOCORR flag. This flag
is subsequently enabled for git remote and git notes.
Additionally, the existing autocorrection logic is extracted from
help.c so subcommand handling can reuse the same config parsing and
prompt/delay logic.
Changes in v2:
- Reworded the explanation of default autocorrection behavior.
Jiamu Sun (5):
parseopt: extract subcommand handling from parse_options_step()
help: refactor command autocorrection handling
parseopt: autocorrect mistyped subcommands
parseopt: enable subcommand autocorrect for remote and notes
help: add tests for subcommand autocorrection
Makefile | 1 +
autocorrect.c | 92 +++++++++++++++++++
autocorrect.h | 23 +++++
builtin/notes.c | 10 +-
builtin/remote.c | 12 +--
help.c | 106 ++++-----------------
parse-options.c | 147 +++++++++++++++++++++++-------
parse-options.h | 1 +
t/t9004-autocorrect-subcommand.sh | 49 ++++++++++
9 files changed, 306 insertions(+), 135 deletions(-)
create mode 100644 autocorrect.c
create mode 100644 autocorrect.h
create mode 100755 t/t9004-autocorrect-subcommand.sh
base-commit: 795c338de725e13bd361214c6b768019fc45a2c1
--
2.53.0
^ permalink raw reply [flat|nested] 67+ messages in thread
* [PATCH v2 1/5] parseopt: extract subcommand handling from parse_options_step()
2026-03-08 23:16 ` [PATCH v2 " Jiamu Sun
@ 2026-03-08 23:16 ` Jiamu Sun
2026-03-08 23:16 ` [PATCH v2 2/5] help: refactor command autocorrection handling Jiamu Sun
` (4 subsequent siblings)
5 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-08 23:16 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
Move the subcommand branch out of parse_options_step() into a new
handle_subcommand() helper. Also, make parse_subcommand() return a
simple success/failure status.
This removes the switch over impossible parse_opt_result values and
makes the non-option path easier to follow and maintain.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
parse-options.c | 75 ++++++++++++++++++++++++++-----------------------
1 file changed, 40 insertions(+), 35 deletions(-)
diff --git a/parse-options.c b/parse-options.c
index c9cafc21b903..6bb0c5697099 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -605,17 +605,43 @@ static enum parse_opt_result parse_nodash_opt(struct parse_opt_ctx_t *p,
return PARSE_OPT_ERROR;
}
-static enum parse_opt_result parse_subcommand(const char *arg,
- const struct option *options)
+static int parse_subcommand(const char *arg, const struct option *options)
{
- for (; options->type != OPTION_END; options++)
- if (options->type == OPTION_SUBCOMMAND &&
- !strcmp(options->long_name, arg)) {
- *(parse_opt_subcommand_fn **)options->value = options->subcommand_fn;
- return PARSE_OPT_SUBCOMMAND;
- }
+ for (; options->type != OPTION_END; options++) {
+ if (options->type != OPTION_SUBCOMMAND ||
+ strcmp(options->long_name, arg))
+ continue;
- return PARSE_OPT_UNKNOWN;
+ parse_opt_subcommand_fn **opt_val = options->value;
+ *opt_val = options->subcommand_fn;
+
+ return 0;
+ }
+
+ return -1;
+}
+
+static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
+ const char *arg,
+ const struct option *options,
+ const char * const usagestr[])
+{
+ int err = parse_subcommand(arg, options);
+
+ if (!err)
+ return PARSE_OPT_SUBCOMMAND;
+
+ /*
+ * arg is neither a short or long option nor a subcommand. Since this
+ * command has a default operation mode, we have to treat this arg and
+ * all remaining args as args meant to that default operation mode.
+ * So we are done parsing.
+ */
+ if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
+ return PARSE_OPT_DONE;
+
+ error(_("unknown subcommand: `%s'"), arg);
+ usage_with_options(usagestr, options);
}
static void check_typos(const char *arg, const struct option *options)
@@ -990,37 +1016,16 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,
if (*arg != '-' || !arg[1]) {
if (parse_nodash_opt(ctx, arg, options) == 0)
continue;
+
if (!ctx->has_subcommands) {
if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)
return PARSE_OPT_NON_OPTION;
ctx->out[ctx->cpidx++] = ctx->argv[0];
continue;
- }
- switch (parse_subcommand(arg, options)) {
- case PARSE_OPT_SUBCOMMAND:
- return PARSE_OPT_SUBCOMMAND;
- case PARSE_OPT_UNKNOWN:
- if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
- /*
- * arg is neither a short or long
- * option nor a subcommand. Since
- * this command has a default
- * operation mode, we have to treat
- * this arg and all remaining args
- * as args meant to that default
- * operation mode.
- * So we are done parsing.
- */
- return PARSE_OPT_DONE;
- error(_("unknown subcommand: `%s'"), arg);
- usage_with_options(usagestr, options);
- case PARSE_OPT_COMPLETE:
- case PARSE_OPT_HELP:
- case PARSE_OPT_ERROR:
- case PARSE_OPT_DONE:
- case PARSE_OPT_NON_OPTION:
- /* Impossible. */
- BUG("parse_subcommand() cannot return these");
+
+ } else {
+ return handle_subcommand(ctx, arg,
+ options, usagestr);
}
}
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v2 2/5] help: refactor command autocorrection handling
2026-03-08 23:16 ` [PATCH v2 " Jiamu Sun
2026-03-08 23:16 ` [PATCH v2 1/5] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
@ 2026-03-08 23:16 ` Jiamu Sun
2026-03-08 23:16 ` [PATCH v2 3/5] parseopt: autocorrect mistyped subcommands Jiamu Sun
` (3 subsequent siblings)
5 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-08 23:16 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
Refactor autocorrect config into struct autocorr, with an explicit mode
enum and a separate delay field. Move config parsing, TTY validation,
and prompt handling into autocorrect.c.
This simplifies help.c and makes state handling for autocorrect explicit
and easier to maintain.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
Makefile | 1 +
autocorrect.c | 92 +++++++++++++++++++++++++++++++++++++++++++
autocorrect.h | 23 +++++++++++
help.c | 106 ++++++++++----------------------------------------
4 files changed, 136 insertions(+), 86 deletions(-)
create mode 100644 autocorrect.c
create mode 100644 autocorrect.h
diff --git a/Makefile b/Makefile
index f3264d0a37cc..6111631c2caa 100644
--- a/Makefile
+++ b/Makefile
@@ -1098,6 +1098,7 @@ LIB_OBJS += archive-tar.o
LIB_OBJS += archive-zip.o
LIB_OBJS += archive.o
LIB_OBJS += attr.o
+LIB_OBJS += autocorrect.o
LIB_OBJS += base85.o
LIB_OBJS += bisect.o
LIB_OBJS += blame.o
diff --git a/autocorrect.c b/autocorrect.c
new file mode 100644
index 000000000000..eaae01645910
--- /dev/null
+++ b/autocorrect.c
@@ -0,0 +1,92 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "git-compat-util.h"
+#include "autocorrect.h"
+#include "config.h"
+#include "parse.h"
+#include "strbuf.h"
+#include "prompt.h"
+#include "gettext.h"
+
+static enum autocorr_mode parse_autocorr(const char *value)
+{
+ switch (git_parse_maybe_bool_text(value)) {
+ case 1:
+ return AUTOCORR_IMMEDIATELY;
+ case 0:
+ return AUTOCORR_HINTONLY;
+ default: /* other random text */
+ break;
+ }
+
+ if (!strcmp(value, "prompt"))
+ return AUTOCORR_PROMPT;
+ else if (!strcmp(value, "never"))
+ return AUTOCORR_NEVER;
+ else if (!strcmp(value, "immediate"))
+ return AUTOCORR_IMMEDIATELY;
+ else if (!strcmp(value, "show"))
+ return AUTOCORR_HINTONLY;
+ else
+ return AUTOCORR_DELAY;
+}
+
+static int config_cb(const char *var, const char *value,
+ const struct config_context *ctx, void *data)
+{
+ struct autocorr *conf = data;
+
+ if (strcmp(var, "help.autocorrect"))
+ return 0;
+
+ conf->mode = parse_autocorr(value);
+
+ /*
+ * Disable autocorrection prompt in a non-interactive session
+ */
+ if (conf->mode == AUTOCORR_PROMPT && (!isatty(0) || !isatty(2)))
+ conf->mode = AUTOCORR_NEVER;
+
+ if (conf->mode == AUTOCORR_DELAY) {
+ conf->delay = git_config_int(var, value, ctx->kvi);
+
+ if (conf->delay == 0)
+ conf->mode = AUTOCORR_HINTONLY;
+ else if (conf->delay <= 1)
+ conf->mode = AUTOCORR_IMMEDIATELY;
+ }
+
+ return 0;
+}
+
+void autocorr_resolve(struct autocorr *conf)
+{
+ read_early_config(the_repository, config_cb, conf);
+}
+
+void autocorr_prompt_or_delay(struct autocorr *conf, const char *assumed)
+{
+ if (conf->mode == AUTOCORR_IMMEDIATELY) {
+ fprintf_ln(stderr,
+ _("Continuing under the assumption that you meant '%s'."),
+ assumed);
+
+ } else if (conf->mode == AUTOCORR_PROMPT) {
+ char *answer;
+ struct strbuf msg = STRBUF_INIT;
+
+ strbuf_addf(&msg, _("Run '%s' instead [y/N]? "), assumed);
+ answer = git_prompt(msg.buf, PROMPT_ECHO);
+ strbuf_release(&msg);
+
+ if (!(starts_with(answer, "y") || starts_with(answer, "Y")))
+ exit(1);
+
+ } else if (conf->mode == AUTOCORR_DELAY) {
+ fprintf_ln(stderr,
+ _("Continuing in %0.1f seconds, assuming that you meant '%s'."),
+ (float)conf->delay / 10.0, assumed);
+
+ sleep_millisec(conf->delay * 100);
+ }
+}
diff --git a/autocorrect.h b/autocorrect.h
new file mode 100644
index 000000000000..ea21811a43f2
--- /dev/null
+++ b/autocorrect.h
@@ -0,0 +1,23 @@
+#ifndef AUTOCORRECT_H
+#define AUTOCORRECT_H
+
+struct config_context;
+
+enum autocorr_mode {
+ AUTOCORR_HINTONLY,
+ AUTOCORR_NEVER,
+ AUTOCORR_PROMPT,
+ AUTOCORR_IMMEDIATELY,
+ AUTOCORR_DELAY,
+};
+
+struct autocorr {
+ enum autocorr_mode mode;
+ int delay;
+};
+
+void autocorr_resolve(struct autocorr *conf);
+
+void autocorr_prompt_or_delay(struct autocorr *conf, const char *assumed);
+
+#endif /* AUTOCORRECT_H */
diff --git a/help.c b/help.c
index 95f576c5c81d..5eec6a34de33 100644
--- a/help.c
+++ b/help.c
@@ -22,6 +22,7 @@
#include "repository.h"
#include "alias.h"
#include "utf8.h"
+#include "autocorrect.h"
#ifndef NO_CURL
#include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -536,70 +537,23 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)
return 0;
}
-struct help_unknown_cmd_config {
- int autocorrect;
- struct cmdnames aliases;
-};
-
-#define AUTOCORRECT_SHOW (-4)
-#define AUTOCORRECT_PROMPT (-3)
-#define AUTOCORRECT_NEVER (-2)
-#define AUTOCORRECT_IMMEDIATELY (-1)
-
-static int parse_autocorrect(const char *value)
+static int resolve_aliases(const char *var, const char *value,
+ const struct config_context *ctx, void *data)
{
- switch (git_parse_maybe_bool_text(value)) {
- case 1:
- return AUTOCORRECT_IMMEDIATELY;
- case 0:
- return AUTOCORRECT_SHOW;
- default: /* other random text */
- break;
- }
-
- if (!strcmp(value, "prompt"))
- return AUTOCORRECT_PROMPT;
- if (!strcmp(value, "never"))
- return AUTOCORRECT_NEVER;
- if (!strcmp(value, "immediate"))
- return AUTOCORRECT_IMMEDIATELY;
- if (!strcmp(value, "show"))
- return AUTOCORRECT_SHOW;
-
- return 0;
-}
-
-static int git_unknown_cmd_config(const char *var, const char *value,
- const struct config_context *ctx,
- void *cb)
-{
- struct help_unknown_cmd_config *cfg = cb;
+ struct cmdnames *aliases = data;
const char *subsection, *key;
size_t subsection_len;
- if (!strcmp(var, "help.autocorrect")) {
- int v = parse_autocorrect(value);
-
- if (!v) {
- v = git_config_int(var, value, ctx->kvi);
- if (v < 0 || v == 1)
- v = AUTOCORRECT_IMMEDIATELY;
- }
-
- cfg->autocorrect = v;
- }
-
- /* Also use aliases for command lookup */
if (!parse_config_key(var, "alias", &subsection, &subsection_len,
&key)) {
if (subsection) {
/* [alias "name"] command = value */
if (!strcmp(key, "command"))
- add_cmdname(&cfg->aliases, subsection,
+ add_cmdname(aliases, subsection,
subsection_len);
} else {
/* alias.name = value */
- add_cmdname(&cfg->aliases, key, strlen(key));
+ add_cmdname(aliases, key, strlen(key));
}
}
@@ -636,28 +590,27 @@ static const char bad_interpreter_advice[] =
char *help_unknown_cmd(const char *cmd)
{
- struct help_unknown_cmd_config cfg = { 0 };
+ struct cmdnames aliases = { 0 };
+ struct autocorr autocorr = { 0 };
+
int i, n, best_similarity = 0;
struct cmdnames main_cmds = { 0 };
struct cmdnames other_cmds = { 0 };
struct cmdname_help *common_cmds;
- read_early_config(the_repository, git_unknown_cmd_config, &cfg);
+ autocorr_resolve(&autocorr);
- /*
- * Disable autocorrection prompt in a non-interactive session
- */
- if ((cfg.autocorrect == AUTOCORRECT_PROMPT) && (!isatty(0) || !isatty(2)))
- cfg.autocorrect = AUTOCORRECT_NEVER;
+ /* Also use aliases for command lookup */
+ read_early_config(the_repository, resolve_aliases, &aliases);
- if (cfg.autocorrect == AUTOCORRECT_NEVER) {
+ if (autocorr.mode == AUTOCORR_NEVER) {
fprintf_ln(stderr, _("git: '%s' is not a git command. See 'git --help'."), cmd);
exit(1);
}
load_command_list("git-", &main_cmds, &other_cmds);
- add_cmd_list(&main_cmds, &cfg.aliases);
+ add_cmd_list(&main_cmds, &aliases);
add_cmd_list(&main_cmds, &other_cmds);
QSORT(main_cmds.names, main_cmds.cnt, cmdname_compare);
uniq(&main_cmds);
@@ -716,37 +669,18 @@ char *help_unknown_cmd(const char *cmd)
n++)
; /* still counting */
}
- if (cfg.autocorrect && cfg.autocorrect != AUTOCORRECT_SHOW && n == 1 &&
+
+ if (autocorr.mode != AUTOCORR_HINTONLY && n == 1 &&
SIMILAR_ENOUGH(best_similarity)) {
char *assumed = xstrdup(main_cmds.names[0]->name);
fprintf_ln(stderr,
- _("WARNING: You called a Git command named '%s', "
- "which does not exist."),
+ _("WARNING: You called a Git command named '%s', which does not exist."),
cmd);
- if (cfg.autocorrect == AUTOCORRECT_IMMEDIATELY)
- fprintf_ln(stderr,
- _("Continuing under the assumption that "
- "you meant '%s'."),
- assumed);
- else if (cfg.autocorrect == AUTOCORRECT_PROMPT) {
- char *answer;
- struct strbuf msg = STRBUF_INIT;
- strbuf_addf(&msg, _("Run '%s' instead [y/N]? "), assumed);
- answer = git_prompt(msg.buf, PROMPT_ECHO);
- strbuf_release(&msg);
- if (!(starts_with(answer, "y") ||
- starts_with(answer, "Y")))
- exit(1);
- } else {
- fprintf_ln(stderr,
- _("Continuing in %0.1f seconds, "
- "assuming that you meant '%s'."),
- (float)cfg.autocorrect/10.0, assumed);
- sleep_millisec(cfg.autocorrect * 100);
- }
- cmdnames_release(&cfg.aliases);
+ autocorr_prompt_or_delay(&autocorr, assumed);
+
+ cmdnames_release(&aliases);
cmdnames_release(&main_cmds);
cmdnames_release(&other_cmds);
return assumed;
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v2 3/5] parseopt: autocorrect mistyped subcommands
2026-03-08 23:16 ` [PATCH v2 " Jiamu Sun
2026-03-08 23:16 ` [PATCH v2 1/5] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
2026-03-08 23:16 ` [PATCH v2 2/5] help: refactor command autocorrection handling Jiamu Sun
@ 2026-03-08 23:16 ` Jiamu Sun
2026-03-08 23:16 ` [PATCH v2 4/5] parseopt: enable subcommand autocorrect for remote and notes Jiamu Sun
` (2 subsequent siblings)
5 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-08 23:16 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
Enable autocorrection for mandatory subcommands parsed through
parse-options.
AUTOCORR_HINTONLY is equivalent to AUTOCORR_NEVER here, because builtins
have a limited number of subcommands. They are currently small enough.
Therefore, displaying all subcommands via usage_with_options() is
good enough. This keeps the autocorrection handling simple, too.
Also, use a dynamic threshold for similar_enough(), which can yield more
accurate typo correction results. Although subcommands are often short,
they can still vary across builtins. And a fixed threshold won't do
better on both short and long subcommands at the same time.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
Changes in v2:
- Minor rewording in the commit message regarding AUTOCORR_HINTONLY.
parse-options.c | 76 +++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 74 insertions(+), 2 deletions(-)
diff --git a/parse-options.c b/parse-options.c
index 6bb0c5697099..cbbb04b9997a 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -6,6 +6,8 @@
#include "strbuf.h"
#include "string-list.h"
#include "utf8.h"
+#include "autocorrect.h"
+#include "levenshtein.h"
static int disallow_abbreviated_options;
@@ -621,6 +623,64 @@ static int parse_subcommand(const char *arg, const struct option *options)
return -1;
}
+static void find_subcommands(struct string_list *list,
+ const struct option *options)
+{
+ for (; options->type != OPTION_END; options++) {
+ if (options->type == OPTION_SUBCOMMAND)
+ string_list_append(list, options->long_name);
+ }
+}
+
+static int similar_enough(const char *cmd, unsigned int dist)
+{
+ size_t len = strlen(cmd);
+ unsigned int threshold = len < 3 ? 1 : len < 6 ? 3 : 6;
+
+ return dist < threshold;
+}
+
+static const char *autocorrect_subcmd(const char *cmd,
+ struct string_list *cmds)
+{
+ struct autocorr autocorr = { 0 };
+
+ autocorr_resolve(&autocorr);
+
+ if (autocorr.mode == AUTOCORR_NEVER ||
+ autocorr.mode == AUTOCORR_HINTONLY)
+ return NULL;
+
+ unsigned int min = -1;
+ unsigned int ties = 0;
+ struct string_list_item *cand;
+ struct string_list_item *best = NULL;
+
+ for_each_string_list_item(cand, cmds) {
+ unsigned int dist = levenshtein(cmd, cand->string, 0, 2, 1, 3);
+
+ if (dist < min) {
+ min = dist;
+ best = cand;
+ ties = 0;
+
+ } else if (dist == min) {
+ ties++;
+ }
+ }
+
+ if (ties == 0 && similar_enough(cmd, min)) {
+ fprintf_ln(stderr,
+ _("WARNING: You called a subcommand named '%s', which does not exist."),
+ cmd);
+
+ autocorr_prompt_or_delay(&autocorr, best->string);
+ return best->string;
+ }
+
+ return NULL;
+}
+
static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
const char *arg,
const struct option *options,
@@ -640,8 +700,20 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
return PARSE_OPT_DONE;
- error(_("unknown subcommand: `%s'"), arg);
- usage_with_options(usagestr, options);
+ struct string_list cmds = STRING_LIST_INIT_NODUP;
+
+ find_subcommands(&cmds, options);
+
+ const char *cmd = autocorrect_subcmd(arg, &cmds);
+
+ if (!cmd) {
+ error(_("unknown subcommand: `%s'"), arg);
+ usage_with_options(usagestr, options);
+ }
+
+ string_list_clear(&cmds, 0);
+ parse_subcommand(cmd, options);
+ return PARSE_OPT_SUBCOMMAND;
}
static void check_typos(const char *arg, const struct option *options)
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v2 4/5] parseopt: enable subcommand autocorrect for remote and notes
2026-03-08 23:16 ` [PATCH v2 " Jiamu Sun
` (2 preceding siblings ...)
2026-03-08 23:16 ` [PATCH v2 3/5] parseopt: autocorrect mistyped subcommands Jiamu Sun
@ 2026-03-08 23:16 ` Jiamu Sun
2026-03-08 23:16 ` [PATCH v2 5/5] help: add tests for subcommand autocorrection Jiamu Sun
2026-03-10 11:40 ` [PATCH v3 0/8] parseopt: add " Jiamu Sun
5 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-08 23:16 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
Add PARSE_OPT_SUBCOMMAND_AUTOCORR to enable autocorrection for
subcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL.
Use it for git-remote and git-notes, so mistyped subcommands can be
automatically corrected, and builtin entry points no longer need
to handle missing subcommand error path themselves.
This is safe for these two builtins, because they either resolve to a
single subcommand or take no subcommand at all. This means that if the
subcommand parser encounters an unknown argument, it must be a mistyped
subcommand.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
builtin/notes.c | 10 +++-------
builtin/remote.c | 12 ++++--------
parse-options.c | 16 +++++++++-------
parse-options.h | 1 +
4 files changed, 17 insertions(+), 22 deletions(-)
diff --git a/builtin/notes.c b/builtin/notes.c
index 9af602bdd7b4..087eb898a441 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -1149,14 +1149,10 @@ int cmd_notes(int argc,
repo_config(the_repository, git_default_config, NULL);
argc = parse_options(argc, argv, prefix, options, git_notes_usage,
- PARSE_OPT_SUBCOMMAND_OPTIONAL);
- if (!fn) {
- if (argc) {
- error(_("unknown subcommand: `%s'"), argv[0]);
- usage_with_options(git_notes_usage, options);
- }
+ PARSE_OPT_SUBCOMMAND_OPTIONAL |
+ PARSE_OPT_SUBCOMMAND_AUTOCORR);
+ if (!fn)
fn = list;
- }
if (override_notes_ref) {
struct strbuf sb = STRBUF_INIT;
diff --git a/builtin/remote.c b/builtin/remote.c
index ace390c671d6..d1d6244a662a 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1949,15 +1949,11 @@ int cmd_remote(int argc,
};
argc = parse_options(argc, argv, prefix, options, builtin_remote_usage,
- PARSE_OPT_SUBCOMMAND_OPTIONAL);
+ PARSE_OPT_SUBCOMMAND_OPTIONAL |
+ PARSE_OPT_SUBCOMMAND_AUTOCORR);
- if (fn) {
+ if (fn)
return !!fn(argc, argv, prefix, repo);
- } else {
- if (argc) {
- error(_("unknown subcommand: `%s'"), argv[0]);
- usage_with_options(builtin_remote_usage, options);
- }
+ else
return !!show_all();
- }
}
diff --git a/parse-options.c b/parse-options.c
index cbbb04b9997a..b7c818e818fe 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -691,14 +691,16 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
if (!err)
return PARSE_OPT_SUBCOMMAND;
- /*
- * arg is neither a short or long option nor a subcommand. Since this
- * command has a default operation mode, we have to treat this arg and
- * all remaining args as args meant to that default operation mode.
- * So we are done parsing.
- */
- if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
+ if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL &&
+ !(ctx->flags & PARSE_OPT_SUBCOMMAND_AUTOCORR)) {
+ /*
+ * arg is neither a short or long option nor a subcommand.
+ * Since this command has a default operation mode, we have to
+ * treat this arg and all remaining args as args meant to that
+ * default operation mode. So we are done parsing.
+ */
return PARSE_OPT_DONE;
+ }
struct string_list cmds = STRING_LIST_INIT_NODUP;
diff --git a/parse-options.h b/parse-options.h
index 706de9729f6b..f29ac337893c 100644
--- a/parse-options.h
+++ b/parse-options.h
@@ -40,6 +40,7 @@ enum parse_opt_flags {
PARSE_OPT_ONE_SHOT = 1 << 5,
PARSE_OPT_SHELL_EVAL = 1 << 6,
PARSE_OPT_SUBCOMMAND_OPTIONAL = 1 << 7,
+ PARSE_OPT_SUBCOMMAND_AUTOCORR = 1 << 8,
};
enum parse_opt_option_flags {
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v2 5/5] help: add tests for subcommand autocorrection
2026-03-08 23:16 ` [PATCH v2 " Jiamu Sun
` (3 preceding siblings ...)
2026-03-08 23:16 ` [PATCH v2 4/5] parseopt: enable subcommand autocorrect for remote and notes Jiamu Sun
@ 2026-03-08 23:16 ` Jiamu Sun
2026-03-10 11:40 ` [PATCH v3 0/8] parseopt: add " Jiamu Sun
5 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-08 23:16 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
These tests cover default behavior (help.autocorrect is unset), no
correction, immediate correction, delayed correction, and rejection
when the typo is too dissimilar.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
t/t9004-autocorrect-subcommand.sh | 49 +++++++++++++++++++++++++++++++
1 file changed, 49 insertions(+)
create mode 100755 t/t9004-autocorrect-subcommand.sh
diff --git a/t/t9004-autocorrect-subcommand.sh b/t/t9004-autocorrect-subcommand.sh
new file mode 100755
index 000000000000..760760c8851a
--- /dev/null
+++ b/t/t9004-autocorrect-subcommand.sh
@@ -0,0 +1,49 @@
+#!/bin/sh
+
+test_description='subcommand auto-correction test
+
+Test autocorrection for subcommands with different
+help.autocorrect mode.'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' "
+ echo '^error: unknown subcommand: ' >grep_unknown
+"
+
+test_expect_success 'default is not to autocorrect' '
+ test_must_fail git worktree lsit 2>actual &&
+ head -n1 actual >first && test_grep -f grep_unknown first
+'
+
+for mode in false no off 0 show never; do
+ test_expect_success "'$mode' disables autocorrection" "
+ test_config help.autocorrect $mode &&
+
+ test_must_fail git worktree lsit 2>actual &&
+ head -n1 actual >first && test_grep -f grep_unknown first
+ "
+done
+
+for mode in -39 immediate 1; do
+ test_expect_success "autocorrect immediately with '$mode'" - <<-EOT
+ test_config help.autocorrect $mode &&
+
+ git worktree lsit 2>actual &&
+ test_grep "you meant 'list'\.$" actual
+ EOT
+done
+
+test_expect_success 'delay path is executed' - <<-\EOT
+ test_config help.autocorrect 2 &&
+
+ git worktree lsit 2>actual &&
+ test_grep '^Continuing in 0.2 seconds, ' actual
+EOT
+
+test_expect_success 'deny if too dissimilar' - <<-\EOT
+ test_must_fail git remote rensnr 2>actual &&
+ head -n1 actual >first && test_grep -f grep_unknown first
+EOT
+
+test_done
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* Re: [PATCH 1/5] parseopt: extract subcommand handling from parse_options_step()
2026-03-08 12:17 ` [PATCH 1/5] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
@ 2026-03-08 23:40 ` Junio C Hamano
2026-03-09 1:56 ` Jiamu Sun
0 siblings, 1 reply; 67+ messages in thread
From: Junio C Hamano @ 2026-03-08 23:40 UTC (permalink / raw)
To: Jiamu Sun; +Cc: git
Jiamu Sun <39@barroit.sh> writes:
> -static enum parse_opt_result parse_subcommand(const char *arg,
> - const struct option *options)
So, this function used to return either PARSE_OPT_SUBCOMMAND or
PARSE_OPT_UNKNOWN, and the caller (in an "switch ()" statement in
the parse_options_step() we see below) expected only these two. We
now instead ...
> +static int parse_subcommand(const char *arg, const struct option *options)
... make it return either 0 (success - we found a subcommand) or -1
(failure - we did not). The updated caller does use the return
value in "if ()" condition to return early when we found a
subcommand successfully, which is a lot more straight-forward than
the original "switch()". The original is even worse in that it
enumerates other PARSE_OPT_* values that are possible at the time of
writing it, instead of catching everything else with "default:",
which makes the switch() statement a maintenance burden. Getting
rid of that switch and clarifying that there are only two possible
outcome from this function alone is a good enough justification to
have this clean-up patch. Very well done.
Editing the hunk to show only the postimage reveals that the new
implementation has CodingGuidelines violation ...
> {
> + for (; options->type != OPTION_END; options++) {
> + if (options->type != OPTION_SUBCOMMAND ||
> + strcmp(options->long_name, arg))
> + continue;
>
> + parse_opt_subcommand_fn **opt_val = options->value;
... here. Move the variable definition up in the block introduced
by the for(;;) statement, perhaps?
> + *opt_val = options->subcommand_fn;
> +
> + return 0;
> + }
> +
> + return -1;
> +}
> +
And the part of the caller of parse_subcommand() that used to be the
switch() statement in the parse_options_step() is now extracted into
yet another helper function here.
> +static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
> + const char *arg,
> + const struct option *options,
> + const char * const usagestr[])
> +{
> + int err = parse_subcommand(arg, options);
> +
> + if (!err)
> + return PARSE_OPT_SUBCOMMAND;
> +
> + /*
> + * arg is neither a short or long option nor a subcommand. Since this
> + * command has a default operation mode, we have to treat this arg and
> + * all remaining args as args meant to that default operation mode.
> + * So we are done parsing.
> + */
Thanks to being a small helper function, we lost two levels of
indentation which made this comment a lot more readable with
reflowing the lines ;-)
> + if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
> + return PARSE_OPT_DONE;
> +
> + error(_("unknown subcommand: `%s'"), arg);
> + usage_with_options(usagestr, options);
> }
It is clear that there is no unintended behaviour change around this
helper function and parse_subcommand(). Very nice.
> @@ -990,37 +1016,16 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,
> if (*arg != '-' || !arg[1]) {
> if (parse_nodash_opt(ctx, arg, options) == 0)
> continue;
> +
> if (!ctx->has_subcommands) {
> if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)
> return PARSE_OPT_NON_OPTION;
> ctx->out[ctx->cpidx++] = ctx->argv[0];
> continue;
> +
> + } else {
> + return handle_subcommand(ctx, arg,
> + options, usagestr);
> }
> }
Editing the hunk to only show the postimage shows us that the blank
line at the end of the "if ()" block is funny. Drop it. Or even
better, as the block either returns or continues anyway, lose the
"else" block, perhaps? Which will make the above read like so:
if (!ctx->has_subcommands) {
if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)
return PARSE_OPT_NON_OPTION;
ctx->out[ctx->cpidx++] = ctx->argv[0];
continue;
}
return has_subcommands(ctx, arg, options, usagestr);
or swapping the logic the other way around, i.e.,
if (ctx->has_subcommands)
return has_subcommands(ctx, arg,
options, usagestr);
if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)
return PARSE_OPT_NON_OPTION;
ctx->out[ctx->cpidx++] = ctx->argv[0];
continue;
may make the result even easier to read, perhaps?
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH 2/5] help: refactor command autocorrection handling
2026-03-08 12:17 ` [PATCH 2/5] help: refactor command autocorrection handling Jiamu Sun
@ 2026-03-08 23:52 ` Junio C Hamano
2026-03-09 2:06 ` Jiamu Sun
0 siblings, 1 reply; 67+ messages in thread
From: Junio C Hamano @ 2026-03-08 23:52 UTC (permalink / raw)
To: Jiamu Sun; +Cc: git
Jiamu Sun <39@barroit.sh> writes:
> Refactor autocorrect config into struct autocorr, with an explicit mode
> enum and a separate delay field. Move config parsing, TTY validation,
> and prompt handling into autocorrect.c.
>
> This simplifies help.c and makes state handling for autocorrect explicit
> and easier to maintain.
>
> Signed-off-by: Jiamu Sun <39@barroit.sh>
> ---
> Makefile | 1 +
> autocorrect.c | 92 +++++++++++++++++++++++++++++++++++++++++++
> autocorrect.h | 23 +++++++++++
> help.c | 106 ++++++++++----------------------------------------
> 4 files changed, 136 insertions(+), 86 deletions(-)
> create mode 100644 autocorrect.c
> create mode 100644 autocorrect.h
>
> diff --git a/Makefile b/Makefile
> index f3264d0a37cc..6111631c2caa 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -1098,6 +1098,7 @@ LIB_OBJS += archive-tar.o
> LIB_OBJS += archive-zip.o
> LIB_OBJS += archive.o
> LIB_OBJS += attr.o
> +LIB_OBJS += autocorrect.o
> LIB_OBJS += base85.o
> LIB_OBJS += bisect.o
> LIB_OBJS += blame.o
> diff --git a/autocorrect.c b/autocorrect.c
> new file mode 100644
> index 000000000000..eaae01645910
> --- /dev/null
> +++ b/autocorrect.c
> @@ -0,0 +1,92 @@
> +#define USE_THE_REPOSITORY_VARIABLE
> +
> +#include "git-compat-util.h"
> +#include "autocorrect.h"
> +#include "config.h"
> +#include "parse.h"
> +#include "strbuf.h"
> +#include "prompt.h"
> +#include "gettext.h"
> +
> +static enum autocorr_mode parse_autocorr(const char *value)
> +{
> + switch (git_parse_maybe_bool_text(value)) {
> + case 1:
> + return AUTOCORR_IMMEDIATELY;
> + case 0:
> + return AUTOCORR_HINTONLY;
> + default: /* other random text */
> + break;
> + }
> +
> + if (!strcmp(value, "prompt"))
> + return AUTOCORR_PROMPT;
> + else if (!strcmp(value, "never"))
> + return AUTOCORR_NEVER;
> + else if (!strcmp(value, "immediate"))
> + return AUTOCORR_IMMEDIATELY;
> + else if (!strcmp(value, "show"))
> + return AUTOCORR_HINTONLY;
> + else
> + return AUTOCORR_DELAY;
> +}
You snuck in unnecessary "style fixes" to make bunch of "if()return"
into if/else if cascade. Also what was AUTOCORRECT_SHOW is now
returned as AUTOCORR_HINTONLY. There is no explanation on the
reason behind these changes in the proposed log message, and hiding
these small changes in a code movement patch makes reviewing the
series harder than necessary.
The patch is doing too many things (namely, (1) code movement that
will make it later reusable as a side effect but has no semantic
changes in the current code, plus (2) change in style (like the one
we see here), semantics (possibly the difference in SHOW and
HINTONLY we see here) and features, possibly including the renaming
of AUTOCORRECT_* into AUTOCORR_*.) Let's have "restructure with
code movement and nothing else", followed by "other changes
I'll stop here, and expect this step to be split into at least two
patches to make it more readable before we can review it again.
Thanks.
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH 3/5] parseopt: autocorrect mistyped subcommands
2026-03-08 12:17 ` [PATCH 3/5] parseopt: autocorrect mistyped subcommands Jiamu Sun
@ 2026-03-09 0:04 ` Junio C Hamano
2026-03-09 2:11 ` Jiamu Sun
0 siblings, 1 reply; 67+ messages in thread
From: Junio C Hamano @ 2026-03-09 0:04 UTC (permalink / raw)
To: Jiamu Sun; +Cc: git
Jiamu Sun <39@barroit.sh> writes:
> +static const char *autocorrect_subcmd(const char *cmd,
> + struct string_list *cmds)
> +{
> + struct autocorr autocorr = { 0 };
> +
> + autocorr_resolve(&autocorr);
> +
> + if (autocorr.mode == AUTOCORR_NEVER ||
> + autocorr.mode == AUTOCORR_HINTONLY)
> + return NULL;
> +
> + unsigned int min = -1;
> + unsigned int ties = 0;
> + struct string_list_item *cand;
> + struct string_list_item *best = NULL;
CodingGuidelines: -Wdeclaration-after-statement is here.
Using "-1" as "the maximum value this unsigned type can represent"
is somewhat misleading. Can't we avoid that?
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH 1/5] parseopt: extract subcommand handling from parse_options_step()
2026-03-08 23:40 ` Junio C Hamano
@ 2026-03-09 1:56 ` Jiamu Sun
0 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-09 1:56 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
On Sun, Mar 08, 2026 at 04:40:44PM -0700, Junio C Hamano wrote:
> ... here. Move the variable definition up in the block introduced
> by the for(;;) statement, perhaps?
>
> > + *opt_val = options->subcommand_fn;
> > +
> > + return 0;
> > + }
Agree. Actually, I forgot to check the CodingGuidelines. Will fix the
coding style issue in v3.
> Editing the hunk to only show the postimage shows us that the blank
> line at the end of the "if ()" block is funny. Drop it. Or even
> better, as the block either returns or continues anyway, lose the
> "else" block, perhaps? Which will make the above read like so:
>
> if (!ctx->has_subcommands) {
> if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)
> return PARSE_OPT_NON_OPTION;
> ctx->out[ctx->cpidx++] = ctx->argv[0];
> continue;
> }
>
> return has_subcommands(ctx, arg, options, usagestr);
>
> or swapping the logic the other way around, i.e.,
>
> if (ctx->has_subcommands)
> return has_subcommands(ctx, arg,
> options, usagestr);
>
> if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)
> return PARSE_OPT_NON_OPTION;
>
> ctx->out[ctx->cpidx++] = ctx->argv[0];
> continue;
>
> may make the result even easier to read, perhaps?
Swapping the logic works better here. Returning early in the
ctx->has_subcommands path lets the rest of the block assume
!ctx->has_subcommands, so the extra check can go away. That makes
the code easier to read. Will do that.
--
Jiamu Sun <39@barroit.sh>
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH 2/5] help: refactor command autocorrection handling
2026-03-08 23:52 ` Junio C Hamano
@ 2026-03-09 2:06 ` Jiamu Sun
0 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-09 2:06 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
On Sun, Mar 08, 2026 at 04:52:54PM -0700, Junio C Hamano wrote:
> You snuck in unnecessary "style fixes" to make bunch of "if()return"
> into if/else if cascade. Also what was AUTOCORRECT_SHOW is now
> returned as AUTOCORR_HINTONLY. There is no explanation on the
> reason behind these changes in the proposed log message, and hiding
> these small changes in a code movement patch makes reviewing the
> series harder than necessary.
>
> The patch is doing too many things (namely, (1) code movement that
> will make it later reusable as a side effect but has no semantic
> changes in the current code, plus (2) change in style (like the one
> we see here), semantics (possibly the difference in SHOW and
> HINTONLY we see here) and features, possibly including the renaming
> of AUTOCORRECT_* into AUTOCORR_*.) Let's have "restructure with
> code movement and nothing else", followed by "other changes
>
> I'll stop here, and expect this step to be split into at least two
> patches to make it more readable before we can review it again.
>
> Thanks.
Ah, I realized this patch is completely unreadable. You are right, the
code movement ended up hiding other changes entirely. Will split it to
make each change obvious.
--
Jiamu Sun <39@barroit.sh>
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH 3/5] parseopt: autocorrect mistyped subcommands
2026-03-09 0:04 ` Junio C Hamano
@ 2026-03-09 2:11 ` Jiamu Sun
0 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-09 2:11 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
On Sun, Mar 08, 2026 at 05:04:49PM -0700, Junio C Hamano wrote:
> > + unsigned int min = -1;
> > + unsigned int ties = 0;
> > + struct string_list_item *cand;
> > + struct string_list_item *best = NULL;
>
> CodingGuidelines: -Wdeclaration-after-statement is here.
Will fix that to follow CodingGuidelines.
> Using "-1" as "the maximum value this unsigned type can represent"
> is somewhat misleading. Can't we avoid that?
>
Yes, UINT_MAX is a better fit here.
--
Jiamu Sun <39@barroit.sh>
^ permalink raw reply [flat|nested] 67+ messages in thread
* [PATCH v3 0/8] parseopt: add subcommand autocorrection
2026-03-08 23:16 ` [PATCH v2 " Jiamu Sun
` (4 preceding siblings ...)
2026-03-08 23:16 ` [PATCH v2 5/5] help: add tests for subcommand autocorrection Jiamu Sun
@ 2026-03-10 11:40 ` Jiamu Sun
2026-03-10 11:41 ` [PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
` (8 more replies)
5 siblings, 9 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-10 11:40 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
Git currently provides auto-correction for builtins and aliases, but
lacks this functionality for subcommands parsed via the parse-options
API. Subcommands are also commands, and typos will occur, too. Like:
git remote add-rul
So, this series introduces subcommand auto-correction.
By default, this implementation enables autocorrection for builtins
with mandatory subcommands. However, for those using
PARSE_OPT_SUBCOMMAND_OPTIONAL, autocorrection is skipped to avoid
misinterpreting legitimate unknown arguments as mistyped subcommands.
To allow builtins with optional subcommands to explicitly opt in,
this series adds the PARSE_OPT_SUBCOMMAND_AUTOCORR flag. This flag
is subsequently enabled for git-remote and git-notes.
Additionally, the existing autocorrection logic is extracted from
help.c so subcommand handling can reuse the same config parsing and
prompt/delay logic.
Some split string literals are also combined so the full text is easier
to grep for.
Changes in v3:
- Align with the coding guildline
Changes in v2:
- Reword the explanation of default autocorrection behavior
Jiamu Sun (8):
parseopt: extract subcommand handling from parse_options_step()
help: make autocorrect handling reusable
help: move tty check for autocorrection to autocorrect.c
autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINTONLY
autocorrect: provide config resolution API
parseopt: autocorrect mistyped subcommands
parseopt: enable subcommand autocorrection for git-remote and
git-notes
help: add tests for subcommand autocorrection
Makefile | 1 +
autocorrect.c | 89 +++++++++++++++++
autocorrect.h | 21 ++++
builtin/notes.c | 10 +-
builtin/remote.c | 12 +--
help.c | 104 ++++----------------
parse-options.c | 156 ++++++++++++++++++++++--------
parse-options.h | 1 +
t/t9004-autocorrect-subcommand.sh | 51 ++++++++++
9 files changed, 305 insertions(+), 140 deletions(-)
create mode 100644 autocorrect.c
create mode 100644 autocorrect.h
create mode 100755 t/t9004-autocorrect-subcommand.sh
base-commit: 795c338de725e13bd361214c6b768019fc45a2c1
--
2.53.0
^ permalink raw reply [flat|nested] 67+ messages in thread
* [PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step()
2026-03-10 11:40 ` [PATCH v3 0/8] parseopt: add " Jiamu Sun
@ 2026-03-10 11:41 ` Jiamu Sun
2026-03-10 12:46 ` Karthik Nayak
2026-03-10 11:41 ` [PATCH v3 2/8] help: make autocorrect handling reusable Jiamu Sun
` (7 subsequent siblings)
8 siblings, 1 reply; 67+ messages in thread
From: Jiamu Sun @ 2026-03-10 11:41 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
Move the subcommand branch out of parse_options_step() into a new
handle_subcommand() helper. Also, make parse_subcommand() return a
simple success/failure status.
This removes the switch over impossible parse_opt_result values and
makes the non-option path easier to follow and maintain.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
Changes in v3:
- Fix coding style issue
parse-options.c | 86 ++++++++++++++++++++++++++-----------------------
1 file changed, 46 insertions(+), 40 deletions(-)
diff --git a/parse-options.c b/parse-options.c
index c9cafc21b903..33f26d6b6179 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -605,17 +605,44 @@ static enum parse_opt_result parse_nodash_opt(struct parse_opt_ctx_t *p,
return PARSE_OPT_ERROR;
}
-static enum parse_opt_result parse_subcommand(const char *arg,
- const struct option *options)
+static int parse_subcommand(const char *arg, const struct option *options)
{
- for (; options->type != OPTION_END; options++)
- if (options->type == OPTION_SUBCOMMAND &&
- !strcmp(options->long_name, arg)) {
- *(parse_opt_subcommand_fn **)options->value = options->subcommand_fn;
- return PARSE_OPT_SUBCOMMAND;
- }
+ for (; options->type != OPTION_END; options++) {
+ parse_opt_subcommand_fn **opt_val;
- return PARSE_OPT_UNKNOWN;
+ if (options->type != OPTION_SUBCOMMAND ||
+ strcmp(options->long_name, arg))
+ continue;
+
+ opt_val = options->value;
+ *opt_val = options->subcommand_fn;
+ return 0;
+ }
+
+ return -1;
+}
+
+static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
+ const char *arg,
+ const struct option *options,
+ const char * const usagestr[])
+{
+ int err = parse_subcommand(arg, options);
+
+ if (!err)
+ return PARSE_OPT_SUBCOMMAND;
+
+ /*
+ * arg is neither a short or long option nor a subcommand. Since this
+ * command has a default operation mode, we have to treat this arg and
+ * all remaining args as args meant to that default operation mode.
+ * So we are done parsing.
+ */
+ if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
+ return PARSE_OPT_DONE;
+
+ error(_("unknown subcommand: `%s'"), arg);
+ usage_with_options(usagestr, options);
}
static void check_typos(const char *arg, const struct option *options)
@@ -990,38 +1017,17 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,
if (*arg != '-' || !arg[1]) {
if (parse_nodash_opt(ctx, arg, options) == 0)
continue;
- if (!ctx->has_subcommands) {
- if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)
- return PARSE_OPT_NON_OPTION;
- ctx->out[ctx->cpidx++] = ctx->argv[0];
- continue;
- }
- switch (parse_subcommand(arg, options)) {
- case PARSE_OPT_SUBCOMMAND:
- return PARSE_OPT_SUBCOMMAND;
- case PARSE_OPT_UNKNOWN:
- if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
- /*
- * arg is neither a short or long
- * option nor a subcommand. Since
- * this command has a default
- * operation mode, we have to treat
- * this arg and all remaining args
- * as args meant to that default
- * operation mode.
- * So we are done parsing.
- */
- return PARSE_OPT_DONE;
- error(_("unknown subcommand: `%s'"), arg);
- usage_with_options(usagestr, options);
- case PARSE_OPT_COMPLETE:
- case PARSE_OPT_HELP:
- case PARSE_OPT_ERROR:
- case PARSE_OPT_DONE:
- case PARSE_OPT_NON_OPTION:
- /* Impossible. */
- BUG("parse_subcommand() cannot return these");
+
+ if (ctx->has_subcommands) {
+ return handle_subcommand(ctx, arg, options,
+ usagestr);
}
+
+ if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)
+ return PARSE_OPT_NON_OPTION;
+
+ ctx->out[ctx->cpidx++] = ctx->argv[0];
+ continue;
}
/* lone -h asks for help */
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v3 2/8] help: make autocorrect handling reusable
2026-03-10 11:40 ` [PATCH v3 0/8] parseopt: add " Jiamu Sun
2026-03-10 11:41 ` [PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
@ 2026-03-10 11:41 ` Jiamu Sun
2026-03-10 12:52 ` Karthik Nayak
2026-03-10 11:41 ` [PATCH v3 3/8] help: move tty check for autocorrection to autocorrect.c Jiamu Sun
` (6 subsequent siblings)
8 siblings, 1 reply; 67+ messages in thread
From: Jiamu Sun @ 2026-03-10 11:41 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
Move config parsing and prompt/delay handling into autocorrect.c and
expose them in autocorrect.h. This makes autocorrect reusable regardless
of which target links against it.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
This patch moves autocorrect handling out of help.c as a preparatory
step for subcommand autocorrection.
This is necessary because help.c uses load_builtin_commands(), which is
provided by git.o. Executables that do not link against git.o, such as
git-daemon, will hit a link error if they use symbols defined in help.o.
A simple and clean fix is to make the relevant functions independent of
help.c and move them to a dedicated file.
Changes in v3:
- Split patch so diffs don't get hidden by code movement
Makefile | 1 +
autocorrect.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++
autocorrect.h | 16 ++++++++++++
help.c | 64 +++------------------------------------------
4 files changed, 93 insertions(+), 60 deletions(-)
create mode 100644 autocorrect.c
create mode 100644 autocorrect.h
diff --git a/Makefile b/Makefile
index f3264d0a37cc..6111631c2caa 100644
--- a/Makefile
+++ b/Makefile
@@ -1098,6 +1098,7 @@ LIB_OBJS += archive-tar.o
LIB_OBJS += archive-zip.o
LIB_OBJS += archive.o
LIB_OBJS += attr.o
+LIB_OBJS += autocorrect.o
LIB_OBJS += base85.o
LIB_OBJS += bisect.o
LIB_OBJS += blame.o
diff --git a/autocorrect.c b/autocorrect.c
new file mode 100644
index 000000000000..1037f032019b
--- /dev/null
+++ b/autocorrect.c
@@ -0,0 +1,72 @@
+#include "git-compat-util.h"
+#include "autocorrect.h"
+#include "config.h"
+#include "parse.h"
+#include "strbuf.h"
+#include "prompt.h"
+#include "gettext.h"
+
+static int parse_autocorrect(const char *value)
+{
+ switch (git_parse_maybe_bool_text(value)) {
+ case 1:
+ return AUTOCORRECT_IMMEDIATELY;
+ case 0:
+ return AUTOCORRECT_SHOW;
+ default: /* other random text */
+ break;
+ }
+
+ if (!strcmp(value, "prompt"))
+ return AUTOCORRECT_PROMPT;
+ if (!strcmp(value, "never"))
+ return AUTOCORRECT_NEVER;
+ if (!strcmp(value, "immediate"))
+ return AUTOCORRECT_IMMEDIATELY;
+ if (!strcmp(value, "show"))
+ return AUTOCORRECT_SHOW;
+
+ return 0;
+}
+
+void autocorr_resolve_config(const char *var, const char *value,
+ const struct config_context *ctx, void *data)
+{
+ int *out = data;
+
+ if (!strcmp(var, "help.autocorrect")) {
+ int v = parse_autocorrect(value);
+
+ if (!v) {
+ v = git_config_int(var, value, ctx->kvi);
+ if (v < 0 || v == 1)
+ v = AUTOCORRECT_IMMEDIATELY;
+ }
+
+ *out = v;
+ }
+}
+
+void autocorr_confirm(int autocorrect, const char *assumed)
+{
+ if (autocorrect == AUTOCORRECT_IMMEDIATELY) {
+ fprintf_ln(stderr,
+ _("Continuing under the assumption that you meant '%s'."),
+ assumed);
+ } else if (autocorrect == AUTOCORRECT_PROMPT) {
+ char *answer;
+ struct strbuf msg = STRBUF_INIT;
+
+ strbuf_addf(&msg, _("Run '%s' instead [y/N]? "), assumed);
+ answer = git_prompt(msg.buf, PROMPT_ECHO);
+ strbuf_release(&msg);
+
+ if (!(starts_with(answer, "y") || starts_with(answer, "Y")))
+ exit(1);
+ } else {
+ fprintf_ln(stderr,
+ _("Continuing in %0.1f seconds, assuming that you meant '%s'."),
+ (float)autocorrect / 10.0, assumed);
+ sleep_millisec(autocorrect * 100);
+ }
+}
diff --git a/autocorrect.h b/autocorrect.h
new file mode 100644
index 000000000000..45609990c77e
--- /dev/null
+++ b/autocorrect.h
@@ -0,0 +1,16 @@
+#ifndef AUTOCORRECT_H
+#define AUTOCORRECT_H
+
+#define AUTOCORRECT_SHOW (-4)
+#define AUTOCORRECT_PROMPT (-3)
+#define AUTOCORRECT_NEVER (-2)
+#define AUTOCORRECT_IMMEDIATELY (-1)
+
+struct config_context;
+
+void autocorr_resolve_config(const char *var, const char *value,
+ const struct config_context *ctx, void *data);
+
+void autocorr_confirm(int autocorr, const char *assumed);
+
+#endif /* AUTOCORRECT_H */
diff --git a/help.c b/help.c
index 95f576c5c81d..6be3ec9dfb12 100644
--- a/help.c
+++ b/help.c
@@ -22,6 +22,7 @@
#include "repository.h"
#include "alias.h"
#include "utf8.h"
+#include "autocorrect.h"
#ifndef NO_CURL
#include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -541,34 +542,6 @@ struct help_unknown_cmd_config {
struct cmdnames aliases;
};
-#define AUTOCORRECT_SHOW (-4)
-#define AUTOCORRECT_PROMPT (-3)
-#define AUTOCORRECT_NEVER (-2)
-#define AUTOCORRECT_IMMEDIATELY (-1)
-
-static int parse_autocorrect(const char *value)
-{
- switch (git_parse_maybe_bool_text(value)) {
- case 1:
- return AUTOCORRECT_IMMEDIATELY;
- case 0:
- return AUTOCORRECT_SHOW;
- default: /* other random text */
- break;
- }
-
- if (!strcmp(value, "prompt"))
- return AUTOCORRECT_PROMPT;
- if (!strcmp(value, "never"))
- return AUTOCORRECT_NEVER;
- if (!strcmp(value, "immediate"))
- return AUTOCORRECT_IMMEDIATELY;
- if (!strcmp(value, "show"))
- return AUTOCORRECT_SHOW;
-
- return 0;
-}
-
static int git_unknown_cmd_config(const char *var, const char *value,
const struct config_context *ctx,
void *cb)
@@ -577,17 +550,7 @@ static int git_unknown_cmd_config(const char *var, const char *value,
const char *subsection, *key;
size_t subsection_len;
- if (!strcmp(var, "help.autocorrect")) {
- int v = parse_autocorrect(value);
-
- if (!v) {
- v = git_config_int(var, value, ctx->kvi);
- if (v < 0 || v == 1)
- v = AUTOCORRECT_IMMEDIATELY;
- }
-
- cfg->autocorrect = v;
- }
+ autocorr_resolve_config(var, value, ctx, &cfg->autocorrect);
/* Also use aliases for command lookup */
if (!parse_config_key(var, "alias", &subsection, &subsection_len,
@@ -724,27 +687,8 @@ char *help_unknown_cmd(const char *cmd)
_("WARNING: You called a Git command named '%s', "
"which does not exist."),
cmd);
- if (cfg.autocorrect == AUTOCORRECT_IMMEDIATELY)
- fprintf_ln(stderr,
- _("Continuing under the assumption that "
- "you meant '%s'."),
- assumed);
- else if (cfg.autocorrect == AUTOCORRECT_PROMPT) {
- char *answer;
- struct strbuf msg = STRBUF_INIT;
- strbuf_addf(&msg, _("Run '%s' instead [y/N]? "), assumed);
- answer = git_prompt(msg.buf, PROMPT_ECHO);
- strbuf_release(&msg);
- if (!(starts_with(answer, "y") ||
- starts_with(answer, "Y")))
- exit(1);
- } else {
- fprintf_ln(stderr,
- _("Continuing in %0.1f seconds, "
- "assuming that you meant '%s'."),
- (float)cfg.autocorrect/10.0, assumed);
- sleep_millisec(cfg.autocorrect * 100);
- }
+
+ autocorr_confirm(cfg.autocorrect, assumed);
cmdnames_release(&cfg.aliases);
cmdnames_release(&main_cmds);
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v3 3/8] help: move tty check for autocorrection to autocorrect.c
2026-03-10 11:40 ` [PATCH v3 0/8] parseopt: add " Jiamu Sun
2026-03-10 11:41 ` [PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
2026-03-10 11:41 ` [PATCH v3 2/8] help: make autocorrect handling reusable Jiamu Sun
@ 2026-03-10 11:41 ` Jiamu Sun
2026-03-10 14:06 ` Karthik Nayak
2026-03-10 11:41 ` [PATCH v3 4/8] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINTONLY Jiamu Sun
` (5 subsequent siblings)
8 siblings, 1 reply; 67+ messages in thread
From: Jiamu Sun @ 2026-03-10 11:41 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
TTY checking is the autocorrect config parser's responsibility. It must
ensure the parsed value is correct and reliable. Thus, move the check to
autocorr_resolve_config().
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
In parse_autocorrect() I kept the if/else if cascade. In my opinion,
this is not a style change. It's a control-flow clarification, because
those returns describe different states of the mode. Chaining them
together is better than leaving them discrete.
Also, I dropped the float cast in the delay calculation, which is
redundant.
autocorrect.c | 50 +++++++++++++++++++++++++++++---------------------
autocorrect.h | 20 ++++++++++++++------
help.c | 17 ++++++-----------
3 files changed, 49 insertions(+), 38 deletions(-)
diff --git a/autocorrect.c b/autocorrect.c
index 1037f032019b..50d7f116d85d 100644
--- a/autocorrect.c
+++ b/autocorrect.c
@@ -6,7 +6,7 @@
#include "prompt.h"
#include "gettext.h"
-static int parse_autocorrect(const char *value)
+static enum autocorr_mode parse_autocorrect(const char *value)
{
switch (git_parse_maybe_bool_text(value)) {
case 1:
@@ -19,41 +19,49 @@ static int parse_autocorrect(const char *value)
if (!strcmp(value, "prompt"))
return AUTOCORRECT_PROMPT;
- if (!strcmp(value, "never"))
+ else if (!strcmp(value, "never"))
return AUTOCORRECT_NEVER;
- if (!strcmp(value, "immediate"))
+ else if (!strcmp(value, "immediate"))
return AUTOCORRECT_IMMEDIATELY;
- if (!strcmp(value, "show"))
+ else if (!strcmp(value, "show"))
return AUTOCORRECT_SHOW;
-
- return 0;
+ else
+ return AUTOCORRECT_DELAY;
}
void autocorr_resolve_config(const char *var, const char *value,
const struct config_context *ctx, void *data)
{
- int *out = data;
+ struct autocorr *conf = data;
+
+ if (strcmp(var, "help.autocorrect"))
+ return;
+
+ conf->mode = parse_autocorrect(value);
- if (!strcmp(var, "help.autocorrect")) {
- int v = parse_autocorrect(value);
+ /*
+ * Disable autocorrection prompt in a non-interactive session.
+ */
+ if (conf->mode == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))
+ conf->mode = AUTOCORRECT_NEVER;
- if (!v) {
- v = git_config_int(var, value, ctx->kvi);
- if (v < 0 || v == 1)
- v = AUTOCORRECT_IMMEDIATELY;
- }
+ if (conf->mode == AUTOCORRECT_DELAY) {
+ conf->delay = git_config_int(var, value, ctx->kvi);
- *out = v;
+ if (!conf->delay)
+ conf->mode = AUTOCORRECT_SHOW;
+ else if (conf->delay <= 1)
+ conf->mode = AUTOCORRECT_IMMEDIATELY;
}
}
-void autocorr_confirm(int autocorrect, const char *assumed)
+void autocorr_confirm(struct autocorr *conf, const char *assumed)
{
- if (autocorrect == AUTOCORRECT_IMMEDIATELY) {
+ if (conf->mode == AUTOCORRECT_IMMEDIATELY) {
fprintf_ln(stderr,
_("Continuing under the assumption that you meant '%s'."),
assumed);
- } else if (autocorrect == AUTOCORRECT_PROMPT) {
+ } else if (conf->mode == AUTOCORRECT_PROMPT) {
char *answer;
struct strbuf msg = STRBUF_INIT;
@@ -63,10 +71,10 @@ void autocorr_confirm(int autocorrect, const char *assumed)
if (!(starts_with(answer, "y") || starts_with(answer, "Y")))
exit(1);
- } else {
+ } else if (conf->mode == AUTOCORRECT_DELAY) {
fprintf_ln(stderr,
_("Continuing in %0.1f seconds, assuming that you meant '%s'."),
- (float)autocorrect / 10.0, assumed);
- sleep_millisec(autocorrect * 100);
+ conf->delay / 10.0, assumed);
+ sleep_millisec(conf->delay * 100);
}
}
diff --git a/autocorrect.h b/autocorrect.h
index 45609990c77e..ce4a68379f2f 100644
--- a/autocorrect.h
+++ b/autocorrect.h
@@ -1,16 +1,24 @@
#ifndef AUTOCORRECT_H
#define AUTOCORRECT_H
-#define AUTOCORRECT_SHOW (-4)
-#define AUTOCORRECT_PROMPT (-3)
-#define AUTOCORRECT_NEVER (-2)
-#define AUTOCORRECT_IMMEDIATELY (-1)
-
struct config_context;
+enum autocorr_mode {
+ AUTOCORRECT_SHOW,
+ AUTOCORRECT_NEVER,
+ AUTOCORRECT_PROMPT,
+ AUTOCORRECT_IMMEDIATELY,
+ AUTOCORRECT_DELAY,
+};
+
+struct autocorr {
+ enum autocorr_mode mode;
+ int delay;
+};
+
void autocorr_resolve_config(const char *var, const char *value,
const struct config_context *ctx, void *data);
-void autocorr_confirm(int autocorr, const char *assumed);
+void autocorr_confirm(struct autocorr *conf, const char *assumed);
#endif /* AUTOCORRECT_H */
diff --git a/help.c b/help.c
index 6be3ec9dfb12..566d33299b9a 100644
--- a/help.c
+++ b/help.c
@@ -538,7 +538,7 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)
}
struct help_unknown_cmd_config {
- int autocorrect;
+ struct autocorr autocorr;
struct cmdnames aliases;
};
@@ -550,7 +550,7 @@ static int git_unknown_cmd_config(const char *var, const char *value,
const char *subsection, *key;
size_t subsection_len;
- autocorr_resolve_config(var, value, ctx, &cfg->autocorrect);
+ autocorr_resolve_config(var, value, ctx, &cfg->autocorr);
/* Also use aliases for command lookup */
if (!parse_config_key(var, "alias", &subsection, &subsection_len,
@@ -607,13 +607,7 @@ char *help_unknown_cmd(const char *cmd)
read_early_config(the_repository, git_unknown_cmd_config, &cfg);
- /*
- * Disable autocorrection prompt in a non-interactive session
- */
- if ((cfg.autocorrect == AUTOCORRECT_PROMPT) && (!isatty(0) || !isatty(2)))
- cfg.autocorrect = AUTOCORRECT_NEVER;
-
- if (cfg.autocorrect == AUTOCORRECT_NEVER) {
+ if (cfg.autocorr.mode == AUTOCORRECT_NEVER) {
fprintf_ln(stderr, _("git: '%s' is not a git command. See 'git --help'."), cmd);
exit(1);
}
@@ -679,7 +673,8 @@ char *help_unknown_cmd(const char *cmd)
n++)
; /* still counting */
}
- if (cfg.autocorrect && cfg.autocorrect != AUTOCORRECT_SHOW && n == 1 &&
+
+ if (cfg.autocorr.mode != AUTOCORRECT_SHOW && n == 1 &&
SIMILAR_ENOUGH(best_similarity)) {
char *assumed = xstrdup(main_cmds.names[0]->name);
@@ -688,7 +683,7 @@ char *help_unknown_cmd(const char *cmd)
"which does not exist."),
cmd);
- autocorr_confirm(cfg.autocorrect, assumed);
+ autocorr_confirm(&cfg.autocorr, assumed);
cmdnames_release(&cfg.aliases);
cmdnames_release(&main_cmds);
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v3 4/8] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINTONLY
2026-03-10 11:40 ` [PATCH v3 0/8] parseopt: add " Jiamu Sun
` (2 preceding siblings ...)
2026-03-10 11:41 ` [PATCH v3 3/8] help: move tty check for autocorrection to autocorrect.c Jiamu Sun
@ 2026-03-10 11:41 ` Jiamu Sun
2026-03-10 14:08 ` Karthik Nayak
2026-03-10 11:41 ` [PATCH v3 5/8] autocorrect: provide config resolution API Jiamu Sun
` (4 subsequent siblings)
8 siblings, 1 reply; 67+ messages in thread
From: Jiamu Sun @ 2026-03-10 11:41 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
AUTOCORRECT_SHOW is ambiguous. Its purpose is to show commands similar
to the unknown one and take no other action. Rename it to fit the
semantics.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
autocorrect.c | 6 +++---
autocorrect.h | 2 +-
help.c | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/autocorrect.c b/autocorrect.c
index 50d7f116d85d..9c4b691fb003 100644
--- a/autocorrect.c
+++ b/autocorrect.c
@@ -12,7 +12,7 @@ static enum autocorr_mode parse_autocorrect(const char *value)
case 1:
return AUTOCORRECT_IMMEDIATELY;
case 0:
- return AUTOCORRECT_SHOW;
+ return AUTOCORRECT_HINTONLY;
default: /* other random text */
break;
}
@@ -24,7 +24,7 @@ static enum autocorr_mode parse_autocorrect(const char *value)
else if (!strcmp(value, "immediate"))
return AUTOCORRECT_IMMEDIATELY;
else if (!strcmp(value, "show"))
- return AUTOCORRECT_SHOW;
+ return AUTOCORRECT_HINTONLY;
else
return AUTOCORRECT_DELAY;
}
@@ -49,7 +49,7 @@ void autocorr_resolve_config(const char *var, const char *value,
conf->delay = git_config_int(var, value, ctx->kvi);
if (!conf->delay)
- conf->mode = AUTOCORRECT_SHOW;
+ conf->mode = AUTOCORRECT_HINTONLY;
else if (conf->delay <= 1)
conf->mode = AUTOCORRECT_IMMEDIATELY;
}
diff --git a/autocorrect.h b/autocorrect.h
index ce4a68379f2f..be4e3e8b2043 100644
--- a/autocorrect.h
+++ b/autocorrect.h
@@ -4,7 +4,7 @@
struct config_context;
enum autocorr_mode {
- AUTOCORRECT_SHOW,
+ AUTOCORRECT_HINTONLY,
AUTOCORRECT_NEVER,
AUTOCORRECT_PROMPT,
AUTOCORRECT_IMMEDIATELY,
diff --git a/help.c b/help.c
index 566d33299b9a..6158545e48a1 100644
--- a/help.c
+++ b/help.c
@@ -674,7 +674,7 @@ char *help_unknown_cmd(const char *cmd)
; /* still counting */
}
- if (cfg.autocorr.mode != AUTOCORRECT_SHOW && n == 1 &&
+ if (cfg.autocorr.mode != AUTOCORRECT_HINTONLY && n == 1 &&
SIMILAR_ENOUGH(best_similarity)) {
char *assumed = xstrdup(main_cmds.names[0]->name);
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v3 5/8] autocorrect: provide config resolution API
2026-03-10 11:40 ` [PATCH v3 0/8] parseopt: add " Jiamu Sun
` (3 preceding siblings ...)
2026-03-10 11:41 ` [PATCH v3 4/8] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINTONLY Jiamu Sun
@ 2026-03-10 11:41 ` Jiamu Sun
2026-03-10 14:15 ` Karthik Nayak
2026-03-10 11:41 ` [PATCH v3 6/8] parseopt: autocorrect mistyped subcommands Jiamu Sun
` (3 subsequent siblings)
8 siblings, 1 reply; 67+ messages in thread
From: Jiamu Sun @ 2026-03-10 11:41 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
Add autocorr_resolve(). This resolves and populates the correct values
for autocorrect config.
Make autocorrect config callback internal. The API is meant to provide
a high-level way to retrieve the config. Allowing access to the config
callback from outside violates that intent.
Additionally, in some cases, without access to the config callback, two
config iterations cannot be merged into one, which can hurt performance.
This is fine, as the code path that calls autocorr_resolve() is cold.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
autocorrect.c | 15 ++++++++++++---
autocorrect.h | 5 +----
help.c | 37 +++++++++++++++----------------------
3 files changed, 28 insertions(+), 29 deletions(-)
diff --git a/autocorrect.c b/autocorrect.c
index 9c4b691fb003..63fa331ef5e2 100644
--- a/autocorrect.c
+++ b/autocorrect.c
@@ -1,3 +1,5 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
#include "git-compat-util.h"
#include "autocorrect.h"
#include "config.h"
@@ -29,13 +31,13 @@ static enum autocorr_mode parse_autocorrect(const char *value)
return AUTOCORRECT_DELAY;
}
-void autocorr_resolve_config(const char *var, const char *value,
- const struct config_context *ctx, void *data)
+static int resolve_autocorr(const char *var, const char *value,
+ const struct config_context *ctx, void *data)
{
struct autocorr *conf = data;
if (strcmp(var, "help.autocorrect"))
- return;
+ return 0;
conf->mode = parse_autocorrect(value);
@@ -53,6 +55,13 @@ void autocorr_resolve_config(const char *var, const char *value,
else if (conf->delay <= 1)
conf->mode = AUTOCORRECT_IMMEDIATELY;
}
+
+ return 0;
+}
+
+void autocorr_resolve(struct autocorr *conf)
+{
+ read_early_config(the_repository, resolve_autocorr, conf);
}
void autocorr_confirm(struct autocorr *conf, const char *assumed)
diff --git a/autocorrect.h b/autocorrect.h
index be4e3e8b2043..5d82c49903be 100644
--- a/autocorrect.h
+++ b/autocorrect.h
@@ -1,8 +1,6 @@
#ifndef AUTOCORRECT_H
#define AUTOCORRECT_H
-struct config_context;
-
enum autocorr_mode {
AUTOCORRECT_HINTONLY,
AUTOCORRECT_NEVER,
@@ -16,8 +14,7 @@ struct autocorr {
int delay;
};
-void autocorr_resolve_config(const char *var, const char *value,
- const struct config_context *ctx, void *data);
+void autocorr_resolve(struct autocorr *conf);
void autocorr_confirm(struct autocorr *conf, const char *assumed);
diff --git a/help.c b/help.c
index 6158545e48a1..4adb1fcf0726 100644
--- a/help.c
+++ b/help.c
@@ -537,32 +537,24 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)
return 0;
}
-struct help_unknown_cmd_config {
- struct autocorr autocorr;
- struct cmdnames aliases;
-};
-
-static int git_unknown_cmd_config(const char *var, const char *value,
- const struct config_context *ctx,
- void *cb)
+static int resolve_aliases(const char *var, const char *value UNUSED,
+ const struct config_context *ctx UNUSED, void *data)
{
- struct help_unknown_cmd_config *cfg = cb;
+ struct cmdnames *aliases = data;
const char *subsection, *key;
size_t subsection_len;
- autocorr_resolve_config(var, value, ctx, &cfg->autocorr);
-
/* Also use aliases for command lookup */
if (!parse_config_key(var, "alias", &subsection, &subsection_len,
&key)) {
if (subsection) {
/* [alias "name"] command = value */
if (!strcmp(key, "command"))
- add_cmdname(&cfg->aliases, subsection,
+ add_cmdname(aliases, subsection,
subsection_len);
} else {
/* alias.name = value */
- add_cmdname(&cfg->aliases, key, strlen(key));
+ add_cmdname(aliases, key, strlen(key));
}
}
@@ -599,22 +591,24 @@ static const char bad_interpreter_advice[] =
char *help_unknown_cmd(const char *cmd)
{
- struct help_unknown_cmd_config cfg = { 0 };
+ struct cmdnames aliases = { 0 };
+ struct autocorr autocorr = { 0 };
int i, n, best_similarity = 0;
struct cmdnames main_cmds = { 0 };
struct cmdnames other_cmds = { 0 };
struct cmdname_help *common_cmds;
- read_early_config(the_repository, git_unknown_cmd_config, &cfg);
+ autocorr_resolve(&autocorr);
- if (cfg.autocorr.mode == AUTOCORRECT_NEVER) {
+ if (autocorr.mode == AUTOCORRECT_NEVER) {
fprintf_ln(stderr, _("git: '%s' is not a git command. See 'git --help'."), cmd);
exit(1);
}
load_command_list("git-", &main_cmds, &other_cmds);
+ read_early_config(the_repository, resolve_aliases, &aliases);
- add_cmd_list(&main_cmds, &cfg.aliases);
+ add_cmd_list(&main_cmds, &aliases);
add_cmd_list(&main_cmds, &other_cmds);
QSORT(main_cmds.names, main_cmds.cnt, cmdname_compare);
uniq(&main_cmds);
@@ -674,18 +668,17 @@ char *help_unknown_cmd(const char *cmd)
; /* still counting */
}
- if (cfg.autocorr.mode != AUTOCORRECT_HINTONLY && n == 1 &&
+ if (autocorr.mode != AUTOCORRECT_HINTONLY && n == 1 &&
SIMILAR_ENOUGH(best_similarity)) {
char *assumed = xstrdup(main_cmds.names[0]->name);
fprintf_ln(stderr,
- _("WARNING: You called a Git command named '%s', "
- "which does not exist."),
+ _("WARNING: You called a Git command named '%s', which does not exist."),
cmd);
- autocorr_confirm(&cfg.autocorr, assumed);
+ autocorr_confirm(&autocorr, assumed);
- cmdnames_release(&cfg.aliases);
+ cmdnames_release(&aliases);
cmdnames_release(&main_cmds);
cmdnames_release(&other_cmds);
return assumed;
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v3 6/8] parseopt: autocorrect mistyped subcommands
2026-03-10 11:40 ` [PATCH v3 0/8] parseopt: add " Jiamu Sun
` (4 preceding siblings ...)
2026-03-10 11:41 ` [PATCH v3 5/8] autocorrect: provide config resolution API Jiamu Sun
@ 2026-03-10 11:41 ` Jiamu Sun
2026-03-10 20:16 ` Junio C Hamano
2026-03-10 11:41 ` [PATCH v3 7/8] parseopt: enable subcommand autocorrection for git-remote and git-notes Jiamu Sun
` (2 subsequent siblings)
8 siblings, 1 reply; 67+ messages in thread
From: Jiamu Sun @ 2026-03-10 11:41 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
Try to autocorrect the mistyped mandatory subcommand before showing an
error and exiting. Subcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL
are skipped.
In autocorrect_subcommand(), AUTOCORR_HINTONLY does the same as
AUTOCORR_NEVER, because builtins have a limited number of subcommands.
Those lists are currently not too large. Therefore, displaying all
subcommands via usage_with_options() is good enough here. This also
keeps the autocorrection handling simple.
Use a dynamic threshold for similar_enough() to check if the result is
usable. This can yield more accurate typo corrections. Even though
subcommands are often short, they can still vary across builtins. And in
the current implementation, a fixed threshold can't do better on both
short and long subcommands at the same time.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
Changes in v3:
- Improve commit message
- Fix coding style issue
parse-options.c | 76 +++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 73 insertions(+), 3 deletions(-)
diff --git a/parse-options.c b/parse-options.c
index 33f26d6b6179..227bc7499115 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -6,6 +6,8 @@
#include "strbuf.h"
#include "string-list.h"
#include "utf8.h"
+#include "autocorrect.h"
+#include "levenshtein.h"
static int disallow_abbreviated_options;
@@ -622,13 +624,72 @@ static int parse_subcommand(const char *arg, const struct option *options)
return -1;
}
+static void find_subcommands(struct string_list *list,
+ const struct option *options)
+{
+ for (; options->type != OPTION_END; options++) {
+ if (options->type == OPTION_SUBCOMMAND)
+ string_list_append(list, options->long_name);
+ }
+}
+
+static int similar_enough(const char *cmd, unsigned int dist)
+{
+ size_t len = strlen(cmd);
+ unsigned int threshold = len < 3 ? 1 : len < 6 ? 3 : 6;
+
+ return dist < threshold;
+}
+
+static const char *autocorrect_subcommand(const char *cmd,
+ struct string_list *cmds)
+{
+ struct autocorr autocorr = { 0 };
+ unsigned int min = UINT_MAX;
+ unsigned int ties = 0;
+ struct string_list_item *cand;
+ struct string_list_item *best = NULL;
+
+ autocorr_resolve(&autocorr);
+
+ if (autocorr.mode == AUTOCORRECT_NEVER ||
+ autocorr.mode == AUTOCORRECT_HINTONLY)
+ return NULL;
+
+ for_each_string_list_item(cand, cmds) {
+ unsigned int dist = levenshtein(cmd, cand->string, 0, 2, 1, 3);
+
+ if (dist < min) {
+ min = dist;
+ best = cand;
+ ties = 0;
+ } else if (dist == min) {
+ ties++;
+ }
+ }
+
+ if (!ties && similar_enough(cmd, min)) {
+ fprintf_ln(stderr,
+ _("WARNING: You called a subcommand named '%s', which does not exist."),
+ cmd);
+
+ autocorr_confirm(&autocorr, best->string);
+ return best->string;
+ }
+
+ return NULL;
+}
+
static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
const char *arg,
const struct option *options,
const char * const usagestr[])
{
- int err = parse_subcommand(arg, options);
+ int err;
+ const char *assumed;
+ struct string_list cmds = STRING_LIST_INIT_NODUP;
+ err = parse_subcommand(arg, options);
if (!err)
return PARSE_OPT_SUBCOMMAND;
@@ -641,8 +702,17 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
return PARSE_OPT_DONE;
- error(_("unknown subcommand: `%s'"), arg);
- usage_with_options(usagestr, options);
+ find_subcommands(&cmds, options);
+ assumed = autocorrect_subcommand(arg, &cmds);
+
+ if (!assumed) {
+ error(_("unknown subcommand: `%s'"), arg);
+ usage_with_options(usagestr, options);
+ }
+
+ string_list_clear(&cmds, 0);
+ parse_subcommand(assumed, options);
+ return PARSE_OPT_SUBCOMMAND;
}
static void check_typos(const char *arg, const struct option *options)
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v3 7/8] parseopt: enable subcommand autocorrection for git-remote and git-notes
2026-03-10 11:40 ` [PATCH v3 0/8] parseopt: add " Jiamu Sun
` (5 preceding siblings ...)
2026-03-10 11:41 ` [PATCH v3 6/8] parseopt: autocorrect mistyped subcommands Jiamu Sun
@ 2026-03-10 11:41 ` Jiamu Sun
2026-03-10 11:41 ` [PATCH v3 8/8] help: add tests for subcommand autocorrection Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 00/10] parseopt: add " Jiamu Sun
8 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-10 11:41 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
Add PARSE_OPT_SUBCOMMAND_AUTOCORR to enable autocorrection for
subcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL.
Use it for git-remote and git-notes, so mistyped subcommands can be
automatically corrected, and builtin entry points no longer need to
handle the unknown subcommand error path themselves.
This is safe for these two builtins, because they either resolve to a
single subcommand or take no subcommand at all. This means that if the
subcommand parser encounters an unknown argument, it must be a mistyped
subcommand.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
Changes in v3:
- Improve commit message
builtin/notes.c | 10 +++-------
builtin/remote.c | 12 ++++--------
parse-options.c | 16 +++++++++-------
parse-options.h | 1 +
4 files changed, 17 insertions(+), 22 deletions(-)
diff --git a/builtin/notes.c b/builtin/notes.c
index 9af602bdd7b4..087eb898a441 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -1149,14 +1149,10 @@ int cmd_notes(int argc,
repo_config(the_repository, git_default_config, NULL);
argc = parse_options(argc, argv, prefix, options, git_notes_usage,
- PARSE_OPT_SUBCOMMAND_OPTIONAL);
- if (!fn) {
- if (argc) {
- error(_("unknown subcommand: `%s'"), argv[0]);
- usage_with_options(git_notes_usage, options);
- }
+ PARSE_OPT_SUBCOMMAND_OPTIONAL |
+ PARSE_OPT_SUBCOMMAND_AUTOCORR);
+ if (!fn)
fn = list;
- }
if (override_notes_ref) {
struct strbuf sb = STRBUF_INIT;
diff --git a/builtin/remote.c b/builtin/remote.c
index ace390c671d6..d1d6244a662a 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1949,15 +1949,11 @@ int cmd_remote(int argc,
};
argc = parse_options(argc, argv, prefix, options, builtin_remote_usage,
- PARSE_OPT_SUBCOMMAND_OPTIONAL);
+ PARSE_OPT_SUBCOMMAND_OPTIONAL |
+ PARSE_OPT_SUBCOMMAND_AUTOCORR);
- if (fn) {
+ if (fn)
return !!fn(argc, argv, prefix, repo);
- } else {
- if (argc) {
- error(_("unknown subcommand: `%s'"), argv[0]);
- usage_with_options(builtin_remote_usage, options);
- }
+ else
return !!show_all();
- }
}
diff --git a/parse-options.c b/parse-options.c
index 227bc7499115..6ac4bd30e23b 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -693,14 +693,16 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
if (!err)
return PARSE_OPT_SUBCOMMAND;
- /*
- * arg is neither a short or long option nor a subcommand. Since this
- * command has a default operation mode, we have to treat this arg and
- * all remaining args as args meant to that default operation mode.
- * So we are done parsing.
- */
- if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
+ if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL &&
+ !(ctx->flags & PARSE_OPT_SUBCOMMAND_AUTOCORR)) {
+ /*
+ * arg is neither a short or long option nor a subcommand.
+ * Since this command has a default operation mode, we have to
+ * treat this arg and all remaining args as args meant to that
+ * default operation mode. So we are done parsing.
+ */
return PARSE_OPT_DONE;
+ }
find_subcommands(&cmds, options);
assumed = autocorrect_subcommand(arg, &cmds);
diff --git a/parse-options.h b/parse-options.h
index 706de9729f6b..f29ac337893c 100644
--- a/parse-options.h
+++ b/parse-options.h
@@ -40,6 +40,7 @@ enum parse_opt_flags {
PARSE_OPT_ONE_SHOT = 1 << 5,
PARSE_OPT_SHELL_EVAL = 1 << 6,
PARSE_OPT_SUBCOMMAND_OPTIONAL = 1 << 7,
+ PARSE_OPT_SUBCOMMAND_AUTOCORR = 1 << 8,
};
enum parse_opt_option_flags {
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v3 8/8] help: add tests for subcommand autocorrection
2026-03-10 11:40 ` [PATCH v3 0/8] parseopt: add " Jiamu Sun
` (6 preceding siblings ...)
2026-03-10 11:41 ` [PATCH v3 7/8] parseopt: enable subcommand autocorrection for git-remote and git-notes Jiamu Sun
@ 2026-03-10 11:41 ` Jiamu Sun
2026-03-11 4:23 ` Junio C Hamano
2026-03-16 15:36 ` [PATCH v4 00/10] parseopt: add " Jiamu Sun
8 siblings, 1 reply; 67+ messages in thread
From: Jiamu Sun @ 2026-03-10 11:41 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jiamu Sun
These tests cover default behavior (help.autocorrect is unset), no
correction, immediate correction, delayed correction, and rejection
when the typo is too dissimilar.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
Changes in v3:
- Fix coding style issue
t/t9004-autocorrect-subcommand.sh | 51 +++++++++++++++++++++++++++++++
1 file changed, 51 insertions(+)
create mode 100755 t/t9004-autocorrect-subcommand.sh
diff --git a/t/t9004-autocorrect-subcommand.sh b/t/t9004-autocorrect-subcommand.sh
new file mode 100755
index 000000000000..d10031659b94
--- /dev/null
+++ b/t/t9004-autocorrect-subcommand.sh
@@ -0,0 +1,51 @@
+#!/bin/sh
+
+test_description='subcommand auto-correction test
+
+Test autocorrection for subcommands with different
+help.autocorrect mode.'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' "
+ echo '^error: unknown subcommand: ' >grep_unknown
+"
+
+test_expect_success 'default is not to autocorrect' '
+ test_must_fail git worktree lsit 2>actual &&
+ head -n1 actual >first && test_grep -f grep_unknown first
+'
+
+for mode in false no off 0 show never
+do
+ test_expect_success "'$mode' disables autocorrection" "
+ test_config help.autocorrect $mode &&
+
+ test_must_fail git worktree lsit 2>actual &&
+ head -n1 actual >first && test_grep -f grep_unknown first
+ "
+done
+
+for mode in -39 immediate 1
+do
+ test_expect_success "autocorrect immediately with '$mode'" - <<-EOT
+ test_config help.autocorrect $mode &&
+
+ git worktree lsit 2>actual &&
+ test_grep "you meant 'list'\.$" actual
+ EOT
+done
+
+test_expect_success 'delay path is executed' - <<-\EOT
+ test_config help.autocorrect 2 &&
+
+ git worktree lsit 2>actual &&
+ test_grep '^Continuing in 0.2 seconds, ' actual
+EOT
+
+test_expect_success 'deny if too dissimilar' - <<-\EOT
+ test_must_fail git remote rensnr 2>actual &&
+ head -n1 actual >first && test_grep -f grep_unknown first
+EOT
+
+test_done
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* Re: [PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step()
2026-03-10 11:41 ` [PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
@ 2026-03-10 12:46 ` Karthik Nayak
2026-03-11 1:49 ` Jiamu Sun
0 siblings, 1 reply; 67+ messages in thread
From: Karthik Nayak @ 2026-03-10 12:46 UTC (permalink / raw)
To: Jiamu Sun, git; +Cc: Junio C Hamano
[-- Attachment #1: Type: text/plain, Size: 1724 bytes --]
Jiamu Sun <39@barroit.sh> writes:
[snip]
> @@ -990,38 +1017,17 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,
> if (*arg != '-' || !arg[1]) {
> if (parse_nodash_opt(ctx, arg, options) == 0)
> continue;
> - if (!ctx->has_subcommands) {
> - if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)
> - return PARSE_OPT_NON_OPTION;
> - ctx->out[ctx->cpidx++] = ctx->argv[0];
> - continue;
> - }
> - switch (parse_subcommand(arg, options)) {
> - case PARSE_OPT_SUBCOMMAND:
> - return PARSE_OPT_SUBCOMMAND;
> - case PARSE_OPT_UNKNOWN:
> - if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
> - /*
> - * arg is neither a short or long
> - * option nor a subcommand. Since
> - * this command has a default
> - * operation mode, we have to treat
> - * this arg and all remaining args
> - * as args meant to that default
> - * operation mode.
> - * So we are done parsing.
> - */
> - return PARSE_OPT_DONE;
> - error(_("unknown subcommand: `%s'"), arg);
> - usage_with_options(usagestr, options);
> - case PARSE_OPT_COMPLETE:
> - case PARSE_OPT_HELP:
> - case PARSE_OPT_ERROR:
> - case PARSE_OPT_DONE:
> - case PARSE_OPT_NON_OPTION:
> - /* Impossible. */
> - BUG("parse_subcommand() cannot return these");
> +
> + if (ctx->has_subcommands) {
> + return handle_subcommand(ctx, arg, options,
> + usagestr);
> }
> +
Nit: we try to avoid braces around single statement blocks.
> + if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)
> + return PARSE_OPT_NON_OPTION;
> +
> + ctx->out[ctx->cpidx++] = ctx->argv[0];
> + continue;
> }
>
> /* lone -h asks for help */
> --
> 2.53.0
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 2/8] help: make autocorrect handling reusable
2026-03-10 11:41 ` [PATCH v3 2/8] help: make autocorrect handling reusable Jiamu Sun
@ 2026-03-10 12:52 ` Karthik Nayak
2026-03-10 20:10 ` Junio C Hamano
2026-03-11 1:58 ` Jiamu Sun
0 siblings, 2 replies; 67+ messages in thread
From: Karthik Nayak @ 2026-03-10 12:52 UTC (permalink / raw)
To: Jiamu Sun, git; +Cc: Junio C Hamano
[-- Attachment #1: Type: text/plain, Size: 3092 bytes --]
Jiamu Sun <39@barroit.sh> writes:
> Move config parsing and prompt/delay handling into autocorrect.c and
> expose them in autocorrect.h. This makes autocorrect reusable regardless
> of which target links against it.
>
> Signed-off-by: Jiamu Sun <39@barroit.sh>
> ---
> This patch moves autocorrect handling out of help.c as a preparatory
> step for subcommand autocorrection.
>
> This is necessary because help.c uses load_builtin_commands(), which is
> provided by git.o. Executables that do not link against git.o, such as
> git-daemon, will hit a link error if they use symbols defined in help.o.
> A simple and clean fix is to make the relevant functions independent of
> help.c and move them to a dedicated file.
>
> Changes in v3:
> - Split patch so diffs don't get hidden by code movement
>
> Makefile | 1 +
> autocorrect.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++
> autocorrect.h | 16 ++++++++++++
> help.c | 64 +++------------------------------------------
> 4 files changed, 93 insertions(+), 60 deletions(-)
> create mode 100644 autocorrect.c
> create mode 100644 autocorrect.h
>
This should also be added to meson.build.
[snip]
> diff --git a/autocorrect.h b/autocorrect.h
> new file mode 100644
> index 000000000000..45609990c77e
> --- /dev/null
> +++ b/autocorrect.h
> @@ -0,0 +1,16 @@
> +#ifndef AUTOCORRECT_H
> +#define AUTOCORRECT_H
> +
> +#define AUTOCORRECT_SHOW (-4)
> +#define AUTOCORRECT_PROMPT (-3)
> +#define AUTOCORRECT_NEVER (-2)
> +#define AUTOCORRECT_IMMEDIATELY (-1)
> +
> +struct config_context;
> +
> +void autocorr_resolve_config(const char *var, const char *value,
> + const struct config_context *ctx, void *data);
> +
> +void autocorr_confirm(int autocorr, const char *assumed);
> +
Why not use s/autocorr/autocorrect/ ? Also would be nice to add some
documentation about each of the functions here.
[snip]
Also got this from running `git-clang-format` on this commit. Generally
applying changes while moving code makes it harder to review. But here
the changes are small enough that we could get away with it. I'll leave
it to you.
diff --git a/autocorrect.c b/autocorrect.c
index 1037f03201..87351fd08f 100644
--- a/autocorrect.c
+++ b/autocorrect.c
@@ -9,12 +9,12 @@
static int parse_autocorrect(const char *value)
{
switch (git_parse_maybe_bool_text(value)) {
- case 1:
- return AUTOCORRECT_IMMEDIATELY;
- case 0:
- return AUTOCORRECT_SHOW;
- default: /* other random text */
- break;
+ case 1:
+ return AUTOCORRECT_IMMEDIATELY;
+ case 0:
+ return AUTOCORRECT_SHOW;
+ default: /* other random text */
+ break;
}
if (!strcmp(value, "prompt"))
diff --git a/autocorrect.h b/autocorrect.h
index 45609990c7..38f1e73131 100644
--- a/autocorrect.h
+++ b/autocorrect.h
@@ -1,9 +1,9 @@
#ifndef AUTOCORRECT_H
#define AUTOCORRECT_H
-#define AUTOCORRECT_SHOW (-4)
-#define AUTOCORRECT_PROMPT (-3)
-#define AUTOCORRECT_NEVER (-2)
+#define AUTOCORRECT_SHOW (-4)
+#define AUTOCORRECT_PROMPT (-3)
+#define AUTOCORRECT_NEVER (-2)
#define AUTOCORRECT_IMMEDIATELY (-1)
struct config_context;
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]
^ permalink raw reply related [flat|nested] 67+ messages in thread
* Re: [PATCH v3 3/8] help: move tty check for autocorrection to autocorrect.c
2026-03-10 11:41 ` [PATCH v3 3/8] help: move tty check for autocorrection to autocorrect.c Jiamu Sun
@ 2026-03-10 14:06 ` Karthik Nayak
2026-03-11 2:16 ` Jiamu Sun
2026-03-12 0:10 ` Jiamu Sun
0 siblings, 2 replies; 67+ messages in thread
From: Karthik Nayak @ 2026-03-10 14:06 UTC (permalink / raw)
To: Jiamu Sun, git; +Cc: Junio C Hamano
[-- Attachment #1: Type: text/plain, Size: 4466 bytes --]
Jiamu Sun <39@barroit.sh> writes:
> TTY checking is the autocorrect config parser's responsibility. It must
> ensure the parsed value is correct and reliable. Thus, move the check to
> autocorr_resolve_config().
>
> Signed-off-by: Jiamu Sun <39@barroit.sh>
> ---
> In parse_autocorrect() I kept the if/else if cascade. In my opinion,
> this is not a style change. It's a control-flow clarification, because
> those returns describe different states of the mode. Chaining them
> together is better than leaving them discrete.
>
> Also, I dropped the float cast in the delay calculation, which is
> redundant.
>
> autocorrect.c | 50 +++++++++++++++++++++++++++++---------------------
> autocorrect.h | 20 ++++++++++++++------
> help.c | 17 ++++++-----------
> 3 files changed, 49 insertions(+), 38 deletions(-)
>
> diff --git a/autocorrect.c b/autocorrect.c
> index 1037f032019b..50d7f116d85d 100644
> --- a/autocorrect.c
> +++ b/autocorrect.c
> @@ -6,7 +6,7 @@
> #include "prompt.h"
> #include "gettext.h"
>
> -static int parse_autocorrect(const char *value)
> +static enum autocorr_mode parse_autocorrect(const char *value)
> {
> switch (git_parse_maybe_bool_text(value)) {
> case 1:
> @@ -19,41 +19,49 @@ static int parse_autocorrect(const char *value)
>
> if (!strcmp(value, "prompt"))
> return AUTOCORRECT_PROMPT;
> - if (!strcmp(value, "never"))
> + else if (!strcmp(value, "never"))
> return AUTOCORRECT_NEVER;
> - if (!strcmp(value, "immediate"))
> + else if (!strcmp(value, "immediate"))
> return AUTOCORRECT_IMMEDIATELY;
> - if (!strcmp(value, "show"))
> + else if (!strcmp(value, "show"))
> return AUTOCORRECT_SHOW;
> -
> - return 0;
> + else
> + return AUTOCORRECT_DELAY;
> }
>
Okay so since we introduce an enum we use that here.
> void autocorr_resolve_config(const char *var, const char *value,
> const struct config_context *ctx, void *data)
> {
> - int *out = data;
> + struct autocorr *conf = data;
> +
> + if (strcmp(var, "help.autocorrect"))
> + return;
> +
> + conf->mode = parse_autocorrect(value);
>
> - if (!strcmp(var, "help.autocorrect")) {
> - int v = parse_autocorrect(value);
> + /*
> + * Disable autocorrection prompt in a non-interactive session.
> + */
> + if (conf->mode == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))
> + conf->mode = AUTOCORRECT_NEVER;
>
> - if (!v) {
> - v = git_config_int(var, value, ctx->kvi);
> - if (v < 0 || v == 1)
> - v = AUTOCORRECT_IMMEDIATELY;
> - }
> + if (conf->mode == AUTOCORRECT_DELAY) {
> + conf->delay = git_config_int(var, value, ctx->kvi);
>
> - *out = v;
> + if (!conf->delay)
> + conf->mode = AUTOCORRECT_SHOW;
> + else if (conf->delay <= 1)
> + conf->mode = AUTOCORRECT_IMMEDIATELY;
> }
> }
>
> -void autocorr_confirm(int autocorrect, const char *assumed)
> +void autocorr_confirm(struct autocorr *conf, const char *assumed)
> {
> - if (autocorrect == AUTOCORRECT_IMMEDIATELY) {
> + if (conf->mode == AUTOCORRECT_IMMEDIATELY) {
> fprintf_ln(stderr,
> _("Continuing under the assumption that you meant '%s'."),
> assumed);
> - } else if (autocorrect == AUTOCORRECT_PROMPT) {
> + } else if (conf->mode == AUTOCORRECT_PROMPT) {
> char *answer;
> struct strbuf msg = STRBUF_INIT;
>
> @@ -63,10 +71,10 @@ void autocorr_confirm(int autocorrect, const char *assumed)
>
> if (!(starts_with(answer, "y") || starts_with(answer, "Y")))
> exit(1);
> - } else {
> + } else if (conf->mode == AUTOCORRECT_DELAY) {
> fprintf_ln(stderr,
> _("Continuing in %0.1f seconds, assuming that you meant '%s'."),
> - (float)autocorrect / 10.0, assumed);
> - sleep_millisec(autocorrect * 100);
> + conf->delay / 10.0, assumed);
> + sleep_millisec(conf->delay * 100);
> }
> }
> diff --git a/autocorrect.h b/autocorrect.h
> index 45609990c77e..ce4a68379f2f 100644
> --- a/autocorrect.h
> +++ b/autocorrect.h
> @@ -1,16 +1,24 @@
> #ifndef AUTOCORRECT_H
> #define AUTOCORRECT_H
>
> -#define AUTOCORRECT_SHOW (-4)
> -#define AUTOCORRECT_PROMPT (-3)
> -#define AUTOCORRECT_NEVER (-2)
> -#define AUTOCORRECT_IMMEDIATELY (-1)
> -
> struct config_context;
>
> +enum autocorr_mode {
> + AUTOCORRECT_SHOW,
> + AUTOCORRECT_NEVER,
> + AUTOCORRECT_PROMPT,
> + AUTOCORRECT_IMMEDIATELY,
> + AUTOCORRECT_DELAY,
> +};
> +
> +struct autocorr {
> + enum autocorr_mode mode;
> + int delay;
> +};
> +
I would say the naming doesn't indicate what it is used for. How about
'autocorrect_config'?
[snip]
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 4/8] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINTONLY
2026-03-10 11:41 ` [PATCH v3 4/8] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINTONLY Jiamu Sun
@ 2026-03-10 14:08 ` Karthik Nayak
2026-03-11 2:46 ` Jiamu Sun
0 siblings, 1 reply; 67+ messages in thread
From: Karthik Nayak @ 2026-03-10 14:08 UTC (permalink / raw)
To: Jiamu Sun, git; +Cc: Junio C Hamano
[-- Attachment #1: Type: text/plain, Size: 498 bytes --]
Jiamu Sun <39@barroit.sh> writes:
> AUTOCORRECT_SHOW is ambiguous. Its purpose is to show commands similar
> to the unknown one and take no other action. Rename it to fit the
> semantics.
>
I'm not sure if AUTOCORRECT_HINTONLY is any better than
AUTOCORRECT_SHOW. The latter indicates that we show the user something.
Doesn't the former also mean the same?
If we do decide to go forward with AUTOCORRECT_HINTONLY, can we rename
it to AUTOCORRECT_HINT_ONLY? It's easier to read that way.
[snip]
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 5/8] autocorrect: provide config resolution API
2026-03-10 11:41 ` [PATCH v3 5/8] autocorrect: provide config resolution API Jiamu Sun
@ 2026-03-10 14:15 ` Karthik Nayak
0 siblings, 0 replies; 67+ messages in thread
From: Karthik Nayak @ 2026-03-10 14:15 UTC (permalink / raw)
To: Jiamu Sun, git; +Cc: Junio C Hamano
[-- Attachment #1: Type: text/plain, Size: 1578 bytes --]
Jiamu Sun <39@barroit.sh> writes:
> Add autocorr_resolve(). This resolves and populates the correct values
> for autocorrect config.
>
> Make autocorrect config callback internal. The API is meant to provide
> a high-level way to retrieve the config. Allowing access to the config
> callback from outside violates that intent.
>
> Additionally, in some cases, without access to the config callback, two
> config iterations cannot be merged into one, which can hurt performance.
> This is fine, as the code path that calls autocorr_resolve() is cold.
>
> Signed-off-by: Jiamu Sun <39@barroit.sh>
> ---
> autocorrect.c | 15 ++++++++++++---
> autocorrect.h | 5 +----
> help.c | 37 +++++++++++++++----------------------
> 3 files changed, 28 insertions(+), 29 deletions(-)
>
> diff --git a/autocorrect.c b/autocorrect.c
> index 9c4b691fb003..63fa331ef5e2 100644
> --- a/autocorrect.c
> +++ b/autocorrect.c
> @@ -1,3 +1,5 @@
> +#define USE_THE_REPOSITORY_VARIABLE
> +
> #include "git-compat-util.h"
> #include "autocorrect.h"
> #include "config.h"
> @@ -29,13 +31,13 @@ static enum autocorr_mode parse_autocorrect(const char *value)
> return AUTOCORRECT_DELAY;
> }
>
> -void autocorr_resolve_config(const char *var, const char *value,
> - const struct config_context *ctx, void *data)
> +static int resolve_autocorr(const char *var, const char *value,
> + const struct config_context *ctx, void *data)
So we made this an internal, the return type was changed as
`read_early_config()` expects that. Okay
[snip]
The rest of the code makes sense too
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 2/8] help: make autocorrect handling reusable
2026-03-10 12:52 ` Karthik Nayak
@ 2026-03-10 20:10 ` Junio C Hamano
2026-03-11 2:05 ` Jiamu Sun
2026-03-11 1:58 ` Jiamu Sun
1 sibling, 1 reply; 67+ messages in thread
From: Junio C Hamano @ 2026-03-10 20:10 UTC (permalink / raw)
To: Karthik Nayak; +Cc: Jiamu Sun, git
Karthik Nayak <karthik.188@gmail.com> writes:
>> +#define AUTOCORRECT_SHOW (-4)
>> +#define AUTOCORRECT_PROMPT (-3)
>> +#define AUTOCORRECT_NEVER (-2)
>> +#define AUTOCORRECT_IMMEDIATELY (-1)
>> +
>> +struct config_context;
>> +
>> +void autocorr_resolve_config(const char *var, const char *value,
>> + const struct config_context *ctx, void *data);
>> +
>> +void autocorr_confirm(int autocorr, const char *assumed);
>> +
>
> Why not use s/autocorr/autocorrect/ ? Also would be nice to add some
> documentation about each of the functions here.
Good.
>
> [snip]
>
> Also got this from running `git-clang-format` on this commit. Generally
> applying changes while moving code makes it harder to review. But here
> the changes are small enough that we could get away with it. I'll leave
> it to you.
No, you cannot leave it to the author.
Leave it to CodingGuidelines; case and switch would start at the
same column.
> diff --git a/autocorrect.c b/autocorrect.c
> index 1037f03201..87351fd08f 100644
> --- a/autocorrect.c
> +++ b/autocorrect.c
> @@ -9,12 +9,12 @@
> static int parse_autocorrect(const char *value)
> {
> switch (git_parse_maybe_bool_text(value)) {
> - case 1:
> - return AUTOCORRECT_IMMEDIATELY;
> - case 0:
> - return AUTOCORRECT_SHOW;
> - default: /* other random text */
> - break;
> + case 1:
> + return AUTOCORRECT_IMMEDIATELY;
> + case 0:
> + return AUTOCORRECT_SHOW;
> + default: /* other random text */
> + break;
> }
>
> if (!strcmp(value, "prompt"))
> diff --git a/autocorrect.h b/autocorrect.h
> index 45609990c7..38f1e73131 100644
> --- a/autocorrect.h
> +++ b/autocorrect.h
> @@ -1,9 +1,9 @@
> #ifndef AUTOCORRECT_H
> #define AUTOCORRECT_H
>
> -#define AUTOCORRECT_SHOW (-4)
> -#define AUTOCORRECT_PROMPT (-3)
> -#define AUTOCORRECT_NEVER (-2)
> +#define AUTOCORRECT_SHOW (-4)
> +#define AUTOCORRECT_PROMPT (-3)
> +#define AUTOCORRECT_NEVER (-2)
> #define AUTOCORRECT_IMMEDIATELY (-1)
>
> struct config_context;
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 6/8] parseopt: autocorrect mistyped subcommands
2026-03-10 11:41 ` [PATCH v3 6/8] parseopt: autocorrect mistyped subcommands Jiamu Sun
@ 2026-03-10 20:16 ` Junio C Hamano
2026-03-11 2:48 ` Jiamu Sun
0 siblings, 1 reply; 67+ messages in thread
From: Junio C Hamano @ 2026-03-10 20:16 UTC (permalink / raw)
To: Jiamu Sun; +Cc: git
Jiamu Sun <39@barroit.sh> writes:
> +static int similar_enough(const char *cmd, unsigned int dist)
> +{
> + size_t len = strlen(cmd);
> + unsigned int threshold = len < 3 ? 1 : len < 6 ? 3 : 6;
> +
> + return dist < threshold;
> +}
There should be some explanation on the reason why this is very
different from SIMILAR_ENOUGH used in help.c for main commands,
especially given that the levenshtein() call here uses identical
weight parameters (0,2,1,3) as used by the call there.
> +static const char *autocorrect_subcommand(const char *cmd,
> + ...
> + for_each_string_list_item(cand, cmds) {
> + unsigned int dist = levenshtein(cmd, cand->string, 0, 2, 1, 3);
> +
> + if (dist < min) {
> + min = dist;
> + best = cand;
> + ties = 0;
> + } else if (dist == min) {
> + ties++;
> + }
> + }
> +
> + if (!ties && similar_enough(cmd, min)) {
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step()
2026-03-10 12:46 ` Karthik Nayak
@ 2026-03-11 1:49 ` Jiamu Sun
2026-03-11 4:20 ` Junio C Hamano
2026-03-11 4:20 ` Junio C Hamano
0 siblings, 2 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-11 1:49 UTC (permalink / raw)
To: Karthik Nayak; +Cc: git, Junio C Hamano
On Tue, Mar 10, 2026 at 05:46:12AM -0700, Karthik Nayak wrote:
> > + if (ctx->has_subcommands) {
> > + return handle_subcommand(ctx, arg, options,
> > + usagestr);
> > }
> > +
>
> Nit: we try to avoid braces around single statement blocks.
I'm not sure if we should drop the braces in this case. I mean, the
statement is indeed a single one, but it spans multiple lines. Keeping
the braces improves readability. Also, CodingGuidelines says: "When the
statement extends over a few lines" use braces. So I think we should
keep those braces?
--
Jiamu Sun <39@barroit.sh>
<sunjiamu@outlook.com>
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 2/8] help: make autocorrect handling reusable
2026-03-10 12:52 ` Karthik Nayak
2026-03-10 20:10 ` Junio C Hamano
@ 2026-03-11 1:58 ` Jiamu Sun
1 sibling, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-11 1:58 UTC (permalink / raw)
To: Karthik Nayak; +Cc: git, Junio C Hamano
On Tue, Mar 10, 2026 at 05:52:58AM -0700, Karthik Nayak wrote:
> > autocorrect.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++
> > autocorrect.h | 16 ++++++++++++
> > help.c | 64 +++------------------------------------------
> > 4 files changed, 93 insertions(+), 60 deletions(-)
> > create mode 100644 autocorrect.c
> > create mode 100644 autocorrect.h
> >
>
> This should also be added to meson.build.
Sure, and if I missed anything else, please tell me, thanks.
--
Jiamu Sun <39@barroit.sh>
<sunjiamu@outlook.com>
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 2/8] help: make autocorrect handling reusable
2026-03-10 20:10 ` Junio C Hamano
@ 2026-03-11 2:05 ` Jiamu Sun
0 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-11 2:05 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Karthik Nayak, git
On Tue, Mar 10, 2026 at 01:10:22PM -0700, Junio C Hamano wrote:
> > Why not use s/autocorr/autocorrect/ ? Also would be nice to add some
> > documentation about each of the functions here.
>
> Good.
Used autocorr just because it's shorter than autocorrect. Will rename it
and add docs in v4.
> > Also got this from running `git-clang-format` on this commit. Generally
> > applying changes while moving code makes it harder to review. But here
> > the changes are small enough that we could get away with it. I'll leave
> > it to you.
>
> No, you cannot leave it to the author.
>
> Leave it to CodingGuidelines; case and switch would start at the
> same column.
Was just copy-pasted as-is. Will fix.
--
Jiamu Sun <39@barroit.sh>
<sunjiamu@outlook.com>
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 3/8] help: move tty check for autocorrection to autocorrect.c
2026-03-10 14:06 ` Karthik Nayak
@ 2026-03-11 2:16 ` Jiamu Sun
2026-03-12 0:10 ` Jiamu Sun
1 sibling, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-11 2:16 UTC (permalink / raw)
To: Karthik Nayak; +Cc: git, Junio C Hamano
On Tue, Mar 10, 2026 at 07:06:17AM -0700, Karthik Nayak wrote:
> > +struct autocorr {
> > + enum autocorr_mode mode;
> > + int delay;
> > +};
> > +
>
> I would say the naming doesn't indicate what it is used for. How about
> 'autocorrect_config'?
Using autocorrect_config is best for semantics. The only problem is that
it's too long. Since currently, this struct contains all fields needed
by our autocorrect_*, using "autocorrect" to describe it is enough,
perhaps?
--
Jiamu Sun <39@barroit.sh>
<sunjiamu@outlook.com>
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 4/8] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINTONLY
2026-03-10 14:08 ` Karthik Nayak
@ 2026-03-11 2:46 ` Jiamu Sun
0 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-11 2:46 UTC (permalink / raw)
To: Karthik Nayak; +Cc: git, Junio C Hamano
On Tue, Mar 10, 2026 at 07:08:52AM -0700, Karthik Nayak wrote:
>
> [snip]
>
> I'm not sure if AUTOCORRECT_HINTONLY is any better than
> AUTOCORRECT_SHOW. The latter indicates that we show the user something.
> Doesn't the former also mean the same?
You are right, it indicates that we show the user something. But show
what? A prompt can also contain a message that acts as a "show", and I
was quite confused when I first saw this part. On the other hand,
HINTONLY has a clear intent, which is "hint".
> If we do decide to go forward with AUTOCORRECT_HINTONLY, can we rename
> it to AUTOCORRECT_HINT_ONLY? It's easier to read that way.
AUTOCORRECT_HINT_ONLY breaks the current naming pattern of the mode
enum. Maybe we can use "AUTOCORRECT_HINT", as the "only" intent is
clear with other enum items.
--
Jiamu Sun <39@barroit.sh>
<sunjiamu@outlook.com>
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 6/8] parseopt: autocorrect mistyped subcommands
2026-03-10 20:16 ` Junio C Hamano
@ 2026-03-11 2:48 ` Jiamu Sun
2026-03-11 23:26 ` Jiamu Sun
0 siblings, 1 reply; 67+ messages in thread
From: Jiamu Sun @ 2026-03-11 2:48 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
On Tue, Mar 10, 2026 at 01:16:39PM -0700, Junio C Hamano wrote:
> > + unsigned int threshold = len < 3 ? 1 : len < 6 ? 3 : 6;
> > +
> > + return dist < threshold;
> > +}
>
> There should be some explanation on the reason why this is very
> different from SIMILAR_ENOUGH used in help.c for main commands,
> especially given that the levenshtein() call here uses identical
> weight parameters (0,2,1,3) as used by the call there.
Will add a comment to explain it.
--
Jiamu Sun <39@barroit.sh>
<sunjiamu@outlook.com>
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step()
2026-03-11 1:49 ` Jiamu Sun
@ 2026-03-11 4:20 ` Junio C Hamano
2026-03-11 4:20 ` Junio C Hamano
1 sibling, 0 replies; 67+ messages in thread
From: Junio C Hamano @ 2026-03-11 4:20 UTC (permalink / raw)
To: Jiamu Sun; +Cc: Karthik Nayak, git
Jiamu Sun <39@barroit.sh> writes:
> On Tue, Mar 10, 2026 at 05:46:12AM -0700, Karthik Nayak wrote:
>> > + if (ctx->has_subcommands) {
>> > + return handle_subcommand(ctx, arg, options,
>> > + usagestr);
>> > }
>> > +
>>
>> Nit: we try to avoid braces around single statement blocks.
>
> I'm not sure if we should drop the braces in this case.
You should.
You can tell that it is a single statement immediately after seeing
the beginning of the line, which says "return". It does not matter
how many lines the function call that follows "return" spans.
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step()
2026-03-11 1:49 ` Jiamu Sun
2026-03-11 4:20 ` Junio C Hamano
@ 2026-03-11 4:20 ` Junio C Hamano
2026-03-11 6:12 ` Jiamu Sun
1 sibling, 1 reply; 67+ messages in thread
From: Junio C Hamano @ 2026-03-11 4:20 UTC (permalink / raw)
To: Jiamu Sun; +Cc: Karthik Nayak, git
Jiamu Sun <39@barroit.sh> writes:
> On Tue, Mar 10, 2026 at 05:46:12AM -0700, Karthik Nayak wrote:
>> > + if (ctx->has_subcommands) {
>> > + return handle_subcommand(ctx, arg, options,
>> > + usagestr);
>> > }
>> > +
>>
>> Nit: we try to avoid braces around single statement blocks.
>
> I'm not sure if we should drop the braces in this case.
You should.
You can tell that it is a single statement immediately after seeing
the beginning of the line, which says "return". It does not matter
how many lines the function call that follows "return" spans.
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 8/8] help: add tests for subcommand autocorrection
2026-03-10 11:41 ` [PATCH v3 8/8] help: add tests for subcommand autocorrection Jiamu Sun
@ 2026-03-11 4:23 ` Junio C Hamano
2026-03-11 6:10 ` Jiamu Sun
0 siblings, 1 reply; 67+ messages in thread
From: Junio C Hamano @ 2026-03-11 4:23 UTC (permalink / raw)
To: Jiamu Sun; +Cc: git
Jiamu Sun <39@barroit.sh> writes:
> These tests cover default behavior (help.autocorrect is unset), no
> correction, immediate correction, delayed correction, and rejection
> when the typo is too dissimilar.
>
> Signed-off-by: Jiamu Sun <39@barroit.sh>
> ---
> Changes in v3:
> - Fix coding style issue
>
> t/t9004-autocorrect-subcommand.sh | 51 +++++++++++++++++++++++++++++++
> 1 file changed, 51 insertions(+)
> create mode 100755 t/t9004-autocorrect-subcommand.sh
t/meson.build needs to be told about this file, or you'll break CI,
like this:
https://github.com/git/git/actions/runs/22929006339/job/66546202060
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 8/8] help: add tests for subcommand autocorrection
2026-03-11 4:23 ` Junio C Hamano
@ 2026-03-11 6:10 ` Jiamu Sun
0 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-11 6:10 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
On Tue, Mar 10, 2026 at 09:23:03PM -0700, Junio C Hamano wrote:
> t/meson.build needs to be told about this file, or you'll break CI,
> like this:
>
> https://github.com/git/git/actions/runs/22929006339/job/66546202060
Will update and verify the meson build. Thanks for the catch.
--
Jiamu Sun <39@barroit.sh>
<sunjiamu@outlook.com>
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step()
2026-03-11 4:20 ` Junio C Hamano
@ 2026-03-11 6:12 ` Jiamu Sun
0 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-11 6:12 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Karthik Nayak, git
On Tue, Mar 10, 2026 at 09:20:30PM -0700, Junio C Hamano wrote:
> You can tell that it is a single statement immediately after seeing
> the beginning of the line, which says "return". It does not matter
> how many lines the function call that follows "return" spans.
Makes sense, will drop it.
--
Jiamu Sun <39@barroit.sh>
<sunjiamu@outlook.com>
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH 5/5] help: add tests for subcommand autocorrection
2026-03-08 12:17 ` [PATCH 5/5] help: add tests for subcommand autocorrection Jiamu Sun
@ 2026-03-11 17:01 ` Aaron Plattner
2026-03-11 22:45 ` Jiamu Sun
0 siblings, 1 reply; 67+ messages in thread
From: Aaron Plattner @ 2026-03-11 17:01 UTC (permalink / raw)
To: Jiamu Sun, git; +Cc: Junio C Hamano
On 3/8/26 5:17 AM, Jiamu Sun wrote:
> These tests cover default behavior (help.autocorrect is unset), no
> correction, immediate correction, delayed correction, and rejection
> when the typo is too dissimilar.
>
> Signed-off-by: Jiamu Sun <39@barroit.sh>
> ---
> t/t9004-autocorrect-subcommand.sh | 49 +++++++++++++++++++++++++++++++
> 1 file changed, 49 insertions(+)
> create mode 100755 t/t9004-autocorrect-subcommand.sh
Adding this causes meson setup to fail:
t/meson.build:1193:6: ERROR: Problem encountered: Test files found, but
not configured:
- t9004-autocorrect-subcommand.sh
I think you just need to add it to meson.build:
diff --git a/t/meson.build b/t/meson.build
index f66a73f8a0..bf0503d705 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -973,6 +973,7 @@ integration_tests = [
't9001-send-email.sh',
't9002-column.sh',
't9003-help-autocorrect.sh',
+ 't9004-autocorrect-subcommand.sh',
't9100-git-svn-basic.sh',
't9101-git-svn-props.sh',
't9102-git-svn-deep-rmdir.sh',
-- Aaron
>
> diff --git a/t/t9004-autocorrect-subcommand.sh b/t/t9004-autocorrect-subcommand.sh
> new file mode 100755
> index 000000000000..760760c8851a
> --- /dev/null
> +++ b/t/t9004-autocorrect-subcommand.sh
> @@ -0,0 +1,49 @@
> +#!/bin/sh
> +
> +test_description='subcommand auto-correction test
> +
> +Test autocorrection for subcommands with different
> +help.autocorrect mode.'
> +
> +. ./test-lib.sh
> +
> +test_expect_success 'setup' "
> + echo '^error: unknown subcommand: ' >grep_unknown
> +"
> +
> +test_expect_success 'default is not to autocorrect' '
> + test_must_fail git worktree lsit 2>actual &&
> + head -n1 actual >first && test_grep -f grep_unknown first
> +'
> +
> +for mode in false no off 0 show never; do
> + test_expect_success "'$mode' disables autocorrection" "
> + test_config help.autocorrect $mode &&
> +
> + test_must_fail git worktree lsit 2>actual &&
> + head -n1 actual >first && test_grep -f grep_unknown first
> + "
> +done
> +
> +for mode in -39 immediate 1; do
> + test_expect_success "autocorrect immediately with '$mode'" - <<-EOT
> + test_config help.autocorrect $mode &&
> +
> + git worktree lsit 2>actual &&
> + test_grep "you meant 'list'\.$" actual
> + EOT
> +done
> +
> +test_expect_success 'delay path is executed' - <<-\EOT
> + test_config help.autocorrect 2 &&
> +
> + git worktree lsit 2>actual &&
> + test_grep '^Continuing in 0.2 seconds, ' actual
> +EOT
> +
> +test_expect_success 'deny if too dissimilar' - <<-\EOT
> + test_must_fail git remote rensnr 2>actual &&
> + head -n1 actual >first && test_grep -f grep_unknown first
> +EOT
> +
> +test_done
^ permalink raw reply related [flat|nested] 67+ messages in thread
* Re: [PATCH 5/5] help: add tests for subcommand autocorrection
2026-03-11 17:01 ` Aaron Plattner
@ 2026-03-11 22:45 ` Jiamu Sun
2026-03-12 13:35 ` Junio C Hamano
0 siblings, 1 reply; 67+ messages in thread
From: Jiamu Sun @ 2026-03-11 22:45 UTC (permalink / raw)
To: Aaron Plattner; +Cc: git, Junio C Hamano
On Wed, Mar 11, 2026 at 10:01:55AM -0700, Aaron Plattner wrote:
> t/meson.build:1193:6: ERROR: Problem encountered: Test files found, but not
> configured:
>
> - t9004-autocorrect-subcommand.sh
>
>
> I think you just need to add it to meson.build:
Will add it, thanks!
--
Jiamu Sun <39@barroit.sh>
<sunjiamu@outlook.com>
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 6/8] parseopt: autocorrect mistyped subcommands
2026-03-11 2:48 ` Jiamu Sun
@ 2026-03-11 23:26 ` Jiamu Sun
2026-03-12 2:38 ` Junio C Hamano
0 siblings, 1 reply; 67+ messages in thread
From: Jiamu Sun @ 2026-03-11 23:26 UTC (permalink / raw)
To: Junio C Hamano, git
On Wed, Mar 11, 2026 at 11:48:41AM +0900, Jiamu Sun wrote:
> > There should be some explanation on the reason why this is very
> > different from SIMILAR_ENOUGH used in help.c for main commands,
> > especially given that the levenshtein() call here uses identical
> > weight parameters (0,2,1,3) as used by the call there.
>
> Will add a comment to explain it.
/* skip and count prefix matches */
for (n = 0; n < main_cmds.cnt && !main_cmds.names[n]->len; n++)
; /* still counting */
if (main_cmds.cnt <= n) {
/* prefix matches with everything? that is too ambiguous */
best_similarity = SIMILARITY_FLOOR + 1;
} else {
/* count all the most similar ones */
for (best_similarity = main_cmds.names[n++]->len;
(n < main_cmds.cnt &&
best_similarity == main_cmds.names[n]->len);
n++)
; /* still counting */
}
if (autocorrect.mode != AUTOCORRECT_HINTONLY && n == 1 &&
If I read the code correctly, for the prefix matched case, the similar
command finding in help.c skips all prefix matched strings and increases
"n". Inside the "else", since that "n++", the "n" will always be greater
than one, thus no correction happens, e.g., it doesn't autocorrect
"commi" to "commit". Do you know if this behavior is by design or
something else?
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 3/8] help: move tty check for autocorrection to autocorrect.c
2026-03-10 14:06 ` Karthik Nayak
2026-03-11 2:16 ` Jiamu Sun
@ 2026-03-12 0:10 ` Jiamu Sun
1 sibling, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-12 0:10 UTC (permalink / raw)
To: Karthik Nayak; +Cc: git, Junio C Hamano
On Tue, Mar 10, 2026 at 07:06:17AM -0700, Karthik Nayak wrote:
> > + return AUTOCORRECT_DELAY;
> > }
> >
>
> Okay so since we introduce an enum we use that here.
Ah actually, I messed up this patch. There was a commit that moved
autocorrect config into a struct and introduced the mode enum. But
somehow it's gone. I'll split this patch into two.
--
Jiamu Sun <39@barroit.sh>
<sunjiamu@outlook.com>
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 6/8] parseopt: autocorrect mistyped subcommands
2026-03-11 23:26 ` Jiamu Sun
@ 2026-03-12 2:38 ` Junio C Hamano
2026-03-12 4:53 ` Jiamu Sun
0 siblings, 1 reply; 67+ messages in thread
From: Junio C Hamano @ 2026-03-12 2:38 UTC (permalink / raw)
To: Jiamu Sun; +Cc: git
Jiamu Sun <39@barroit.sh> writes:
> "commi" to "commit". Do you know if this behavior is by design or
> something else?
I don't, but others who have their hand in the code may. "git
blame" or "git shortlog --no-merges" may be good tools to use to
find out who they are.
Thanks.
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v3 6/8] parseopt: autocorrect mistyped subcommands
2026-03-12 2:38 ` Junio C Hamano
@ 2026-03-12 4:53 ` Jiamu Sun
0 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-12 4:53 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, Erik Faye-Lund
On Wed, Mar 11, 2026 at 07:38:51PM -0700, Junio C Hamano wrote:
> I don't, but others who have their hand in the code may. "git
> blame" or "git shortlog --no-merges" may be good tools to use to
> find out who they are.
Thanks for the pointer!
+Cc Erik Faye-Lund <kusmabite@gmail.com>
The reason I asked is that I'm documenting similar_enough() for
subcommand autocorrection in this series. It allows "lis" to correct to
"list", but avoids "ad" to "add".
I noticed that the command autocorrection ignores prefix-matched strings
entirely (e.g., not correcting "commi" to "commit", as introduced in
commit 6612b9e471a3). I wanted to understand the rationale behind this
to see if subcommands should align with that design.
Thanks.
--
Jiamu Sun <39@barroit.sh>
<sunjiamu@outlook.com>
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH 5/5] help: add tests for subcommand autocorrection
2026-03-11 22:45 ` Jiamu Sun
@ 2026-03-12 13:35 ` Junio C Hamano
0 siblings, 0 replies; 67+ messages in thread
From: Junio C Hamano @ 2026-03-12 13:35 UTC (permalink / raw)
To: Jiamu Sun; +Cc: Aaron Plattner, git
Jiamu Sun <39@barroit.sh> writes:
> On Wed, Mar 11, 2026 at 10:01:55AM -0700, Aaron Plattner wrote:
>> t/meson.build:1193:6: ERROR: Problem encountered: Test files found, but not
>> configured:
>>
>> - t9004-autocorrect-subcommand.sh
>>
>>
>> I think you just need to add it to meson.build:
>
> Will add it, thanks!
In the meantime, I have the following as a fixup patch on top of the
series. The 'seen' branch with the topic still seems to be failing
with meson build in my environment, but I didn't have time to dig
further.
diff --git a/meson.build b/meson.build
index 4b536e0124..0429e80a5c 100644
--- a/meson.build
+++ b/meson.build
@@ -283,6 +283,7 @@ libgit_sources = [
'archive-zip.c',
'archive.c',
'attr.c',
+ 'autocorrect.c',
'base85.c',
'bisect.c',
'blame.c',
diff --git a/t/meson.build b/t/meson.build
index f66a73f8a0..bf0503d705 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -973,6 +973,7 @@ integration_tests = [
't9001-send-email.sh',
't9002-column.sh',
't9003-help-autocorrect.sh',
+ 't9004-autocorrect-subcommand.sh',
't9100-git-svn-basic.sh',
't9101-git-svn-props.sh',
't9102-git-svn-deep-rmdir.sh',
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v4 00/10] parseopt: add subcommand autocorrection
2026-03-10 11:40 ` [PATCH v3 0/8] parseopt: add " Jiamu Sun
` (7 preceding siblings ...)
2026-03-10 11:41 ` [PATCH v3 8/8] help: add tests for subcommand autocorrection Jiamu Sun
@ 2026-03-16 15:36 ` Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 01/10] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
` (9 more replies)
8 siblings, 10 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-16 15:36 UTC (permalink / raw)
To: git; +Cc: Aaron Plattner, Junio C Hamano, Karthik Nayak, Jiamu Sun
Git currently provides auto-correction for builtins and aliases, but
lacks this functionality for subcommands parsed via the parse-options
API. Subcommands are also commands, and typos will occur, too. Like:
git remote add-rul
So, this series introduces subcommand auto-correction.
By default, this implementation enables autocorrection for builtins
with mandatory subcommands. However, for those using
PARSE_OPT_SUBCOMMAND_OPTIONAL, autocorrection is skipped to avoid
misinterpreting legitimate unknown arguments as mistyped subcommands.
To allow builtins with optional subcommands to explicitly opt in,
this series adds the PARSE_OPT_SUBCOMMAND_AUTOCORR flag, and enables
it for git-remote and git-notes.
Additionally, the existing autocorrection logic is extracted from
help.c so subcommand handling can reuse the same config parsing and
prompt/delay logic.
Some split string literals are also combined so the full text is easier
to grep for.
Changes in v4:
- Add missing files to Meson build
- Change API prefix from autocorr to autocorrect
- Split the commit that moves tty code
- Add API documentation
- Use standard Damerau-Levenshtein distance and common practice
fuzziness thresholds
- Rename AUTOCORRECT_HINTONLY to AUTOCORRECT_HINT
- Change commit subject prefix for tests from "help:" to "parseopt:"
- Fix coding style issues
Note on the autocorrection thresholds:
In v3, there was a discussion about why specific thresholds were chosen.
While attempting to document the legacy magic penalties (0, 2, 1, 3), I
realized those weights exist in a system where prefix matches are
completely ignored (due to a historical side-effect). Applying them to
subcommands (which do evaluate prefix matches) makes the tolerance
overly permissive. Therefore, v4 abandons the legacy weights in favor of
a standard Damerau-Levenshtein distance and common practice length-based
thresholds.
Changes in v3:
- Align with the coding guildline
- Split patch so diffs don't get hidden by code movement
- Improve commit messages
Changes in v2:
- Reword the explanation of default autocorrection behavior
Jiamu Sun (10):
parseopt: extract subcommand handling from parse_options_step()
help: make autocorrect handling reusable
help: move tty check for autocorrection to autocorrect.c
autocorrect: use mode and delay instead of magic numbers
autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINT
autocorrect: provide config resolution API
parseopt: autocorrect mistyped subcommands
parseopt: enable subcommand autocorrection for git-remote and
git-notes
parseopt: add tests for subcommand autocorrection
doc: document autocorrect API
Makefile | 1 +
autocorrect.c | 89 ++++++++++++++++
autocorrect.h | 32 ++++++
builtin/notes.c | 10 +-
builtin/remote.c | 12 +--
help.c | 107 ++++----------------
meson.build | 1 +
parse-options.c | 162 ++++++++++++++++++++++--------
parse-options.h | 1 +
t/meson.build | 1 +
t/t9004-autocorrect-subcommand.sh | 51 ++++++++++
11 files changed, 325 insertions(+), 142 deletions(-)
create mode 100644 autocorrect.c
create mode 100644 autocorrect.h
create mode 100755 t/t9004-autocorrect-subcommand.sh
base-commit: 795c338de725e13bd361214c6b768019fc45a2c1
--
2.53.0
^ permalink raw reply [flat|nested] 67+ messages in thread
* [PATCH v4 01/10] parseopt: extract subcommand handling from parse_options_step()
2026-03-16 15:36 ` [PATCH v4 00/10] parseopt: add " Jiamu Sun
@ 2026-03-16 15:36 ` Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 02/10] help: make autocorrect handling reusable Jiamu Sun
` (8 subsequent siblings)
9 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-16 15:36 UTC (permalink / raw)
To: git; +Cc: Aaron Plattner, Junio C Hamano, Karthik Nayak, Jiamu Sun
Move the subcommand branch out of parse_options_step() into a new
handle_subcommand() helper. Also, make parse_subcommand() return a
simple success/failure status.
This removes the switch over impossible parse_opt_result values and
makes the non-option path easier to follow and maintain.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
parse-options.c | 87 ++++++++++++++++++++++++++-----------------------
1 file changed, 46 insertions(+), 41 deletions(-)
diff --git a/parse-options.c b/parse-options.c
index c9cafc21b903..02a4f00919f6 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -605,17 +605,44 @@ static enum parse_opt_result parse_nodash_opt(struct parse_opt_ctx_t *p,
return PARSE_OPT_ERROR;
}
-static enum parse_opt_result parse_subcommand(const char *arg,
- const struct option *options)
+static int parse_subcommand(const char *arg, const struct option *options)
{
- for (; options->type != OPTION_END; options++)
- if (options->type == OPTION_SUBCOMMAND &&
- !strcmp(options->long_name, arg)) {
- *(parse_opt_subcommand_fn **)options->value = options->subcommand_fn;
- return PARSE_OPT_SUBCOMMAND;
- }
+ for (; options->type != OPTION_END; options++) {
+ parse_opt_subcommand_fn **opt_val;
- return PARSE_OPT_UNKNOWN;
+ if (options->type != OPTION_SUBCOMMAND ||
+ strcmp(options->long_name, arg))
+ continue;
+
+ opt_val = options->value;
+ *opt_val = options->subcommand_fn;
+ return 0;
+ }
+
+ return -1;
+}
+
+static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
+ const char *arg,
+ const struct option *options,
+ const char * const usagestr[])
+{
+ int err = parse_subcommand(arg, options);
+
+ if (!err)
+ return PARSE_OPT_SUBCOMMAND;
+
+ /*
+ * arg is neither a short or long option nor a subcommand. Since this
+ * command has a default operation mode, we have to treat this arg and
+ * all remaining args as args meant to that default operation mode.
+ * So we are done parsing.
+ */
+ if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
+ return PARSE_OPT_DONE;
+
+ error(_("unknown subcommand: `%s'"), arg);
+ usage_with_options(usagestr, options);
}
static void check_typos(const char *arg, const struct option *options)
@@ -990,38 +1017,16 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,
if (*arg != '-' || !arg[1]) {
if (parse_nodash_opt(ctx, arg, options) == 0)
continue;
- if (!ctx->has_subcommands) {
- if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)
- return PARSE_OPT_NON_OPTION;
- ctx->out[ctx->cpidx++] = ctx->argv[0];
- continue;
- }
- switch (parse_subcommand(arg, options)) {
- case PARSE_OPT_SUBCOMMAND:
- return PARSE_OPT_SUBCOMMAND;
- case PARSE_OPT_UNKNOWN:
- if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
- /*
- * arg is neither a short or long
- * option nor a subcommand. Since
- * this command has a default
- * operation mode, we have to treat
- * this arg and all remaining args
- * as args meant to that default
- * operation mode.
- * So we are done parsing.
- */
- return PARSE_OPT_DONE;
- error(_("unknown subcommand: `%s'"), arg);
- usage_with_options(usagestr, options);
- case PARSE_OPT_COMPLETE:
- case PARSE_OPT_HELP:
- case PARSE_OPT_ERROR:
- case PARSE_OPT_DONE:
- case PARSE_OPT_NON_OPTION:
- /* Impossible. */
- BUG("parse_subcommand() cannot return these");
- }
+
+ if (ctx->has_subcommands)
+ return handle_subcommand(ctx, arg, options,
+ usagestr);
+
+ if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)
+ return PARSE_OPT_NON_OPTION;
+
+ ctx->out[ctx->cpidx++] = ctx->argv[0];
+ continue;
}
/* lone -h asks for help */
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v4 02/10] help: make autocorrect handling reusable
2026-03-16 15:36 ` [PATCH v4 00/10] parseopt: add " Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 01/10] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
@ 2026-03-16 15:36 ` Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 03/10] help: move tty check for autocorrection to autocorrect.c Jiamu Sun
` (7 subsequent siblings)
9 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-16 15:36 UTC (permalink / raw)
To: git; +Cc: Aaron Plattner, Junio C Hamano, Karthik Nayak, Jiamu Sun
Move config parsing and prompt/delay handling into autocorrect.c and
expose them in autocorrect.h. This makes autocorrect reusable regardless
of which target links against it.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
Makefile | 1 +
autocorrect.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++
autocorrect.h | 16 ++++++++++++
help.c | 64 +++------------------------------------------
meson.build | 1 +
5 files changed, 94 insertions(+), 60 deletions(-)
create mode 100644 autocorrect.c
create mode 100644 autocorrect.h
diff --git a/Makefile b/Makefile
index f3264d0a37cc..6111631c2caa 100644
--- a/Makefile
+++ b/Makefile
@@ -1098,6 +1098,7 @@ LIB_OBJS += archive-tar.o
LIB_OBJS += archive-zip.o
LIB_OBJS += archive.o
LIB_OBJS += attr.o
+LIB_OBJS += autocorrect.o
LIB_OBJS += base85.o
LIB_OBJS += bisect.o
LIB_OBJS += blame.o
diff --git a/autocorrect.c b/autocorrect.c
new file mode 100644
index 000000000000..97145d3a53ce
--- /dev/null
+++ b/autocorrect.c
@@ -0,0 +1,72 @@
+#include "git-compat-util.h"
+#include "autocorrect.h"
+#include "config.h"
+#include "parse.h"
+#include "strbuf.h"
+#include "prompt.h"
+#include "gettext.h"
+
+static int parse_autocorrect(const char *value)
+{
+ switch (git_parse_maybe_bool_text(value)) {
+ case 1:
+ return AUTOCORRECT_IMMEDIATELY;
+ case 0:
+ return AUTOCORRECT_SHOW;
+ default: /* other random text */
+ break;
+ }
+
+ if (!strcmp(value, "prompt"))
+ return AUTOCORRECT_PROMPT;
+ if (!strcmp(value, "never"))
+ return AUTOCORRECT_NEVER;
+ if (!strcmp(value, "immediate"))
+ return AUTOCORRECT_IMMEDIATELY;
+ if (!strcmp(value, "show"))
+ return AUTOCORRECT_SHOW;
+
+ return 0;
+}
+
+void autocorrect_resolve_config(const char *var, const char *value,
+ const struct config_context *ctx, void *data)
+{
+ int *out = data;
+
+ if (!strcmp(var, "help.autocorrect")) {
+ int v = parse_autocorrect(value);
+
+ if (!v) {
+ v = git_config_int(var, value, ctx->kvi);
+ if (v < 0 || v == 1)
+ v = AUTOCORRECT_IMMEDIATELY;
+ }
+
+ *out = v;
+ }
+}
+
+void autocorrect_confirm(int autocorrect, const char *assumed)
+{
+ if (autocorrect == AUTOCORRECT_IMMEDIATELY) {
+ fprintf_ln(stderr,
+ _("Continuing under the assumption that you meant '%s'."),
+ assumed);
+ } else if (autocorrect == AUTOCORRECT_PROMPT) {
+ char *answer;
+ struct strbuf msg = STRBUF_INIT;
+
+ strbuf_addf(&msg, _("Run '%s' instead [y/N]? "), assumed);
+ answer = git_prompt(msg.buf, PROMPT_ECHO);
+ strbuf_release(&msg);
+
+ if (!(starts_with(answer, "y") || starts_with(answer, "Y")))
+ exit(1);
+ } else {
+ fprintf_ln(stderr,
+ _("Continuing in %0.1f seconds, assuming that you meant '%s'."),
+ (float)autocorrect / 10.0, assumed);
+ sleep_millisec(autocorrect * 100);
+ }
+}
diff --git a/autocorrect.h b/autocorrect.h
new file mode 100644
index 000000000000..f5fadf9d9605
--- /dev/null
+++ b/autocorrect.h
@@ -0,0 +1,16 @@
+#ifndef AUTOCORRECT_H
+#define AUTOCORRECT_H
+
+#define AUTOCORRECT_SHOW (-4)
+#define AUTOCORRECT_PROMPT (-3)
+#define AUTOCORRECT_NEVER (-2)
+#define AUTOCORRECT_IMMEDIATELY (-1)
+
+struct config_context;
+
+void autocorrect_resolve_config(const char *var, const char *value,
+ const struct config_context *ctx, void *data);
+
+void autocorrect_confirm(int autocorrect, const char *assumed);
+
+#endif /* AUTOCORRECT_H */
diff --git a/help.c b/help.c
index 95f576c5c81d..4acb6ca585ff 100644
--- a/help.c
+++ b/help.c
@@ -22,6 +22,7 @@
#include "repository.h"
#include "alias.h"
#include "utf8.h"
+#include "autocorrect.h"
#ifndef NO_CURL
#include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -541,34 +542,6 @@ struct help_unknown_cmd_config {
struct cmdnames aliases;
};
-#define AUTOCORRECT_SHOW (-4)
-#define AUTOCORRECT_PROMPT (-3)
-#define AUTOCORRECT_NEVER (-2)
-#define AUTOCORRECT_IMMEDIATELY (-1)
-
-static int parse_autocorrect(const char *value)
-{
- switch (git_parse_maybe_bool_text(value)) {
- case 1:
- return AUTOCORRECT_IMMEDIATELY;
- case 0:
- return AUTOCORRECT_SHOW;
- default: /* other random text */
- break;
- }
-
- if (!strcmp(value, "prompt"))
- return AUTOCORRECT_PROMPT;
- if (!strcmp(value, "never"))
- return AUTOCORRECT_NEVER;
- if (!strcmp(value, "immediate"))
- return AUTOCORRECT_IMMEDIATELY;
- if (!strcmp(value, "show"))
- return AUTOCORRECT_SHOW;
-
- return 0;
-}
-
static int git_unknown_cmd_config(const char *var, const char *value,
const struct config_context *ctx,
void *cb)
@@ -577,17 +550,7 @@ static int git_unknown_cmd_config(const char *var, const char *value,
const char *subsection, *key;
size_t subsection_len;
- if (!strcmp(var, "help.autocorrect")) {
- int v = parse_autocorrect(value);
-
- if (!v) {
- v = git_config_int(var, value, ctx->kvi);
- if (v < 0 || v == 1)
- v = AUTOCORRECT_IMMEDIATELY;
- }
-
- cfg->autocorrect = v;
- }
+ autocorrect_resolve_config(var, value, ctx, &cfg->autocorrect);
/* Also use aliases for command lookup */
if (!parse_config_key(var, "alias", &subsection, &subsection_len,
@@ -724,27 +687,8 @@ char *help_unknown_cmd(const char *cmd)
_("WARNING: You called a Git command named '%s', "
"which does not exist."),
cmd);
- if (cfg.autocorrect == AUTOCORRECT_IMMEDIATELY)
- fprintf_ln(stderr,
- _("Continuing under the assumption that "
- "you meant '%s'."),
- assumed);
- else if (cfg.autocorrect == AUTOCORRECT_PROMPT) {
- char *answer;
- struct strbuf msg = STRBUF_INIT;
- strbuf_addf(&msg, _("Run '%s' instead [y/N]? "), assumed);
- answer = git_prompt(msg.buf, PROMPT_ECHO);
- strbuf_release(&msg);
- if (!(starts_with(answer, "y") ||
- starts_with(answer, "Y")))
- exit(1);
- } else {
- fprintf_ln(stderr,
- _("Continuing in %0.1f seconds, "
- "assuming that you meant '%s'."),
- (float)cfg.autocorrect/10.0, assumed);
- sleep_millisec(cfg.autocorrect * 100);
- }
+
+ autocorrect_confirm(cfg.autocorrect, assumed);
cmdnames_release(&cfg.aliases);
cmdnames_release(&main_cmds);
diff --git a/meson.build b/meson.build
index 4b536e012481..0429e80a5c96 100644
--- a/meson.build
+++ b/meson.build
@@ -283,6 +283,7 @@ libgit_sources = [
'archive-zip.c',
'archive.c',
'attr.c',
+ 'autocorrect.c',
'base85.c',
'bisect.c',
'blame.c',
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v4 03/10] help: move tty check for autocorrection to autocorrect.c
2026-03-16 15:36 ` [PATCH v4 00/10] parseopt: add " Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 01/10] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 02/10] help: make autocorrect handling reusable Jiamu Sun
@ 2026-03-16 15:36 ` Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 04/10] autocorrect: use mode and delay instead of magic numbers Jiamu Sun
` (6 subsequent siblings)
9 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-16 15:36 UTC (permalink / raw)
To: git; +Cc: Aaron Plattner, Junio C Hamano, Karthik Nayak, Jiamu Sun
TTY checking is the autocorrect config parser's responsibility. It must
ensure the parsed value is correct and reliable. Thus, move the check to
autocorrect_resolve_config().
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
autocorrect.c | 24 ++++++++++++++++--------
help.c | 6 ------
2 files changed, 16 insertions(+), 14 deletions(-)
diff --git a/autocorrect.c b/autocorrect.c
index 97145d3a53ce..887d2396da44 100644
--- a/autocorrect.c
+++ b/autocorrect.c
@@ -33,18 +33,26 @@ void autocorrect_resolve_config(const char *var, const char *value,
const struct config_context *ctx, void *data)
{
int *out = data;
+ int parsed;
- if (!strcmp(var, "help.autocorrect")) {
- int v = parse_autocorrect(value);
+ if (strcmp(var, "help.autocorrect"))
+ return;
- if (!v) {
- v = git_config_int(var, value, ctx->kvi);
- if (v < 0 || v == 1)
- v = AUTOCORRECT_IMMEDIATELY;
- }
+ parsed = parse_autocorrect(value);
- *out = v;
+ /*
+ * Disable autocorrection prompt in a non-interactive session
+ */
+ if (parsed == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))
+ parsed = AUTOCORRECT_NEVER;
+
+ if (!parsed) {
+ parsed = git_config_int(var, value, ctx->kvi);
+ if (parsed < 0 || parsed == 1)
+ parsed = AUTOCORRECT_IMMEDIATELY;
}
+
+ *out = parsed;
}
void autocorrect_confirm(int autocorrect, const char *assumed)
diff --git a/help.c b/help.c
index 4acb6ca585ff..983057970e7c 100644
--- a/help.c
+++ b/help.c
@@ -607,12 +607,6 @@ char *help_unknown_cmd(const char *cmd)
read_early_config(the_repository, git_unknown_cmd_config, &cfg);
- /*
- * Disable autocorrection prompt in a non-interactive session
- */
- if ((cfg.autocorrect == AUTOCORRECT_PROMPT) && (!isatty(0) || !isatty(2)))
- cfg.autocorrect = AUTOCORRECT_NEVER;
-
if (cfg.autocorrect == AUTOCORRECT_NEVER) {
fprintf_ln(stderr, _("git: '%s' is not a git command. See 'git --help'."), cmd);
exit(1);
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v4 04/10] autocorrect: use mode and delay instead of magic numbers
2026-03-16 15:36 ` [PATCH v4 00/10] parseopt: add " Jiamu Sun
` (2 preceding siblings ...)
2026-03-16 15:36 ` [PATCH v4 03/10] help: move tty check for autocorrection to autocorrect.c Jiamu Sun
@ 2026-03-16 15:36 ` Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 05/10] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINT Jiamu Sun
` (5 subsequent siblings)
9 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-16 15:36 UTC (permalink / raw)
To: git; +Cc: Aaron Plattner, Junio C Hamano, Karthik Nayak, Jiamu Sun
Drop magic numbers and describe autocorrect config with a mode enum and
an integer delay. This reduces errors when mutating config values and
makes the values easier to access.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
autocorrect.c | 46 +++++++++++++++++++++++-----------------------
autocorrect.h | 20 ++++++++++++++------
help.c | 9 +++++----
3 files changed, 42 insertions(+), 33 deletions(-)
diff --git a/autocorrect.c b/autocorrect.c
index 887d2396da44..2484546fc731 100644
--- a/autocorrect.c
+++ b/autocorrect.c
@@ -6,7 +6,7 @@
#include "prompt.h"
#include "gettext.h"
-static int parse_autocorrect(const char *value)
+static enum autocorrect_mode parse_autocorrect(const char *value)
{
switch (git_parse_maybe_bool_text(value)) {
case 1:
@@ -19,49 +19,49 @@ static int parse_autocorrect(const char *value)
if (!strcmp(value, "prompt"))
return AUTOCORRECT_PROMPT;
- if (!strcmp(value, "never"))
+ else if (!strcmp(value, "never"))
return AUTOCORRECT_NEVER;
- if (!strcmp(value, "immediate"))
+ else if (!strcmp(value, "immediate"))
return AUTOCORRECT_IMMEDIATELY;
- if (!strcmp(value, "show"))
+ else if (!strcmp(value, "show"))
return AUTOCORRECT_SHOW;
-
- return 0;
+ else
+ return AUTOCORRECT_DELAY;
}
void autocorrect_resolve_config(const char *var, const char *value,
const struct config_context *ctx, void *data)
{
- int *out = data;
- int parsed;
+ struct autocorrect *conf = data;
if (strcmp(var, "help.autocorrect"))
return;
- parsed = parse_autocorrect(value);
+ conf->mode = parse_autocorrect(value);
/*
* Disable autocorrection prompt in a non-interactive session
*/
- if (parsed == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))
- parsed = AUTOCORRECT_NEVER;
+ if (conf->mode == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))
+ conf->mode = AUTOCORRECT_NEVER;
- if (!parsed) {
- parsed = git_config_int(var, value, ctx->kvi);
- if (parsed < 0 || parsed == 1)
- parsed = AUTOCORRECT_IMMEDIATELY;
- }
+ if (conf->mode == AUTOCORRECT_DELAY) {
+ conf->delay = git_config_int(var, value, ctx->kvi);
- *out = parsed;
+ if (!conf->delay)
+ conf->mode = AUTOCORRECT_SHOW;
+ else if (conf->delay < 0 || conf->delay == 1)
+ conf->mode = AUTOCORRECT_IMMEDIATELY;
+ }
}
-void autocorrect_confirm(int autocorrect, const char *assumed)
+void autocorrect_confirm(struct autocorrect *conf, const char *assumed)
{
- if (autocorrect == AUTOCORRECT_IMMEDIATELY) {
+ if (conf->mode == AUTOCORRECT_IMMEDIATELY) {
fprintf_ln(stderr,
_("Continuing under the assumption that you meant '%s'."),
assumed);
- } else if (autocorrect == AUTOCORRECT_PROMPT) {
+ } else if (conf->mode == AUTOCORRECT_PROMPT) {
char *answer;
struct strbuf msg = STRBUF_INIT;
@@ -71,10 +71,10 @@ void autocorrect_confirm(int autocorrect, const char *assumed)
if (!(starts_with(answer, "y") || starts_with(answer, "Y")))
exit(1);
- } else {
+ } else if (conf->mode == AUTOCORRECT_DELAY) {
fprintf_ln(stderr,
_("Continuing in %0.1f seconds, assuming that you meant '%s'."),
- (float)autocorrect / 10.0, assumed);
- sleep_millisec(autocorrect * 100);
+ conf->delay / 10.0, assumed);
+ sleep_millisec(conf->delay * 100);
}
}
diff --git a/autocorrect.h b/autocorrect.h
index f5fadf9d9605..5506a36f11a7 100644
--- a/autocorrect.h
+++ b/autocorrect.h
@@ -1,16 +1,24 @@
#ifndef AUTOCORRECT_H
#define AUTOCORRECT_H
-#define AUTOCORRECT_SHOW (-4)
-#define AUTOCORRECT_PROMPT (-3)
-#define AUTOCORRECT_NEVER (-2)
-#define AUTOCORRECT_IMMEDIATELY (-1)
-
struct config_context;
+enum autocorrect_mode {
+ AUTOCORRECT_SHOW,
+ AUTOCORRECT_NEVER,
+ AUTOCORRECT_PROMPT,
+ AUTOCORRECT_IMMEDIATELY,
+ AUTOCORRECT_DELAY,
+};
+
+struct autocorrect {
+ enum autocorrect_mode mode;
+ int delay;
+};
+
void autocorrect_resolve_config(const char *var, const char *value,
const struct config_context *ctx, void *data);
-void autocorrect_confirm(int autocorrect, const char *assumed);
+void autocorrect_confirm(struct autocorrect *conf, const char *assumed);
#endif /* AUTOCORRECT_H */
diff --git a/help.c b/help.c
index 983057970e7c..a89ac5aced99 100644
--- a/help.c
+++ b/help.c
@@ -538,7 +538,7 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)
}
struct help_unknown_cmd_config {
- int autocorrect;
+ struct autocorrect autocorrect;
struct cmdnames aliases;
};
@@ -607,7 +607,7 @@ char *help_unknown_cmd(const char *cmd)
read_early_config(the_repository, git_unknown_cmd_config, &cfg);
- if (cfg.autocorrect == AUTOCORRECT_NEVER) {
+ if (cfg.autocorrect.mode == AUTOCORRECT_NEVER) {
fprintf_ln(stderr, _("git: '%s' is not a git command. See 'git --help'."), cmd);
exit(1);
}
@@ -673,7 +673,8 @@ char *help_unknown_cmd(const char *cmd)
n++)
; /* still counting */
}
- if (cfg.autocorrect && cfg.autocorrect != AUTOCORRECT_SHOW && n == 1 &&
+
+ if (cfg.autocorrect.mode != AUTOCORRECT_SHOW && n == 1 &&
SIMILAR_ENOUGH(best_similarity)) {
char *assumed = xstrdup(main_cmds.names[0]->name);
@@ -682,7 +683,7 @@ char *help_unknown_cmd(const char *cmd)
"which does not exist."),
cmd);
- autocorrect_confirm(cfg.autocorrect, assumed);
+ autocorrect_confirm(&cfg.autocorrect, assumed);
cmdnames_release(&cfg.aliases);
cmdnames_release(&main_cmds);
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v4 05/10] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINT
2026-03-16 15:36 ` [PATCH v4 00/10] parseopt: add " Jiamu Sun
` (3 preceding siblings ...)
2026-03-16 15:36 ` [PATCH v4 04/10] autocorrect: use mode and delay instead of magic numbers Jiamu Sun
@ 2026-03-16 15:36 ` Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 06/10] autocorrect: provide config resolution API Jiamu Sun
` (4 subsequent siblings)
9 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-16 15:36 UTC (permalink / raw)
To: git; +Cc: Aaron Plattner, Junio C Hamano, Karthik Nayak, Jiamu Sun
AUTOCORRECT_SHOW is ambiguous. Its purpose is to show commands similar
to the unknown one and take no other action. Rename it to fit the
semantics.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
autocorrect.c | 6 +++---
autocorrect.h | 2 +-
help.c | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/autocorrect.c b/autocorrect.c
index 2484546fc731..de0fa282c934 100644
--- a/autocorrect.c
+++ b/autocorrect.c
@@ -12,7 +12,7 @@ static enum autocorrect_mode parse_autocorrect(const char *value)
case 1:
return AUTOCORRECT_IMMEDIATELY;
case 0:
- return AUTOCORRECT_SHOW;
+ return AUTOCORRECT_HINT;
default: /* other random text */
break;
}
@@ -24,7 +24,7 @@ static enum autocorrect_mode parse_autocorrect(const char *value)
else if (!strcmp(value, "immediate"))
return AUTOCORRECT_IMMEDIATELY;
else if (!strcmp(value, "show"))
- return AUTOCORRECT_SHOW;
+ return AUTOCORRECT_HINT;
else
return AUTOCORRECT_DELAY;
}
@@ -49,7 +49,7 @@ void autocorrect_resolve_config(const char *var, const char *value,
conf->delay = git_config_int(var, value, ctx->kvi);
if (!conf->delay)
- conf->mode = AUTOCORRECT_SHOW;
+ conf->mode = AUTOCORRECT_HINT;
else if (conf->delay < 0 || conf->delay == 1)
conf->mode = AUTOCORRECT_IMMEDIATELY;
}
diff --git a/autocorrect.h b/autocorrect.h
index 5506a36f11a7..328807242c15 100644
--- a/autocorrect.h
+++ b/autocorrect.h
@@ -4,7 +4,7 @@
struct config_context;
enum autocorrect_mode {
- AUTOCORRECT_SHOW,
+ AUTOCORRECT_HINT,
AUTOCORRECT_NEVER,
AUTOCORRECT_PROMPT,
AUTOCORRECT_IMMEDIATELY,
diff --git a/help.c b/help.c
index a89ac5aced99..2d441ded3f14 100644
--- a/help.c
+++ b/help.c
@@ -674,7 +674,7 @@ char *help_unknown_cmd(const char *cmd)
; /* still counting */
}
- if (cfg.autocorrect.mode != AUTOCORRECT_SHOW && n == 1 &&
+ if (cfg.autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&
SIMILAR_ENOUGH(best_similarity)) {
char *assumed = xstrdup(main_cmds.names[0]->name);
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v4 06/10] autocorrect: provide config resolution API
2026-03-16 15:36 ` [PATCH v4 00/10] parseopt: add " Jiamu Sun
` (4 preceding siblings ...)
2026-03-16 15:36 ` [PATCH v4 05/10] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINT Jiamu Sun
@ 2026-03-16 15:36 ` Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 07/10] parseopt: autocorrect mistyped subcommands Jiamu Sun
` (3 subsequent siblings)
9 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-16 15:36 UTC (permalink / raw)
To: git; +Cc: Aaron Plattner, Junio C Hamano, Karthik Nayak, Jiamu Sun
Add autocorrect_resolve(). This resolves and populates the correct
values for autocorrect config.
Make autocorrect config callback internal. The API is meant to provide
a high-level way to retrieve the config. Allowing access to the config
callback from outside violates that intent.
Additionally, in some cases, without access to the config callback, two
config iterations cannot be merged into one, which can hurt performance.
This is fine, as the code path that calls autocorrect_resolve() is cold.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
autocorrect.c | 15 ++++++++++++---
autocorrect.h | 5 +----
help.c | 40 +++++++++++++++++-----------------------
3 files changed, 30 insertions(+), 30 deletions(-)
diff --git a/autocorrect.c b/autocorrect.c
index de0fa282c934..b2ee9f51e8c0 100644
--- a/autocorrect.c
+++ b/autocorrect.c
@@ -1,3 +1,5 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
#include "git-compat-util.h"
#include "autocorrect.h"
#include "config.h"
@@ -29,13 +31,13 @@ static enum autocorrect_mode parse_autocorrect(const char *value)
return AUTOCORRECT_DELAY;
}
-void autocorrect_resolve_config(const char *var, const char *value,
- const struct config_context *ctx, void *data)
+static int resolve_autocorrect(const char *var, const char *value,
+ const struct config_context *ctx, void *data)
{
struct autocorrect *conf = data;
if (strcmp(var, "help.autocorrect"))
- return;
+ return 0;
conf->mode = parse_autocorrect(value);
@@ -53,6 +55,13 @@ void autocorrect_resolve_config(const char *var, const char *value,
else if (conf->delay < 0 || conf->delay == 1)
conf->mode = AUTOCORRECT_IMMEDIATELY;
}
+
+ return 0;
+}
+
+void autocorrect_resolve(struct autocorrect *conf)
+{
+ read_early_config(the_repository, resolve_autocorrect, conf);
}
void autocorrect_confirm(struct autocorrect *conf, const char *assumed)
diff --git a/autocorrect.h b/autocorrect.h
index 328807242c15..0d3e819262ed 100644
--- a/autocorrect.h
+++ b/autocorrect.h
@@ -1,8 +1,6 @@
#ifndef AUTOCORRECT_H
#define AUTOCORRECT_H
-struct config_context;
-
enum autocorrect_mode {
AUTOCORRECT_HINT,
AUTOCORRECT_NEVER,
@@ -16,8 +14,7 @@ struct autocorrect {
int delay;
};
-void autocorrect_resolve_config(const char *var, const char *value,
- const struct config_context *ctx, void *data);
+void autocorrect_resolve(struct autocorrect *conf);
void autocorrect_confirm(struct autocorrect *conf, const char *assumed);
diff --git a/help.c b/help.c
index 2d441ded3f14..81efdb13d4a3 100644
--- a/help.c
+++ b/help.c
@@ -537,32 +537,23 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)
return 0;
}
-struct help_unknown_cmd_config {
- struct autocorrect autocorrect;
- struct cmdnames aliases;
-};
-
-static int git_unknown_cmd_config(const char *var, const char *value,
- const struct config_context *ctx,
- void *cb)
+static int resolve_aliases(const char *var, const char *value UNUSED,
+ const struct config_context *ctx UNUSED, void *data)
{
- struct help_unknown_cmd_config *cfg = cb;
+ struct cmdnames *aliases = data;
const char *subsection, *key;
size_t subsection_len;
- autocorrect_resolve_config(var, value, ctx, &cfg->autocorrect);
-
- /* Also use aliases for command lookup */
if (!parse_config_key(var, "alias", &subsection, &subsection_len,
&key)) {
if (subsection) {
/* [alias "name"] command = value */
if (!strcmp(key, "command"))
- add_cmdname(&cfg->aliases, subsection,
+ add_cmdname(aliases, subsection,
subsection_len);
} else {
/* alias.name = value */
- add_cmdname(&cfg->aliases, key, strlen(key));
+ add_cmdname(aliases, key, strlen(key));
}
}
@@ -599,22 +590,26 @@ static const char bad_interpreter_advice[] =
char *help_unknown_cmd(const char *cmd)
{
- struct help_unknown_cmd_config cfg = { 0 };
+ struct cmdnames aliases = { 0 };
+ struct autocorrect autocorrect = { 0 };
int i, n, best_similarity = 0;
struct cmdnames main_cmds = { 0 };
struct cmdnames other_cmds = { 0 };
struct cmdname_help *common_cmds;
- read_early_config(the_repository, git_unknown_cmd_config, &cfg);
+ autocorrect_resolve(&autocorrect);
- if (cfg.autocorrect.mode == AUTOCORRECT_NEVER) {
+ if (autocorrect.mode == AUTOCORRECT_NEVER) {
fprintf_ln(stderr, _("git: '%s' is not a git command. See 'git --help'."), cmd);
exit(1);
}
load_command_list("git-", &main_cmds, &other_cmds);
- add_cmd_list(&main_cmds, &cfg.aliases);
+ /* Also use aliases for command lookup */
+ read_early_config(the_repository, resolve_aliases, &aliases);
+
+ add_cmd_list(&main_cmds, &aliases);
add_cmd_list(&main_cmds, &other_cmds);
QSORT(main_cmds.names, main_cmds.cnt, cmdname_compare);
uniq(&main_cmds);
@@ -674,18 +669,17 @@ char *help_unknown_cmd(const char *cmd)
; /* still counting */
}
- if (cfg.autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&
+ if (autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&
SIMILAR_ENOUGH(best_similarity)) {
char *assumed = xstrdup(main_cmds.names[0]->name);
fprintf_ln(stderr,
- _("WARNING: You called a Git command named '%s', "
- "which does not exist."),
+ _("WARNING: You called a Git command named '%s', which does not exist."),
cmd);
- autocorrect_confirm(&cfg.autocorrect, assumed);
+ autocorrect_confirm(&autocorrect, assumed);
- cmdnames_release(&cfg.aliases);
+ cmdnames_release(&aliases);
cmdnames_release(&main_cmds);
cmdnames_release(&other_cmds);
return assumed;
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v4 07/10] parseopt: autocorrect mistyped subcommands
2026-03-16 15:36 ` [PATCH v4 00/10] parseopt: add " Jiamu Sun
` (5 preceding siblings ...)
2026-03-16 15:36 ` [PATCH v4 06/10] autocorrect: provide config resolution API Jiamu Sun
@ 2026-03-16 15:36 ` Jiamu Sun
2026-03-16 19:41 ` Junio C Hamano
2026-03-16 15:36 ` [PATCH v4 08/10] parseopt: enable subcommand autocorrection for git-remote and git-notes Jiamu Sun
` (2 subsequent siblings)
9 siblings, 1 reply; 67+ messages in thread
From: Jiamu Sun @ 2026-03-16 15:36 UTC (permalink / raw)
To: git; +Cc: Aaron Plattner, Junio C Hamano, Karthik Nayak, Jiamu Sun
Try to autocorrect the mistyped mandatory subcommand before showing an
error and exiting. Subcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL
are skipped.
Use standard Damerau-Levenshtein distance (weights 1, 1, 1, 1) to
establish a predictable, mathematically sound baseline.
Scale the allowed edit distance based on input length to prevent
false positives on short commands, following common practice for
fuzziness thresholds (e.g., Elasticsearch's AUTO fuzziness):
- Length 0-2: 0 edits allowed
- Length 3-5: 1 edit allowed
- Length 6+: 2 edits allowed
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
parse-options.c | 81 +++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 78 insertions(+), 3 deletions(-)
diff --git a/parse-options.c b/parse-options.c
index 02a4f00919f6..1f1b72762790 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -6,6 +6,8 @@
#include "strbuf.h"
#include "string-list.h"
#include "utf8.h"
+#include "autocorrect.h"
+#include "levenshtein.h"
static int disallow_abbreviated_options;
@@ -622,13 +624,77 @@ static int parse_subcommand(const char *arg, const struct option *options)
return -1;
}
+static void find_subcommands(struct string_list *list,
+ const struct option *options)
+{
+ for (; options->type != OPTION_END; options++) {
+ if (options->type == OPTION_SUBCOMMAND)
+ string_list_append(list, options->long_name);
+ }
+}
+
+static int similar_enough(const char *cmd, unsigned int edit)
+{
+ size_t len = strlen(cmd);
+ unsigned int allowed = len < 3 ? 0 : len < 6 ? 1 : 2;
+
+ return edit <= allowed;
+}
+
+static const char *autocorrect_subcommand(const char *cmd,
+ struct string_list *cmds)
+{
+ struct autocorrect autocorrect = { 0 };
+ unsigned int min = UINT_MAX;
+ unsigned int ties = 0;
+ struct string_list_item *cand;
+ struct string_list_item *best = NULL;
+
+ autocorrect_resolve(&autocorrect);
+
+ /*
+ * Builtin subcommands are small enough that printing them all via
+ * usage_with_options() is sufficient. Therefore, AUTOCORRECT_HINT
+ * acts like AUTOCORRECT_NEVER.
+ */
+ if (autocorrect.mode == AUTOCORRECT_HINT ||
+ autocorrect.mode == AUTOCORRECT_NEVER)
+ return NULL;
+
+ for_each_string_list_item(cand, cmds) {
+ unsigned int edit = levenshtein(cmd, cand->string, 1, 1, 1, 1);
+
+ if (edit < min) {
+ min = edit;
+ best = cand;
+ ties = 0;
+ } else if (edit == min) {
+ ties++;
+ }
+ }
+
+ if (!ties && similar_enough(cmd, min)) {
+ fprintf_ln(stderr,
+ _("WARNING: You called a subcommand named '%s', which does not exist."),
+ cmd);
+
+ autocorrect_confirm(&autocorrect, best->string);
+ return best->string;
+ }
+
+ return NULL;
+}
+
static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
const char *arg,
const struct option *options,
const char * const usagestr[])
{
- int err = parse_subcommand(arg, options);
+ int err;
+ const char *assumed;
+ struct string_list cmds = STRING_LIST_INIT_NODUP;
+ err = parse_subcommand(arg, options);
if (!err)
return PARSE_OPT_SUBCOMMAND;
@@ -641,8 +707,17 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
return PARSE_OPT_DONE;
- error(_("unknown subcommand: `%s'"), arg);
- usage_with_options(usagestr, options);
+ find_subcommands(&cmds, options);
+ assumed = autocorrect_subcommand(arg, &cmds);
+
+ if (!assumed) {
+ error(_("unknown subcommand: `%s'"), arg);
+ usage_with_options(usagestr, options);
+ }
+
+ string_list_clear(&cmds, 0);
+ parse_subcommand(assumed, options);
+ return PARSE_OPT_SUBCOMMAND;
}
static void check_typos(const char *arg, const struct option *options)
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v4 08/10] parseopt: enable subcommand autocorrection for git-remote and git-notes
2026-03-16 15:36 ` [PATCH v4 00/10] parseopt: add " Jiamu Sun
` (6 preceding siblings ...)
2026-03-16 15:36 ` [PATCH v4 07/10] parseopt: autocorrect mistyped subcommands Jiamu Sun
@ 2026-03-16 15:36 ` Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 09/10] parseopt: add tests for subcommand autocorrection Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 10/10] doc: document autocorrect API Jiamu Sun
9 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-16 15:36 UTC (permalink / raw)
To: git; +Cc: Aaron Plattner, Junio C Hamano, Karthik Nayak, Jiamu Sun
Add PARSE_OPT_SUBCOMMAND_AUTOCORR to enable autocorrection for
subcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL.
Use it for git-remote and git-notes, so mistyped subcommands can be
automatically corrected, and builtin entry points no longer need to
handle the unknown subcommand error path themselves.
This is safe for these two builtins, because they either resolve to a
single subcommand or take no subcommand at all. This means that if the
subcommand parser encounters an unknown argument, it must be a mistyped
subcommand.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
builtin/notes.c | 10 +++-------
builtin/remote.c | 12 ++++--------
parse-options.c | 16 +++++++++-------
parse-options.h | 1 +
4 files changed, 17 insertions(+), 22 deletions(-)
diff --git a/builtin/notes.c b/builtin/notes.c
index 9af602bdd7b4..087eb898a441 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -1149,14 +1149,10 @@ int cmd_notes(int argc,
repo_config(the_repository, git_default_config, NULL);
argc = parse_options(argc, argv, prefix, options, git_notes_usage,
- PARSE_OPT_SUBCOMMAND_OPTIONAL);
- if (!fn) {
- if (argc) {
- error(_("unknown subcommand: `%s'"), argv[0]);
- usage_with_options(git_notes_usage, options);
- }
+ PARSE_OPT_SUBCOMMAND_OPTIONAL |
+ PARSE_OPT_SUBCOMMAND_AUTOCORR);
+ if (!fn)
fn = list;
- }
if (override_notes_ref) {
struct strbuf sb = STRBUF_INIT;
diff --git a/builtin/remote.c b/builtin/remote.c
index ace390c671d6..d1d6244a662a 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1949,15 +1949,11 @@ int cmd_remote(int argc,
};
argc = parse_options(argc, argv, prefix, options, builtin_remote_usage,
- PARSE_OPT_SUBCOMMAND_OPTIONAL);
+ PARSE_OPT_SUBCOMMAND_OPTIONAL |
+ PARSE_OPT_SUBCOMMAND_AUTOCORR);
- if (fn) {
+ if (fn)
return !!fn(argc, argv, prefix, repo);
- } else {
- if (argc) {
- error(_("unknown subcommand: `%s'"), argv[0]);
- usage_with_options(builtin_remote_usage, options);
- }
+ else
return !!show_all();
- }
}
diff --git a/parse-options.c b/parse-options.c
index 1f1b72762790..0b84061a3811 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -698,14 +698,16 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
if (!err)
return PARSE_OPT_SUBCOMMAND;
- /*
- * arg is neither a short or long option nor a subcommand. Since this
- * command has a default operation mode, we have to treat this arg and
- * all remaining args as args meant to that default operation mode.
- * So we are done parsing.
- */
- if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
+ if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL &&
+ !(ctx->flags & PARSE_OPT_SUBCOMMAND_AUTOCORR)) {
+ /*
+ * arg is neither a short or long option nor a subcommand.
+ * Since this command has a default operation mode, we have to
+ * treat this arg and all remaining args as args meant to that
+ * default operation mode. So we are done parsing.
+ */
return PARSE_OPT_DONE;
+ }
find_subcommands(&cmds, options);
assumed = autocorrect_subcommand(arg, &cmds);
diff --git a/parse-options.h b/parse-options.h
index 706de9729f6b..f29ac337893c 100644
--- a/parse-options.h
+++ b/parse-options.h
@@ -40,6 +40,7 @@ enum parse_opt_flags {
PARSE_OPT_ONE_SHOT = 1 << 5,
PARSE_OPT_SHELL_EVAL = 1 << 6,
PARSE_OPT_SUBCOMMAND_OPTIONAL = 1 << 7,
+ PARSE_OPT_SUBCOMMAND_AUTOCORR = 1 << 8,
};
enum parse_opt_option_flags {
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v4 09/10] parseopt: add tests for subcommand autocorrection
2026-03-16 15:36 ` [PATCH v4 00/10] parseopt: add " Jiamu Sun
` (7 preceding siblings ...)
2026-03-16 15:36 ` [PATCH v4 08/10] parseopt: enable subcommand autocorrection for git-remote and git-notes Jiamu Sun
@ 2026-03-16 15:36 ` Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 10/10] doc: document autocorrect API Jiamu Sun
9 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-16 15:36 UTC (permalink / raw)
To: git; +Cc: Aaron Plattner, Junio C Hamano, Karthik Nayak, Jiamu Sun
These tests cover default behavior (help.autocorrect is unset), no
correction, immediate correction, delayed correction, and rejection
when the typo is too dissimilar.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
t/meson.build | 1 +
t/t9004-autocorrect-subcommand.sh | 51 +++++++++++++++++++++++++++++++
2 files changed, 52 insertions(+)
create mode 100755 t/t9004-autocorrect-subcommand.sh
diff --git a/t/meson.build b/t/meson.build
index 106c68df3d01..311be82af346 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -972,6 +972,7 @@ integration_tests = [
't9001-send-email.sh',
't9002-column.sh',
't9003-help-autocorrect.sh',
+ 't9004-autocorrect-subcommand.sh',
't9100-git-svn-basic.sh',
't9101-git-svn-props.sh',
't9102-git-svn-deep-rmdir.sh',
diff --git a/t/t9004-autocorrect-subcommand.sh b/t/t9004-autocorrect-subcommand.sh
new file mode 100755
index 000000000000..d10031659b94
--- /dev/null
+++ b/t/t9004-autocorrect-subcommand.sh
@@ -0,0 +1,51 @@
+#!/bin/sh
+
+test_description='subcommand auto-correction test
+
+Test autocorrection for subcommands with different
+help.autocorrect mode.'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' "
+ echo '^error: unknown subcommand: ' >grep_unknown
+"
+
+test_expect_success 'default is not to autocorrect' '
+ test_must_fail git worktree lsit 2>actual &&
+ head -n1 actual >first && test_grep -f grep_unknown first
+'
+
+for mode in false no off 0 show never
+do
+ test_expect_success "'$mode' disables autocorrection" "
+ test_config help.autocorrect $mode &&
+
+ test_must_fail git worktree lsit 2>actual &&
+ head -n1 actual >first && test_grep -f grep_unknown first
+ "
+done
+
+for mode in -39 immediate 1
+do
+ test_expect_success "autocorrect immediately with '$mode'" - <<-EOT
+ test_config help.autocorrect $mode &&
+
+ git worktree lsit 2>actual &&
+ test_grep "you meant 'list'\.$" actual
+ EOT
+done
+
+test_expect_success 'delay path is executed' - <<-\EOT
+ test_config help.autocorrect 2 &&
+
+ git worktree lsit 2>actual &&
+ test_grep '^Continuing in 0.2 seconds, ' actual
+EOT
+
+test_expect_success 'deny if too dissimilar' - <<-\EOT
+ test_must_fail git remote rensnr 2>actual &&
+ head -n1 actual >first && test_grep -f grep_unknown first
+EOT
+
+test_done
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* [PATCH v4 10/10] doc: document autocorrect API
2026-03-16 15:36 ` [PATCH v4 00/10] parseopt: add " Jiamu Sun
` (8 preceding siblings ...)
2026-03-16 15:36 ` [PATCH v4 09/10] parseopt: add tests for subcommand autocorrection Jiamu Sun
@ 2026-03-16 15:36 ` Jiamu Sun
9 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-16 15:36 UTC (permalink / raw)
To: git; +Cc: Aaron Plattner, Junio C Hamano, Karthik Nayak, Jiamu Sun
Explain behaviors for autocorrect_resolve(), autocorrect_confirm(), and
struct autocorrect.
Signed-off-by: Jiamu Sun <39@barroit.sh>
---
autocorrect.h | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/autocorrect.h b/autocorrect.h
index 0d3e819262ed..bfa3ba20a4fb 100644
--- a/autocorrect.h
+++ b/autocorrect.h
@@ -9,13 +9,24 @@ enum autocorrect_mode {
AUTOCORRECT_DELAY,
};
+/**
+ * `mode` indicates which action will be performed by autocorrect_confirm().
+ * `delay` is the timeout before autocorrect_confirm() returns, in tenths of a
+ * second. Use it only with AUTOCORRECT_DELAY.
+ */
struct autocorrect {
enum autocorrect_mode mode;
int delay;
};
+/**
+ * Resolve the autocorrect configuration into `conf`.
+ */
void autocorrect_resolve(struct autocorrect *conf);
+/**
+ * Interact with the user in different ways depending on `conf->mode`.
+ */
void autocorrect_confirm(struct autocorrect *conf, const char *assumed);
#endif /* AUTOCORRECT_H */
--
2.53.0
^ permalink raw reply related [flat|nested] 67+ messages in thread
* Re: [PATCH v4 07/10] parseopt: autocorrect mistyped subcommands
2026-03-16 15:36 ` [PATCH v4 07/10] parseopt: autocorrect mistyped subcommands Jiamu Sun
@ 2026-03-16 19:41 ` Junio C Hamano
2026-03-17 3:21 ` Jiamu Sun
0 siblings, 1 reply; 67+ messages in thread
From: Junio C Hamano @ 2026-03-16 19:41 UTC (permalink / raw)
To: Jiamu Sun; +Cc: git, Aaron Plattner, Karthik Nayak
Jiamu Sun <39@barroit.sh> writes:
> Try to autocorrect the mistyped mandatory subcommand before showing an
> error and exiting. Subcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL
> are skipped.
>
> Use standard Damerau-Levenshtein distance (weights 1, 1, 1, 1) to
> establish a predictable, mathematically sound baseline.
>
> Scale the allowed edit distance based on input length to prevent
> false positives on short commands, following common practice for
> fuzziness thresholds (e.g., Elasticsearch's AUTO fuzziness):
> - Length 0-2: 0 edits allowed
> - Length 3-5: 1 edit allowed
> - Length 6+: 2 edits allowed
Is there a reason why this needs to differ from the settings for the
typo detection/fixes for main commands? Would the same reasoning
apply to both, and if not why not?
I would have expected that we would just emulate what we already do
to the main commands, and later with experience with the subcommand
typo detection/fixes, would tweak the parameters either only to the
subcommand part or to the both with justifications.
> + /*
> + * Builtin subcommands are small enough that printing them all via
> + * usage_with_options() is sufficient. Therefore, AUTOCORRECT_HINT
> + * acts like AUTOCORRECT_NEVER.
> + */
Sorry, but I am a bit confused with this reference to "Builtin
subcommands". Are there subcommands that are not built-in?
^ permalink raw reply [flat|nested] 67+ messages in thread
* Re: [PATCH v4 07/10] parseopt: autocorrect mistyped subcommands
2026-03-16 19:41 ` Junio C Hamano
@ 2026-03-17 3:21 ` Jiamu Sun
0 siblings, 0 replies; 67+ messages in thread
From: Jiamu Sun @ 2026-03-17 3:21 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, Aaron Plattner, Karthik Nayak
On Mon, Mar 16, 2026 at 12:41:30PM -0700, Junio C Hamano wrote:
> Is there a reason why this needs to differ from the settings for the
> typo detection/fixes for main commands? Would the same reasoning
> apply to both, and if not why not?
>
> I would have expected that we would just emulate what we already do
> to the main commands, and later with experience with the subcommand
> typo detection/fixes, would tweak the parameters either only to the
> subcommand part or to the both with justifications.
I initially wanted to emulate the existing behavior. However, I noticed
a huge difference in how prefix-matched commands are handled, which
affects the autocorrection behavior. And we can only use the same
parameters if we do the exact same thing on prefix match handling in
subcommand autocorrection.
I looked through the old mailing list. If I didn't miss anything, the
patch that introduced this behavior (e.g., not correcting "statu" to
"status") was only trying to fix a UX issue, where the suggestion output
didn't include prefix-matched commands. In that thread, they didn't
mention that this would also change the typo detection behavior, and no
one discussed this side effect. I treat it as an accident.
Because of this, I was confused about whether I should copy this
behavior. I chose not to.
However, if we want the main commands and subcommands to act the same, I
can do that and try to move the logic to autocorrect.c so both places
share the exact same typo detection.
Do you want me to do this?
> > + /*
> > + * Builtin subcommands are small enough that printing them all via
> > + * usage_with_options() is sufficient. Therefore, AUTOCORRECT_HINT
> > + * acts like AUTOCORRECT_NEVER.
> > + */
>
> Sorry, but I am a bit confused with this reference to "Builtin
> subcommands". Are there subcommands that are not built-in?
No, that's just poor wording. Will fix it.
--
Jiamu Sun <39@barroit.sh>
<sunjiamu@outlook.com>
^ permalink raw reply [flat|nested] 67+ messages in thread
end of thread, other threads:[~2026-03-17 3:21 UTC | newest]
Thread overview: 67+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-08 12:17 [PATCH 0/5] parseopt: add subcommand autocorrection Jiamu Sun
2026-03-08 12:17 ` [PATCH 1/5] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
2026-03-08 23:40 ` Junio C Hamano
2026-03-09 1:56 ` Jiamu Sun
2026-03-08 12:17 ` [PATCH 2/5] help: refactor command autocorrection handling Jiamu Sun
2026-03-08 23:52 ` Junio C Hamano
2026-03-09 2:06 ` Jiamu Sun
2026-03-08 12:17 ` [PATCH 3/5] parseopt: autocorrect mistyped subcommands Jiamu Sun
2026-03-09 0:04 ` Junio C Hamano
2026-03-09 2:11 ` Jiamu Sun
2026-03-08 12:17 ` [PATCH 4/5] parseopt: enable subcommand autocorrect for remote and notes Jiamu Sun
2026-03-08 12:17 ` [PATCH 5/5] help: add tests for subcommand autocorrection Jiamu Sun
2026-03-11 17:01 ` Aaron Plattner
2026-03-11 22:45 ` Jiamu Sun
2026-03-12 13:35 ` Junio C Hamano
2026-03-08 20:11 ` [PATCH 0/5] parseopt: add " Junio C Hamano
2026-03-08 22:07 ` Jiamu Sun
2026-03-08 23:16 ` [PATCH v2 " Jiamu Sun
2026-03-08 23:16 ` [PATCH v2 1/5] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
2026-03-08 23:16 ` [PATCH v2 2/5] help: refactor command autocorrection handling Jiamu Sun
2026-03-08 23:16 ` [PATCH v2 3/5] parseopt: autocorrect mistyped subcommands Jiamu Sun
2026-03-08 23:16 ` [PATCH v2 4/5] parseopt: enable subcommand autocorrect for remote and notes Jiamu Sun
2026-03-08 23:16 ` [PATCH v2 5/5] help: add tests for subcommand autocorrection Jiamu Sun
2026-03-10 11:40 ` [PATCH v3 0/8] parseopt: add " Jiamu Sun
2026-03-10 11:41 ` [PATCH v3 1/8] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
2026-03-10 12:46 ` Karthik Nayak
2026-03-11 1:49 ` Jiamu Sun
2026-03-11 4:20 ` Junio C Hamano
2026-03-11 4:20 ` Junio C Hamano
2026-03-11 6:12 ` Jiamu Sun
2026-03-10 11:41 ` [PATCH v3 2/8] help: make autocorrect handling reusable Jiamu Sun
2026-03-10 12:52 ` Karthik Nayak
2026-03-10 20:10 ` Junio C Hamano
2026-03-11 2:05 ` Jiamu Sun
2026-03-11 1:58 ` Jiamu Sun
2026-03-10 11:41 ` [PATCH v3 3/8] help: move tty check for autocorrection to autocorrect.c Jiamu Sun
2026-03-10 14:06 ` Karthik Nayak
2026-03-11 2:16 ` Jiamu Sun
2026-03-12 0:10 ` Jiamu Sun
2026-03-10 11:41 ` [PATCH v3 4/8] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINTONLY Jiamu Sun
2026-03-10 14:08 ` Karthik Nayak
2026-03-11 2:46 ` Jiamu Sun
2026-03-10 11:41 ` [PATCH v3 5/8] autocorrect: provide config resolution API Jiamu Sun
2026-03-10 14:15 ` Karthik Nayak
2026-03-10 11:41 ` [PATCH v3 6/8] parseopt: autocorrect mistyped subcommands Jiamu Sun
2026-03-10 20:16 ` Junio C Hamano
2026-03-11 2:48 ` Jiamu Sun
2026-03-11 23:26 ` Jiamu Sun
2026-03-12 2:38 ` Junio C Hamano
2026-03-12 4:53 ` Jiamu Sun
2026-03-10 11:41 ` [PATCH v3 7/8] parseopt: enable subcommand autocorrection for git-remote and git-notes Jiamu Sun
2026-03-10 11:41 ` [PATCH v3 8/8] help: add tests for subcommand autocorrection Jiamu Sun
2026-03-11 4:23 ` Junio C Hamano
2026-03-11 6:10 ` Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 00/10] parseopt: add " Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 01/10] parseopt: extract subcommand handling from parse_options_step() Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 02/10] help: make autocorrect handling reusable Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 03/10] help: move tty check for autocorrection to autocorrect.c Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 04/10] autocorrect: use mode and delay instead of magic numbers Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 05/10] autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINT Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 06/10] autocorrect: provide config resolution API Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 07/10] parseopt: autocorrect mistyped subcommands Jiamu Sun
2026-03-16 19:41 ` Junio C Hamano
2026-03-17 3:21 ` Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 08/10] parseopt: enable subcommand autocorrection for git-remote and git-notes Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 09/10] parseopt: add tests for subcommand autocorrection Jiamu Sun
2026-03-16 15:36 ` [PATCH v4 10/10] doc: document autocorrect API Jiamu Sun
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox