worktree: add -z option for list subcommand

Add a -z option to be used in conjunction with --porcelain that gives
NUL-terminated output. As 'worktree list --porcelain' does not quote
worktree paths this enables it to handle worktree paths that contain
newlines.

Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Phillip Wood 2022-03-31 16:21:28 +00:00 committed by Junio C Hamano
parent dab1b7905d
commit d97eb302ea
3 changed files with 55 additions and 20 deletions

View file

@ -10,7 +10,7 @@ SYNOPSIS
-------- --------
[verse] [verse]
'git worktree add' [-f] [--detach] [--checkout] [--lock [--reason <string>]] [-b <new-branch>] <path> [<commit-ish>] 'git worktree add' [-f] [--detach] [--checkout] [--lock [--reason <string>]] [-b <new-branch>] <path> [<commit-ish>]
'git worktree list' [-v | --porcelain] 'git worktree list' [-v | --porcelain [-z]]
'git worktree lock' [--reason <string>] <worktree> 'git worktree lock' [--reason <string>] <worktree>
'git worktree move' <worktree> <new-path> 'git worktree move' <worktree> <new-path>
'git worktree prune' [-n] [-v] [--expire <expire>] 'git worktree prune' [-n] [-v] [--expire <expire>]
@ -223,7 +223,14 @@ This can also be set up as the default behaviour by using the
--porcelain:: --porcelain::
With `list`, output in an easy-to-parse format for scripts. With `list`, output in an easy-to-parse format for scripts.
This format will remain stable across Git versions and regardless of user This format will remain stable across Git versions and regardless of user
configuration. See below for details. configuration. It is recommended to combine this with `-z`.
See below for details.
-z::
Terminate each line with a NUL rather than a newline when
`--porcelain` is specified with `list`. This makes it possible
to parse the output when a worktree path contains a newline
character.
-q:: -q::
--quiet:: --quiet::
@ -411,7 +418,8 @@ working tree itself.
Porcelain Format Porcelain Format
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
The porcelain format has a line per attribute. Attributes are listed with a The porcelain format has a line per attribute. If `-z` is given then the lines
are terminated with NUL rather than a newline. Attributes are listed with a
label and value separated by a single space. Boolean attributes (like `bare` label and value separated by a single space. Boolean attributes (like `bare`
and `detached`) are listed as a label only, and are present only and `detached`) are listed as a label only, and are present only
if the value is true. Some attributes (like `locked`) can be listed as a label if the value is true. Some attributes (like `locked`) can be listed as a label
@ -449,7 +457,7 @@ prunable gitdir file points to non-existent location
------------ ------------
If the lock reason contains "unusual" characters such as newline, they Unless `-z` is used any "unusual" characters in the lock reason such as newlines
are escaped and the entire reason is quoted as explained for the are escaped and the entire reason is quoted as explained for the
configuration variable `core.quotePath` (see linkgit:git-config[1]). configuration variable `core.quotePath` (see linkgit:git-config[1]).
For Example: For Example:

View file

