Skip to content

Commit 2461beb

Browse files
committed
Merge branch 'js/parseopt-subcommand-autocorrection' into jch
The parse-options library learned to auto-correct misspelled subcommand names. * 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 eabe6a7 + 916b450 commit 2461beb

File tree

11 files changed

+325
-142
lines changed

11 files changed

+325
-142
lines changed

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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#ifndef AUTOCORRECT_H
2+
#define AUTOCORRECT_H
3+
4+
enum autocorrect_mode {
5+
AUTOCORRECT_HINT,
6+
AUTOCORRECT_NEVER,
7+
AUTOCORRECT_PROMPT,
8+
AUTOCORRECT_IMMEDIATELY,
9+
AUTOCORRECT_DELAY,
10+
};
11+
12+
/**
13+
* `mode` indicates which action will be performed by autocorrect_confirm().
14+
* `delay` is the timeout before autocorrect_confirm() returns, in tenths of a
15+
* second. Use it only with AUTOCORRECT_DELAY.
16+
*/
17+
struct autocorrect {
18+
enum autocorrect_mode mode;
19+
int delay;
20+
};
21+
22+
/**
23+
* Resolve the autocorrect configuration into `conf`.
24+
*/
25+
void autocorrect_resolve(struct autocorrect *conf);
26+
27+
/**
28+
* Interact with the user in different ways depending on `conf->mode`.
29+
*/
30+
void autocorrect_confirm(struct autocorrect *conf, const char *assumed);
31+
32+
#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_AUTOCORR);
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_AUTOCORR);
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: 20 additions & 87 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,70 +537,23 @@ 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)
540+
static int resolve_aliases(const char *var, const char *value UNUSED,
541+
const struct config_context *ctx UNUSED, void *data)
550542
{
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)
575-
{
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
if (subsection) {
596550
/* [alias "name"] command = value */
597551
if (!strcmp(key, "command"))
598-
add_cmdname(&cfg->aliases, subsection,
552+
add_cmdname(aliases, subsection,
599553
subsection_len);
600554
} else {
601555
/* alias.name = value */
602-
add_cmdname(&cfg->aliases, key, strlen(key));
556+
add_cmdname(aliases, key, strlen(key));
603557
}
604558
}
605559

@@ -636,28 +590,26 @@ static const char bad_interpreter_advice[] =
636590

637591
char *help_unknown_cmd(const char *cmd)
638592
{
639-
struct help_unknown_cmd_config cfg = { 0 };
593+
struct cmdnames aliases = { 0 };
594+
struct autocorrect autocorrect = { 0 };
640595
int i, n, best_similarity = 0;
641596
struct cmdnames main_cmds = { 0 };
642597
struct cmdnames other_cmds = { 0 };
643598
struct cmdname_help *common_cmds;
644599

645-
read_early_config(the_repository, git_unknown_cmd_config, &cfg);
646-
647-
/*
648-
* Disable autocorrection prompt in a non-interactive session
649-
*/
650-
if ((cfg.autocorrect == AUTOCORRECT_PROMPT) && (!isatty(0) || !isatty(2)))
651-
cfg.autocorrect = AUTOCORRECT_NEVER;
600+
autocorrect_resolve(&autocorrect);
652601

653-
if (cfg.autocorrect == AUTOCORRECT_NEVER) {
602+
if (autocorrect.mode == AUTOCORRECT_NEVER) {
654603
fprintf_ln(stderr, _("git: '%s' is not a git command. See 'git --help'."), cmd);
655604
exit(1);
656605
}
657606

658607
load_command_list("git-", &main_cmds, &other_cmds);
659608

660-
add_cmd_list(&main_cmds, &cfg.aliases);
609+
/* Also use aliases for command lookup */
610+
read_early_config(the_repository, resolve_aliases, &aliases);
611+
612+
add_cmd_list(&main_cmds, &aliases);
661613
add_cmd_list(&main_cmds, &other_cmds);
662614
QSORT(main_cmds.names, main_cmds.cnt, cmdname_compare);
663615
uniq(&main_cmds);
@@ -716,37 +668,18 @@ char *help_unknown_cmd(const char *cmd)
716668
n++)
717669
; /* still counting */
718670
}
719-
if (cfg.autocorrect && cfg.autocorrect != AUTOCORRECT_SHOW && n == 1 &&
671+
672+
if (autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&
720673
SIMILAR_ENOUGH(best_similarity)) {
721674
char *assumed = xstrdup(main_cmds.names[0]->name);
722675

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

749-
cmdnames_release(&cfg.aliases);
680+
autocorrect_confirm(&autocorrect, assumed);
681+
682+
cmdnames_release(&aliases);
750683
cmdnames_release(&main_cmds);
751684
cmdnames_release(&other_cmds);
752685
return assumed;

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)