worktree remove: new command

This command allows to delete a worktree. Like 'move' you cannot
remove the main worktree, or one with submodules inside [1].

For deleting $GIT_WORK_TREE, Untracked files or any staged entries are
considered precious and therefore prevent removal by default. Ignored
files are not precious.

When it comes to deleting $GIT_DIR, there's no "clean" check because
there should not be any valuable data in there, except:

- HEAD reflog. There is nothing we can do about this until somebody
  steps up and implements the ref graveyard.

- Detached HEAD. Technically it can still be recovered. Although it
  may be nice to warn about orphan commits like 'git checkout' does.

[1] We do 'git status' with --ignore-submodules=all for safety
    anyway. But this needs a closer look by submodule people before we
    can allow deletion. For example, if a submodule is totally clean,
    but its repo not absorbed to the main .git dir, then deleting
    worktree also deletes the valuable .submodule repo too.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Nguyễn Thái Ngọc Duy 2018-02-12 16:49:39 +07:00 committed by Junio C Hamano
parent 78d986b252
commit cc73385cf6
4 changed files with 179 additions and 11 deletions

View file

@ -14,6 +14,7 @@ SYNOPSIS
'git worktree lock' [--reason <string>] <worktree> 'git worktree lock' [--reason <string>] <worktree>
'git worktree move' <worktree> <new-path> 'git worktree move' <worktree> <new-path>
'git worktree prune' [-n] [-v] [--expire <expire>] 'git worktree prune' [-n] [-v] [--expire <expire>]
'git worktree remove' [--force] <worktree>
'git worktree unlock' <worktree> 'git worktree unlock' <worktree>
DESCRIPTION DESCRIPTION
@ -85,6 +86,13 @@ prune::
Prune working tree information in $GIT_DIR/worktrees. Prune working tree information in $GIT_DIR/worktrees.
remove::
Remove a working tree. Only clean working trees (no untracked files
and no modification in tracked files) can be removed. Unclean working
trees or ones with submodules can be removed with `--force`. The main
working tree cannot be removed.
unlock:: unlock::
Unlock a working tree, allowing it to be pruned, moved or deleted. Unlock a working tree, allowing it to be pruned, moved or deleted.
@ -94,9 +102,10 @@ OPTIONS
-f:: -f::
--force:: --force::
By default, `add` refuses to create a new working tree when `<commit-ish>` is a branch name and By default, `add` refuses to create a new working tree when
is already checked out by another working tree. This option overrides `<commit-ish>` is a branch name and is already checked out by
that safeguard. another working tree and `remove` refuses to remove an unclean
working tree. This option overrides that safeguard.
-b <new-branch>:: -b <new-branch>::
-B <new-branch>:: -B <new-branch>::
@ -278,12 +287,6 @@ Multiple checkout in general is still experimental, and the support
for submodules is incomplete. It is NOT recommended to make multiple for submodules is incomplete. It is NOT recommended to make multiple
checkouts of a superproject. checkouts of a superproject.
git-worktree could provide more automation for tasks currently
performed manually, such as:
- `remove` to remove a linked working tree and its administrative files (and
warn if the working tree is dirty)
GIT GIT
--- ---
Part of the linkgit:git[1] suite Part of the linkgit:git[1] suite

View file

