Skip to content

Commit 684fe22

Browse files
committed
Merge branch 'js/parseopt-subcommand-autocorrection' into seen
The parse-options library learned to auto-correct misspelled subcommand names. Comments? * js/parseopt-subcommand-autocorrection: doc: document autocorrect API parseopt: add tests for subcommand autocorrection parseopt: enable subcommand autocorrection for git-remote and git-notes parseopt: autocorrect mistyped subcommands autocorrect: provide config resolution API autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINT autocorrect: use mode and delay instead of magic numbers help: move tty check for autocorrection to autocorrect.c help: make autocorrect handling reusable parseopt: extract subcommand handling from parse_options_step()
2 parents 5ceb988 + 67e653a commit 684fe22

13 files changed

Lines changed: 367 additions & 156 deletions

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,7 @@ LIB_OBJS += archive-tar.o
11001100
LIB_OBJS += archive-zip.o
11011101
LIB_OBJS += archive.o
11021102
LIB_OBJS += attr.o
1103+
LIB_OBJS += autocorrect.o
11031104
LIB_OBJS += base85.o
11041105
LIB_OBJS += bisect.o
11051106
LIB_OBJS += blame.o

autocorrect.c

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#define USE_THE_REPOSITORY_VARIABLE
2+
3+
#include "git-compat-util.h"
4+
#include "autocorrect.h"
5+
#include "config.h"
6+
#include "parse.h"
7+
#include "strbuf.h"
8+
#include "prompt.h"
9+
#include "gettext.h"
10+
11+
static enum autocorrect_mode parse_autocorrect(const char *value)
12+
{
13+
switch (git_parse_maybe_bool_text(value)) {
14+
case 1:
15+
return AUTOCORRECT_IMMEDIATELY;
16+
case 0:
17+
return AUTOCORRECT_HINT;
18+
default: /* other random text */
19+
break;
20+
}
21+
22+
if (!strcmp(value, "prompt"))
23+
return AUTOCORRECT_PROMPT;
24+
else if (!strcmp(value, "never"))
25+
return AUTOCORRECT_NEVER;
26+
else if (!strcmp(value, "immediate"))
27+
return AUTOCORRECT_IMMEDIATELY;
28+
else if (!strcmp(value, "show"))
29+
return AUTOCORRECT_HINT;
30+
else
31+
return AUTOCORRECT_DELAY;
32+
}
33+
34+
static int resolve_autocorrect(const char *var, const char *value,
35+
const struct config_context *ctx, void *data)
36+
{
37+
struct autocorrect *conf = data;
38+
39+
if (strcmp(var, "help.autocorrect"))
40+
return 0;
41+
42+
conf->mode = parse_autocorrect(value);
43+
44+
/*
45+
* Disable autocorrection prompt in a non-interactive session
46+
*/
47+
if (conf->mode == AUTOCORRECT_PROMPT && (!isatty(0) || !isatty(2)))
48+
conf->mode = AUTOCORRECT_NEVER;
49+
50+
if (conf->mode == AUTOCORRECT_DELAY) {
51+
conf->delay = git_config_int(var, value, ctx->kvi);
52+
53+
if (!conf->delay)
54+
conf->mode = AUTOCORRECT_HINT;
55+
else if (conf->delay < 0 || conf->delay == 1)
56+
conf->mode = AUTOCORRECT_IMMEDIATELY;
57+
}
58+
59+
return 0;
60+
}
61+
62+
void autocorrect_resolve(struct autocorrect *conf)
63+
{
64+
read_early_config(the_repository, resolve_autocorrect, conf);
65+
}
66+
67+
void autocorrect_confirm(struct autocorrect *conf, const char *assumed)
68+
{
69+
if (conf->mode == AUTOCORRECT_IMMEDIATELY) {
70+
fprintf_ln(stderr,
71+
_("Continuing under the assumption that you meant '%s'."),
72+
assumed);
73+
} else if (conf->mode == AUTOCORRECT_PROMPT) {
74+
char *answer;
75+
struct strbuf msg = STRBUF_INIT;
76+
77+
strbuf_addf(&msg, _("Run '%s' instead [y/N]? "), assumed);
78+
answer = git_prompt(msg.buf, PROMPT_ECHO);
79+
strbuf_release(&msg);
80+
81+
if (!(starts_with(answer, "y") || starts_with(answer, "Y")))
82+
exit(1);
83+
} else if (conf->mode == AUTOCORRECT_DELAY) {
84+
fprintf_ln(stderr,
85+
_("Continuing in %0.1f seconds, assuming that you meant '%s'."),
86+
conf->delay / 10.0, assumed);
87+
sleep_millisec(conf->delay * 100);
88+
}
89+
}

