Skip to content

Commit 02fb216

Browse files
committed
Merge branch 'rs/worktree-list-verbose'
`git worktree list` now annotates worktrees as prunable, shows locked and prunable attributes in --porcelain mode, and gained a --verbose option. * rs/worktree-list-verbose: worktree: teach `list` verbose mode worktree: teach `list` to annotate prunable worktree worktree: teach `list --porcelain` to annotate locked worktree t2402: ensure locked worktree is properly cleaned up worktree: teach worktree_lock_reason() to gently handle main worktree worktree: teach worktree to lazy-load "prunable" reason worktree: libify should_prune_worktree()
2 parents 7e94720 + 076b444 commit 02fb216

File tree

5 files changed

+314
-80
lines changed

5 files changed

+314
-80
lines changed

Documentation/git-worktree.txt

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,9 @@ list::
9797
List details of each working tree. The main working tree is listed first,
9898
followed by each of the linked working trees. The output details include
9999
whether the working tree is bare, the revision currently checked out, the
100-
branch currently checked out (or "detached HEAD" if none), and "locked" if
101-
the worktree is locked.
100+
branch currently checked out (or "detached HEAD" if none), "locked" if
101+
the worktree is locked, "prunable" if the worktree can be pruned by `prune`
102+
command.
102103

103104
lock::
104105

@@ -231,9 +232,14 @@ This can also be set up as the default behaviour by using the
231232
-v::
232233
--verbose::
233234
With `prune`, report all removals.
235+
+
236+
With `list`, output additional information about worktrees (see below).
234237

235238
--expire <time>::
236239
With `prune`, only expire unused working trees older than `<time>`.
240+
+
241+
With `list`, annotate missing working trees as prunable if they are
242+
older than `<time>`.
237243

238244
--reason <string>::
239245
With `lock`, an explanation why the working tree is locked.
@@ -372,13 +378,46 @@ $ git worktree list
372378
/path/to/other-linked-worktree 1234abc (detached HEAD)
373379
------------
374380

381+
The command also shows annotations for each working tree, according to its state.
382+
These annotations are:
383+
384+
* `locked`, if the working tree is locked.
385+
* `prunable`, if the working tree can be pruned via `git worktree prune`.
386+
387+
------------
388+
$ git worktree list
389+
/path/to/linked-worktree abcd1234 [master]
390+
/path/to/locked-worktreee acbd5678 (brancha) locked
391+
/path/to/prunable-worktree 5678abc (detached HEAD) prunable
392+
------------
393+
394+
For these annotations, a reason might also be available and this can be
395+
seen using the verbose mode. The annotation is then moved to the next line
396+
indented followed by the additional information.
397+
398+
------------
399+
$ git worktree list --verbose
400+
/path/to/linked-worktree abcd1234 [master]
401+
/path/to/locked-worktree-no-reason abcd5678 (detached HEAD) locked
402+
/path/to/locked-worktree-with-reason 1234abcd (brancha)
403+
locked: working tree path is mounted on a portable device
404+
/path/to/prunable-worktree 5678abc1 (detached HEAD)
405+
prunable: gitdir file points to non-existent location
406+
------------
407+
408+
Note that the annotation is moved to the next line if the additional
409+
information is available, otherwise it stays on the same line as the
410+
working tree itself.
411+
375412
Porcelain Format
376413
~~~~~~~~~~~~~~~~
377414
The porcelain format has a line per attribute. Attributes are listed with a
378415
label and value separated by a single space. Boolean attributes (like `bare`
379416
and `detached`) are listed as a label only, and are present only
380-
if the value is true. The first attribute of a working tree is always
381-
`worktree`, an empty line indicates the end of the record. For example:
417+
if the value is true. Some attributes (like `locked`) can be listed as a label
418+
only or with a value depending upon whether a reason is available. The first
419+
attribute of a working tree is always `worktree`, an empty line indicates the
420+
end of the record. For example:
382421

383422
------------
384423
$ git worktree list --porcelain
@@ -393,6 +432,33 @@ worktree /path/to/other-linked-worktree
393432
HEAD 1234abc1234abc1234abc1234abc1234abc1234a
394433
detached
395434

