Skip to content

Commit 4a3ce47

Browse files
sunshinecogitster
authored andcommitted
worktree: prune duplicate entries referencing same worktree path
A fundamental restriction of linked working trees is that there must only ever be a single worktree associated with a particular path, thus "git worktree add" explicitly disallows creation of a new worktree at the same location as an existing registered worktree. Nevertheless, users can still "shoot themselves in the foot" by mucking with administrative files in .git/worktree/<id>/. Worse, "git worktree move" is careless[1] and allows a worktree to be moved atop a registered but missing worktree (which can happen, for instance, if the worktree is on removable media). For instance: $ git clone foo.git $ cd foo $ git worktree add ../bar $ git worktree add ../baz $ rm -rf ../bar $ git worktree move ../baz ../bar $ git worktree list .../foo beefd00f [master] .../bar beefd00f [bar] .../bar beefd00f [baz] Help users recover from this form of corruption by teaching "git worktree prune" to detect when multiple worktrees are associated with the same path. [1]: A subsequent commit will fix "git worktree move" validation to be more strict. Signed-off-by: Eric Sunshine <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent dd9609a commit 4a3ce47

File tree

2 files changed

+55
-6
lines changed

2 files changed

+55
-6
lines changed

builtin/worktree.c

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,20 @@ static void delete_worktrees_dir_if_empty(void)
6767
rmdir(git_path("worktrees")); /* ignore failed removal */
6868
}
6969

70-
static int should_prune_worktree(const char *id, struct strbuf *reason)
70+
/*
71+
* Return true if worktree entry should be pruned, along with the reason for
72+
* pruning. Otherwise, return false and the worktree's path, or NULL if it
73+
* cannot be determined. Caller is responsible for freeing returned path.
74+
*/
75+
static int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath)
7176
{
7277
struct stat st;
7378
char *path;
7479
int fd;
7580
size_t len;
7681
ssize_t read_result;
7782

83+
*wtpath = NULL;
7884
if (!is_directory(git_path("worktrees/%s", id))) {
7985
strbuf_addstr(reason, _("not a valid directory"));
8086
return 1;
@@ -120,16 +126,17 @@ static int should_prune_worktree(const char *id, struct strbuf *reason)
120126
}
121127
path[len] = '\0';
122128
if (!file_exists(path)) {
123-
free(path);
124129
if (stat(git_path("worktrees/%s/index", id), &st) ||
125130
st.st_mtime <= expire) {
126131
strbuf_addstr(reason, _("gitdir file points to non-existent location"));
132+
free(path);
127133
return 1;
128134
} else {
135+
*wtpath = path;
129136
return 0;
130137
}
131138
}
132-
free(path);
139+
*wtpath = path;
133140
return 0;
134141
}
135142

@@ -141,22 +148,52 @@ static void prune_worktree(const char *id, const char *reason)
141148
delete_git_dir(id);
142149
}
143150

151+
static int prune_cmp(const void *a, const void *b)
152+
{
153+
const struct string_list_item *x = a;
154+
const struct string_list_item *y = b;
155+
int c;
156+
157+
if ((c = fspathcmp(x->string, y->string)))
158+
return c;
159+
/* paths same; sort by .git/worktrees/<id> */
160+
return strcmp(x->util, y->util);
161+
}
162+
163+
static void prune_dups(struct string_list *l)
164+
{
165+
int i;
166+
167+
QSORT(l->items, l->nr, prune_cmp);
168+
for (i = 1; i < l->nr; i++) {
169+
if (!fspathcmp(l->items[i].string, l->items[i - 1].string))
170+
prune_worktree(l->items[i].util, "duplicate entry");
171+
}
172+
}
173+
144174
static void prune_worktrees(void)
145175
{
146176
struct strbuf reason = STRBUF_INIT;
177+
struct string_list kept = STRING_LIST_INIT_NODUP;
147178
DIR *dir = opendir(git_path("worktrees"));
148179
struct dirent *d;
149180
if (!dir)
150181
return;
151182
while ((d = readdir(dir)) != NULL) {
183+
char *path;
152184
if (is_dot_or_dotdot(d->d_name))
153185
continue;
154186
strbuf_reset(&reason);
155-
if (!should_prune_worktree(d->d_name, &reason))
156-
continue;
157-
prune_worktree(d->d_name, reason.buf);
187+
if (should_prune_worktree(d->d_name, &reason, &path))
188+
prune_worktree(d->d_name, reason.buf);
189+
else if (path)
190+
string_list_append(&kept, path)->util = xstrdup(d->d_name);
158191
}
159192
closedir(dir);
193+
194+
prune_dups(&kept);
195+
string_list_clear(&kept, 1);
196+
160197
if (!show_only)
161198
delete_worktrees_dir_if_empty();
162199
strbuf_release(&reason);

t/t2401-worktree-prune.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,16 @@ test_expect_success 'not prune proper checkouts' '
9292
test -d .git/worktrees/nop
9393
'
9494

95+
test_expect_success 'prune duplicate (linked/linked)' '
96+
test_when_finished rm -fr .git/worktrees w1 w2 &&
97+
git worktree add --detach w1 &&
98+
git worktree add --detach w2 &&
99+
sed "s/w2/w1/" .git/worktrees/w2/gitdir >.git/worktrees/w2/gitdir.new &&
100+
mv .git/worktrees/w2/gitdir.new .git/worktrees/w2/gitdir &&
101+
git worktree prune --verbose >actual &&
102+
test_i18ngrep "duplicate entry" actual &&
103+
test -d .git/worktrees/w1 &&
104+
! test -d .git/worktrees/w2
105+
'
106+
95107
test_done

0 commit comments

Comments
 (0)