Skip to content

Commit 277cd4c

Browse files
committed
Merge branch 'ar/autospell'
* ar/autospell: Add help.autocorrect to enable/disable autocorrecting git wrapper: DWIM mistyped commands
2 parents cd50988 + f0e9071 commit 277cd4c

File tree

8 files changed

+159
-4
lines changed

8 files changed

+159
-4
lines changed

Documentation/config.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,15 @@ help.format::
790790
Values 'man', 'info', 'web' and 'html' are supported. 'man' is
791791
the default. 'web' and 'html' are the same.
792792

793+
help.autocorrect::
794+
Automatically correct and execute mistyped commands after
795+
waiting for the given number of deciseconds (0.1 sec). If more
796+
than one command can be deduced from the entered text, nothing
797+
will be executed. If the value of this option is negative,
798+
the corrected command will be executed immediately. If the
799+
value is 0 - the command will be just shown but not executed.
800+
This is the default.
801+
793802
http.proxy::
794803
Override the HTTP proxy, normally configured using the 'http_proxy'
795804
environment variable (see linkgit:curl[1]). This can be overridden

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ LIB_H += graph.h
358358
LIB_H += grep.h
359359
LIB_H += hash.h
360360
LIB_H += help.h
361+
LIB_H += levenshtein.h
361362
LIB_H += list-objects.h
362363
LIB_H += ll-merge.h
363364
LIB_H += log-tree.h
@@ -434,6 +435,7 @@ LIB_OBJS += hash.o
434435
LIB_OBJS += help.o
435436
LIB_OBJS += ident.o
436437
LIB_OBJS += interpolate.o
438+
LIB_OBJS += levenshtein.o
437439
LIB_OBJS += list-objects.o
438440
LIB_OBJS += ll-merge.o
439441
LIB_OBJS += lockfile.o

builtin.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ extern const char git_usage_string[];
1111
extern const char git_more_info_string[];
1212

1313
extern void list_common_cmds_help(void);
14-
extern void help_unknown_cmd(const char *cmd);
14+
extern const char *help_unknown_cmd(const char *cmd);
1515
extern void prune_packed_objects(int);
1616
extern int read_line_with_nul(char *buf, int size, FILE *file);
1717
extern int fmt_merge_msg(int merge_summary, struct strbuf *in,

git.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,9 @@ int main(int argc, const char **argv)
499499
cmd, argv[0]);
500500
exit(1);
501501
}
502-
help_unknown_cmd(cmd);
502+
argv[0] = help_unknown_cmd(cmd);
503+
handle_internal_command(argc, argv);
504+
execv_dashed_external(argv);
503505
}
504506

505507
fprintf(stderr, "Failed to run command '%s': %s\n",

help.c

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "cache.h"
22
#include "builtin.h"
33
#include "exec_cmd.h"
4+
#include "levenshtein.h"
45
#include "help.h"
56

67
/* most GUI terminals set COLUMNS (although some don't export it) */
@@ -37,6 +38,16 @@ void add_cmdname(struct cmdnames *cmds, const char *name, int len)
3738
cmds->names[cmds->cnt++] = ent;
3839
}
3940

41+
static void clean_cmdnames(struct cmdnames *cmds)
42+
{
43+
int i;
44+
for (i = 0; i < cmds->cnt; ++i)
45+
free(cmds->names[i]);
46+
free(cmds->names);
47+
cmds->cnt = 0;
48+
cmds->alloc = 0;
49+
}
50+
4051
static int cmdname_compare(const void *a_, const void *b_)
4152
{
4253
struct cmdname *a = *(struct cmdname **)a_;
@@ -250,9 +261,85 @@ int is_in_cmdlist(struct cmdnames *c, const char *s)
250261
return 0;
251262
}
252263

