Skip to content

Commit 1c515bf

Browse files
committed
Merge branch 'es/worktree-forced-ops-fix'
Fix a bug in which the same path could be registered under multiple worktree entries if the path was missing (for instance, was removed manually). Also, as a convenience, expand the number of cases in which --force is applicable. * es/worktree-forced-ops-fix: doc-diff: force worktree add worktree: delete .git/worktrees if empty after 'remove' worktree: teach 'remove' to override lock when --force given twice worktree: teach 'move' to override lock when --force given twice worktree: teach 'add' to respect --force for registered but missing path worktree: disallow adding same path multiple times worktree: prepare for more checks of whether path can become worktree worktree: generalize delete_git_dir() to reduce code duplication worktree: move delete_git_dir() earlier in file for upcoming new callers worktree: don't die() in library function find_worktree()
2 parents 2af0b1c + 684e742 commit 1c515bf

File tree

6 files changed

+155
-40
lines changed

6 files changed

+155
-40
lines changed

Documentation/doc-diff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ fi
7575
# results that don't differ between the two trees.
7676
if ! test -d "$tmp/worktree"
7777
then
78-
git worktree add --detach "$tmp/worktree" "$from" &&
78+
git worktree add -f --detach "$tmp/worktree" "$from" &&
7979
dots=$(echo "$tmp/worktree" | sed 's#[^/]*#..#g') &&
8080
ln -s "$dots/config.mak" "$tmp/worktree/config.mak"
8181
fi

Documentation/git-worktree.txt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,16 @@ OPTIONS
120120
--force::
121121
By default, `add` refuses to create a new working tree when
122122
`<commit-ish>` is a branch name and is already checked out by
123-
another working tree and `remove` refuses to remove an unclean
124-
working tree. This option overrides these safeguards.
123+
another working tree, or if `<path>` is already assigned to some
124+
working tree but is missing (for instance, if `<path>` was deleted
125+
manually). This option overrides these safeguards. To add a missing but
126+
locked working tree path, specify `--force` twice.
127+
+
128+
`move` refuses to move a locked working tree unless `--force` is specified
129+
twice.
130+
+
131+
`remove` refuses to remove an unclean working tree unless `--force` is used.
132+
To remove a locked working tree, specify `--force` twice.
125133

126134
-b <new-branch>::
127135
-B <new-branch>::

builtin/worktree.c

Lines changed: 77 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,26 @@ static int git_worktree_config(const char *var, const char *value, void *cb)
4747
return git_default_config(var, value, cb);
4848
}
4949

50+
static int delete_git_dir(const char *id)
51+
{
52+
struct strbuf sb = STRBUF_INIT;
53+
int ret;
54+
55+
strbuf_addstr(&sb, git_common_path("worktrees/%s", id));
56+
ret = remove_dir_recursively(&sb, 0);
57+
if (ret < 0 && errno == ENOTDIR)
58+
ret = unlink(sb.buf);
59+
if (ret)
60+
error_errno(_("failed to delete '%s'"), sb.buf);
61+
strbuf_release(&sb);
62+
return ret;
63+
}
64+
65+
static void delete_worktrees_dir_if_empty(void)
66+
{
67+
rmdir(git_path("worktrees")); /* ignore failed removal */
68+
}
69+
5070
static int prune_worktree(const char *id, struct strbuf *reason)
5171
{
5272
struct stat st;
@@ -116,10 +136,8 @@ static int prune_worktree(const char *id, struct strbuf *reason)
116136
static void prune_worktrees(void)
117137
{
118138
struct strbuf reason = STRBUF_INIT;
119-
struct strbuf path = STRBUF_INIT;
120139
DIR *dir = opendir(git_path("worktrees"));
121140
struct dirent *d;
122-
int ret;
123141
if (!dir)
124142
return;
125143
while ((d = readdir(dir)) != NULL) {
@@ -132,18 +150,12 @@ static void prune_worktrees(void)
132150
printf("%s\n", reason.buf);
133151
if (show_only)
134152
continue;
135-
git_path_buf(&path, "worktrees/%s", d->d_name);
136-
ret = remove_dir_recursively(&path, 0);
137-
if (ret < 0 && errno == ENOTDIR)
138-
ret = unlink(path.buf);
139-
if (ret)
140-
error_errno(_("failed to remove '%s'"), path.buf);
153+
delete_git_dir(d->d_name);
141154
}
142155
closedir(dir);
143156
if (!show_only)
144-
rmdir(git_path("worktrees"));
157+
delete_worktrees_dir_if_empty();
145158
strbuf_release(&reason);
146-
strbuf_release(&path);
147159
}
148160

149161
static int prune(int ac, const char **av, const char *prefix)
@@ -212,6 +224,43 @@ static const char *worktree_basename(const char *path, int *olen)
212224
return name;
213225
}
214226

