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>

Phillip Wood committed Mar 31, 2022 at 16:21 UTC d97eb302ea848ff6f8f9af50a176734930964b9a
3 files changed +55 -20
Documentation/git-worktree.txt
+12 -4
@@ -10,7 +10,7 @@ SYNOPSIS
10 --------
11 [verse]
12 'git worktree add' [-f] [--detach] [--checkout] [--lock [--reason <string>]] [-b <new-branch>] <path> [<commit-ish>]
13 -'git worktree list' [-v | --porcelain]
13 +'git worktree list' [-v | --porcelain [-z]]
14 'git worktree lock' [--reason <string>] <worktree>
15 'git worktree move' <worktree> <new-path>
16 'git worktree prune' [-n] [-v] [--expire <expire>]
@@ -223,7 +223,14 @@ This can also be set up as the default behaviour by using the
223 --porcelain::
224 With `list`, output in an easy-to-parse format for scripts.
225 This format will remain stable across Git versions and regardless of user
226 - configuration. See below for details.
226 + configuration. It is recommended to combine this with `-z`.
227 + See below for details.
228 +
229 +-z::
230 + Terminate each line with a NUL rather than a newline when
231 + `--porcelain` is specified with `list`. This makes it possible
232 + to parse the output when a worktree path contains a newline
233 + character.
234
235 -q::
236 --quiet::
@@ -411,7 +418,8 @@ working tree itself.
418
419 Porcelain Format
420 ~~~~~~~~~~~~~~~~
414 -The porcelain format has a line per attribute. Attributes are listed with a
421 +The porcelain format has a line per attribute. If `-z` is given then the lines
422 +are terminated with NUL rather than a newline. Attributes are listed with a
423 label and value separated by a single space. Boolean attributes (like `bare`
424 and `detached`) are listed as a label only, and are present only
425 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
457
458 ------------
459
452 -If the lock reason contains "unusual" characters such as newline, they
460 +Unless `-z` is used any "unusual" characters in the lock reason such as newlines
461 are escaped and the entire reason is quoted as explained for the
462 configuration variable `core.quotePath` (see linkgit:git-config[1]).
463 For Example:
builtin/worktree.c
+24 -16
@@ -575,35 +575,37 @@ static int add(int ac, const char **av, const char *prefix)
575 return add_worktree(path, branch, &opts);
576 }
577
578 -static void show_worktree_porcelain(struct worktree *wt)
578 +static void show_worktree_porcelain(struct worktree *wt, int line_terminator)
579 {
580 const char *reason;
581
582 - printf("worktree %s\n", wt->path);
582 + printf("worktree %s%c", wt->path, line_terminator);
583 if (wt->is_bare)
584 - printf("bare\n");
584 + printf("bare%c", line_terminator);
585 else {
586 - printf("HEAD %s\n", oid_to_hex(&wt->head_oid));
586 + printf("HEAD %s%c", oid_to_hex(&wt->head_oid), line_terminator);
587 if (wt->is_detached)
588 - printf("detached\n");
588 + printf("detached%c", line_terminator);
589 else if (wt->head_ref)
590 - printf("branch %s\n", wt->head_ref);
590 + printf("branch %s%c", wt->head_ref, line_terminator);
591 }
592
593 reason = worktree_lock_reason(wt);
594 - if (reason && *reason) {
595 - struct strbuf sb = STRBUF_INIT;
596 - quote_c_style(reason, &sb, NULL, 0);
597 - printf("locked %s\n", sb.buf);
598 - strbuf_release(&sb);
599 - } else if (reason)
600 - printf("locked\n");
594 + if (reason) {
595 + fputs("locked", stdout);
596 + if (*reason) {
597 + fputc(' ', stdout);
598 + write_name_quoted(reason, stdout, line_terminator);
599 + } else {
600 + fputc(line_terminator, stdout);
601 + }
602 + }
603
604 reason = worktree_prune_reason(wt, expire);
605 if (reason)
604 - printf("prunable %s\n", reason);
606 + printf("prunable %s%c", reason, line_terminator);
607
606 - printf("\n");
608 + fputc(line_terminator, stdout);
609 }
610
611 static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
@@ -681,12 +683,15 @@ static void pathsort(struct worktree **wt)
683 static int list(int ac, const char **av, const char *prefix)
684 {
685 int porcelain = 0;
686 + int line_terminator = '\n';
687
688 struct option options[] = {
689 OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
690 OPT__VERBOSE(&verbose, N_("show extended annotations and reasons, if available")),
691 OPT_EXPIRY_DATE(0, "expire", &expire,
692 N_("add 'prunable' annotation to worktrees older than <time>")),
693 + OPT_SET_INT('z', NULL, &line_terminator,
694 + N_("terminate records with a NUL character"), '\0'),
695 OPT_END()
696 };
697
@@ -696,6 +701,8 @@ static int list(int ac, const char **av, const char *prefix)
701 usage_with_options(worktree_usage, options);
702 else if (verbose && porcelain)
703 die(_("options '%s' and '%s' cannot be used together"), "--verbose", "--porcelain");
704 + else if (!line_terminator && !porcelain)
705 + die(_("the option '%s' requires '%s'"), "-z", "--porcelain");
706 else {
707 struct worktree **worktrees = get_worktrees();
708 int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i;
@@ -708,7 +715,8 @@ static int list(int ac, const char **av, const char *prefix)
715
716 for (i = 0; worktrees[i]; i++) {
717 if (porcelain)
711 - show_worktree_porcelain(worktrees[i]);
718 + show_worktree_porcelain(worktrees[i],
719 + line_terminator);
720 else
721 show_worktree(worktrees[i], path_maxlen, abbrev);
722 }
t/t2402-worktree-list.sh
+19
@@ -64,6 +64,25 @@ test_expect_success '"list" all worktrees --porcelain' '
64 test_cmp expect actual
65 '
66
67 +test_expect_success '"list" all worktrees --porcelain -z' '
68 + test_when_finished "rm -rf here _actual actual expect &&
69 + git worktree prune" &&
70 + printf "worktree %sQHEAD %sQbranch %sQQ" \
71 + "$(git rev-parse --show-toplevel)" \
72 + $(git rev-parse HEAD --symbolic-full-name HEAD) >expect &&
73 + git worktree add --detach here main &&
74 + printf "worktree %sQHEAD %sQdetachedQQ" \
75 + "$(git -C here rev-parse --show-toplevel)" \
76 + "$(git rev-parse HEAD)" >>expect &&
77 + git worktree list --porcelain -z >_actual &&
78 + nul_to_q <_actual >actual &&
79 + test_cmp expect actual
80 +'
81 +
82 +test_expect_success '"list" -z fails without --porcelain' '
83 + test_must_fail git worktree list -z
84 +'
85 +
86 test_expect_success '"list" all worktrees with locked annotation' '
87 test_when_finished "rm -rf locked unlocked out && git worktree prune" &&
88 git worktree add --detach locked main &&