435+
worktree /path/to/linked-worktree-locked-no-reason
436+
HEAD 5678abc5678abc5678abc5678abc5678abc5678c
437+
branch refs/heads/locked-no-reason
438+
locked
439+
440+
worktree /path/to/linked-worktree-locked-with-reason
441+
HEAD 3456def3456def3456def3456def3456def3456b
442+
branch refs/heads/locked-with-reason
443+
locked reason why is locked
444+
445+
worktree /path/to/linked-worktree-prunable
446+
HEAD 1233def1234def1234def1234def1234def1234b
447+
detached
448+
prunable gitdir file points to non-existent location
449+
450+
------------
451+
452+
If the lock reason contains "unusual" characters such as newline, they
453+
are escaped and the entire reason is quoted as explained for the
454+
configuration variable `core.quotePath` (see linkgit:git-config[1]).
455+
For Example:
456+
457+
------------
458+
$ git worktree list --porcelain
459+
...
460+
locked "reason\nwhy is locked"
461+
...
396462
------------
397463

398464
EXAMPLES

builtin/worktree.c

Lines changed: 35 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "submodule.h"
1313
#include "utf8.h"
1414
#include "worktree.h"
15+
#include "quote.h"
1516

1617
static const char * const worktree_usage[] = {
1718
N_("git worktree add [<options>] <path> [<commit-ish>]"),
@@ -67,79 +68,6 @@ static void delete_worktrees_dir_if_empty(void)
6768
rmdir(git_path("worktrees")); /* ignore failed removal */
6869
}
6970

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)
76-
{
77-
struct stat st;
78-
char *path;
79-
int fd;
80-
size_t len;
81-
ssize_t read_result;
82-
83-
*wtpath = NULL;
84-
if (!is_directory(git_path("worktrees/%s", id))) {
85-
strbuf_addstr(reason, _("not a valid directory"));
86-
return 1;
87-
}
88-
if (file_exists(git_path("worktrees/%s/locked", id)))
89-
return 0;
90-
if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
91-
strbuf_addstr(reason, _("gitdir file does not exist"));
92-
return 1;
93-
}
94-
fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
95-
if (fd < 0) {
96-
strbuf_addf(reason, _("unable to read gitdir file (%s)"),
97-
strerror(errno));
98-
return 1;
99-
}
100-
len = xsize_t(st.st_size);
101-
path = xmallocz(len);
102-
103-
read_result = read_in_full(fd, path, len);
104-
if (read_result < 0) {
105-
strbuf_addf(reason, _("unable to read gitdir file (%s)"),
106-
strerror(errno));
107-
close(fd);
108-
free(path);
109-
return 1;
110-
}
111-
close(fd);
112-
113-
if (read_result != len) {
114-
strbuf_addf(reason,
115-
_("short read (expected %"PRIuMAX" bytes, read %"PRIuMAX")"),
116-
(uintmax_t)len, (uintmax_t)read_result);
117-
free(path);
118-
return 1;
119-
}
120-
while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
121-
len--;
122-
if (!len) {
123-
strbuf_addstr(reason, _("invalid gitdir file"));
124-
free(path);
125-
return 1;
126-
}
127-
path[len] = '\0';
128-
if (!file_exists(path)) {
129-
if (stat(git_path("worktrees/%s/index", id), &st) ||
130-
st.st_mtime <= expire) {
131-
strbuf_addstr(reason, _("gitdir file points to non-existent location"));
132-
free(path);
133-
return 1;
134-
} else {
135-
*wtpath = path;
136-
return 0;
137-
}
138-
}
139-
*wtpath = path;
140-
return 0;
141-
}
142-
14371
static void prune_worktree(const char *id, const char *reason)
14472
{
14573
if (show_only || verbose)
@@ -195,7 +123,7 @@ static void prune_worktrees(void)
195123
if (is_dot_or_dotdot(d->d_name))
196124
continue;
197125
strbuf_reset(&reason);
198-
if (should_prune_worktree(d->d_name, &reason, &path))
126+
if (should_prune_worktree(d->d_name, &reason, &path, expire))
199127
prune_worktree(d->d_name, reason.buf);
200128
else if (path)
201129
string_list_append(&kept, path)->util = xstrdup(d->d_name);
@@ -642,6 +570,8 @@ static int add(int ac, const char **av, const char *prefix)
642570

643571
static void show_worktree_porcelain(struct worktree *wt)
644572
{
573+
const char *reason;
574+
645575
printf("worktree %s\n", wt->path);
646576
if (wt->is_bare)
647577
printf("bare\n");
@@ -652,6 +582,20 @@ static void show_worktree_porcelain(struct worktree *wt)
652582
else if (wt->head_ref)
653583
printf("branch %s\n", wt->head_ref);
654584
}
585+
586+
reason = worktree_lock_reason(wt);
587+
if (reason && *reason) {
588+
struct strbuf sb = STRBUF_INIT;
589+
quote_c_style(reason, &sb, NULL, 0);
590+
printf("locked %s\n", sb.buf);
591+
strbuf_release(&sb);
592+
} else if (reason)
593+
printf("locked\n");
594+
595+
reason = worktree_prune_reason(wt, expire);
596+
if (reason)
597+
printf("prunable %s\n", reason);
598+
655599
printf("\n");
656600
}
657601