@ -575,35 +575,37 @@ static int add(int ac, const char **av, const char *prefix)
return add_worktree(path, branch, &opts); return add_worktree(path, branch, &opts);
} }
static void show_worktree_porcelain(struct worktree *wt) static void show_worktree_porcelain(struct worktree *wt, int line_terminator)
{ {
const char *reason; const char *reason;
printf("worktree %s\n", wt->path); printf("worktree %s%c", wt->path, line_terminator);
if (wt->is_bare) if (wt->is_bare)
printf("bare\n"); printf("bare%c", line_terminator);
else { else {
printf("HEAD %s\n", oid_to_hex(&wt->head_oid)); printf("HEAD %s%c", oid_to_hex(&wt->head_oid), line_terminator);
if (wt->is_detached) if (wt->is_detached)
printf("detached\n"); printf("detached%c", line_terminator);
else if (wt->head_ref) else if (wt->head_ref)
printf("branch %s\n", wt->head_ref); printf("branch %s%c", wt->head_ref, line_terminator);
} }
reason = worktree_lock_reason(wt); reason = worktree_lock_reason(wt);
if (reason && *reason) { if (reason) {
struct strbuf sb = STRBUF_INIT; fputs("locked", stdout);
quote_c_style(reason, &sb, NULL, 0); if (*reason) {
printf("locked %s\n", sb.buf); fputc(' ', stdout);
strbuf_release(&sb); write_name_quoted(reason, stdout, line_terminator);
} else if (reason) } else {
printf("locked\n"); fputc(line_terminator, stdout);
}
}
reason = worktree_prune_reason(wt, expire); reason = worktree_prune_reason(wt, expire);
if (reason) if (reason)
printf("prunable %s\n", reason); printf("prunable %s%c", reason, line_terminator);
printf("\n"); fputc(line_terminator, stdout);
} }
static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len) static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
@ -681,12 +683,15 @@ static void pathsort(struct worktree **wt)
static int list(int ac, const char **av, const char *prefix) static int list(int ac, const char **av, const char *prefix)
{ {
int porcelain = 0; int porcelain = 0;
int line_terminator = '\n';
struct option options[] = { struct option options[] = {
OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")), OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
OPT__VERBOSE(&verbose, N_("show extended annotations and reasons, if available")), OPT__VERBOSE(&verbose, N_("show extended annotations and reasons, if available")),
OPT_EXPIRY_DATE(0, "expire", &expire, OPT_EXPIRY_DATE(0, "expire", &expire,
N_("add 'prunable' annotation to worktrees older than <time>")), N_("add 'prunable' annotation to worktrees older than <time>")),
OPT_SET_INT('z', NULL, &line_terminator,
N_("terminate records with a NUL character"), '\0'),
OPT_END() OPT_END()
}; };
@ -696,6 +701,8 @@ static int list(int ac, const char **av, const char *prefix)
usage_with_options(worktree_usage, options); usage_with_options(worktree_usage, options);
else if (verbose && porcelain) else if (verbose && porcelain)
die(_("options '%s' and '%s' cannot be used together"), "--verbose", "--porcelain"); die(_("options '%s' and '%s' cannot be used together"), "--verbose", "--porcelain");
else if (!line_terminator && !porcelain)
die(_("the option '%s' requires '%s'"), "-z", "--porcelain");
else { else {
struct worktree **worktrees = get_worktrees(); struct worktree **worktrees = get_worktrees();
int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i; int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i;
@ -708,7 +715,8 @@ static int list(int ac, const char **av, const char *prefix)
for (i = 0; worktrees[i]; i++) { for (i = 0; worktrees[i]; i++) {
if (porcelain) if (porcelain)
show_worktree_porcelain(worktrees[i]); show_worktree_porcelain(worktrees[i],
line_terminator);
else else
show_worktree(worktrees[i], path_maxlen, abbrev); show_worktree(worktrees[i], path_maxlen, abbrev);
} }

View file

@ -64,6 +64,25 @@ test_expect_success '"list" all worktrees --porcelain' '
test_cmp expect actual test_cmp expect actual
' '
test_expect_success '"list" all worktrees --porcelain -z' '
test_when_finished "rm -rf here _actual actual expect &&
git worktree prune" &&
printf "worktree %sQHEAD %sQbranch %sQQ" \
"$(git rev-parse --show-toplevel)" \
$(git rev-parse HEAD --symbolic-full-name HEAD) >expect &&
git worktree add --detach here main &&
printf "worktree %sQHEAD %sQdetachedQQ" \
"$(git -C here rev-parse --show-toplevel)" \
"$(git rev-parse HEAD)" >>expect &&
git worktree list --porcelain -z >_actual &&
nul_to_q <_actual >actual &&
test_cmp expect actual
'
test_expect_success '"list" -z fails without --porcelain' '
test_must_fail git worktree list -z
'
test_expect_success '"list" all worktrees with locked annotation' ' test_expect_success '"list" all worktrees with locked annotation' '
test_when_finished "rm -rf locked unlocked out && git worktree prune" && test_when_finished "rm -rf locked unlocked out && git worktree prune" &&
git worktree add --detach locked main && git worktree add --detach locked main &&