diff options
-rw-r--r-- | builtin/checkout.c | 86 | ||||
-rwxr-xr-x | t/t2025-checkout-to.sh | 25 |
2 files changed, 104 insertions, 7 deletions
diff --git a/builtin/checkout.c b/builtin/checkout.c index 645135a812..01a28b4864 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -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; } diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh index 49634157bf..edd34049cf 100755 --- a/t/t2025-checkout-to.sh +++ b/t/t2025-checkout-to.sh @@ -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 |