{"thread":{"id":"44836","subject":"[PATCH 0/6] git worktree move/remove","startedAt":"2017-01-08T09:40:40Z","lastAt":"2017-01-09T17:34:55Z","messageCount":8,"participants":["Nguyễn Thái Ngọc Duy","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":6},"messages":[{"id":"308952","messageId":"20170108094003.637-1-pclouds@gmail.com","threadId":"44836","inReplyTo":null,"subject":"[PATCH 0/6] git worktree move/remove","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2017-01-08T09:39:57Z","receivedAt":"2017-01-08T09:40:40Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"This version is the same as nd/worktree-move but with the recursive\ndirectory copy code removed. We rely on rename() to move directories.\n\nNguyễn Thái Ngọc Duy (6):\n  worktree.c: add validate_worktree()\n  worktree.c: add update_worktree_location()\n  worktree move: new command\n  worktree move: accept destination as directory\n  worktree move: refuse to move worktrees with submodules\n  worktree remove: new command\n\n Documentation/git-worktree.txt         |  28 ++++--\n builtin/worktree.c                     | 162 +++++++++++++++++++++++++++++++++\n contrib/completion/git-completion.bash |   5 +-\n t/t2028-worktree-move.sh               |  56 ++++++++++++\n worktree.c                             |  84 +++++++++++++++++\n worktree.h                             |  11 +++\n 6 files changed, 335 insertions(+), 11 deletions(-)\n\n-- \n2.8.2.524.g6ff3d78\n\n"},{"id":"308953","messageId":"20170108094003.637-2-pclouds@gmail.com","threadId":"44836","inReplyTo":"20170108094003.637-1-pclouds@gmail.com","subject":"[PATCH 1/6] worktree.c: add validate_worktree()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2017-01-08T09:39:58Z","receivedAt":"2017-01-08T09:40:51Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"This function is later used by \"worktree move\" and \"worktree remove\"\nto ensure that we have a good connection between the repository and\nthe worktree. For example, if a worktree is moved manually, the\nworktree location recorded in $GIT_DIR/worktrees/.../gitdir is\nincorrect and we should not move that one.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n worktree.c | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n worktree.h |  5 +++++\n 2 files changed, 68 insertions(+)\n\ndiff --git a/worktree.c b/worktree.c\nindex eb61212..929072a 100644\n--- a/worktree.c\n+++ b/worktree.c\n@@ -291,6 +291,69 @@ const char *is_worktree_locked(struct worktree *wt)\n \treturn wt->lock_reason;\n }\n \n+static int report(int quiet, const char *fmt, ...)\n+{\n+\tva_list params;\n+\n+\tif (quiet)\n+\t\treturn -1;\n+\n+\tva_start(params, fmt);\n+\tvfprintf(stderr, fmt, params);\n+\tfputc('\\n', stderr);\n+\tva_end(params);\n+\treturn -1;\n+}\n+\n+int validate_worktree(const struct worktree *wt, int quiet)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tconst char *path;\n+\tint err;\n+\n+\tif (is_main_worktree(wt)) {\n+\t\t/*\n+\t\t * Main worktree using .git file to point to the\n+\t\t * repository would make it impossible to know where\n+\t\t * the actual worktree is if this function is executed\n+\t\t * from another worktree. No .git file support for now.\n+\t\t */\n+\t\tstrbuf_addf(&sb, \"%s/.git\", wt->path);\n+\t\tif (!is_directory(sb.buf)) {\n+\t\t\tstrbuf_release(&sb);\n+\t\t\treturn report(quiet, _(\"'%s/.git' at main worktree is not the repository directory\"),\n+\t\t\t\t      wt->path);\n+\t\t}\n+\t\treturn 0;\n+\t}\n+\n+\t/*\n+\t * Make sure \"gitdir\" file points to a real .git file and that\n+\t * file points back here.\n+\t */\n+\tif (!is_absolute_path(wt->path))\n+\t\treturn report(quiet, _(\"'%s' file does not contain absolute path to the worktree location\"),\n+\t\t\t      git_common_path(\"worktrees/%s/gitdir\", wt->id));\n+\n+\tstrbuf_addf(&sb, \"%s/.git\", wt->path);\n+\tif (!file_exists(sb.buf)) {\n+\t\tstrbuf_release(&sb);\n+\t\treturn report(quiet, _(\"'%s/.git' does not exist\"), wt->path);\n+\t}\n+\n+\tpath = read_gitfile_gently(sb.buf, &err);\n+\tstrbuf_release(&sb);\n+\tif (!path)\n+\t\treturn report(quiet, _(\"'%s/.git' is not a .git file, error code %d\"),\n+\t\t\t      wt->path, err);\n+\n+\tif (fspathcmp(path, real_path(git_common_path(\"worktrees/%s\", wt->id))))\n+\t\treturn report(quiet, _(\"'%s' does not point back to\"),\n+\t\t\t      wt->path, git_common_path(\"worktrees/%s\", wt->id));\n+\n+\treturn 0;\n+}\n+\n int is_worktree_being_rebased(const struct worktree *wt,\n \t\t\t      const char *target)\n {\ndiff --git a/worktree.h b/worktree.h\nindex d59ce1f..4433db2 100644\n--- a/worktree.h\n+++ b/worktree.h\n@@ -52,6 +52,11 @@ extern int is_main_worktree(const struct worktree *wt);\n  */\n extern const char *is_worktree_locked(struct worktree *wt);\n \n+/*\n+ * Return zero if the worktree is in good condition.\n+ */\n+extern int validate_worktree(const struct worktree *wt, int quiet);\n+\n /*\n  * Free up the memory for worktree(s)\n  */\n-- \n2.8.2.524.g6ff3d78\n\n"},{"id":"308954","messageId":"20170108094003.637-3-pclouds@gmail.com","threadId":"44836","inReplyTo":"20170108094003.637-1-pclouds@gmail.com","subject":"[PATCH 2/6] worktree.c: add update_worktree_location()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2017-01-08T09:39:59Z","receivedAt":"2017-01-08T09:40:59Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n worktree.c | 21 +++++++++++++++++++++\n worktree.h |  6 ++++++\n 2 files changed, 27 insertions(+)\n\ndiff --git a/worktree.c b/worktree.c\nindex 929072a..7684951 100644\n--- a/worktree.c\n+++ b/worktree.c\n@@ -354,6 +354,27 @@ int validate_worktree(const struct worktree *wt, int quiet)\n \treturn 0;\n }\n \n+int update_worktree_location(struct worktree *wt, const char *path_)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\tint ret = 0;\n+\n+\tif (is_main_worktree(wt))\n+\t\treturn 0;\n+\n+\tstrbuf_add_absolute_path(&path, path_);\n+\tif (fspathcmp(wt->path, path.buf)) {\n+\t\twrite_file(git_common_path(\"worktrees/%s/gitdir\",\n+\t\t\t\t\t   wt->id),\n+\t\t\t   \"%s/.git\", real_path(path.buf));\n+\t\tfree(wt->path);\n+\t\twt->path = strbuf_detach(&path, NULL);\n+\t\tret = 0;\n+\t}\n+\tstrbuf_release(&path);\n+\treturn ret;\n+}\n+\n int is_worktree_being_rebased(const struct worktree *wt,\n \t\t\t      const char *target)\n {\ndiff --git a/worktree.h b/worktree.h\nindex 4433db2..1ee03f4 100644\n--- a/worktree.h\n+++ b/worktree.h\n@@ -57,6 +57,12 @@ extern const char *is_worktree_locked(struct worktree *wt);\n  */\n extern int validate_worktree(const struct worktree *wt, int quiet);\n \n+/*\n+ * Update worktrees/xxx/gitdir with the new path.\n+ */\n+extern int update_worktree_location(struct worktree *wt,\n+\t\t\t\t    const char *path_);\n+\n /*\n  * Free up the memory for worktree(s)\n  */\n-- \n2.8.2.524.g6ff3d78\n\n"},{"id":"308955","messageId":"20170108094003.637-4-pclouds@gmail.com","threadId":"44836","inReplyTo":"20170108094003.637-1-pclouds@gmail.com","subject":"[PATCH 3/6] worktree move: new command","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2017-01-08T09:40:00Z","receivedAt":"2017-01-08T09:41:08Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"There are two options to move the main worktree, but both have\ncomplications, so it's not implemented yet. Anyway the options are:\n\n - convert the main worktree to a linked one and move it away, leave the\n   git repository where it is. The repo essentially becomes bare after\n   this move.\n\n - move the repository with the main worktree. The tricky part is make\n   sure all file descriptors to the repository are closed, or it may\n   fail on Windows.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/git-worktree.txt         |  7 +++++-\n builtin/worktree.c                     | 43 ++++++++++++++++++++++++++++++++++\n contrib/completion/git-completion.bash |  2 +-\n t/t2028-worktree-move.sh               | 30 ++++++++++++++++++++++++\n 4 files changed, 80 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt\nindex e257c19..1310513 100644\n--- a/Documentation/git-worktree.txt\n+++ b/Documentation/git-worktree.txt\n@@ -12,6 +12,7 @@ SYNOPSIS\n 'git worktree add' [-f] [--detach] [--checkout] [-b <new-branch>] <path> [<branch>]\n 'git worktree list' [--porcelain]\n 'git worktree lock' [--reason <string>] <worktree>\n+'git worktree move' <worktree> <new-path>\n 'git worktree prune' [-n] [-v] [--expire <expire>]\n 'git worktree unlock' <worktree>\n \n@@ -71,6 +72,11 @@ files from being pruned automatically. This also prevents it from\n being moved or deleted. Optionally, specify a reason for the lock\n with `--reason`.\n \n+move::\n+\n+Move a working tree to a new location. Note that the main working tree\n+cannot be moved yet.\n+\n prune::\n \n Prune working tree information in $GIT_DIR/worktrees.\n@@ -252,7 +258,6 @@ performed manually, such as:\n \n - `remove` to remove a linked working tree and its administrative files (and\n   warn if the working tree is dirty)\n-- `mv` to move or rename a working tree and update its administrative files\n \n GIT\n ---\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 9a97e37..0d8b57c 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -15,6 +15,7 @@ static const char * const worktree_usage[] = {\n \tN_(\"git worktree add [<options>] <path> [<branch>]\"),\n \tN_(\"git worktree list [<options>]\"),\n \tN_(\"git worktree lock [<options>] <path>\"),\n+\tN_(\"git worktree move <worktree> <new-path>\"),\n \tN_(\"git worktree prune [<options>]\"),\n \tN_(\"git worktree unlock <path>\"),\n \tNULL\n@@ -524,6 +525,46 @@ static int unlock_worktree(int ac, const char **av, const char *prefix)\n \treturn ret;\n }\n \n+static int move_worktree(int ac, const char **av, const char *prefix)\n+{\n+\tstruct option options[] = {\n+\t\tOPT_END()\n+\t};\n+\tstruct worktree **worktrees, *wt;\n+\tstruct strbuf dst = STRBUF_INIT;\n+\tconst char *reason;\n+\n+\tac = parse_options(ac, av, prefix, options, worktree_usage, 0);\n+\tif (ac != 2)\n+\t\tusage_with_options(worktree_usage, options);\n+\n+\tstrbuf_addstr(&dst, prefix_filename(prefix,\n+\t\t\t\t\t    strlen(prefix),\n+\t\t\t\t\t    av[1]));\n+\tif (file_exists(dst.buf))\n+\t\tdie(_(\"target '%s' already exists\"), av[1]);\n+\n+\tworktrees = get_worktrees(0);\n+\twt = find_worktree(worktrees, prefix, av[0]);\n+\tif (!wt)\n+\t\tdie(_(\"'%s' is not a working directory\"), av[0]);\n+\tif (is_main_worktree(wt))\n+\t\tdie(_(\"'%s' is a main working directory\"), av[0]);\n+\treason = is_worktree_locked(wt);\n+\tif (reason) {\n+\t\tif (*reason)\n+\t\t\tdie(_(\"already locked, reason: %s\"), reason);\n+\t\tdie(_(\"already locked, no reason\"));\n+\t}\n+\tif (validate_worktree(wt, 0))\n+\t\treturn -1;\n+\n+\tif (rename(wt->path, dst.buf) == -1)\n+\t\tdie_errno(_(\"failed to move '%s' to '%s'\"), wt->path, dst.buf);\n+\n+\treturn update_worktree_location(wt, dst.buf);\n+}\n+\n int cmd_worktree(int ac, const char **av, const char *prefix)\n {\n \tstruct option options[] = {\n@@ -546,5 +587,7 @@ int cmd_worktree(int ac, const char **av, const char *prefix)\n \t\treturn lock_worktree(ac - 1, av + 1, prefix);\n \tif (!strcmp(av[1], \"unlock\"))\n \t\treturn unlock_worktree(ac - 1, av + 1, prefix);\n+\tif (!strcmp(av[1], \"move\"))\n+\t\treturn move_worktree(ac - 1, av + 1, prefix);\n \tusage_with_options(worktree_usage, options);\n }\ndiff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash\nindex 6721ff8..e80392b 100644\n--- a/contrib/completion/git-completion.bash\n+++ b/contrib/completion/git-completion.bash\n@@ -2715,7 +2715,7 @@ _git_whatchanged ()\n \n _git_worktree ()\n {\n-\tlocal subcommands=\"add list lock prune unlock\"\n+\tlocal subcommands=\"add list lock move prune unlock\"\n \tlocal subcommand=\"$(__git_find_on_cmdline \"$subcommands\")\"\n \tif [ -z \"$subcommand\" ]; then\n \t\t__gitcomp \"$subcommands\"\ndiff --git a/t/t2028-worktree-move.sh b/t/t2028-worktree-move.sh\nindex 8298aaf..74070bd 100755\n--- a/t/t2028-worktree-move.sh\n+++ b/t/t2028-worktree-move.sh\n@@ -59,4 +59,34 @@ test_expect_success 'unlock worktree twice' '\n \ttest_path_is_missing .git/worktrees/source/locked\n '\n \n+test_expect_success 'move non-worktree' '\n+\tmkdir abc &&\n+\ttest_must_fail git worktree move abc def\n+'\n+\n+test_expect_success 'move locked worktree' '\n+\tgit worktree lock source &&\n+\ttest_must_fail git worktree move source destination &&\n+\tgit worktree unlock source\n+'\n+\n+test_expect_success 'move worktree' '\n+\tgit worktree move source destination &&\n+\ttest_path_is_missing source &&\n+\tgit worktree list --porcelain | grep \"^worktree\" >actual &&\n+\tcat <<-EOF >expected &&\n+\tworktree $TRASH_DIRECTORY\n+\tworktree $TRASH_DIRECTORY/destination\n+\tworktree $TRASH_DIRECTORY/elsewhere\n+\tEOF\n+\ttest_cmp expected actual &&\n+\tgit -C destination log --format=%s >actual2 &&\n+\techo init >expected2 &&\n+\ttest_cmp expected2 actual2\n+'\n+\n+test_expect_success 'move main worktree' '\n+\ttest_must_fail git worktree move . def\n+'\n+\n test_done\n-- \n2.8.2.524.g6ff3d78\n\n"},{"id":"308956","messageId":"20170108094003.637-5-pclouds@gmail.com","threadId":"44836","inReplyTo":"20170108094003.637-1-pclouds@gmail.com","subject":"[PATCH 4/6] worktree move: accept destination as directory","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2017-01-08T09:40:01Z","receivedAt":"2017-01-08T09:41:22Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Similar to \"mv a b/\", which is actually \"mv a b/a\", we extract basename\nof source worktree and create a directory of the same name at\ndestination if dst path is a directory.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n builtin/worktree.c | 19 ++++++++++++++++++-\n 1 file changed, 18 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 0d8b57c..900b68b 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -541,7 +541,13 @@ static int move_worktree(int ac, const char **av, const char *prefix)\n \tstrbuf_addstr(&dst, prefix_filename(prefix,\n \t\t\t\t\t    strlen(prefix),\n \t\t\t\t\t    av[1]));\n-\tif (file_exists(dst.buf))\n+\tif (is_directory(dst.buf))\n+\t\t/*\n+\t\t * keep going, dst will be appended after we get the\n+\t\t * source's absolute path\n+\t\t */\n+\t\t;\n+\telse if (file_exists(dst.buf))\n \t\tdie(_(\"target '%s' already exists\"), av[1]);\n \n \tworktrees = get_worktrees(0);\n@@ -559,6 +565,17 @@ static int move_worktree(int ac, const char **av, const char *prefix)\n \tif (validate_worktree(wt, 0))\n \t\treturn -1;\n \n+\tif (is_directory(dst.buf)) {\n+\t\tconst char *sep = find_last_dir_sep(wt->path);\n+\n+\t\tif (!sep)\n+\t\t\tdie(_(\"could not figure out destination name from '%s'\"),\n+\t\t\t    wt->path);\n+\t\tstrbuf_addstr(&dst, sep);\n+\t\tif (file_exists(dst.buf))\n+\t\t\tdie(_(\"target '%s' already exists\"), dst.buf);\n+\t}\n+\n \tif (rename(wt->path, dst.buf) == -1)\n \t\tdie_errno(_(\"failed to move '%s' to '%s'\"), wt->path, dst.buf);\n \n-- \n2.8.2.524.g6ff3d78\n\n"},{"id":"308957","messageId":"20170108094003.637-6-pclouds@gmail.com","threadId":"44836","inReplyTo":"20170108094003.637-1-pclouds@gmail.com","subject":"[PATCH 5/6] worktree move: refuse to move worktrees with submodules","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2017-01-08T09:40:02Z","receivedAt":"2017-01-08T09:41:33Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Submodules contains .git files with relative paths. After a worktree\nmove, these files need to be updated or they may point to nowhere.\n\nThis is a bandage patch to make sure \"worktree move\" don't break\npeople's worktrees by accident. When .git file update code is in\nplace, this validate_no_submodules() could be removed.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n builtin/worktree.c | 23 +++++++++++++++++++++++\n 1 file changed, 23 insertions(+)\n\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 900b68b..64d0264 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -525,6 +525,27 @@ static int unlock_worktree(int ac, const char **av, const char *prefix)\n \treturn ret;\n }\n \n+static void validate_no_submodules(const struct worktree *wt)\n+{\n+\tstruct index_state istate = {0};\n+\tint i, found_submodules = 0;\n+\n+\tif (read_index_from(&istate, worktree_git_path(wt, \"index\")) > 0) {\n+\t\tfor (i = 0; i < istate.cache_nr; i++) {\n+\t\t\tstruct cache_entry *ce = istate.cache[i];\n+\n+\t\t\tif (S_ISGITLINK(ce->ce_mode)) {\n+\t\t\t\tfound_submodules = 1;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\t}\n+\tdiscard_index(&istate);\n+\n+\tif (found_submodules)\n+\t\tdie(_(\"This working tree contains submodules and cannot be moved yet\"));\n+}\n+\n static int move_worktree(int ac, const char **av, const char *prefix)\n {\n \tstruct option options[] = {\n@@ -565,6 +586,8 @@ static int move_worktree(int ac, const char **av, const char *prefix)\n \tif (validate_worktree(wt, 0))\n \t\treturn -1;\n \n+\tvalidate_no_submodules(wt);\n+\n \tif (is_directory(dst.buf)) {\n \t\tconst char *sep = find_last_dir_sep(wt->path);\n \n-- \n2.8.2.524.g6ff3d78\n\n"},{"id":"308958","messageId":"20170108094003.637-7-pclouds@gmail.com","threadId":"44836","inReplyTo":"20170108094003.637-1-pclouds@gmail.com","subject":"[PATCH 6/6] worktree remove: new command","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2017-01-08T09:40:03Z","receivedAt":"2017-01-08T09:42:10Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/git-worktree.txt         | 21 +++++----\n builtin/worktree.c                     | 79 ++++++++++++++++++++++++++++++++++\n contrib/completion/git-completion.bash |  5 ++-\n t/t2028-worktree-move.sh               | 26 +++++++++++\n 4 files changed, 121 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt\nindex 1310513..bbde6b8 100644\n--- a/Documentation/git-worktree.txt\n+++ b/Documentation/git-worktree.txt\n@@ -14,6 +14,7 @@ SYNOPSIS\n 'git worktree lock' [--reason <string>] <worktree>\n 'git worktree move' <worktree> <new-path>\n 'git worktree prune' [-n] [-v] [--expire <expire>]\n+'git worktree remove' [--force] <worktree>\n 'git worktree unlock' <worktree>\n \n DESCRIPTION\n@@ -81,6 +82,13 @@ prune::\n \n Prune working tree information in $GIT_DIR/worktrees.\n \n+remove::\n+\n+Remove a working tree. Only clean working trees (no untracked files\n+and no modification in tracked files) can be removed. Unclean working\n+trees can be removed with `--force`. The main working tree cannot be\n+removed.\n+\n unlock::\n \n Unlock a working tree, allowing it to be pruned, moved or deleted.\n@@ -90,9 +98,10 @@ OPTIONS\n \n -f::\n --force::\n-\tBy default, `add` refuses to create a new working tree when `<branch>`\n-\tis already checked out by another working tree. This option overrides\n-\tthat safeguard.\n+\tBy default, `add` refuses to create a new working tree when\n+\t`<branch>` is already checked out by another working tree and\n+\t`remove` refuses to remove an unclean working tree. This option\n+\toverrides that safeguard.\n \n -b <new-branch>::\n -B <new-branch>::\n@@ -253,12 +262,6 @@ Multiple checkout in general is still experimental, and the support\n for submodules is incomplete. It is NOT recommended to make multiple\n checkouts of a superproject.\n \n-git-worktree could provide more automation for tasks currently\n-performed manually, such as:\n-\n-- `remove` to remove a linked working tree and its administrative files (and\n-  warn if the working tree is dirty)\n-\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 64d0264..339c622e 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -17,6 +17,7 @@ static const char * const worktree_usage[] = {\n \tN_(\"git worktree lock [<options>] <path>\"),\n \tN_(\"git worktree move <worktree> <new-path>\"),\n \tN_(\"git worktree prune [<options>]\"),\n+\tN_(\"git worktree remove [<options>] <worktree>\"),\n \tN_(\"git worktree unlock <path>\"),\n \tNULL\n };\n@@ -605,6 +606,82 @@ static int move_worktree(int ac, const char **av, const char *prefix)\n \treturn update_worktree_location(wt, dst.buf);\n }\n \n+static int remove_worktree(int ac, const char **av, const char *prefix)\n+{\n+\tint force = 0;\n+\tstruct option options[] = {\n+\t\tOPT_BOOL(0, \"force\", &force,\n+\t\t\t N_(\"force removing even if the worktree is dirty\")),\n+\t\tOPT_END()\n+\t};\n+\tstruct worktree **worktrees, *wt;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tconst char *reason;\n+\tint ret = 0;\n+\n+\tac = parse_options(ac, av, prefix, options, worktree_usage, 0);\n+\tif (ac != 1)\n+\t\tusage_with_options(worktree_usage, options);\n+\n+\tworktrees = get_worktrees(0);\n+\twt = find_worktree(worktrees, prefix, av[0]);\n+\tif (!wt)\n+\t\tdie(_(\"'%s' is not a working directory\"), av[0]);\n+\tif (is_main_worktree(wt))\n+\t\tdie(_(\"'%s' is a main working directory\"), av[0]);\n+\treason = is_worktree_locked(wt);\n+\tif (reason) {\n+\t\tif (*reason)\n+\t\t\tdie(_(\"already locked, reason: %s\"), reason);\n+\t\tdie(_(\"already locked, no reason\"));\n+\t}\n+\tif (validate_worktree(wt, 0))\n+\t\treturn -1;\n+\n+\tif (!force) {\n+\t\tstruct argv_array child_env = ARGV_ARRAY_INIT;\n+\t\tstruct child_process cp;\n+\t\tchar buf[1];\n+\n+\t\targv_array_pushf(&child_env, \"%s=%s/.git\",\n+\t\t\t\t GIT_DIR_ENVIRONMENT, wt->path);\n+\t\targv_array_pushf(&child_env, \"%s=%s\",\n+\t\t\t\t GIT_WORK_TREE_ENVIRONMENT, wt->path);\n+\t\tmemset(&cp, 0, sizeof(cp));\n+\t\targv_array_pushl(&cp.args, \"status\", \"--porcelain\", NULL);\n+\t\tcp.env = child_env.argv;\n+\t\tcp.git_cmd = 1;\n+\t\tcp.dir = wt->path;\n+\t\tcp.out = -1;\n+\t\tret = start_command(&cp);\n+\t\tif (ret)\n+\t\t\tdie_errno(_(\"failed to run git-status on '%s', code %d\"),\n+\t\t\t\t  av[0], ret);\n+\t\tret = xread(cp.out, buf, sizeof(buf));\n+\t\tif (ret)\n+\t\t\tdie(_(\"'%s' is dirty, use --force to delete it\"), av[0]);\n+\t\tclose(cp.out);\n+\t\tret = finish_command(&cp);\n+\t\tif (ret)\n+\t\t\tdie_errno(_(\"failed to run git-status on '%s', code %d\"),\n+\t\t\t\t  av[0], ret);\n+\t}\n+\tstrbuf_addstr(&sb, wt->path);\n+\tif (remove_dir_recursively(&sb, 0)) {\n+\t\terror_errno(_(\"failed to delete '%s'\"), sb.buf);\n+\t\tret = -1;\n+\t}\n+\tstrbuf_reset(&sb);\n+\tstrbuf_addstr(&sb, git_common_path(\"worktrees/%s\", wt->id));\n+\tif (remove_dir_recursively(&sb, 0)) {\n+\t\terror_errno(_(\"failed to delete '%s'\"), sb.buf);\n+\t\tret = -1;\n+\t}\n+\tstrbuf_release(&sb);\n+\tfree_worktrees(worktrees);\n+\treturn ret;\n+}\n+\n int cmd_worktree(int ac, const char **av, const char *prefix)\n {\n \tstruct option options[] = {\n@@ -629,5 +706,7 @@ int cmd_worktree(int ac, const char **av, const char *prefix)\n \t\treturn unlock_worktree(ac - 1, av + 1, prefix);\n \tif (!strcmp(av[1], \"move\"))\n \t\treturn move_worktree(ac - 1, av + 1, prefix);\n+\tif (!strcmp(av[1], \"remove\"))\n+\t\treturn remove_worktree(ac - 1, av + 1, prefix);\n \tusage_with_options(worktree_usage, options);\n }\ndiff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash\nindex e80392b..f413431 100644\n--- a/contrib/completion/git-completion.bash\n+++ b/contrib/completion/git-completion.bash\n@@ -2715,7 +2715,7 @@ _git_whatchanged ()\n \n _git_worktree ()\n {\n-\tlocal subcommands=\"add list lock move prune unlock\"\n+\tlocal subcommands=\"add list lock move prune remove unlock\"\n \tlocal subcommand=\"$(__git_find_on_cmdline \"$subcommands\")\"\n \tif [ -z \"$subcommand\" ]; then\n \t\t__gitcomp \"$subcommands\"\n@@ -2733,6 +2733,9 @@ _git_worktree ()\n \t\tprune,--*)\n \t\t\t__gitcomp \"--dry-run --expire --verbose\"\n \t\t\t;;\n+\t\tremove,--*)\n+\t\t\t__gitcomp \"--force\"\n+\t\t\t;;\n \t\t*)\n \t\t\t;;\n \t\tesac\ndiff --git a/t/t2028-worktree-move.sh b/t/t2028-worktree-move.sh\nindex 74070bd..084acc6 100755\n--- a/t/t2028-worktree-move.sh\n+++ b/t/t2028-worktree-move.sh\n@@ -89,4 +89,30 @@ test_expect_success 'move main worktree' '\n \ttest_must_fail git worktree move . def\n '\n \n+test_expect_success 'remove main worktree' '\n+\ttest_must_fail git worktree remove .\n+'\n+\n+test_expect_success 'remove locked worktree' '\n+\tgit worktree lock destination &&\n+\ttest_must_fail git worktree remove destination &&\n+\tgit worktree unlock destination\n+'\n+\n+test_expect_success 'remove worktree with dirty tracked file' '\n+\techo dirty >>destination/init.t &&\n+\ttest_must_fail git worktree remove destination\n+'\n+\n+test_expect_success 'remove worktree with untracked file' '\n+\tgit -C destination checkout init.t &&\n+\t: >destination/untracked &&\n+\ttest_must_fail git worktree remove destination\n+'\n+\n+test_expect_success 'force remove worktree with untracked file' '\n+\tgit worktree remove --force destination &&\n+\ttest_path_is_missing destination\n+'\n+\n test_done\n-- \n2.8.2.524.g6ff3d78\n\n"},{"id":"309043","messageId":"xmqqd1fwuogg.fsf@gitster.mtv.corp.google.com","threadId":"44836","inReplyTo":"20170108094003.637-1-pclouds@gmail.com","subject":"Re: [PATCH 0/6] git worktree move/remove","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2017-01-09T17:34:39Z","receivedAt":"2017-01-09T17:34:55Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Nguyễn Thái Ngọc Duy  <pclouds@gmail.com> writes:\n\n> This version is the same as nd/worktree-move but with the recursive\n> directory copy code removed. We rely on rename() to move directories.\n\nMuch simpler ;-)\n\nWill replace; thanks.\n\n> Nguyễn Thái Ngọc Duy (6):\n>   worktree.c: add validate_worktree()\n>   worktree.c: add update_worktree_location()\n>   worktree move: new command\n>   worktree move: accept destination as directory\n>   worktree move: refuse to move worktrees with submodules\n>   worktree remove: new command\n>\n>  Documentation/git-worktree.txt         |  28 ++++--\n>  builtin/worktree.c                     | 162 +++++++++++++++++++++++++++++++++\n>  contrib/completion/git-completion.bash |   5 +-\n>  t/t2028-worktree-move.sh               |  56 ++++++++++++\n>  worktree.c                             |  84 +++++++++++++++++\n>  worktree.h                             |  11 +++\n>  6 files changed, 335 insertions(+), 11 deletions(-)\n"}]}