227+
static void validate_worktree_add(const char *path, const struct add_opts *opts)
228+
{
229+
struct worktree **worktrees;
230+
struct worktree *wt;
231+
int locked;
232+
233+
if (file_exists(path) && !is_empty_dir(path))
234+
die(_("'%s' already exists"), path);
235+
236+
worktrees = get_worktrees(0);
237+
/*
238+
* find_worktree()'s suffix matching may undesirably find the main
239+
* rather than a linked worktree (for instance, when the basenames
240+
* of the main worktree and the one being created are the same).
241+
* We're only interested in linked worktrees, so skip the main
242+
* worktree with +1.
243+
*/
244+
wt = find_worktree(worktrees + 1, NULL, path);
245+
if (!wt)
246+
goto done;
247+
248+
locked = !!is_worktree_locked(wt);
249+
if ((!locked && opts->force) || (locked && opts->force > 1)) {
250+
if (delete_git_dir(wt->id))
251+
die(_("unable to re-add worktree '%s'"), path);
252+
goto done;
253+
}
254+
255+
if (locked)
256+
die(_("'%s' is a missing but locked worktree;\nuse 'add -f -f' to override, or 'unlock' and 'prune' or 'remove' to clear"), path);
257+
else
258+
die(_("'%s' is a missing but already registered worktree;\nuse 'add -f' to override, or 'prune' or 'remove' to clear"), path);
259+
260+
done:
261+
free_worktrees(worktrees);
262+
}
263+
215264
static int add_worktree(const char *path, const char *refname,
216265
const struct add_opts *opts)
217266
{
@@ -226,8 +275,7 @@ static int add_worktree(const char *path, const char *refname,
226275
struct commit *commit = NULL;
227276
int is_branch = 0;
228277

229-
if (file_exists(path) && !is_empty_dir(path))
230-
die(_("'%s' already exists"), path);
278+
validate_worktree_add(path, opts);
231279

232280
/* is 'refname' a branch or commit? */
233281
if (!opts->detach && !strbuf_check_branch_ref(&symref, refname) &&
@@ -697,13 +745,17 @@ static void validate_no_submodules(const struct worktree *wt)
697745

698746
static int move_worktree(int ac, const char **av, const char *prefix)
699747
{
748+
int force = 0;
700749
struct option options[] = {
750+
OPT__FORCE(&force,
751+
N_("force move even if worktree is dirty or locked"),
752+
PARSE_OPT_NOCOMPLETE),
701753
OPT_END()
702754
};
703755
struct worktree **worktrees, *wt;
704756
struct strbuf dst = STRBUF_INIT;
705757
struct strbuf errmsg = STRBUF_INIT;
706-
const char *reason;
758+
const char *reason = NULL;
707759
char *path;
708760

709761
ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
@@ -734,12 +786,13 @@ static int move_worktree(int ac, const char **av, const char *prefix)
734786

735787
validate_no_submodules(wt);
736788

737-
reason = is_worktree_locked(wt);
789+
if (force < 2)
790+
reason = is_worktree_locked(wt);
738791
if (reason) {
739792
if (*reason)
740-
die(_("cannot move a locked working tree, lock reason: %s"),
793+
die(_("cannot move a locked working tree, lock reason: %s\nuse 'move -f -f' to override or unlock first"),
741794
reason);
742-
die(_("cannot move a locked working tree"));
795+
die(_("cannot move a locked working tree;\nuse 'move -f -f' to override or unlock first"));
743796
}
744797
if (validate_worktree(wt, &errmsg, 0))
745798
die(_("validation failed, cannot move working tree: %s"),
@@ -822,32 +875,18 @@ static int delete_git_work_tree(struct worktree *wt)
822875
return ret;
823876
}
824877

825-
static int delete_git_dir(struct worktree *wt)
826-
{
827-
struct strbuf sb = STRBUF_INIT;
828-
int ret = 0;
829-
830-
strbuf_addstr(&sb, git_common_path("worktrees/%s", wt->id));
831-
if (remove_dir_recursively(&sb, 0)) {
832-
error_errno(_("failed to delete '%s'"), sb.buf);
833-
ret = -1;
834-
}
835-
strbuf_release(&sb);
836-
return ret;
837-
}
838-
839878
static int remove_worktree(int ac, const char **av, const char *prefix)
840879
{
841880
int force = 0;
842881
struct option options[] = {
843882
OPT__FORCE(&force,
844-
N_("force removing even if the worktree is dirty"),
883+
N_("force removal even if worktree is dirty or locked"),
845884
PARSE_OPT_NOCOMPLETE),
846885
OPT_END()
847886
};
848887
struct worktree **worktrees, *wt;
849888
struct strbuf errmsg = STRBUF_INIT;
850-
const char *reason;
889+
const char *reason = NULL;
851890
int ret = 0;
852891

853892
ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
@@ -860,12 +899,13 @@ static int remove_worktree(int ac, const char **av, const char *prefix)
860899
die(_("'%s' is not a working tree"), av[0]);
861900
if (is_main_worktree(wt))
862901
die(_("'%s' is a main working tree"), av[0]);
863-
reason = is_worktree_locked(wt);
902+
if (force < 2)
903+
reason = is_worktree_locked(wt);
864904
if (reason) {
865905
if (*reason)
866-
die(_("cannot remove a locked working tree, lock reason: %s"),
906+
die(_("cannot remove a locked working tree, lock reason: %s\nuse 'remove -f -f' to override or unlock first"),
867907
reason);
868-
die(_("cannot remove a locked working tree"));
908+
die(_("cannot remove a locked working tree;\nuse 'remove -f -f' to override or unlock first"));
869909
}
870910
if (validate_worktree(wt, &errmsg, WT_VALIDATE_WORKTREE_MISSING_OK))
871911
die(_("validation failed, cannot remove working tree: %s"),
@@ -882,7 +922,8 @@ static int remove_worktree(int ac, const char **av, const char *prefix)
882922
* continue on even if ret is non-zero, there's no going back
883923
* from here.
884924
*/
885-
ret |= delete_git_dir(wt);
925+
ret |= delete_git_dir(wt->id);
926+
delete_worktrees_dir_if_empty();
886927

887928
free_worktrees(worktrees);
888929
return ret;

t/t2025-worktree-add.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,4 +552,22 @@ test_expect_success '"add" in bare repo invokes post-checkout hook' '
552552
test_cmp hook.expect goozy/hook.actual
553553
'
554554

555+
test_expect_success '"add" an existing but missing worktree' '
556+
git worktree add --detach pneu &&
557+
test_must_fail git worktree add --detach pneu &&
558+
rm -fr pneu &&
559+
test_must_fail git worktree add --detach pneu &&
560+
git worktree add --force --detach pneu
561+
'
562+
563+
test_expect_success '"add" an existing locked but missing worktree' '
564+
git worktree add --detach gnoo &&
565+
git worktree lock gnoo &&
566+
test_when_finished "git worktree unlock gnoo || :" &&
567+
rm -fr gnoo &&
568+
test_must_fail git worktree add --detach gnoo &&
569+
test_must_fail git worktree add --force --detach gnoo &&
570+
git worktree add --force --force --detach gnoo
571+
'
572+
555573
test_done

t/t2028-worktree-move.sh

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,20 @@ test_expect_success 'move worktree to another dir' '
9898
test_cmp expected2 actual2
9999
'
100100

101+
test_expect_success 'move locked worktree (force)' '
102+
test_when_finished "
103+
git worktree unlock flump || :
104+
git worktree remove flump || :
105+
git worktree unlock ploof || :
106+
git worktree remove ploof || :
107+
" &&
108+
git worktree add --detach flump &&
109+
git worktree lock flump &&
110+
test_must_fail git worktree move flump ploof" &&
111+
test_must_fail git worktree move --force flump ploof" &&
112+
git worktree move --force --force flump ploof
113+
'
114+
101115
test_expect_success 'remove main worktree' '
102116
test_must_fail git worktree remove .
103117
'
@@ -141,4 +155,34 @@ test_expect_success 'NOT remove missing-but-locked worktree' '
141155
test_path_is_dir .git/worktrees/gone-but-locked
142156
'
143157

158+
test_expect_success 'proper error when worktree not found' '
159+
for i in noodle noodle/bork
160+
do
161+
test_must_fail git worktree lock $i 2>err &&
162+
test_i18ngrep "not a working tree" err || return 1
163+
done
164+
'
165+
166+
test_expect_success 'remove locked worktree (force)' '
167+
git worktree add --detach gumby &&
168+
test_when_finished "git worktree remove gumby || :" &&
169+
git worktree lock gumby &&
170+
test_when_finished "git worktree unlock gumby || :" &&
171+
test_must_fail git worktree remove gumby &&
172+
test_must_fail git worktree remove --force gumby &&
173+
git worktree remove --force --force gumby
174+
'
175+
176+
test_expect_success 'remove cleans up .git/worktrees when empty' '
177+
git init moog &&
178+
(
179+
cd moog &&
180+
test_commit bim &&
181+
git worktree add --detach goom &&
182+
test_path_exists .git/worktrees &&
183+
git worktree remove goom &&
184+
test_path_is_missing .git/worktrees
185+
)
186+
'
187+
144188
test_done

worktree.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,11 @@ struct worktree *find_worktree(struct worktree **list,
217217

218218
if (prefix)
219219
arg = to_free = prefix_filename(prefix, arg);
220-
path = real_pathdup(arg, 1);
220+
path = real_pathdup(arg, 0);
221+
if (!path) {
222+
free(to_free);
223+
return NULL;
224+
}
221225
for (; *list; list++)
222226
if (!fspathcmp(path, real_path((*list)->path)))
223227
break;

0 commit comments

Comments
 (0)