Skip to content

Commit e3de372

Browse files
committed
Merge branch 'jc/checkout-auto-track'
* jc/checkout-auto-track: git checkout --no-guess DWIM "git checkout frotz" to "git checkout -b frotz origin/frotz" check_filename(): make verify_filename() callable without dying
2 parents 68d00fd + 46148dd commit e3de372

File tree

3 files changed

+83
-21
lines changed

3 files changed

+83
-21
lines changed

builtin-checkout.c

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,40 @@ static int interactive_checkout(const char *revision, const char **pathspec,
572572
return run_add_interactive(revision, "--patch=checkout", pathspec);
573573
}
574574

575+
struct tracking_name_data {
576+
const char *name;
577+
char *remote;
578+
int unique;
579+
};
580+
581+
static int check_tracking_name(const char *refname, const unsigned char *sha1,
582+
int flags, void *cb_data)
583+
{
584+
struct tracking_name_data *cb = cb_data;
585+
const char *slash;
586+
587+
if (prefixcmp(refname, "refs/remotes/"))
588+
return 0;
589+
slash = strchr(refname + 13, '/');
590+
if (!slash || strcmp(slash + 1, cb->name))
591+
return 0;
592+
if (cb->remote) {
593+
cb->unique = 0;
594+
return 0;
595+
}
596+
cb->remote = xstrdup(refname);
597+
return 0;
598+
}
599+
600+
static const char *unique_tracking_name(const char *name)
601+
{
602+
struct tracking_name_data cb_data = { name, NULL, 1 };
603+
for_each_ref(check_tracking_name, &cb_data);
604+
if (cb_data.unique)
605+
return cb_data.remote;
606+
free(cb_data.remote);
607+
return NULL;
608+
}
575609

576610
int cmd_checkout(int argc, const char **argv, const char *prefix)
577611
{
@@ -582,6 +616,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
582616
struct tree *source_tree = NULL;
583617
char *conflict_style = NULL;
584618
int patch_mode = 0;
619+
int dwim_new_local_branch = 1;
585620
struct option options[] = {
586621
OPT__QUIET(&opts.quiet),
587622
OPT_STRING('b', NULL, &opts.new_branch, "new branch", "branch"),
@@ -597,6 +632,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
597632
OPT_STRING(0, "conflict", &conflict_style, "style",
598633
"conflict style (merge or diff3)"),
599634
OPT_BOOLEAN('p', "patch", &patch_mode, "select hunks interactively"),
635+
{ OPTION_BOOLEAN, 0, "guess", &dwim_new_local_branch, NULL,
636+
"second guess 'git checkout no-such-branch'",
637+
PARSE_OPT_NOARG | PARSE_OPT_HIDDEN },
600638
OPT_END(),
601639
};
602640
int has_dash_dash;
@@ -630,8 +668,6 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
630668
opts.new_branch = argv0 + 1;
631669
}
632670

633-
if (opts.track == BRANCH_TRACK_UNSPECIFIED)
634-
opts.track = git_branch_track;
635671
if (conflict_style) {
636672
opts.merge = 1; /* implied */
637673
git_xmerge_config("merge.conflictstyle", conflict_style, NULL);
@@ -655,6 +691,11 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
655691
* With no paths, if <something> is a commit, that is to
656692
* switch to the branch or detach HEAD at it.
657693
*
694+
* With no paths, if <something> is _not_ a commit, no -t nor -b
695+
* was given, and there is a tracking branch whose name is
696+
* <something> in one and only one remote, then this is a short-hand
697+
* to fork local <something> from that remote tracking branch.
698+
*
658699
* Otherwise <something> shall not be ambiguous.
659700
* - If it's *only* a reference, treat it like case (1).
660701
* - If it's only a path, treat it like case (2).
@@ -677,7 +718,21 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
677718
if (get_sha1(arg, rev)) {
678719
if (has_dash_dash) /* case (1) */
679720
die("invalid reference: %s", arg);
680-
goto no_reference; /* case (3 -> 2) */
721+
if (!patch_mode &&
722+
dwim_new_local_branch &&
723+
opts.track == BRANCH_TRACK_UNSPECIFIED &&
724+
!opts.new_branch &&
725+
!check_filename(NULL, arg) &&
726+
argc == 1) {
727+
const char *remote = unique_tracking_name(arg);
728+
if (!remote || get_sha1(remote, rev))
729+
goto no_reference;
730+
opts.new_branch = arg;
731+
arg = remote;
732+
/* DWIMmed to create local branch */
733+
}
734+
else
735+
goto no_reference;
681736
}
682737

683738
/* we can't end up being in (2) anymore, eat the argument */
@@ -715,6 +770,10 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
715770
}
716771