@@ -660,6 +604,7 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
660604
struct strbuf sb = STRBUF_INIT;
661605
int cur_path_len = strlen(wt->path);
662606
int path_adj = cur_path_len - utf8_strwidth(wt->path);
607+
const char *reason;
663608

664609
strbuf_addf(&sb, "%-*s ", 1 + path_maxlen + path_adj, wt->path);
665610
if (wt->is_bare)
@@ -677,9 +622,18 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
677622
strbuf_addstr(&sb, "(error)");
678623
}
679624

680-
if (!is_main_worktree(wt) && worktree_lock_reason(wt))
625+
reason = worktree_lock_reason(wt);
626+
if (verbose && reason && *reason)
627+
strbuf_addf(&sb, "\n\tlocked: %s", reason);
628+
else if (reason)
681629
strbuf_addstr(&sb, " locked");
682630

631+
reason = worktree_prune_reason(wt, expire);
632+
if (verbose && reason)
633+
strbuf_addf(&sb, "\n\tprunable: %s", reason);
634+
else if (reason)
635+
strbuf_addstr(&sb, " prunable");
636+
683637
printf("%s\n", sb.buf);
684638
strbuf_release(&sb);
685639
}
@@ -723,12 +677,18 @@ static int list(int ac, const char **av, const char *prefix)
723677

724678
struct option options[] = {
725679
OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
680+
OPT__VERBOSE(&verbose, N_("show extended annotations and reasons, if available")),
681+
OPT_EXPIRY_DATE(0, "expire", &expire,
682+
N_("add 'prunable' annotation to worktrees older than <time>")),
726683
OPT_END()
727684
};
728685

