checkout: reject if the branch is already checked out elsewhere
authorNguyễn Thái Ngọc Duy <pclouds@gmail.com>
Sun, 30 Nov 2014 08:24:49 +0000 (15:24 +0700)
committerJunio C Hamano <gitster@pobox.com>
Mon, 1 Dec 2014 19:00:17 +0000 (11:00 -0800)
One branch obviously can't be checked out at two places (but detached
heads are ok). Give the user a choice in this case: --detach, -b
new-branch, switch branch in the other checkout first or simply 'cd'
and continue to work there.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
builtin/checkout.c
t/t2025-checkout-to.sh
index 645135a812344b45c08f3605e57080c9ab14cd5f..01a28b4864683118c665743cf956b05bbd2b625c 100644 (file)
@@ -430,6 +430,11 @@ struct branch_info {
        const char *name; /* The short name used */
        const char *path; /* The full name of a real branch */
        struct commit *commit; /* The named commit */
+       /*
+        * if not null the branch is detached because it's already
+        * checked out in this checkout
+        */
+       char *checkout;
 };
 
 static void setup_branch_path(struct branch_info *branch)
@@ -958,12 +963,78 @@ static const char *unique_tracking_name(const char *name, unsigned char *sha1)
        return NULL;
 }
 
+static void check_linked_checkout(struct branch_info *new, const char *id)
+{
+       struct strbuf sb = STRBUF_INIT;
+       struct strbuf path = STRBUF_INIT;
+       struct strbuf gitdir = STRBUF_INIT;
+       const char *start, *end;
+
+       if (id)
+               strbuf_addf(&path, "%s/worktrees/%s/HEAD", get_git_common_dir(), id);
+       else
+               strbuf_addf(&path, "%s/HEAD", get_git_common_dir());
+
+       if (strbuf_read_file(&sb, path.buf, 0) < 0 ||
+           !skip_prefix(sb.buf, "ref:", &start))
+               goto done;
+       while (isspace(*start))
+               start++;
+       end = start;
+       while (*end && !isspace(*end))
+               end++;
+       if (strncmp(start, new->path, end - start) || new->path[end - start] != '\0')
+               goto done;
+       if (id) {
+               strbuf_reset(&path);
+               strbuf_addf(&path, "%s/worktrees/%s/gitdir", get_git_common_dir(), id);
+               if (strbuf_read_file(&gitdir, path.buf, 0) <= 0)
+                       goto done;
+               strbuf_rtrim(&gitdir);
+       } else
+               strbuf_addstr(&gitdir, get_git_common_dir());
+       die(_("'%s' is already checked out at '%s'"), new->name, gitdir.buf);
+done:
+       strbuf_release(&path);
+       strbuf_release(&sb);
+       strbuf_release(&gitdir);
+}
+
+static void check_linked_checkouts(struct branch_info *new)
+{
+       struct strbuf path = STRBUF_INIT;
+       DIR *dir;
+       struct dirent *d;
+
+       strbuf_addf(&path, "%s/worktrees", get_git_common_dir());
+       if ((dir = opendir(path.buf)) == NULL) {
+               strbuf_release(&path);
+               return;
+       }
+
+       /*
+        * $GIT_COMMON_DIR/HEAD is practically outside
+        * $GIT_DIR so resolve_ref_unsafe() won't work (it
+        * uses git_path). Parse the ref ourselves.
+        */
+       check_linked_checkout(new, NULL);
+
+       while ((d = readdir(dir)) != NULL) {
+               if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+                       continue;
+               check_linked_checkout(new, d->d_name);
+       }
+       strbuf_release(&path);
+       closedir(dir);
+}
+
 static int parse_branchname_arg(int argc, const char **argv,
                                int dwim_new_local_branch_ok,
                                struct branch_info *new,
                                struct tree **source_tree,
                                unsigned char rev[20],
-                               const char **new_branch)
+                               const char **new_branch,
+                               int force_detach)
 {
        int argcount = 0;
        unsigned char branch_rev[20];
@@ -1085,6 +1156,16 @@ static int parse_branchname_arg(int argc, const char **argv,
        else
                new->path = NULL; /* not an existing branch */
 
+       if (new->path && !force_detach && !*new_branch) {
+               unsigned char sha1[20];
+               int flag;
+               char *head_ref = resolve_refdup("HEAD", 0, sha1, &flag);
+               if (head_ref &&
+                   (!(flag & REF_ISSYMREF) || strcmp(head_ref, new->path)))
+                       check_linked_checkouts(new);
+               free(head_ref);
+       }
+
        new->commit = lookup_commit_reference_gently(rev, 1);
        if (!new->commit) {
                /* not a commit */
@@ -1289,7 +1370,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
                        !opts.new_branch;
                int n = parse_branchname_arg(argc, argv, dwim_ok,
                                             &new, &opts.source_tree,
-                                            rev, &opts.new_branch);
+                                            rev, &opts.new_branch,
+                                            opts.force_detach);
                argv += n;
                argc -= n;
        }
index 49634157bf66e9e7d3b31edd94380678a1ade163..edd34049cf8aea5db097e65b3ce8ff8af4caf268 100755 (executable)
@@ -18,13 +18,14 @@ test_expect_success 'checkout --to an existing worktree' '
 '
 
 test_expect_success 'checkout --to a new worktree' '
-       git checkout --to here master &&
+       git rev-parse HEAD >expect &&
+       git checkout --detach --to here master &&
        (
                cd here &&
                test_cmp ../init.t init.t &&
-               git symbolic-ref HEAD >actual &&
-               echo refs/heads/master >expect &&
-               test_cmp expect actual &&
+               test_must_fail git symbolic-ref HEAD &&
+               git rev-parse HEAD >actual &&
+               test_cmp ../expect actual &&
                git fsck
        )
 '
@@ -42,7 +43,7 @@ test_expect_success 'checkout --to a new worktree from a subdir' '
 test_expect_success 'checkout --to from a linked checkout' '
        (
                cd here &&
-               git checkout --to nested-here master &&
+               git checkout --detach --to nested-here master &&
                cd nested-here &&
                git fsck
        )
@@ -60,4 +61,18 @@ test_expect_success 'checkout --to a new worktree creating new branch' '
        )
 '
 
+test_expect_success 'die the same branch is already checked out' '
+       (
+               cd here &&
+               test_must_fail git checkout newmaster
+       )
+'
+
+test_expect_success 'not die on re-checking out current branch' '
+       (
+               cd there &&
+               git checkout newmaster
+       )
+'
+
 test_done