@ -19,6 +19,7 @@ static const char * const worktree_usage[] = {
N_("git worktree lock [<options>] <path>"), N_("git worktree lock [<options>] <path>"),
N_("git worktree move <worktree> <new-path>"), N_("git worktree move <worktree> <new-path>"),
N_("git worktree prune [<options>]"), N_("git worktree prune [<options>]"),
N_("git worktree remove [<options>] <worktree>"),
N_("git worktree unlock <path>"), N_("git worktree unlock <path>"),
NULL NULL
}; };
@ -624,7 +625,7 @@ static void validate_no_submodules(const struct worktree *wt)
discard_index(&istate); discard_index(&istate);
if (found_submodules) if (found_submodules)
die(_("working trees containing submodules cannot be moved")); die(_("working trees containing submodules cannot be moved or removed"));
} }
static int move_worktree(int ac, const char **av, const char *prefix) static int move_worktree(int ac, const char **av, const char *prefix)
@ -688,6 +689,135 @@ static int move_worktree(int ac, const char **av, const char *prefix)
return 0; return 0;
} }
/*
* Note, "git status --porcelain" is used to determine if it's safe to
* delete a whole worktree. "git status" does not ignore user
* configuration, so if a normal "git status" shows "clean" for the
* user, then it's ok to remove it.
*
* This assumption may be a bad one. We may want to ignore
* (potentially bad) user settings and only delete a worktree when
* it's absolutely safe to do so from _our_ point of view because we
* know better.
*/
static void check_clean_worktree(struct worktree *wt,
const char *original_path)
{
struct argv_array child_env = ARGV_ARRAY_INIT;
struct child_process cp;
char buf[1];
int ret;
/*
* Until we sort this out, all submodules are "dirty" and
* will abort this function.
*/
validate_no_submodules(wt);
argv_array_pushf(&child_env, "%s=%s/.git",
GIT_DIR_ENVIRONMENT, wt->path);
argv_array_pushf(&child_env, "%s=%s",
GIT_WORK_TREE_ENVIRONMENT, wt->path);
memset(&cp, 0, sizeof(cp));
argv_array_pushl(&cp.args, "status",
"--porcelain", "--ignore-submodules=none",
NULL);
cp.env = child_env.argv;
cp.git_cmd = 1;
cp.dir = wt->path;
cp.out = -1;
ret = start_command(&cp);
if (ret)
die_errno(_("failed to run 'git status' on '%s'"),
original_path);
ret = xread(cp.out, buf, sizeof(buf));
if (ret)
die(_("'%s' is dirty, use --force to delete it"),
original_path);
close(cp.out);
ret = finish_command(&cp);
if (ret)
die_errno(_("failed to run 'git status' on '%s', code %d"),
original_path, ret);
}
static int delete_git_work_tree(struct worktree *wt)
{
struct strbuf sb = STRBUF_INIT;
int ret = 0;
strbuf_addstr(&sb, wt->path);
if (remove_dir_recursively(&sb, 0)) {
error_errno(_("failed to delete '%s'"), sb.buf);
ret = -1;
}
strbuf_release(&sb);
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_BOOL(0, "force", &force,
N_("force removing even if the worktree is dirty")),
OPT_END()
};
struct worktree **worktrees, *wt;
struct strbuf errmsg = STRBUF_INIT;
const char *reason;
int ret = 0;
ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
if (ac != 1)
usage_with_options(worktree_usage, options);
worktrees = get_worktrees(0);
wt = find_worktree(worktrees, prefix, av[0]);
if (!wt)
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 (reason) {
if (*reason)
die(_("cannot remove a locked working tree, lock reason: %s"),
reason);
die(_("cannot remove a locked working tree"));
}
if (validate_worktree(wt, &errmsg))
die(_("validation failed, cannot remove working tree: %s"),
errmsg.buf);
strbuf_release(&errmsg);
if (!force)
check_clean_worktree(wt, av[0]);
ret |= delete_git_work_tree(wt);
/*
* continue on even if ret is non-zero, there's no going back
* from here.
*/
ret |= delete_git_dir(wt);
free_worktrees(worktrees);
return ret;
}
int cmd_worktree(int ac, const char **av, const char *prefix) int cmd_worktree(int ac, const char **av, const char *prefix)
{ {
struct option options[] = { struct option options[] = {
@ -712,5 +842,7 @@ int cmd_worktree(int ac, const char **av, const char *prefix)
return unlock_worktree(ac - 1, av + 1, prefix); return unlock_worktree(ac - 1, av + 1, prefix);
if (!strcmp(av[1], "move")) if (!strcmp(av[1], "move"))
return move_worktree(ac - 1, av + 1, prefix); return move_worktree(ac - 1, av + 1, prefix);
if (!strcmp(av[1], "remove"))
return remove_worktree(ac - 1, av + 1, prefix);
usage_with_options(worktree_usage, options); usage_with_options(worktree_usage, options);
} }

View file

@ -3087,7 +3087,7 @@ _git_whatchanged ()
_git_worktree () _git_worktree ()
{ {
local subcommands="add list lock move prune unlock" local subcommands="add list lock move prune remove unlock"
local subcommand="$(__git_find_on_cmdline "$subcommands")" local subcommand="$(__git_find_on_cmdline "$subcommands")"
if [ -z "$subcommand" ]; then if [ -z "$subcommand" ]; then
__gitcomp "$subcommands" __gitcomp "$subcommands"
@ -3105,6 +3105,9 @@ _git_worktree ()
prune,--*) prune,--*)
__gitcomp "--dry-run --expire --verbose" __gitcomp "--dry-run --expire --verbose"
;; ;;
remove,--*)
__gitcomp "--force"
;;
*) *)
;; ;;
esac esac

View file

@ -96,4 +96,34 @@ test_expect_success 'move worktree to another dir' '
test_cmp expected2 actual2 test_cmp expected2 actual2
' '
test_expect_success 'remove main worktree' '
test_must_fail git worktree remove .
'
test_expect_success 'move some-dir/destination back' '
git worktree move some-dir/destination destination
'
test_expect_success 'remove locked worktree' '
git worktree lock destination &&
test_when_finished "git worktree unlock destination" &&
test_must_fail git worktree remove destination
'
test_expect_success 'remove worktree with dirty tracked file' '
echo dirty >>destination/init.t &&
test_when_finished "git -C destination checkout init.t" &&
test_must_fail git worktree remove destination
'
test_expect_success 'remove worktree with untracked file' '
: >destination/untracked &&
test_must_fail git worktree remove destination
'
test_expect_success 'force remove worktree with untracked file' '
git worktree remove --force destination &&
test_path_is_missing destination
'
test_done test_done