]> git.ipfire.org Git - thirdparty/git.git/commitdiff
parseopt: autocorrect mistyped subcommands
authorJiamu Sun <39@barroit.sh>
Thu, 23 Apr 2026 01:37:57 +0000 (10:37 +0900)
committerJunio C Hamano <gitster@pobox.com>
Thu, 23 Apr 2026 02:02:28 +0000 (11:02 +0900)
Try to autocorrect the mistyped mandatory subcommand before showing an
error and exiting. Subcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL
are skipped.

The subcommand autocorrection behaves the same as the command
autocorrection.

Signed-off-by: Jiamu Sun <39@barroit.sh>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
autocorrect.h
help.c
parse-options.c
t/t0040-parse-options.sh
t/t7900-maintenance.sh

index 0d3e819262edeedf187914c8142c6e4da2e0ddfb..14ee7c4548d3f79e041b9356efb957078014284f 100644 (file)
@@ -1,6 +1,10 @@
 #ifndef AUTOCORRECT_H
 #define AUTOCORRECT_H
 
+/* An empirically derived magic number */
+#define AUTOCORRECT_SIMILARITY_FLOOR 7
+#define AUTOCORRECT_SIMILAR_ENOUGH(x) ((x) < AUTOCORRECT_SIMILARITY_FLOOR)
+
 enum autocorrect_mode {
        AUTOCORRECT_HINT,
        AUTOCORRECT_NEVER,
diff --git a/help.c b/help.c
index 81efdb13d4a375825cafdeadf6faca2d0d7058d7..16e5de973bcc0948f949ab1df9b86b9229f8a83f 100644 (file)
--- a/help.c
+++ b/help.c
@@ -580,10 +580,6 @@ static void add_cmd_list(struct cmdnames *cmds, struct cmdnames *old)
        old->cnt = 0;
 }
 
-/* An empirically derived magic number */
-#define SIMILARITY_FLOOR 7
-#define SIMILAR_ENOUGH(x) ((x) < SIMILARITY_FLOOR)
-
 static const char bad_interpreter_advice[] =
        N_("'%s' appears to be a git command, but we were not\n"
        "able to execute it. Maybe git-%s is broken?");
