diff --git a/Documentation/doc-diff b/Documentation/doc-diff index cece4fd537..dfd9418778 100755 --- a/Documentation/doc-diff +++ b/Documentation/doc-diff @@ -75,7 +75,7 @@ fi # results that don't differ between the two trees. if ! test -d "$tmp/worktree" then - git worktree add --detach "$tmp/worktree" "$from" && + git worktree add -f --detach "$tmp/worktree" "$from" && dots=$(echo "$tmp/worktree" | sed 's#[^/]*#..#g') && ln -s "$dots/config.mak" "$tmp/worktree/config.mak" fi diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt index 29a5b7e252..e2ee9fc21b 100644 --- a/Documentation/git-worktree.txt +++ b/Documentation/git-worktree.txt @@ -120,8 +120,16 @@ OPTIONS --force:: By default, `add` refuses to create a new working tree when `` is a branch name and is already checked out by - another working tree and `remove` refuses to remove an unclean - working tree. This option overrides these safeguards. + another working tree, or if `` is already assigned to some + working tree but is missing (for instance, if `` was deleted + manually). This option overrides these safeguards. To add a missing but + locked working tree path, specify `--force` twice. ++ +`move` refuses to move a locked working tree unless `--force` is specified +twice. ++ +`remove` refuses to remove an unclean working tree unless `--force` is used. +To remove a locked working tree, specify `--force` twice. -b :: -B :: diff --git a/builtin/worktree.c b/builtin/worktree.c index 41e7714396..c4abbde2b8 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -47,6 +47,26 @@ static int git_worktree_config(const char *var, const char *value, void *cb) return git_default_config(var, value, cb); } +static int delete_git_dir(const char *id) +{ + struct strbuf sb = STRBUF_INIT; + int ret; + + strbuf_addstr(&sb, git_common_path("worktrees/%s", id)); + ret = remove_dir_recursively(&sb, 0); + if (ret < 0 && errno == ENOTDIR) + ret = unlink(sb.buf); + if (ret) + error_errno(_("failed to delete '%s'"), sb.buf); + strbuf_release(&sb); + return ret; +} + +static void delete_worktrees_dir_if_empty(void) +{ + rmdir(git_path("worktrees")); /* ignore failed removal */ +} + static int prune_worktree(const char *id, struct strbuf *reason) { struct stat st; @@ -116,10 +136,8 @@ static int prune_worktree(const char *id, struct strbuf *reason) static void prune_worktrees(void) { struct strbuf reason = STRBUF_INIT; - struct strbuf path = STRBUF_INIT; DIR *dir = opendir(git_path("worktrees")); struct dirent *d; - int ret; if (!dir) return; while ((d = readdir(dir)) != NULL) { @@ -132,18 +150,12 @@ static void prune_worktrees(void) printf("%s\n", reason.buf); if (show_only) continue; - git_path_buf(&path, "worktrees/%s", d->d_name); - ret = remove_dir_recursively(&path, 0); - if (ret < 0 && errno == ENOTDIR) - ret = unlink(path.buf); - if (ret) - error_errno(_("failed to remove '%s'"), path.buf); + delete_git_dir(d->d_name); } closedir(dir); if (!show_only) - rmdir(git_path("worktrees")); + delete_worktrees_dir_if_empty(); strbuf_release(&reason); - strbuf_release(&path); } static int prune(int ac, const char **av, const char *prefix) @@ -212,6 +224,43 @@ static const char *worktree_basename(const char *path, int *olen) return name; } +static void validate_worktree_add(const char *path, const struct add_opts *opts) +{ + struct worktree **worktrees; + struct worktree *wt; + int locked; + + if (file_exists(path) && !is_empty_dir(path)) + die(_("'%s' already exists"), path); + + worktrees = get_worktrees(0); + /* + * find_worktree()'s suffix matching may undesirably find the main + * rather than a linked worktree (for instance, when the basenames + * of the main worktree and the one being created are the same). + * We're only interested in linked worktrees, so skip the main + * worktree with +1. + */ + wt = find_worktree(worktrees + 1, NULL, path); + if (!wt) + goto done; + + locked = !!is_worktree_locked(wt); + if ((!locked && opts->force) || (locked && opts->force > 1)) { + if (delete_git_dir(wt->id)) + die(_("unable to re-add worktree '%s'"), path); + goto done; + } + + if (locked) + die(_("'%s' is a missing but locked worktree;\nuse 'add -f -f' to override, or 'unlock' and 'prune' or 'remove' to clear"), path); + else + die(_("'%s' is a missing but already registered worktree;\nuse 'add -f' to override, or 'prune' or 'remove' to clear"), path); + +done: + free_worktrees(worktrees); +} + static int add_worktree(const char *path, const char *refname, const struct add_opts *opts) { @@ -226,8 +275,7 @@ static int add_worktree(const char *path, const char *refname, struct commit *commit = NULL; int is_branch = 0; - if (file_exists(path) && !is_empty_dir(path)) - die(_("'%s' already exists"), path); + validate_worktree_add(path, opts); /* is 'refname' a branch or commit? */ if (!opts->detach && !strbuf_check_branch_ref(&symref, refname) && @@ -697,13 +745,17 @@ static void validate_no_submodules(const struct worktree *wt) static int move_worktree(int ac, const char **av, const char *prefix) { + int force = 0; struct option options[] = { + OPT__FORCE(&force, + N_("force move even if worktree is dirty or locked"), + PARSE_OPT_NOCOMPLETE), OPT_END() }; struct worktree **worktrees, *wt; struct strbuf dst = STRBUF_INIT; struct strbuf errmsg = STRBUF_INIT; - const char *reason; + const char *reason = NULL; char *path; ac = parse_options(ac, av, prefix, options, worktree_usage, 0); @@ -734,12 +786,13 @@ static int move_worktree(int ac, const char **av, const char *prefix) validate_no_submodules(wt); - reason = is_worktree_locked(wt); + if (force < 2) + reason = is_worktree_locked(wt); if (reason) { if (*reason) - die(_("cannot move a locked working tree, lock reason: %s"), + die(_("cannot move a locked working tree, lock reason: %s\nuse 'move -f -f' to override or unlock first"), reason); - die(_("cannot move a locked working tree")); + die(_("cannot move a locked working tree;\nuse 'move -f -f' to override or unlock first")); } if (validate_worktree(wt, &errmsg, 0)) die(_("validation failed, cannot move working tree: %s"), @@ -822,32 +875,18 @@ static int delete_git_work_tree(struct worktree *wt) return ret; } -static int delete_git_dir(struct worktree *wt) -{ - struct strbuf sb = STRBUF_INIT; - int ret = 0; - - strbuf_addstr(&sb, git_common_path("worktrees/%s", wt->id)); - if (remove_dir_recursively(&sb, 0)) { - error_errno(_("failed to delete '%s'"), sb.buf); - ret = -1; - } - strbuf_release(&sb); - return ret; -} - static int remove_worktree(int ac, const char **av, const char *prefix) { int force = 0; struct option options[] = { OPT__FORCE(&force, - N_("force removing even if the worktree is dirty"), + N_("force removal even if worktree is dirty or locked"), PARSE_OPT_NOCOMPLETE), OPT_END() }; struct worktree **worktrees, *wt; struct strbuf errmsg = STRBUF_INIT; - const char *reason; + const char *reason = NULL; int ret = 0; ac = parse_options(ac, av, prefix, options, worktree_usage, 0); @@ -860,12 +899,13 @@ static int remove_worktree(int ac, const char **av, const char *prefix) die(_("'%s' is not a working tree"), av[0]); if (is_main_worktree(wt)) die(_("'%s' is a main working tree"), av[0]); - reason = is_worktree_locked(wt); + if (force < 2) + reason = is_worktree_locked(wt); if (reason) { if (*reason) - die(_("cannot remove a locked working tree, lock reason: %s"), + die(_("cannot remove a locked working tree, lock reason: %s\nuse 'remove -f -f' to override or unlock first"), reason); - die(_("cannot remove a locked working tree")); + die(_("cannot remove a locked working tree;\nuse 'remove -f -f' to override or unlock first")); } if (validate_worktree(wt, &errmsg, WT_VALIDATE_WORKTREE_MISSING_OK)) die(_("validation failed, cannot remove working tree: %s"), @@ -882,7 +922,8 @@ static int remove_worktree(int ac, const char **av, const char *prefix) * continue on even if ret is non-zero, there's no going back * from here. */ - ret |= delete_git_dir(wt); + ret |= delete_git_dir(wt->id); + delete_worktrees_dir_if_empty(); free_worktrees(worktrees); return ret; diff --git a/t/t2025-worktree-add.sh b/t/t2025-worktree-add.sh index 07d292317c..286bba35d8 100755 --- a/t/t2025-worktree-add.sh +++ b/t/t2025-worktree-add.sh @@ -552,4 +552,22 @@ test_expect_success '"add" in bare repo invokes post-checkout hook' ' test_cmp hook.expect goozy/hook.actual ' +test_expect_success '"add" an existing but missing worktree' ' + git worktree add --detach pneu && + test_must_fail git worktree add --detach pneu && + rm -fr pneu && + test_must_fail git worktree add --detach pneu && + git worktree add --force --detach pneu +' + +test_expect_success '"add" an existing locked but missing worktree' ' + git worktree add --detach gnoo && + git worktree lock gnoo && + test_when_finished "git worktree unlock gnoo || :" && + rm -fr gnoo && + test_must_fail git worktree add --detach gnoo && + test_must_fail git worktree add --force --detach gnoo && + git worktree add --force --force --detach gnoo +' + test_done diff --git a/t/t2028-worktree-move.sh b/t/t2028-worktree-move.sh index 5f7d45b7b7..33c0337733 100755 --- a/t/t2028-worktree-move.sh +++ b/t/t2028-worktree-move.sh @@ -98,6 +98,20 @@ test_expect_success 'move worktree to another dir' ' test_cmp expected2 actual2 ' +test_expect_success 'move locked worktree (force)' ' + test_when_finished " + git worktree unlock flump || : + git worktree remove flump || : + git worktree unlock ploof || : + git worktree remove ploof || : + " && + git worktree add --detach flump && + git worktree lock flump && + test_must_fail git worktree move flump ploof" && + test_must_fail git worktree move --force flump ploof" && + git worktree move --force --force flump ploof +' + test_expect_success 'remove main worktree' ' test_must_fail git worktree remove . ' @@ -141,4 +155,34 @@ test_expect_success 'NOT remove missing-but-locked worktree' ' test_path_is_dir .git/worktrees/gone-but-locked ' +test_expect_success 'proper error when worktree not found' ' + for i in noodle noodle/bork + do + test_must_fail git worktree lock $i 2>err && + test_i18ngrep "not a working tree" err || return 1 + done +' + +test_expect_success 'remove locked worktree (force)' ' + git worktree add --detach gumby && + test_when_finished "git worktree remove gumby || :" && + git worktree lock gumby && + test_when_finished "git worktree unlock gumby || :" && + test_must_fail git worktree remove gumby && + test_must_fail git worktree remove --force gumby && + git worktree remove --force --force gumby +' + +test_expect_success 'remove cleans up .git/worktrees when empty' ' + git init moog && + ( + cd moog && + test_commit bim && + git worktree add --detach goom && + test_path_exists .git/worktrees && + git worktree remove goom && + test_path_is_missing .git/worktrees + ) +' + test_done diff --git a/worktree.c b/worktree.c index 97cda5f97b..b0d0b5426d 100644 --- a/worktree.c +++ b/worktree.c @@ -217,7 +217,11 @@ struct worktree *find_worktree(struct worktree **list, if (prefix) arg = to_free = prefix_filename(prefix, arg); - path = real_pathdup(arg, 1); + path = real_pathdup(arg, 0); + if (!path) { + free(to_free); + return NULL; + } for (; *list; list++) if (!fspathcmp(path, real_path((*list)->path))) break;