autocorrect.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#ifndef AUTOCORRECT_H
2+
#define AUTOCORRECT_H
3+
4+
/* An empirically derived magic number */
5+
#define AUTOCORRECT_SIMILARITY_FLOOR 7
6+
#define AUTOCORRECT_SIMILAR_ENOUGH(x) ((x) < AUTOCORRECT_SIMILARITY_FLOOR)
7+
8+
enum autocorrect_mode {
9+
AUTOCORRECT_HINT,
10+
AUTOCORRECT_NEVER,
11+
AUTOCORRECT_PROMPT,
12+
AUTOCORRECT_IMMEDIATELY,
13+
AUTOCORRECT_DELAY,
14+
};
15+
16+
/**
17+
* `mode` indicates which action will be performed by autocorrect_confirm().
18+
* `delay` is the timeout before autocorrect_confirm() returns, in tenths of a
19+
* second. Use it only with AUTOCORRECT_DELAY.
20+
*/
21+
struct autocorrect {
22+
enum autocorrect_mode mode;
23+
int delay;
24+
};
25+
26+
/**
27+
* Resolve the autocorrect configuration into `conf`.
28+
*/
29+
void autocorrect_resolve(struct autocorrect *conf);
30+
31+
/**
32+
* Interact with the user in different ways depending on `conf->mode`.
33+
*/
34+
void autocorrect_confirm(struct autocorrect *conf, const char *assumed);
35+
36+
#endif /* AUTOCORRECT_H */

builtin/notes.c

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,14 +1149,10 @@ int cmd_notes(int argc,
11491149

11501150
repo_config(the_repository, git_default_config, NULL);
11511151
argc = parse_options(argc, argv, prefix, options, git_notes_usage,
1152-
PARSE_OPT_SUBCOMMAND_OPTIONAL);
1153-
if (!fn) {
1154-
if (argc) {
1155-
error(_("unknown subcommand: `%s'"), argv[0]);
1156-
usage_with_options(git_notes_usage, options);
1157-
}
1152+
PARSE_OPT_SUBCOMMAND_OPTIONAL |
1153+
PARSE_OPT_SUBCOMMAND_AUTOCORRECT);
1154+
if (!fn)
11581155
fn = list;
1159-
}
11601156

11611157
if (override_notes_ref) {
11621158
struct strbuf sb = STRBUF_INIT;

builtin/remote.c

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1953,15 +1953,11 @@ int cmd_remote(int argc,
19531953
};
19541954

19551955
argc = parse_options(argc, argv, prefix, options, builtin_remote_usage,
1956-
PARSE_OPT_SUBCOMMAND_OPTIONAL);
1956+
PARSE_OPT_SUBCOMMAND_OPTIONAL |
1957+
PARSE_OPT_SUBCOMMAND_AUTOCORRECT);
19571958

1958-
if (fn) {
1959+
if (fn)
19591960
return !!fn(argc, argv, prefix, repo);
1960-
} else {
1961-
if (argc) {
1962-
error(_("unknown subcommand: `%s'"), argv[0]);
1963-
usage_with_options(builtin_remote_usage, options);
1964-
}
1961+
else
19651962
return !!show_all();
1966-
}
19671963
}

help.c

Lines changed: 25 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include "repository.h"
2323
#include "alias.h"
2424
#include "utf8.h"
25+
#include "autocorrect.h"
2526

2627
#ifndef NO_CURL
2728
#include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -536,77 +537,30 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)
536537
return 0;
537538
}
538539