253-
void help_unknown_cmd(const char *cmd)
264+
static int autocorrect;
265+
266+
static int git_unknown_cmd_config(const char *var, const char *value, void *cb)
267+
{
268+
if (!strcmp(var, "help.autocorrect"))
269+
autocorrect = git_config_int(var,value);
270+
271+
return git_default_config(var, value, cb);
272+
}
273+
274+
static int levenshtein_compare(const void *p1, const void *p2)
254275
{
276+
const struct cmdname *const *c1 = p1, *const *c2 = p2;
277+
const char *s1 = (*c1)->name, *s2 = (*c2)->name;
278+
int l1 = (*c1)->len;
279+
int l2 = (*c2)->len;
280+
return l1 != l2 ? l1 - l2 : strcmp(s1, s2);
281+
}
282+
283+
const char *help_unknown_cmd(const char *cmd)
284+
{
285+
int i, n, best_similarity = 0;
286+
struct cmdnames main_cmds, other_cmds;
287+
288+
memset(&main_cmds, 0, sizeof(main_cmds));
289+
memset(&other_cmds, 0, sizeof(main_cmds));
290+
291+
git_config(git_unknown_cmd_config, NULL);
292+
293+
load_command_list("git-", &main_cmds, &other_cmds);
294+
295+
ALLOC_GROW(main_cmds.names, main_cmds.cnt + other_cmds.cnt,
296+
main_cmds.alloc);
297+
memcpy(main_cmds.names + main_cmds.cnt, other_cmds.names,
298+
other_cmds.cnt * sizeof(other_cmds.names[0]));
299+
main_cmds.cnt += other_cmds.cnt;
300+
free(other_cmds.names);
301+
302+
/* This reuses cmdname->len for similarity index */
303+
for (i = 0; i < main_cmds.cnt; ++i)
304+
main_cmds.names[i]->len =
305+
levenshtein(cmd, main_cmds.names[i]->name, 0, 2, 1, 4);
306+
307+
qsort(main_cmds.names, main_cmds.cnt,
308+
sizeof(*main_cmds.names), levenshtein_compare);
309+
310+
if (!main_cmds.cnt)
311+
die ("Uh oh. Your system reports no Git commands at all.");
312+
313+
best_similarity = main_cmds.names[0]->len;
314+
n = 1;
315+
while (n < main_cmds.cnt && best_similarity == main_cmds.names[n]->len)
316+
++n;
317+
if (autocorrect && n == 1) {
318+
const char *assumed = main_cmds.names[0]->name;
319+
main_cmds.names[0] = NULL;
320+
clean_cmdnames(&main_cmds);
321+
fprintf(stderr, "WARNING: You called a Git program named '%s', "
322+
"which does not exist.\n"
323+
"Continuing under the assumption that you meant '%s'\n",
324+
cmd, assumed);
325+
if (autocorrect > 0) {
326+
fprintf(stderr, "in %0.1f seconds automatically...\n",
327+
(float)autocorrect/10.0);
328+
poll(NULL, 0, autocorrect * 100);
329+
}
330+
return assumed;
331+
}
332+
255333
fprintf(stderr, "git: '%s' is not a git-command. See 'git --help'.\n", cmd);
334+
335+
if (best_similarity < 6) {
336+
fprintf(stderr, "\nDid you mean %s?\n",
337+
n < 2 ? "this": "one of these");
338+
339+
for (i = 0; i < n; i++)
340+
fprintf(stderr, "\t%s\n", main_cmds.names[i]->name);
341+
}
342+
256343
exit(1);
257344
}
258345

help.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ struct cmdnames {
55
int alloc;
66
int cnt;
77
struct cmdname {
8-
size_t len;
8+
size_t len; /* also used for similarity index in help.c */
99
char name[FLEX_ARRAY];
1010
} **names;
1111
};

levenshtein.c

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#include "cache.h"
2+
#include "levenshtein.h"
3+
4+
int levenshtein(const char *string1, const char *string2,
5+
int w, int s, int a, int d)
6+
{
7+
int len1 = strlen(string1), len2 = strlen(string2);
8+
int *row0 = xmalloc(sizeof(int) * (len2 + 1));
9+
int *row1 = xmalloc(sizeof(int) * (len2 + 1));
10+
int *row2 = xmalloc(sizeof(int) * (len2 + 1));
11+
int i, j;
12+
13+
for (j = 0; j <= len2; j++)
14+
row1[j] = j * a;
15+
for (i = 0; i < len1; i++) {
16+
int *dummy;
17+
18+
row2[0] = (i + 1) * d;
19+
for (j = 0; j < len2; j++) {
20+
/* substitution */
21+
row2[j + 1] = row1[j] + s * (string1[i] != string2[j]);
22+
/* swap */
23+
if (i > 0 && j > 0 && string1[i - 1] == string2[j] &&
24+
string1[i] == string2[j - 1] &&
25+
row2[j + 1] > row0[j - 1] + w)
26+
row2[j + 1] = row0[j - 1] + w;
27+
/* deletion */
28+
if (j + 1 < len2 && row2[j + 1] > row1[j + 1] + d)
29+
row2[j + 1] = row1[j + 1] + d;
30+
/* insertion */
31+
if (row2[j + 1] > row2[j] + a)
32+
row2[j + 1] = row2[j] + a;
33+
}
34+
35+
dummy = row0;
36+
row0 = row1;
37+
row1 = row2;
38+
row2 = dummy;
39+
}
40+
41+
i = row1[len2];
42+
free(row0);
43+
free(row1);
44+
free(row2);
45+
46+
return i;
47+
}

levenshtein.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#ifndef LEVENSHTEIN_H
2+
#define LEVENSHTEIN_H
3+
4+
int levenshtein(const char *string1, const char *string2,
5+
int swap_penalty, int substition_penalty,
6+
int insertion_penalty, int deletion_penalty);
7+
8+
#endif

0 commit comments

Comments
 (0)