checkout: disambiguate dwim tracking branches and local files

When checkout dwim is added in [1], it is restricted to only dwim when certain conditions are met and fall back to default checkout behavior otherwise. It turns out falling back could be confusing. One of the conditions to turn git checkout frotz to git checkout -b frotz origin/frotz is that frotz must not exist as a file. But when the user comes to expect "git checkout frotz" to create the branch "frotz" and there happens to be a file named "frotz", git's silently reverting "frotz" file content is not helping. This is reported in Git mailing list [2] and even used as an example of "Git is bad" elsewhere [3]. We normally try to do the right thing, but when there are multiple "right things" to do, it's best to leave it to the user to decide. Check this case, ask the user to to disambiguate: - "git checkout -- foo" will check out path "foo" - "git checkout foo --" will dwim and create branch "foo" [4] For users who do not want dwim, use --no-guess. It's useless in this particular case because "git checkout --no-guess foo --" will just fail. But it could be used by scripts. [1] 70c9ac2f19 (DWIM "git checkout frotz" to "git checkout -b frotz origin/frotz" - 2009-10-18) [2] https://public-inbox.org/git/CACsJy8B2TVr1g+k+eSQ=pBEO3WN4_LtgLo9gpur8X7Z9GOFL_A@mail.gmail.com/ [3] https://news.ycombinator.com/item?id=18230655 [4] a047fafc78 (checkout: allow dwim for branch creation for "git checkout $branch --" - 2013-10-18) Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Nguyễn Thái Ngọc Duy committed Nov 13, 2018 at 18:52 UTC be4908f1038861892878db29441be5a4a024784d
4 files changed +50 -6
Documentation/git-checkout.txt
+4
@@ -276,6 +276,10 @@ section of linkgit:git-add[1] to learn how to operate the `--patch` mode.
276 Just like linkgit:git-submodule[1], this will detach the
277 submodules HEAD.
278
279 +--no-guess::
280 + Do not attempt to create a branch if a remote tracking branch
281 + of the same name exists.
282 +
283 <branch>::
284 Branch to checkout; if it refers to a branch (i.e., a name that,
285 when prepended with "refs/heads/", is a valid ref), then that
builtin/checkout.c
+13 -5
@@ -1079,9 +1079,12 @@ static int parse_branchname_arg(int argc, const char **argv,
1079 */
1080 int recover_with_dwim = dwim_new_local_branch_ok;
1081
1082 - if (!has_dash_dash &&
1083 - (check_filename(opts->prefix, arg) || !no_wildcard(arg)))
1082 + int could_be_checkout_paths = !has_dash_dash &&
1083 + check_filename(opts->prefix, arg);
1084 +
1085 + if (!has_dash_dash && !no_wildcard(arg))
1086 recover_with_dwim = 0;
1087 +
1088 /*
1089 * Accept "git checkout foo" and "git checkout foo --"
1090 * as candidates for dwim.
@@ -1094,6 +1097,10 @@ static int parse_branchname_arg(int argc, const char **argv,
1097 const char *remote = unique_tracking_name(arg, rev,
1098 dwim_remotes_matched);
1099 if (remote) {
1100 + if (could_be_checkout_paths)
1101 + die(_("'%s' could be both a local file and a tracking branch.\n"
1102 + "Please use -- (and optionally --no-guess) to disambiguate"),
1103 + arg);
1104 *new_branch = arg;
1105 arg = remote;
1106 /* DWIMmed to create local branch, case (3).(b) */
@@ -1228,7 +1235,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
1235 struct checkout_opts opts;
1236 struct branch_info new_branch_info;
1237 char *conflict_style = NULL;
1231 - int dwim_new_local_branch = 1;
1238 + int dwim_new_local_branch, no_dwim_new_local_branch = 0;
1239 int dwim_remotes_matched = 0;
1240 struct option options[] = {
1241 OPT__QUIET(&opts.quiet, N_("suppress progress reporting")),
@@ -1258,8 +1265,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
1265 OPT_BOOL('p', "patch", &opts.patch_mode, N_("select hunks interactively")),
1266 OPT_BOOL(0, "ignore-skip-worktree-bits", &opts.ignore_skipworktree,
1267 N_("do not limit pathspecs to sparse entries only")),
1261 - OPT_HIDDEN_BOOL(0, "guess", &dwim_new_local_branch,
1262 - N_("second guess 'git checkout <no-such-branch>'")),
1268 + OPT_BOOL(0, "no-guess", &no_dwim_new_local_branch,
1269 + N_("do not second guess 'git checkout <no-such-branch>'")),
1270 OPT_BOOL(0, "ignore-other-worktrees", &opts.ignore_other_worktrees,
1271 N_("do not check if another worktree is holding the given ref")),
1272 { OPTION_CALLBACK, 0, "recurse-submodules", NULL,
@@ -1282,6 +1289,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
1289 argc = parse_options(argc, argv, prefix, options, checkout_usage,
1290 PARSE_OPT_KEEP_DASHDASH);
1291
1292 + dwim_new_local_branch = !no_dwim_new_local_branch;
1293 if (opts.show_progress < 0) {
1294 if (opts.quiet)
1295 opts.show_progress = 0;
t/t2024-checkout-dwim.sh
+31
@@ -278,4 +278,35 @@ test_expect_success 'loosely defined local base branch is reported correctly' '
278 test_cmp expect actual
279 '
280
281 +test_expect_success 'reject when arg could be part of dwim branch' '
282 + git remote add foo file://non-existent-place &&
283 + git update-ref refs/remotes/foo/dwim-arg HEAD &&
284 + echo foo >dwim-arg &&
285 + git add dwim-arg &&
286 + echo bar >dwim-arg &&
287 + test_must_fail git checkout dwim-arg &&
288 + test_must_fail git rev-parse refs/heads/dwim-arg -- &&
289 + grep bar dwim-arg
290 +'
291 +
292 +test_expect_success 'disambiguate dwim branch and checkout path (1)' '
293 + git update-ref refs/remotes/foo/dwim-arg1 HEAD &&
294 + echo foo >dwim-arg1 &&
295 + git add dwim-arg1 &&
296 + echo bar >dwim-arg1 &&
297 + git checkout -- dwim-arg1 &&
298 + test_must_fail git rev-parse refs/heads/dwim-arg1 -- &&
299 + grep foo dwim-arg1
300 +'
301 +
302 +test_expect_success 'disambiguate dwim branch and checkout path (2)' '
303 + git update-ref refs/remotes/foo/dwim-arg2 HEAD &&
304 + echo foo >dwim-arg2 &&
305 + git add dwim-arg2 &&
306 + echo bar >dwim-arg2 &&
307 + git checkout dwim-arg2 -- &&
308 + git rev-parse refs/heads/dwim-arg2 -- &&
309 + grep bar dwim-arg2
310 +'
311 +
312 test_done
t/t9902-completion.sh
+2 -1
@@ -1434,7 +1434,8 @@ test_expect_success 'double dash "git checkout"' '
1434 --ignore-other-worktrees Z
1435 --recurse-submodules Z
1436 --progress Z
1437 - --no-quiet Z
1437 + --guess Z
1438 + --no-guess Z
1439 --no-... Z
1440 EOF
1441 '