Skip to content

Commit bd0f794

Browse files
committed
Merge branch 'nd/worktree-move'
"git worktree" learned move and remove subcommands. * nd/worktree-move: t2028: fix minor error and issues in newly-added "worktree move" tests worktree remove: allow it when $GIT_WORK_TREE is already gone worktree remove: new command worktree move: refuse to move worktrees with submodules worktree move: accept destination as directory worktree move: new command worktree.c: add update_worktree_location() worktree.c: add validate_worktree()
2 parents 436d18f + 7f19def commit bd0f794

File tree

8 files changed

+452
-17
lines changed

8 files changed

+452
-17
lines changed

Documentation/git-worktree.txt

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ SYNOPSIS
1212
'git worktree add' [-f] [--detach] [--checkout] [--lock] [-b <new-branch>] <path> [<commit-ish>]
1313
'git worktree list' [--porcelain]
1414
'git worktree lock' [--reason <string>] <worktree>
15+
'git worktree move' <worktree> <new-path>
1516
'git worktree prune' [-n] [-v] [--expire <expire>]
17+
'git worktree remove' [--force] <worktree>
1618
'git worktree unlock' <worktree>
1719

1820
DESCRIPTION
@@ -34,10 +36,6 @@ The working tree's administrative files in the repository (see
3436
`git worktree prune` in the main or any linked working tree to
3537
clean up any stale administrative files.
3638

37-
If you move a linked working tree, you need to manually update the
38-
administrative files so that they do not get pruned automatically. See
39-
section "DETAILS" for more information.
40-
4139
If a linked working tree is stored on a portable device or network share
4240
which is not always mounted, you can prevent its administrative files from
4341
being pruned by issuing the `git worktree lock` command, optionally
@@ -80,10 +78,22 @@ files from being pruned automatically. This also prevents it from
8078
being moved or deleted. Optionally, specify a reason for the lock
8179
with `--reason`.
8280

81+
move::
82+
83+
Move a working tree to a new location. Note that the main working tree
84+
or linked working trees containing submodules cannot be moved.
85+
8386
prune::
8487

8588
Prune working tree information in $GIT_DIR/worktrees.
8689

90+
remove::
91+
92+
Remove a working tree. Only clean working trees (no untracked files
93+
and no modification in tracked files) can be removed. Unclean working
94+
trees or ones with submodules can be removed with `--force`. The main
95+
working tree cannot be removed.
96+
8797
unlock::
8898

8999
Unlock a working tree, allowing it to be pruned, moved or deleted.
@@ -93,9 +103,10 @@ OPTIONS
93103

94104
-f::
95105
--force::
96-
By default, `add` refuses to create a new working tree when `<commit-ish>` is a branch name and
97-
is already checked out by another working tree. This option overrides
98-
that safeguard.
106+
By default, `add` refuses to create a new working tree when
107+
`<commit-ish>` is a branch name and is already checked out by
108+
another working tree and `remove` refuses to remove an unclean
109+
working tree. This option overrides that safeguard.
99110

100111
-b <new-branch>::
101112
-B <new-branch>::
@@ -197,7 +208,7 @@ thumb is do not make any assumption about whether a path belongs to
197208
$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
198209
inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
199210

200-
If you move a linked working tree, you need to update the 'gitdir' file
211+
If you manually move a linked working tree, you need to update the 'gitdir' file
201212
in the entry's directory. For example, if a linked working tree is moved
202213
to `/newpath/test-next` and its `.git` file points to
203214
`/path/main/.git/worktrees/test-next`, then update
@@ -277,13 +288,6 @@ Multiple checkout in general is still experimental, and the support
277288
for submodules is incomplete. It is NOT recommended to make multiple
278289
checkouts of a superproject.
279290

280-
git-worktree could provide more automation for tasks currently
281-
performed manually, such as:
282-
283-
- `remove` to remove a linked working tree and its administrative files (and
284-
warn if the working tree is dirty)
285-
- `mv` to move or rename a working tree and update its administrative files
286-
287291
GIT
288292
---
289293
Part of the linkgit:git[1] suite

builtin/worktree.c

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ static const char * const worktree_usage[] = {
1717
N_("git worktree add [<options>] <path> [<commit-ish>]"),
1818
N_("git worktree list [<options>]"),
1919
N_("git worktree lock [<options>] <path>"),
20+
N_("git worktree move <worktree> <new-path>"),
2021
N_("git worktree prune [<options>]"),
22+
N_("git worktree remove [<options>] <worktree>"),
2123
N_("git worktree unlock <path>"),
2224
NULL
2325
};
@@ -619,6 +621,220 @@ static int unlock_worktree(int ac, const char **av, const char *prefix)
619621
return ret;
620622
}
621623

