This command allows to relocate linked worktrees. Main worktree cannot
(yet) be moved.

There are two options to move the main worktree, but both have
complications, so it's not implemented yet. Anyway the options are:

- convert the main worktree to a linked one and move it away, leave
  the git repository where it is. The repo essentially becomes bare
  after this move.

- move the repository with the main worktree. The tricky part is make
  sure all file descriptors to the repository are closed, or it may
  fail on Windows.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclo...@gmail.com>
---
 Documentation/git-worktree.txt         | 13 ++++---
 builtin/worktree.c                     | 53 ++++++++++++++++++++++++++
 contrib/completion/git-completion.bash |  2 +-
 t/t2028-worktree-move.sh               | 26 +++++++++++++
 4 files changed, 87 insertions(+), 7 deletions(-)

diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index 41585f535d..4fa1dd3a48 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -12,6 +12,7 @@ SYNOPSIS
 'git worktree add' [-f] [--detach] [--checkout] [--lock] [-b <new-branch>] 
<path> [<commit-ish>]
 'git worktree list' [--porcelain]
 'git worktree lock' [--reason <string>] <worktree>
+'git worktree move' <worktree> <new-path>
 'git worktree prune' [-n] [-v] [--expire <expire>]
 'git worktree unlock' <worktree>
 
@@ -34,10 +35,6 @@ The working tree's administrative files in the repository 
(see
 `git worktree prune` in the main or any linked working tree to
 clean up any stale administrative files.
 
-If you move a linked working tree, you need to manually update the
-administrative files so that they do not get pruned automatically. See
-section "DETAILS" for more information.
-
 If a linked working tree is stored on a portable device or network share
 which is not always mounted, you can prevent its administrative files from
 being pruned by issuing the `git worktree lock` command, optionally
@@ -79,6 +76,11 @@ files from being pruned automatically. This also prevents it 
from
 being moved or deleted. Optionally, specify a reason for the lock
 with `--reason`.
 
+move::
+
+Move a working tree to a new location. Note that the main working tree
+cannot be moved.
+
 prune::
 
 Prune working tree information in $GIT_DIR/worktrees.
@@ -196,7 +198,7 @@ thumb is do not make any assumption about whether a path 
belongs to
 $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
 inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
 
-If you move a linked working tree, you need to update the 'gitdir' file
+If you manually move a linked working tree, you need to update the 'gitdir' 
file
 in the entry's directory. For example, if a linked working tree is moved
 to `/newpath/test-next` and its `.git` file points to
 `/path/main/.git/worktrees/test-next`, then update
@@ -281,7 +283,6 @@ performed manually, such as:
 
 - `remove` to remove a linked working tree and its administrative files (and
   warn if the working tree is dirty)
-- `mv` to move or rename a working tree and update its administrative files
 
 GIT
 ---
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 7cef5b120b..8b9482d1e5 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -17,6 +17,7 @@ static const char * const worktree_usage[] = {
        N_("git worktree add [<options>] <path> [<branch>]"),
        N_("git worktree list [<options>]"),
        N_("git worktree lock [<options>] <path>"),
+       N_("git worktree move <worktree> <new-path>"),
        N_("git worktree prune [<options>]"),
        N_("git worktree unlock <path>"),
        NULL
@@ -605,6 +606,56 @@ static int unlock_worktree(int ac, const char **av, const 
char *prefix)
        return ret;
 }
 
+static int move_worktree(int ac, const char **av, const char *prefix)
+{
+       struct option options[] = {
+               OPT_END()
+       };
+       struct worktree **worktrees, *wt;
+       struct strbuf dst = STRBUF_INIT;
+       struct strbuf errmsg = STRBUF_INIT;
+       const char *reason;
+       char *path;
+
+       ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
+       if (ac != 2)
+               usage_with_options(worktree_usage, options);
+
+       path = prefix_filename(prefix, av[1]);
+       strbuf_addstr(&dst, path);
+       free(path);
+
+       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]);
+       if (file_exists(dst.buf))
+               die(_("target '%s' already exists"), av[1]);
+
+       reason = is_worktree_locked(wt);
+       if (reason) {
+               if (*reason)
+                       die(_("cannot move a locked working tree, lock reason: 
%s"),
+                           reason);
+               die(_("cannot move a locked working tree"));
+       }
+       if (validate_worktree(wt, &errmsg))
+               die(_("validation failed, cannot move working tree: %s"),
+                   errmsg.buf);
+       strbuf_release(&errmsg);
+
+       if (rename(wt->path, dst.buf) == -1)
+               die_errno(_("failed to move '%s' to '%s'"), wt->path, dst.buf);
+
+       update_worktree_location(wt, dst.buf);
+
+       strbuf_release(&dst);
+       free_worktrees(worktrees);
+       return 0;
+}
+
 int cmd_worktree(int ac, const char **av, const char *prefix)
 {
        struct option options[] = {
@@ -627,5 +678,7 @@ int cmd_worktree(int ac, const char **av, const char 
*prefix)
                return lock_worktree(ac - 1, av + 1, prefix);
        if (!strcmp(av[1], "unlock"))
                return unlock_worktree(ac - 1, av + 1, prefix);
+       if (!strcmp(av[1], "move"))
+               return move_worktree(ac - 1, av + 1, prefix);
        usage_with_options(worktree_usage, options);
 }
diff --git a/contrib/completion/git-completion.bash 
b/contrib/completion/git-completion.bash
index 3683c772c5..b87d387686 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -3087,7 +3087,7 @@ _git_whatchanged ()
 
 _git_worktree ()
 {
-       local subcommands="add list lock prune unlock"
+       local subcommands="add list lock move prune unlock"
        local subcommand="$(__git_find_on_cmdline "$subcommands")"
        if [ -z "$subcommand" ]; then
                __gitcomp "$subcommands"
diff --git a/t/t2028-worktree-move.sh b/t/t2028-worktree-move.sh
index 8298aaf97f..0f8abc0854 100755
--- a/t/t2028-worktree-move.sh
+++ b/t/t2028-worktree-move.sh
@@ -59,4 +59,30 @@ test_expect_success 'unlock worktree twice' '
        test_path_is_missing .git/worktrees/source/locked
 '
 
+test_expect_success 'move non-worktree' '
+       mkdir abc &&
+       test_must_fail git worktree move abc def
+'
+
+test_expect_success 'move locked worktree' '
+       git worktree lock source &&
+       test_when_finished "git worktree unlock source" &&
+       test_must_fail git worktree move source destination
+'
+
+test_expect_success 'move worktree' '
+       toplevel="$(pwd)" &&
+       git worktree move source destination &&
+       test_path_is_missing source &&
+       git worktree list --porcelain | grep "^worktree.*/destination" &&
+       ! git worktree list --porcelain | grep "^worktree.*/source" >empty &&
+       git -C destination log --format=%s >actual2 &&
+       echo init >expected2 &&
+       test_cmp expected2 actual2
+'
+
+test_expect_success 'move main worktree' '
+       test_must_fail git worktree move . def
+'
+
 test_done
-- 
2.16.1.399.g632f88eed1

Reply via email to