686+
expire = TIME_MAX;
729687
ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
730688
if (ac)
731689
usage_with_options(worktree_usage, options);
690+
else if (verbose && porcelain)
691+
die(_("--verbose and --porcelain are mutually exclusive"));
732692
else {
733693
struct worktree **worktrees = get_worktrees();
734694
int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i;

t/t2402-worktree-list.sh

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,107 @@ test_expect_success '"list" all worktrees with locked annotation' '
6969
git worktree add --detach locked main &&
7070
git worktree add --detach unlocked main &&
7171
git worktree lock locked &&
72+
test_when_finished "git worktree unlock locked" &&
7273
git worktree list >out &&
7374
grep "/locked *[0-9a-f].* locked$" out &&
7475
! grep "/unlocked *[0-9a-f].* locked$" out
7576
'
7677

78+
test_expect_success '"list" all worktrees --porcelain with locked' '
79+
test_when_finished "rm -rf locked1 locked2 unlocked out actual expect && git worktree prune" &&
80+
echo "locked" >expect &&
81+
echo "locked with reason" >>expect &&
82+
git worktree add --detach locked1 &&
83+
git worktree add --detach locked2 &&
84+
# unlocked worktree should not be annotated with "locked"
85+
git worktree add --detach unlocked &&
86+
git worktree lock locked1 &&
87+
test_when_finished "git worktree unlock locked1" &&
88+
git worktree lock locked2 --reason "with reason" &&
89+
test_when_finished "git worktree unlock locked2" &&
90+
git worktree list --porcelain >out &&
91+
grep "^locked" out >actual &&
92+
test_cmp expect actual
93+
'
94+
95+
test_expect_success '"list" all worktrees --porcelain with locked reason newline escaped' '
96+
test_when_finished "rm -rf locked_lf locked_crlf out actual expect && git worktree prune" &&
97+
printf "locked \"locked\\\\r\\\\nreason\"\n" >expect &&
98+
printf "locked \"locked\\\\nreason\"\n" >>expect &&
99+
git worktree add --detach locked_lf &&
100+
git worktree add --detach locked_crlf &&
101+
git worktree lock locked_lf --reason "$(printf "locked\nreason")" &&
102+
test_when_finished "git worktree unlock locked_lf" &&
103+
git worktree lock locked_crlf --reason "$(printf "locked\r\nreason")" &&
104+
test_when_finished "git worktree unlock locked_crlf" &&
105+
git worktree list --porcelain >out &&
106+
grep "^locked" out >actual &&
107+
test_cmp expect actual
108+
'
109+
110+
test_expect_success '"list" all worktrees with prunable annotation' '
111+
test_when_finished "rm -rf prunable unprunable out && git worktree prune" &&
112+
git worktree add --detach prunable &&
113+
git worktree add --detach unprunable &&
114+
rm -rf prunable &&
115+
git worktree list >out &&
116+
grep "/prunable *[0-9a-f].* prunable$" out &&
117+
! grep "/unprunable *[0-9a-f].* prunable$"
118+
'
119+
120+
test_expect_success '"list" all worktrees --porcelain with prunable' '
121+
test_when_finished "rm -rf prunable out && git worktree prune" &&
122+
git worktree add --detach prunable &&
123+
rm -rf prunable &&
124+
git worktree list --porcelain >out &&
125+
sed -n "/^worktree .*\/prunable$/,/^$/p" <out >only_prunable &&
126+
test_i18ngrep "^prunable gitdir file points to non-existent location$" only_prunable
127+
'
128+
129+
test_expect_success '"list" all worktrees with prunable consistent with "prune"' '
130+
test_when_finished "rm -rf prunable unprunable out && git worktree prune" &&
131+
git worktree add --detach prunable &&
132+
git worktree add --detach unprunable &&
133+
rm -rf prunable &&
134+
git worktree list >out &&
135+
grep "/prunable *[0-9a-f].* prunable$" out &&
136+
! grep "/unprunable *[0-9a-f].* unprunable$" out &&
137+
git worktree prune --verbose >out &&
138+
test_i18ngrep "^Removing worktrees/prunable" out &&
139+
test_i18ngrep ! "^Removing worktrees/unprunable" out
140+
'
141+
142+
test_expect_success '"list" --verbose and --porcelain mutually exclusive' '
143+
test_must_fail git worktree list --verbose --porcelain
144+
'
145+
146+
test_expect_success '"list" all worktrees --verbose with locked' '
147+
test_when_finished "rm -rf locked1 locked2 out actual expect && git worktree prune" &&
148+
git worktree add locked1 --detach &&
149+
git worktree add locked2 --detach &&
150+
git worktree lock locked1 &&
151+
test_when_finished "git worktree unlock locked1" &&
152+
git worktree lock locked2 --reason "with reason" &&
153+
test_when_finished "git worktree unlock locked2" &&
154+
echo "$(git -C locked2 rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
155+
printf "\tlocked: with reason\n" >>expect &&
156+
git worktree list --verbose >out &&
157+
grep "/locked1 *[0-9a-f].* locked$" out &&
158+
sed -n "s/ */ /g;/\/locked2 *[0-9a-f].*$/,/locked: .*$/p" <out >actual &&
159+
test_cmp actual expect
160+
'
161+
162+
test_expect_success '"list" all worktrees --verbose with prunable' '
163+
test_when_finished "rm -rf prunable out actual expect && git worktree prune" &&
164+
git worktree add prunable --detach &&
165+
echo "$(git -C prunable rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
166+
printf "\tprunable: gitdir file points to non-existent location\n" >>expect &&
167+
rm -rf prunable &&
168+
git worktree list --verbose >out &&
169+
sed -n "s/ */ /g;/\/prunable *[0-9a-f].*$/,/prunable: .*$/p" <out >actual &&
170+
test_i18ncmp actual expect
171+
'
172+
77173
test_expect_success 'bare repo setup' '
78174
git init --bare bare1 &&
79175
echo "data" >file1 &&

0 commit comments

Comments
 (0)