Skip to content

Commit 85d2533

Browse files
checkout: add fuzzy branch name suggestions
When 'git checkout <branch>' fails because the branch doesn't exist, suggest similar branch names using Levenshtein distance matching, similar to how Git suggests similar commands for typos. This helps users in large teams where remembering exact branch names can be difficult, providing a Unix tab-completion-like experience. The feature: - Shows up to 5 similar branch names when checkout fails - Uses the same Levenshtein distance algorithm as Git's help system - Only suggests branches that are similar enough (distance < 7) - Works for both 'git checkout' and 'git switch' commands - Handles both invalid reference errors and pathspec errors - Avoids suggestions for file paths (containing '/') Includes comprehensive test coverage for the new functionality. Signed-off-by: rajath.k <rajathk95@gmail.com>
1 parent a99f379 commit 85d2533

File tree

5 files changed

+159
-2
lines changed

5 files changed

+159
-2
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,6 +1091,7 @@ LIB_OBJS += blame.o
10911091
LIB_OBJS += blob.o
10921092
LIB_OBJS += bloom.o
10931093
LIB_OBJS += branch.o
1094+
LIB_OBJS += branch-suggestions.o
10941095
LIB_OBJS += bundle-uri.o
10951096
LIB_OBJS += bundle.o
10961097
LIB_OBJS += cache-tree.o

branch-suggestions.c

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#define USE_THE_REPOSITORY_VARIABLE
2+
3+
#include "git-compat-util.h"
4+
#include "branch-suggestions.h"
5+
#include "refs.h"
6+
#include "string-list.h"
7+
#include "levenshtein.h"
8+
#include "gettext.h"
9+
#include "repository.h"
10+
#include "strbuf.h"
11+
12+
#define SIMILARITY_FLOOR 7
13+
#define SIMILAR_ENOUGH(x) ((x) < SIMILARITY_FLOOR)
14+
#define MAX_SUGGESTIONS 5
15+
16+
struct branch_suggestion_cb {
17+
const char *attempted_name;
18+
struct string_list *suggestions;
19+
};
20+
21+
static int collect_branch_cb(const char *refname, const char *referent UNUSED,
22+
const struct object_id *oid UNUSED,
23+
int flags UNUSED, void *cb_data)
24+
{
25+
struct branch_suggestion_cb *cb = cb_data;
26+
const char *branch_name;
27+
28+
/* Since we're using refs_for_each_ref_in with "refs/heads/",
29+
* the refname might already be stripped or might still have the prefix */
30+
if (starts_with(refname, "refs/heads/")) {
31+
branch_name = refname + strlen("refs/heads/");
32+
} else {
33+
branch_name = refname;
34+
}
35+
36+
/* Skip the attempted name itself */
37+
if (!strcmp(branch_name, cb->attempted_name))
38+
return 0;
39+
40+
string_list_append(cb->suggestions, branch_name);
41+
return 0;
42+
}
43+
44+
void suggest_similar_branch_names(const char *attempted_name)
45+
{
46+
struct string_list branches = STRING_LIST_INIT_DUP;
47+
struct branch_suggestion_cb cb_data;
48+
size_t i;
49+
int best_similarity = INT_MAX;
50+
int suggestion_count = 0;
51+
52+
cb_data.attempted_name = attempted_name;
53+
cb_data.suggestions = &branches;
54+
55+
/* Collect all local branch names */
56+
refs_for_each_ref_in(get_main_ref_store(the_repository), "refs/heads/",
57+
collect_branch_cb, &cb_data);
58+
59+
if (!branches.nr)
60+
goto cleanup;
61+
62+
/* Calculate Levenshtein distances */
63+
for (i = 0; i < branches.nr; i++) {
64+
const char *branch_name = branches.items[i].string;
65+
int distance;
66+
67+
/* Give prefix matches a very good score */
68+
if (starts_with(branch_name, attempted_name)) {
69+
distance = 0;
70+
} else {
71+
distance = levenshtein(attempted_name, branch_name, 0, 2, 1, 3);
72+
}
73+
74+
branches.items[i].util = (void *)(intptr_t)distance;
75+
76+
if (distance < best_similarity)
77+
best_similarity = distance;
78+
}
79+
80+
/* Only show suggestions if they're similar enough */
81+
if (!SIMILAR_ENOUGH(best_similarity))
82+
goto cleanup;
83+
84+
/* Count and display similar branches */
85+
for (i = 0; i < branches.nr && suggestion_count < MAX_SUGGESTIONS; i++) {
86+
int distance = (int)(intptr_t)branches.items[i].util;
87+
88+
if (distance <= best_similarity && SIMILAR_ENOUGH(distance)) {
89+
if (suggestion_count == 0) {
90+
fprintf(stderr, "%s\n", _("hint: Did you mean one of these?"));
91+
}
92+
fprintf(stderr, "hint: %s\n", branches.items[i].string);
93+
suggestion_count++;
94+
}
95+
}
96+
97+
cleanup:
98+
string_list_clear(&branches, 0);
99+
}