624+
static void validate_no_submodules(const struct worktree *wt)
625+
{
626+
struct index_state istate = { NULL };
627+
int i, found_submodules = 0;
628+
629+
if (read_index_from(&istate, worktree_git_path(wt, "index"),
630+
get_worktree_git_dir(wt)) > 0) {
631+
for (i = 0; i < istate.cache_nr; i++) {
632+
struct cache_entry *ce = istate.cache[i];
633+
634+
if (S_ISGITLINK(ce->ce_mode)) {
635+
found_submodules = 1;
636+
break;
637+
}
638+
}
639+
}
640+
discard_index(&istate);
641+
642+
if (found_submodules)
643+
die(_("working trees containing submodules cannot be moved or removed"));
644+
}
645+
646+
static int move_worktree(int ac, const char **av, const char *prefix)
647+
{
648+
struct option options[] = {
649+
OPT_END()
650+
};
651+
struct worktree **worktrees, *wt;
652+
struct strbuf dst = STRBUF_INIT;
653+
struct strbuf errmsg = STRBUF_INIT;
654+
const char *reason;
655+
char *path;
656+
657+
ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
658+
if (ac != 2)
659+
usage_with_options(worktree_usage, options);
660+
661+
path = prefix_filename(prefix, av[1]);
662+
strbuf_addstr(&dst, path);
663+
free(path);
664+
665+
worktrees = get_worktrees(0);
666+
wt = find_worktree(worktrees, prefix, av[0]);
667+
if (!wt)
668+
die(_("'%s' is not a working tree"), av[0]);
669+
if (is_main_worktree(wt))
670+
die(_("'%s' is a main working tree"), av[0]);
671+
if (is_directory(dst.buf)) {
672+
const char *sep = find_last_dir_sep(wt->path);
673+
674+
if (!sep)
675+
die(_("could not figure out destination name from '%s'"),
676+
wt->path);
677+
strbuf_trim_trailing_dir_sep(&dst);
678+
strbuf_addstr(&dst, sep);
679+
}
680+
if (file_exists(dst.buf))
681+
die(_("target '%s' already exists"), dst.buf);
682+
683+
validate_no_submodules(wt);
684+
685+
reason = is_worktree_locked(wt);
686+
if (reason) {
687+
if (*reason)
688+
die(_("cannot move a locked working tree, lock reason: %s"),
689+
reason);
690+
die(_("cannot move a locked working tree"));
691+
}
692+
if (validate_worktree(wt, &errmsg, 0))
693+
die(_("validation failed, cannot move working tree: %s"),
694+
errmsg.buf);
695+
strbuf_release(&errmsg);
696+
697+
if (rename(wt->path, dst.buf) == -1)
698+
die_errno(_("failed to move '%s' to '%s'"), wt->path, dst.buf);
699+
700+
update_worktree_location(wt, dst.buf);
701+
702+
strbuf_release(&dst);
703+
free_worktrees(worktrees);
704+
return 0;
705+
}
706+
707+
/*
708+
* Note, "git status --porcelain" is used to determine if it's safe to
709+
* delete a whole worktree. "git status" does not ignore user
710+
* configuration, so if a normal "git status" shows "clean" for the
711+
* user, then it's ok to remove it.
712+
*
713+
* This assumption may be a bad one. We may want to ignore
714+
* (potentially bad) user settings and only delete a worktree when
715+
* it's absolutely safe to do so from _our_ point of view because we
716+
* know better.
717+
*/
718+
static void check_clean_worktree(struct worktree *wt,
719+
const char *original_path)
720+
{
721+
struct argv_array child_env = ARGV_ARRAY_INIT;
722+
struct child_process cp;
723+
char buf[1];
724+
int ret;
725+
726+
/*
727+
* Until we sort this out, all submodules are "dirty" and
728+
* will abort this function.
729+
*/
730+
validate_no_submodules(wt);
731+
732+
argv_array_pushf(&child_env, "%s=%s/.git",
733+
GIT_DIR_ENVIRONMENT, wt->path);
734+
argv_array_pushf(&child_env, "%s=%s",
735+
GIT_WORK_TREE_ENVIRONMENT, wt->path);
736+
memset(&cp, 0, sizeof(cp));
737+
argv_array_pushl(&cp.args, "status",
738+
"--porcelain", "--ignore-submodules=none",
739+
NULL);
740+
cp.env = child_env.argv;
741+
cp.git_cmd = 1;
742+
cp.dir = wt->path;
743+
cp.out = -1;
744+
ret = start_command(&cp);
745+
if (ret)
746+
die_errno(_("failed to run 'git status' on '%s'"),
747+
original_path);
748+
ret = xread(cp.out, buf, sizeof(buf));
749+
if (ret)
750+
die(_("'%s' is dirty, use --force to delete it"),
751+
original_path);
752+
close(cp.out);
753+
ret = finish_command(&cp);
754+
if (ret)
755+
die_errno(_("failed to run 'git status' on '%s', code %d"),
756+
original_path, ret);
757+
}
758+
759+
static int delete_git_work_tree(struct worktree *wt)
760+
{
761+
struct strbuf sb = STRBUF_INIT;
762+
int ret = 0;
763+
764+
strbuf_addstr(&sb, wt->path);
765+
if (remove_dir_recursively(&sb, 0)) {
766+
error_errno(_("failed to delete '%s'"), sb.buf);
767+
ret = -1;
768+
}
769+
strbuf_release(&sb);
770+
return ret;
771+
}
772+
773+
static int delete_git_dir(struct worktree *wt)
774+
{
775+
struct strbuf sb = STRBUF_INIT;
776+
int ret = 0;
777+
778+
strbuf_addstr(&sb, git_common_path("worktrees/%s", wt->id));
779+
if (remove_dir_recursively(&sb, 0)) {
780+
error_errno(_("failed to delete '%s'"), sb.buf);
781+
ret = -1;
782+
}
783+
strbuf_release(&sb);
784+
return ret;
785+
}
786+
787+
static int remove_worktree(int ac, const char **av, const char *prefix)
788+
{
789+
int force = 0;
790+
struct option options[] = {
791+
OPT_BOOL(0, "force", &force,
792+
N_("force removing even if the worktree is dirty")),
793+
OPT_END()
794+
};
795+
struct worktree **worktrees, *wt;
796+
struct strbuf errmsg = STRBUF_INIT;
797+
const char *reason;
798+
int ret = 0;
799+
800+
ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
801+
if (ac != 1)
802+
usage_with_options(worktree_usage, options);
803+
804+
worktrees = get_worktrees(0);
805+
wt = find_worktree(worktrees, prefix, av[0]);
806+
if (!wt)
807+
die(_("'%s' is not a working tree"), av[0]);
808+
if (is_main_worktree(wt))
809+
die(_("'%s' is a main working tree"), av[0]);
810+
reason = is_worktree_locked(wt);
811+
if (reason) {
812+
if (*reason)
813+
die(_("cannot remove a locked working tree, lock reason: %s"),
814+
reason);
815+
die(_("cannot remove a locked working tree"));
816+
}
817+
if (validate_worktree(wt, &errmsg, WT_VALIDATE_WORKTREE_MISSING_OK))
818+
die(_("validation failed, cannot remove working tree: %s"),
819+
errmsg.buf);
820+
strbuf_release(&errmsg);
821+
822+
if (file_exists(wt->path)) {
823+
if (!force)
824+
check_clean_worktree(wt, av[0]);
825+
826+
ret |= delete_git_work_tree(wt);
827+
}
828+
/*
829+
* continue on even if ret is non-zero, there's no going back
830+
* from here.
831+
*/
832+
ret |= delete_git_dir(wt);
833+
834+
free_worktrees(worktrees);
835+
return ret;
836+
}
837+
622838
int cmd_worktree(int ac, const char **av, const char *prefix)
623839
{
624840
struct option options[] = {
@@ -641,5 +857,9 @@ int cmd_worktree(int ac, const char **av, const char *prefix)
641857
return lock_worktree(ac - 1, av + 1, prefix);
642858
if (!strcmp(av[1], "unlock"))
643859
return unlock_worktree(ac - 1, av + 1, prefix);
860+
if (!strcmp(av[1], "move"))
861+
return move_worktree(ac - 1, av + 1, prefix);
862+
if (!strcmp(av[1], "remove"))
863+
return remove_worktree(ac - 1, av + 1, prefix);
644864
usage_with_options(worktree_usage, options);
645865
}

contrib/completion/git-completion.bash

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3087,7 +3087,7 @@ _git_whatchanged ()
30873087

30883088
_git_worktree ()
30893089
{
3090-
local subcommands="add list lock prune unlock"
3090+
local subcommands="add list lock move prune remove unlock"
30913091
local subcommand="$(__git_find_on_cmdline "$subcommands")"
30923092
if [ -z "$subcommand" ]; then
30933093
__gitcomp "$subcommands"
@@ -3105,6 +3105,9 @@ _git_worktree ()
31053105
prune,--*)
31063106
__gitcomp "--dry-run --expire --verbose"
31073107
;;
3108+
remove,--*)
3109+
__gitcomp "--force"
3110+
;;
31083111
*)
31093112
;;
31103113
esac

