worktree list: fix column spacing

The output of "git worktree list" displays a table containing the worktree path, HEAD OID and branch name for each worktree. The code aligns the columns by measuring the visual width of the worktree path when it is printed. Unfortunately it fails to use the visual width when calculating the width of the column so, if any of the paths contain a multibyte character, we can end up with excess padding between columns. The simplest fix would be to replace strlen() with utf8_strwidth() in measure_widths(). However that leaves us measuring the visual width twice and the byte length once. By caching the visual width and printing the padding separately to the worktree path, we only need to calculate the visual width once and do not need the byte length at all. The visual widths are stored in an arrays of structs rather than an array of ints as the next commit will add more struct members. Even if there are no multibyte characters in any of the paths we still print an extra space between the path and the object id as the field width is calculated as one plus the length of the path and we print an explicit space as well. This is fixed by not printing the extra space. The tests are updated to include multibyte characters in one of the worktree paths and to check the spacing of the columns. Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Phillip Wood committed Nov 18, 2025 at 16:07 UTC a6238ee16371247517e39da36782614a229184ff
2 files changed +33 -24
builtin/worktree.c
+23 -12
@@ -979,14 +979,17 @@ static void show_worktree_porcelain(struct worktree *wt, int line_terminator)
979 fputc(line_terminator, stdout);
980 }
981
982 -static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
982 +struct worktree_display {
983 + int width;
984 +};
985 +
986 +static void show_worktree(struct worktree *wt, struct worktree_display *display,
987 + int path_maxwidth, int abbrev_len)
988 {
989 struct strbuf sb = STRBUF_INIT;
985 - int cur_path_len = strlen(wt->path);
986 - int path_adj = cur_path_len - utf8_strwidth(wt->path);
990 const char *reason;
991
989 - strbuf_addf(&sb, "%-*s ", 1 + path_maxlen + path_adj, wt->path);
992 + strbuf_addf(&sb, "%s%*s", wt->path, 1 + path_maxwidth - display->width, "");
993 if (wt->is_bare)
994 strbuf_addstr(&sb, "(bare)");
995 else {
@@ -1020,20 +1023,24 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
1023 strbuf_release(&sb);
1024 }
1025
1023 -static void measure_widths(struct worktree **wt, int *abbrev, int *maxlen)
1026 +static void measure_widths(struct worktree **wt, int *abbrev,
1027 + struct worktree_display **d, int *maxwidth)
1028 {
1025 - int i;
1029 + int i, display_alloc = 0;
1030 + struct worktree_display *display = NULL;
1031
1032 for (i = 0; wt[i]; i++) {
1033 int sha1_len;
1029 - int path_len = strlen(wt[i]->path);
1034 + ALLOC_GROW(display, i + 1, display_alloc);
1035 + display[i].width = utf8_strwidth(wt[i]->path);
1036
1031 - if (path_len > *maxlen)
1032 - *maxlen = path_len;
1037 + if (display[i].width > *maxwidth)
1038 + *maxwidth = display[i].width;
1039 sha1_len = strlen(repo_find_unique_abbrev(the_repository, &wt[i]->head_oid, *abbrev));
1040 if (sha1_len > *abbrev)
1041 *abbrev = sha1_len;
1042 }
1043 + *d = display;
1044 }
1045
1046 static int pathcmp(const void *a_, const void *b_)
@@ -1079,21 +1086,25 @@ static int list(int ac, const char **av, const char *prefix,
1086 die(_("the option '%s' requires '%s'"), "-z", "--porcelain");
1087 else {
1088 struct worktree **worktrees = get_worktrees();
1082 - int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i;
1089 + int path_maxwidth = 0, abbrev = DEFAULT_ABBREV, i;
1090 + struct worktree_display *display = NULL;
1091
1092 /* sort worktrees by path but keep main worktree at top */
1093 pathsort(worktrees + 1);
1094
1095 if (!porcelain)
1088 - measure_widths(worktrees, &abbrev, &path_maxlen);
1096 + measure_widths(worktrees, &abbrev,
1097 + &display, &path_maxwidth);
1098
1099 for (i = 0; worktrees[i]; i++) {
1100 if (porcelain)
1101 show_worktree_porcelain(worktrees[i],
1102 line_terminator);
1103 else
1095 - show_worktree(worktrees[i], path_maxlen, abbrev);
1104 + show_worktree(worktrees[i],
1105 + &display[i], path_maxwidth, abbrev);
1106 }
1107 + free(display);
1108 free_worktrees(worktrees);
1109 }
1110 return 0;
t/t2402-worktree-list.sh
+10 -12
@@ -30,22 +30,20 @@ test_expect_success 'rev-parse --git-path objects linked worktree' '
30 '
31
32 test_expect_success '"list" all worktrees from main' '
33 - echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect &&
34 - test_when_finished "rm -rf here out actual expect && git worktree prune" &&
35 - git worktree add --detach here main &&
36 - echo "$(git -C here rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >>expect &&
37 - git worktree list >out &&
38 - sed "s/ */ /g" <out >actual &&
33 + echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect &&
34 + test_when_finished "rm -rf áááá out actual expect && git worktree prune" &&
35 + git worktree add --detach áááá main &&
36 + echo "$(git -C áááá rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >>expect &&
37 + git worktree list >actual &&
38 test_cmp expect actual
39 '
40
41 test_expect_success '"list" all worktrees from linked' '
43 - echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect &&
44 - test_when_finished "rm -rf here out actual expect && git worktree prune" &&
45 - git worktree add --detach here main &&
46 - echo "$(git -C here rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >>expect &&
47 - git -C here worktree list >out &&
48 - sed "s/ */ /g" <out >actual &&
42 + echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect &&
43 + test_when_finished "rm -rf áááá out actual expect && git worktree prune" &&
44 + git worktree add --detach áááá main &&
45 + echo "$(git -C áááá rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >>expect &&
46 + git -C áááá worktree list >actual &&
47 test_cmp expect actual
48 '
49