@@ -659,7 +655,7 @@ char *help_unknown_cmd(const char *cmd)
 
        if (main_cmds.cnt <= n) {
                /* prefix matches with everything? that is too ambiguous */
-               best_similarity = SIMILARITY_FLOOR + 1;
+               best_similarity = AUTOCORRECT_SIMILARITY_FLOOR + 1;
        } else {
                /* count all the most similar ones */
                for (best_similarity = main_cmds.names[n++]->len;
@@ -670,7 +666,7 @@ char *help_unknown_cmd(const char *cmd)
        }
 
        if (autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&
-           SIMILAR_ENOUGH(best_similarity)) {
+           AUTOCORRECT_SIMILAR_ENOUGH(best_similarity)) {
                char *assumed = xstrdup(main_cmds.names[0]->name);
 
                fprintf_ln(stderr,
@@ -687,11 +683,10 @@ char *help_unknown_cmd(const char *cmd)
 
        fprintf_ln(stderr, _("git: '%s' is not a git command. See 'git --help'."), cmd);
 
-       if (SIMILAR_ENOUGH(best_similarity)) {
+       if (AUTOCORRECT_SIMILAR_ENOUGH(best_similarity)) {
                fprintf_ln(stderr,
                           Q_("\nThe most similar command is",
-                             "\nThe most similar commands are",
-                          n));
+                             "\nThe most similar commands are", n));
 
                for (i = 0; i < n; i++)
                        fprintf(stderr, "\t%s\n", main_cmds.names[i]->name);
index 02a4f00919f6d600bddc2e1aa224029411bc0c51..faf357b729c4ec1e2e76b61d1b63103b59779a98 100644 (file)
@@ -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,98 @@ 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 levenshtein_compare(const void *p1, const void *p2)
+{
+       const struct string_list_item *i1 = p1, *i2 = p2;
+       const char *s1 = i1->string, *s2 = i2->string;
+       int l1 = (intptr_t)i1->util;
+       int l2 = (intptr_t)i2->util;
+
+       return l1 != l2 ? l1 - l2 : strcmp(s1, s2);
+}
+
+static const char *autocorrect_subcommand(const char *cmd,
+                                         struct string_list *cmds)
+{
+       struct autocorrect autocorrect = { 0 };
+       unsigned int n = 0, best = 0;
+       struct string_list_item *cand;
+
+       autocorrect_resolve(&autocorrect);
+
+       if (autocorrect.mode == AUTOCORRECT_NEVER)
+               return NULL;
+
+       for_each_string_list_item(cand, cmds) {
+               if (starts_with(cand->string, cmd)) {
+                       cand->util = 0;
+               } else {
+                       int edit = levenshtein(cmd, cand->string,
+                                              0, 2, 1, 3) + 1;
+
+                       cand->util = (void *)(intptr_t)edit;
+               }
+       }
+
+       QSORT(cmds->items, cmds->nr, levenshtein_compare);
+
+       /* Match help.c:help_unknown_cmd */
+       for (; n < cmds->nr && !cmds->items[n].util; n++);
+
+       if (n == cmds->nr)
+               /* prefix matches with every subcommands */
+               best = AUTOCORRECT_SIMILARITY_FLOOR + 1;
+       else
+               for (best = (intptr_t)cmds->items[n++].util;
+                    (n < cmds->nr && best == (intptr_t)cmds->items[n].util);
+                    n++);
+
+       if (autocorrect.mode != AUTOCORRECT_HINT &&  n == 1 &&
+           AUTOCORRECT_SIMILAR_ENOUGH(best)) {
+               fprintf_ln(stderr,
+                          _("WARNING: You called a subcommand named '%s', which does not exist."),
+                          cmd);
+
+               autocorrect_confirm(&autocorrect, cmds->items[0].string);
+               return cmds->items[0].string;
+       }
+
+       if (AUTOCORRECT_SIMILAR_ENOUGH(best)) {
+               error(_("'%s' is not a subcommand."), cmd);
+
+               fprintf_ln(stderr,
+                          Q_("\nThe most similar subcommand is",
+                             "\nThe most similar subcommands are",
+                          n));
+
+               for (unsigned int i = 0; i < n; i++)
+                       fprintf(stderr, "\t%s\n", cmds->items[i].string);
+
+               exit(129);
+       }
+
+       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 +728,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)
index ca55ea8228c3789c5089d9141412224589ac18f7..2a2fef1e17dc4f3fafa6e731f63270b3a4c76787 100755 (executable)
@@ -632,8 +632,9 @@ test_expect_success 'subcommand - unknown subcommand shows error and usage' '
 
 test_expect_success 'subcommand - subcommands cannot be abbreviated' '
        test_expect_code 129 test-tool parse-subcommand cmd subcmd-o 2>err &&
-       grep "^error: unknown subcommand: \`subcmd-o$SQ$" err &&
-       grep ^usage: err
+       grep "^The most similar subcommands are$" err &&
+       grep "subcmd-one$" err &&
+       grep "subcmd-two$" err
 '
 
 test_expect_success 'subcommand - no negated subcommands' '
index 4700beacc18281413fc8d88ff35ea6ce392b0208..7174b993282972600f7fe4a790da10a6970ad400 100755 (executable)
@@ -37,8 +37,8 @@ test_systemd_analyze_verify () {
 test_expect_success 'help text' '
        test_expect_code 129 git maintenance -h >actual &&
        test_grep "usage: git maintenance <subcommand>" actual &&
-       test_expect_code 129 git maintenance barf 2>err &&
-       test_grep "unknown subcommand: \`barf'\''" err &&
+       test_expect_code 129 git maintenance abarf 2>err &&
+       test_grep "unknown subcommand: \`abarf'\''" err &&
        test_grep "usage: git maintenance" err &&
        test_expect_code 129 git maintenance 2>err &&
        test_grep "error: need a subcommand" err &&