strbuf.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,21 @@ void strbuf_trim(struct strbuf *sb)
9595
strbuf_rtrim(sb);
9696
strbuf_ltrim(sb);
9797
}
98+
9899
void strbuf_rtrim(struct strbuf *sb)
99100
{
100101
while (sb->len > 0 && isspace((unsigned char)sb->buf[sb->len - 1]))
101102
sb->len--;
102103
sb->buf[sb->len] = '\0';
103104
}
104105

106+
void strbuf_trim_trailing_dir_sep(struct strbuf *sb)
107+
{
108+
while (sb->len > 0 && is_dir_sep((unsigned char)sb->buf[sb->len - 1]))
109+
sb->len--;
110+
sb->buf[sb->len] = '\0';
111+
}
112+
105113
void strbuf_ltrim(struct strbuf *sb)
106114
{
107115
char *b = sb->buf;

strbuf.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ extern void strbuf_trim(struct strbuf *);
179179
extern void strbuf_rtrim(struct strbuf *);
180180
extern void strbuf_ltrim(struct strbuf *);
181181

182+
/* Strip trailing directory separators */
183+
extern void strbuf_trim_trailing_dir_sep(struct strbuf *);
184+
182185
/**
183186
* Replace the contents of the strbuf with a reencoded form. Returns -1
184187
* on error, 0 on success.

0 commit comments

Comments
 (0)