branch-suggestions.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#ifndef BRANCH_SUGGESTIONS_H
2+
#define BRANCH_SUGGESTIONS_H
3+
4+
/**
5+
* Suggest similar branch names when a branch checkout fails.
6+
* This function analyzes local branches and suggests ones that are
7+
* similar to the attempted branch name using fuzzy matching.
8+
*/
9+
void suggest_similar_branch_names(const char *attempted_name);
10+
11+
#endif /* BRANCH_SUGGESTIONS_H */

builtin/checkout.c

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "builtin.h"
55
#include "advice.h"
66
#include "branch.h"
7+
#include "branch-suggestions.h"
78
#include "cache-tree.h"
89
#include "checkout.h"
910
#include "commit.h"
@@ -605,6 +606,10 @@ static int checkout_paths(const struct checkout_opts *opts,
605606
opts);
606607

607608
if (report_path_error(ps_matched, &opts->pathspec)) {
609+
/* If there's only one pathspec and it looks like a branch name, suggest similar branches */
610+
if (opts->pathspec.nr == 1 && !strchr(opts->pathspec.items[0].original, '/')) {
611+
suggest_similar_branch_names(opts->pathspec.items[0].original);
612+
}
608613
free(ps_matched);
609614
return 1;
610615
}
@@ -1447,8 +1452,10 @@ static int parse_branchname_arg(int argc, const char **argv,
14471452
}
14481453

14491454
if (!recover_with_dwim) {
1450-
if (has_dash_dash)
1455+
if (has_dash_dash) {
1456+
suggest_similar_branch_names(arg);
14511457
die(_("invalid reference: %s"), arg);
1458+
}
14521459
return argcount;
14531460
}
14541461
}
@@ -1635,9 +1642,11 @@ static int checkout_branch(struct checkout_opts *opts,
16351642
} else if (opts->track == BRANCH_TRACK_UNSPECIFIED)
16361643
opts->track = git_branch_track;
16371644

1638-
if (new_branch_info->name && !new_branch_info->commit)
1645+
if (new_branch_info->name && !new_branch_info->commit) {
1646+
suggest_similar_branch_names(new_branch_info->name);
16391647
die(_("Cannot switch branch to a non-commit '%s'"),
16401648
new_branch_info->name);
1649+
}
16411650

16421651
if (noop_switch &&
16431652
!opts->switch_branch_doing_nothing_is_ok)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/bin/sh
2+
3+
test_description='checkout branch suggestions'
4+
5+
. ./test-lib.sh
6+
7+
test_expect_success 'setup' '
8+
test_commit initial &&
9+
git branch feature-authentication &&
10+
git branch feature-authorization &&
11+
git branch bugfix-auth-issue
12+
'
13+
14+
test_expect_success 'suggest similar branch names on checkout failure' '
15+
test_must_fail git checkout feature-auth 2>err &&
16+
grep "hint: Did you mean one of these?" err &&
17+
grep "feature-authentication" err &&
18+
grep "feature-authorization" err
19+
'
20+
21+
test_expect_success 'suggest single branch name on close match' '
22+
test_must_fail git checkout feature-authent 2>err &&
23+
grep "hint: Did you mean one of these?" err &&
24+
grep "feature-authentication" err
25+
'
26+
27+
test_expect_success 'no suggestions for very different names' '
28+
test_must_fail git checkout completely-different-name 2>err &&
29+
! grep "hint: Did you mean" err
30+
'
31+
32+
test_expect_success 'no suggestions for paths with slashes' '
33+
test_must_fail git checkout nonexistent/file.txt 2>err &&
34+
! grep "hint: Did you mean" err
35+
'
36+
37+
test_done

0 commit comments

Comments
 (0)