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>

Nguyễn Thái Ngọc Duy committed Feb 12, 2018 at 16:49 UTC cc73385cf6c5c229458775bc92e7dbbe24d11611
4 files changed +179 -11
Documentation/git-worktree.txt
+12 -9
@@ -14,6 +14,7 @@ SYNOPSIS
14 'git worktree lock' [--reason <string>] <worktree>
15 'git worktree move' <worktree> <new-path>
16 'git worktree prune' [-n] [-v] [--expire <expire>]
17 +'git worktree remove' [--force] <worktree>
18 'git worktree unlock' <worktree>
19
20 DESCRIPTION
@@ -85,6 +86,13 @@ prune::
86
87 Prune working tree information in $GIT_DIR/worktrees.
88
89 +remove::
90 +
91 +Remove a working tree. Only clean working trees (no untracked files
92 +and no modification in tracked files) can be removed. Unclean working
93 +trees or ones with submodules can be removed with `--force`. The main
94 +working tree cannot be removed.
95 +
96 unlock::
97
98 Unlock a working tree, allowing it to be pruned, moved or deleted.
@@ -94,9 +102,10 @@ OPTIONS
102
103 -f::
104 --force::
97 - By default, `add` refuses to create a new working tree when `<commit-ish>` is a branch name and
98 - is already checked out by another working tree. This option overrides
99 - that safeguard.
105 + By default, `add` refuses to create a new working tree when
106 + `<commit-ish>` is a branch name and is already checked out by
107 + another working tree and `remove` refuses to remove an unclean
108 + working tree. This option overrides that safeguard.
109
110 -b <new-branch>::
111 -B <new-branch>::
@@ -278,12 +287,6 @@ Multiple checkout in general is still experimental, and the support
287 for submodules is incomplete. It is NOT recommended to make multiple
288 checkouts of a superproject.
289
281 -git-worktree could provide more automation for tasks currently
282 -performed manually, such as:
283 -
284 -- `remove` to remove a linked working tree and its administrative files (and
285 - warn if the working tree is dirty)
286 -
290 GIT
291 ---
292 Part of the linkgit:git[1] suite
builtin/worktree.c
+133 -1
@@ -19,6 +19,7 @@ static const char * const worktree_usage[] = {
19 N_("git worktree lock [<options>] <path>"),
20 N_("git worktree move <worktree> <new-path>"),
21 N_("git worktree prune [<options>]"),
22 + N_("git worktree remove [<options>] <worktree>"),
23 N_("git worktree unlock <path>"),
24 NULL
25 };
@@ -624,7 +625,7 @@ static void validate_no_submodules(const struct worktree *wt)
625 discard_index(&istate);
626
627 if (found_submodules)
627 - die(_("working trees containing submodules cannot be moved"));
628 + die(_("working trees containing submodules cannot be moved or removed"));
629 }
630
631 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)
689 return 0;
690 }
691
692 +/*
693 + * Note, "git status --porcelain" is used to determine if it's safe to
694 + * delete a whole worktree. "git status" does not ignore user
695 + * configuration, so if a normal "git status" shows "clean" for the
696 + * user, then it's ok to remove it.
697 + *
698 + * This assumption may be a bad one. We may want to ignore
699 + * (potentially bad) user settings and only delete a worktree when
700 + * it's absolutely safe to do so from _our_ point of view because we
701 + * know better.
702 + */
703 +static void check_clean_worktree(struct worktree *wt,
704 + const char *original_path)
705 +{
706 + struct argv_array child_env = ARGV_ARRAY_INIT;
707 + struct child_process cp;
708 + char buf[1];
709 + int ret;
710 +
711 + /*
712 + * Until we sort this out, all submodules are "dirty" and
713 + * will abort this function.
714 + */
715 + validate_no_submodules(wt);
716 +
717 + argv_array_pushf(&child_env, "%s=%s/.git",
718 + GIT_DIR_ENVIRONMENT, wt->path);
719 + argv_array_pushf(&child_env, "%s=%s",
720 + GIT_WORK_TREE_ENVIRONMENT, wt->path);
721 + memset(&cp, 0, sizeof(cp));
722 + argv_array_pushl(&cp.args, "status",
723 + "--porcelain", "--ignore-submodules=none",
724 + NULL);
725 + cp.env = child_env.argv;
726 + cp.git_cmd = 1;
727 + cp.dir = wt->path;
728 + cp.out = -1;
729 + ret = start_command(&cp);
730 + if (ret)
731 + die_errno(_("failed to run 'git status' on '%s'"),
732 + original_path);
733 + ret = xread(cp.out, buf, sizeof(buf));
734 + if (ret)
735 + die(_("'%s' is dirty, use --force to delete it"),
736 + original_path);
737 + close(cp.out);
738 + ret = finish_command(&cp);
739 + if (ret)
740 + die_errno(_("failed to run 'git status' on '%s', code %d"),
741 + original_path, ret);
742 +}
743 +
744 +static int delete_git_work_tree(struct worktree *wt)
745 +{
746 + struct strbuf sb = STRBUF_INIT;
747 + int ret = 0;
748 +
749 + strbuf_addstr(&sb, wt->path);
750 + if (remove_dir_recursively(&sb, 0)) {
751 + error_errno(_("failed to delete '%s'"), sb.buf);
752 + ret = -1;
753 + }
754 + strbuf_release(&sb);
755 + return ret;
756 +}
757 +
758 +static int delete_git_dir(struct worktree *wt)
759 +{
760 + struct strbuf sb = STRBUF_INIT;
761 + int ret = 0;
762 +
763 + strbuf_addstr(&sb, git_common_path("worktrees/%s", wt->id));
764 + if (remove_dir_recursively(&sb, 0)) {
765 + error_errno(_("failed to delete '%s'"), sb.buf);
766 + ret = -1;
767 + }
768 + strbuf_release(&sb);
769 + return ret;
770 +}
771 +
772 +static int remove_worktree(int ac, const char **av, const char *prefix)
773 +{
774 + int force = 0;
775 + struct option options[] = {
776 + OPT_BOOL(0, "force", &force,
777 + N_("force removing even if the worktree is dirty")),
778 + OPT_END()
779 + };
780 + struct worktree **worktrees, *wt;
781 + struct strbuf errmsg = STRBUF_INIT;
782 + const char *reason;
783 + int ret = 0;
784 +
785 + ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
786 + if (ac != 1)
787 + usage_with_options(worktree_usage, options);
788 +
789 + worktrees = get_worktrees(0);
790 + wt = find_worktree(worktrees, prefix, av[0]);
791 + if (!wt)
792 + die(_("'%s' is not a working tree"), av[0]);
793 + if (is_main_worktree(wt))
794 + die(_("'%s' is a main working tree"), av[0]);
795 + reason = is_worktree_locked(wt);
796 + if (reason) {
797 + if (*reason)
798 + die(_("cannot remove a locked working tree, lock reason: %s"),
799 + reason);
800 + die(_("cannot remove a locked working tree"));
801 + }
802 + if (validate_worktree(wt, &errmsg))
803 + die(_("validation failed, cannot remove working tree: %s"),
804 + errmsg.buf);
805 + strbuf_release(&errmsg);
806 +
807 + if (!force)
808 + check_clean_worktree(wt, av[0]);
809 +
810 + ret |= delete_git_work_tree(wt);
811 + /*
812 + * continue on even if ret is non-zero, there's no going back
813 + * from here.
814 + */
815 + ret |= delete_git_dir(wt);
816 +
817 + free_worktrees(worktrees);
818 + return ret;
819 +}
820 +
821 int cmd_worktree(int ac, const char **av, const char *prefix)
822 {
823 struct option options[] = {
@@ -712,5 +842,7 @@ int cmd_worktree(int ac, const char **av, const char *prefix)
842 return unlock_worktree(ac - 1, av + 1, prefix);
843 if (!strcmp(av[1], "move"))
844 return move_worktree(ac - 1, av + 1, prefix);
845 + if (!strcmp(av[1], "remove"))
846 + return remove_worktree(ac - 1, av + 1, prefix);
847 usage_with_options(worktree_usage, options);
848 }
contrib/completion/git-completion.bash
+4 -1
@@ -3087,7 +3087,7 @@ _git_whatchanged ()
3087
3088 _git_worktree ()
3089 {
3090 - local subcommands="add list lock move prune unlock"
3090 + local subcommands="add list lock move prune remove unlock"
3091 local subcommand="$(__git_find_on_cmdline "$subcommands")"
3092 if [ -z "$subcommand" ]; then
3093 __gitcomp "$subcommands"
@@ -3105,6 +3105,9 @@ _git_worktree ()
3105 prune,--*)
3106 __gitcomp "--dry-run --expire --verbose"
3107 ;;
3108 + remove,--*)
3109 + __gitcomp "--force"
3110 + ;;
3111 *)
3112 ;;
3113 esac
t/t2028-worktree-move.sh
+30
@@ -96,4 +96,34 @@ test_expect_success 'move worktree to another dir' '
96 test_cmp expected2 actual2
97 '
98
99 +test_expect_success 'remove main worktree' '
100 + test_must_fail git worktree remove .
101 +'
102 +
103 +test_expect_success 'move some-dir/destination back' '
104 + git worktree move some-dir/destination destination
105 +'
106 +
107 +test_expect_success 'remove locked worktree' '
108 + git worktree lock destination &&
109 + test_when_finished "git worktree unlock destination" &&
110 + test_must_fail git worktree remove destination
111 +'
112 +
113 +test_expect_success 'remove worktree with dirty tracked file' '
114 + echo dirty >>destination/init.t &&
115 + test_when_finished "git -C destination checkout init.t" &&
116 + test_must_fail git worktree remove destination
117 +'
118 +
119 +test_expect_success 'remove worktree with untracked file' '
120 + : >destination/untracked &&
121 + test_must_fail git worktree remove destination
122 +'
123 +
124 +test_expect_success 'force remove worktree with untracked file' '
125 + git worktree remove --force destination &&
126 + test_path_is_missing destination
127 +'
128 +
129 test_done