717772
no_reference:
773+
774+
if (opts.track == BRANCH_TRACK_UNSPECIFIED)
775+
opts.track = git_branch_track;
776+
718777
if (argc) {
719778
const char **pathspec = get_pathspec(prefix, argv);
720779

cache.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ extern const char *setup_git_directory_gently(int *);
396396
extern const char *setup_git_directory(void);
397397
extern const char *prefix_path(const char *prefix, int len, const char *path);
398398
extern const char *prefix_filename(const char *prefix, int len, const char *path);
399+
extern int check_filename(const char *prefix, const char *name);
399400
extern void verify_filename(const char *prefix, const char *name);
400401
extern void verify_non_filename(const char *prefix, const char *name);
401402

setup.c

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@ const char *prefix_filename(const char *pfx, int pfx_len, const char *arg)
6161
return path;
6262
}
6363

64+
int check_filename(const char *prefix, const char *arg)
65+
{
66+
const char *name;
67+
struct stat st;
68+
69+
name = prefix ? prefix_filename(prefix, strlen(prefix), arg) : arg;
70+
if (!lstat(name, &st))
71+
return 1; /* file exists */
72+
if (errno == ENOENT || errno == ENOTDIR)
73+
return 0; /* file does not exist */
74+
die_errno("failed to stat '%s'", arg);
75+
}
76+
6477
/*
6578
* Verify a filename that we got as an argument for a pathspec
6679
* entry. Note that a filename that begins with "-" never verifies
@@ -70,18 +83,12 @@ const char *prefix_filename(const char *pfx, int pfx_len, const char *arg)
7083
*/
7184
void verify_filename(const char *prefix, const char *arg)
7285
{
73-
const char *name;
74-
struct stat st;
75-
7686
if (*arg == '-')
7787
die("bad flag '%s' used after filename", arg);
78-
name = prefix ? prefix_filename(prefix, strlen(prefix), arg) : arg;
79-
if (!lstat(name, &st))
88+
if (check_filename(prefix, arg))
8089
return;
81-
if (errno == ENOENT)
82-
die("ambiguous argument '%s': unknown revision or path not in the working tree.\n"
83-
"Use '--' to separate paths from revisions", arg);
84-
die_errno("failed to stat '%s'", arg);
90+
die("ambiguous argument '%s': unknown revision or path not in the working tree.\n"
91+
"Use '--' to separate paths from revisions", arg);
8592
}
8693

8794
/*
@@ -91,19 +98,14 @@ void verify_filename(const char *prefix, const char *arg)
9198
*/
9299
void verify_non_filename(const char *prefix, const char *arg)
93100
{
94-
const char *name;
95-
struct stat st;
96-
97101
if (!is_inside_work_tree() || is_inside_git_dir())
98102
return;
99103
if (*arg == '-')
100104
return; /* flag */
101-
name = prefix ? prefix_filename(prefix, strlen(prefix), arg) : arg;
102-
if (!lstat(name, &st))
103-
die("ambiguous argument '%s': both revision and filename\n"
104-
"Use '--' to separate filenames from revisions", arg);
105-
if (errno != ENOENT && errno != ENOTDIR)
106-
die_errno("failed to stat '%s'", arg);
105+
if (!check_filename(prefix, arg))
106+
return;
107+
die("ambiguous argument '%s': both revision and filename\n"
108+
"Use '--' to separate filenames from revisions", arg);
107109
}
108110

109111
const char **get_pathspec(const char *prefix, const char **pathspec)

0 commit comments

Comments
 (0)