539-
struct help_unknown_cmd_config {
540-
int autocorrect;
541-
struct cmdnames aliases;
542-
};
543-
544-
#define AUTOCORRECT_SHOW (-4)
545-
#define AUTOCORRECT_PROMPT (-3)
546-
#define AUTOCORRECT_NEVER (-2)
547-
#define AUTOCORRECT_IMMEDIATELY (-1)
548-
549-
static int parse_autocorrect(const char *value)
550-
{
551-
switch (git_parse_maybe_bool_text(value)) {
552-
case 1:
553-
return AUTOCORRECT_IMMEDIATELY;
554-
case 0:
555-
return AUTOCORRECT_SHOW;
556-
default: /* other random text */
557-
break;
558-
}
559-
560-
if (!strcmp(value, "prompt"))
561-
return AUTOCORRECT_PROMPT;
562-
if (!strcmp(value, "never"))
563-
return AUTOCORRECT_NEVER;
564-
if (!strcmp(value, "immediate"))
565-
return AUTOCORRECT_IMMEDIATELY;
566-
if (!strcmp(value, "show"))
567-
return AUTOCORRECT_SHOW;
568-
569-
return 0;
570-
}
571-
572-
static int git_unknown_cmd_config(const char *var, const char *value,
573-
const struct config_context *ctx,
574-
void *cb)
540+
static int resolve_aliases(const char *var, const char *value UNUSED,
541+
const struct config_context *ctx UNUSED, void *data)
575542
{
576-
struct help_unknown_cmd_config *cfg = cb;
543+
struct cmdnames *aliases = data;
577544
const char *subsection, *key;
578545
size_t subsection_len;
579546

580-
if (!strcmp(var, "help.autocorrect")) {
581-
int v = parse_autocorrect(value);
582-
583-
if (!v) {
584-
v = git_config_int(var, value, ctx->kvi);
585-
if (v < 0 || v == 1)
586-
v = AUTOCORRECT_IMMEDIATELY;
587-
}
588-
589-
cfg->autocorrect = v;
590-
}
591-
592-
/* Also use aliases for command lookup */
593547
if (!parse_config_key(var, "alias", &subsection, &subsection_len,
594548
&key)) {
595549
size_t key_len = strlen(key);
596550

597551
if (subsection) {
598552
/* [alias "name"] command = value */
599553
if (!strcmp(key, "command"))
600-
add_cmdname(&cfg->aliases, subsection,
554+
add_cmdname(aliases, subsection,
601555
subsection_len);
602556
else {
603557
key = var + strlen("alias.");
604558
key_len = strlen(key);
605-
add_cmdname(&cfg->aliases, key, key_len);
559+
add_cmdname(aliases, key, key_len);
606560
}
607561
} else {
608562
/* alias.name = value */
609-
add_cmdname(&cfg->aliases, key, key_len);
563+
add_cmdname(aliases, key, key_len);
610564
}
611565
}
612566

@@ -633,38 +587,32 @@ static void add_cmd_list(struct cmdnames *cmds, struct cmdnames *old)
633587
old->cnt = 0;
634588
}
635589

636-
/* An empirically derived magic number */
637-
#define SIMILARITY_FLOOR 7
638-
#define SIMILAR_ENOUGH(x) ((x) < SIMILARITY_FLOOR)
639-
640590
static const char bad_interpreter_advice[] =
641591
N_("'%s' appears to be a git command, but we were not\n"
642592
"able to execute it. Maybe git-%s is broken?");
643593

644594
char *help_unknown_cmd(const char *cmd)
645595
{
646-
struct help_unknown_cmd_config cfg = { 0 };
596+
struct cmdnames aliases = { 0 };
597+
struct autocorrect autocorrect = { 0 };
647598
int i, n, best_similarity = 0;
648599
struct cmdnames main_cmds = { 0 };
649600
struct cmdnames other_cmds = { 0 };
650601
struct cmdname_help *common_cmds;
651602

652-
read_early_config(the_repository, git_unknown_cmd_config, &cfg);
653-
654-
/*
655-
* Disable autocorrection prompt in a non-interactive session
656-
*/
657-
if ((cfg.autocorrect == AUTOCORRECT_PROMPT) && (!isatty(0) || !isatty(2)))
658-
cfg.autocorrect = AUTOCORRECT_NEVER;
603+
autocorrect_resolve(&autocorrect);
659604

660-
if (cfg.autocorrect == AUTOCORRECT_NEVER) {
605+
if (autocorrect.mode == AUTOCORRECT_NEVER) {
661606
fprintf_ln(stderr, _("git: '%s' is not a git command. See 'git --help'."), cmd);
662607
exit(1);
663608
}
664609

665610
load_command_list("git-", &main_cmds, &other_cmds);
666611

667-
add_cmd_list(&main_cmds, &cfg.aliases);
612+
/* Also use aliases for command lookup */
613+
read_early_config(the_repository, resolve_aliases, &aliases);
614+
615+
add_cmd_list(&main_cmds, &aliases);
668616
add_cmd_list(&main_cmds, &other_cmds);
669617
QSORT(main_cmds.names, main_cmds.cnt, cmdname_compare);
670618
uniq(&main_cmds);
@@ -714,7 +662,7 @@ char *help_unknown_cmd(const char *cmd)
714662

715663
if (main_cmds.cnt <= n) {
716664
/* prefix matches with everything? that is too ambiguous */
717-
best_similarity = SIMILARITY_FLOOR + 1;
665+
best_similarity = AUTOCORRECT_SIMILARITY_FLOOR + 1;
718666
} else {
719667
/* count all the most similar ones */
720668
for (best_similarity = main_cmds.names[n++]->len;
@@ -723,49 +671,29 @@ char *help_unknown_cmd(const char *cmd)
723671
n++)
724672
; /* still counting */
725673
}
726-
if (cfg.autocorrect && cfg.autocorrect != AUTOCORRECT_SHOW && n == 1 &&
727-
SIMILAR_ENOUGH(best_similarity)) {
674+
675+
if (autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&
676+
AUTOCORRECT_SIMILAR_ENOUGH(best_similarity)) {
728677
char *assumed = xstrdup(main_cmds.names[0]->name);
729678

730679
fprintf_ln(stderr,
731-
_("WARNING: You called a Git command named '%s', "
732-
"which does not exist."),
680+
_("WARNING: You called a Git command named '%s', which does not exist."),
733681
cmd);
734-
if (cfg.autocorrect == AUTOCORRECT_IMMEDIATELY)
735-
fprintf_ln(stderr,
736-
_("Continuing under the assumption that "
737-
"you meant '%s'."),
738-
assumed);
739-
else if (cfg.autocorrect == AUTOCORRECT_PROMPT) {
740-
char *answer;
741-
struct strbuf msg = STRBUF_INIT;
742-
strbuf_addf(&msg, _("Run '%s' instead [y/N]? "), assumed);
743-
answer = git_prompt(msg.buf, PROMPT_ECHO);
744-
strbuf_release(&msg);
745-
if (!(starts_with(answer, "y") ||
746-
starts_with(answer, "Y")))
747-
exit(1);
748-
} else {
749-
fprintf_ln(stderr,
750-
_("Continuing in %0.1f seconds, "
751-
"assuming that you meant '%s'."),
752-
(float)cfg.autocorrect/10.0, assumed);
753-
sleep_millisec(cfg.autocorrect * 100);
754-
}
755682

756-
cmdnames_release(&cfg.aliases);
683+
autocorrect_confirm(&autocorrect, assumed);
684+
685+
cmdnames_release(&aliases);
757686
cmdnames_release(&main_cmds);
758687
cmdnames_release(&other_cmds);
759688
return assumed;
760689
}
761690

762691
fprintf_ln(stderr, _("git: '%s' is not a git command. See 'git --help'."), cmd);
763692

764-
if (SIMILAR_ENOUGH(best_similarity)) {
693+
if (AUTOCORRECT_SIMILAR_ENOUGH(best_similarity)) {
765694
fprintf_ln(stderr,
766695
Q_("\nThe most similar command is",
767-
"\nThe most similar commands are",
768-
n));
696+
"\nThe most similar commands are", n));
769697

770698
for (i = 0; i < n; i++)
771699
fprintf(stderr, "\t%s\n", main_cmds.names[i]->name);

meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ libgit_sources = [
290290
'archive-zip.c',
291291
'archive.c',
292292
'attr.c',
293+
'autocorrect.c',
293294
'base85.c',
294295
'bisect.c',
295296
'blame.c',

0 commit comments

Comments
 (0)