Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,7 @@ LIB_OBJS += blame.o
LIB_OBJS += blob.o
LIB_OBJS += bloom.o
LIB_OBJS += branch.o
LIB_OBJS += branch-suggestions.o
LIB_OBJS += bundle-uri.o
LIB_OBJS += bundle.o
LIB_OBJS += cache-tree.o
Expand Down
99 changes: 99 additions & 0 deletions branch-suggestions.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#define USE_THE_REPOSITORY_VARIABLE

#include "git-compat-util.h"
#include "branch-suggestions.h"
#include "refs.h"
#include "string-list.h"
#include "levenshtein.h"
#include "gettext.h"
#include "repository.h"
#include "strbuf.h"

#define SIMILARITY_FLOOR 7
#define SIMILAR_ENOUGH(x) ((x) < SIMILARITY_FLOOR)
#define MAX_SUGGESTIONS 5

struct branch_suggestion_cb {
const char *attempted_name;
struct string_list *suggestions;
};

static int collect_branch_cb(const char *refname, const char *referent UNUSED,
const struct object_id *oid UNUSED,
int flags UNUSED, void *cb_data)
{
struct branch_suggestion_cb *cb = cb_data;
const char *branch_name;

/* Since we're using refs_for_each_ref_in with "refs/heads/",
* the refname might already be stripped or might still have the prefix */
if (starts_with(refname, "refs/heads/")) {
branch_name = refname + strlen("refs/heads/");
} else {
branch_name = refname;
}

/* Skip the attempted name itself */
if (!strcmp(branch_name, cb->attempted_name))
return 0;

string_list_append(cb->suggestions, branch_name);
return 0;
}

void suggest_similar_branch_names(const char *attempted_name)
{
struct string_list branches = STRING_LIST_INIT_DUP;
struct branch_suggestion_cb cb_data;
size_t i;
int best_similarity = INT_MAX;
int suggestion_count = 0;

cb_data.attempted_name = attempted_name;
cb_data.suggestions = &branches;

/* Collect all local branch names */
refs_for_each_ref_in(get_main_ref_store(the_repository), "refs/heads/",
collect_branch_cb, &cb_data);

if (!branches.nr)
goto cleanup;

/* Calculate Levenshtein distances */
for (i = 0; i < branches.nr; i++) {
const char *branch_name = branches.items[i].string;
int distance;

/* Give prefix matches a very good score */
if (starts_with(branch_name, attempted_name)) {
distance = 0;
} else {
distance = levenshtein(attempted_name, branch_name, 0, 2, 1, 3);
}

branches.items[i].util = (void *)(intptr_t)distance;

if (distance < best_similarity)
best_similarity = distance;
}

/* Only show suggestions if they're similar enough */
if (!SIMILAR_ENOUGH(best_similarity))
goto cleanup;

/* Count and display similar branches */
for (i = 0; i < branches.nr && suggestion_count < MAX_SUGGESTIONS; i++) {
int distance = (int)(intptr_t)branches.items[i].util;

if (distance <= best_similarity && SIMILAR_ENOUGH(distance)) {
if (suggestion_count == 0) {
fprintf(stderr, "%s\n", _("hint: Did you mean one of these?"));
}
fprintf(stderr, "hint: %s\n", branches.items[i].string);
suggestion_count++;
}
}

cleanup:
string_list_clear(&branches, 0);
}
11 changes: 11 additions & 0 deletions branch-suggestions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#ifndef BRANCH_SUGGESTIONS_H
#define BRANCH_SUGGESTIONS_H

/**
* Suggest similar branch names when a branch checkout fails.
* This function analyzes local branches and suggests ones that are
* similar to the attempted branch name using fuzzy matching.
*/
void suggest_similar_branch_names(const char *attempted_name);

#endif /* BRANCH_SUGGESTIONS_H */
13 changes: 11 additions & 2 deletions builtin/checkout.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "builtin.h"
#include "advice.h"
#include "branch.h"
#include "branch-suggestions.h"
#include "cache-tree.h"
#include "checkout.h"
#include "commit.h"
Expand Down Expand Up @@ -605,6 +606,10 @@ static int checkout_paths(const struct checkout_opts *opts,
opts);

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

if (!recover_with_dwim) {
if (has_dash_dash)
if (has_dash_dash) {
suggest_similar_branch_names(arg);
die(_("invalid reference: %s"), arg);
}
return argcount;
}
}
Expand Down Expand Up @@ -1635,9 +1642,11 @@ static int checkout_branch(struct checkout_opts *opts,
} else if (opts->track == BRANCH_TRACK_UNSPECIFIED)
opts->track = git_branch_track;

if (new_branch_info->name && !new_branch_info->commit)
if (new_branch_info->name && !new_branch_info->commit) {
suggest_similar_branch_names(new_branch_info->name);
die(_("Cannot switch branch to a non-commit '%s'"),
new_branch_info->name);
}

if (noop_switch &&
!opts->switch_branch_doing_nothing_is_ok)
Expand Down
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ libgit_sources = [
'blob.c',
'bloom.c',
'branch.c',
'branch-suggestions.c',
'bundle-uri.c',
'bundle.c',
'cache-tree.c',
Expand Down
37 changes: 37 additions & 0 deletions t/t2028-checkout-branch-suggestions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/sh

test_description='checkout branch suggestions'

. ./test-lib.sh

test_expect_success 'setup' '
test_commit initial &&
git branch feature-authentication &&
git branch feature-authorization &&
git branch bugfix-auth-issue
'

test_expect_success 'suggest similar branch names on checkout failure' '
test_must_fail git checkout feature-auth 2>err &&
grep "hint: Did you mean one of these?" err &&
grep "feature-authentication" err &&
grep "feature-authorization" err
'

test_expect_success 'suggest single branch name on close match' '
test_must_fail git checkout feature-authent 2>err &&
grep "hint: Did you mean one of these?" err &&
grep "feature-authentication" err
'

test_expect_success 'no suggestions for very different names' '
test_must_fail git checkout completely-different-name 2>err &&
! grep "hint: Did you mean" err
'

test_expect_success 'no suggestions for paths with slashes' '
test_must_fail git checkout nonexistent/file.txt 2>err &&
! grep "hint: Did you mean" err
'

test_done
Loading