All of lore.kernel.org
 help / color / mirror / Atom feed
From: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
To: git@vger.kernel.org
Cc: jltobler@gmail.com, lucasseikioshiro@gmail.com,
	K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
Subject: [GSoC Patch 2/2] repo: add Unicode support for `repo structure` output
Date: Fri, 21 Aug 2026 19:23:45 +0530	[thread overview]
Message-ID: <20260821135410.429698-3-jayatheerthkulkarni2005@gmail.com> (raw)
In-Reply-To: <20260821135410.429698-1-jayatheerthkulkarni2005@gmail.com>

ASCII output ignores locale support for UTF-8. Use box-drawing
characters and bullets when the locale supports UTF-8, since modern
terminals render tables cleanly this way, and fall back to ASCII
otherwise.

Tests now discover an available UTF-8 locale to set a `UTF8_LOCALE`
prerequisite, and existing ASCII table tests are explicitly pinned
to `LC_ALL=C` so their behavior remains deterministic regardless of the
runner's environment.

Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
---
 builtin/repo.c            | 110 ++++++++++++++++-------------
 t/t1901-repo-structure.sh | 141 +++++++++++++++++++++++++++++++++++++-
 2 files changed, 203 insertions(+), 48 deletions(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index 84e012f83f..2c1cca6f2e 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -498,82 +498,86 @@ static void stats_table_setup_structure(struct stats_table *table,
 	size_t object_count_total;
 	size_t disk_object_total;
 	size_t ref_total;
+	const int utf8 = is_utf8_locale();
+	const char *bullet_l0 = utf8 ? "•" : "*";
+	const char *bullet_l1 = utf8 ? "  •" : "  *";
+	const char *bullet_l2 = utf8 ? "    •" : "    *";
 
 	ref_total = get_total_reference_count(refs);
-	stats_table_addf(table, "* %s", _("References"));
-	stats_table_count_addf(table, ref_total, "  * %s", _("Count"));
-	stats_table_count_addf(table, refs->branches, "    * %s", _("Branches"));
-	stats_table_count_addf(table, refs->tags, "    * %s", _("Tags"));
-	stats_table_count_addf(table, refs->remotes, "    * %s", _("Remotes"));
-	stats_table_count_addf(table, refs->others, "    * %s", _("Others"));
+	stats_table_addf(table, "%s %s", bullet_l0, _("References"));
+	stats_table_count_addf(table, ref_total, "%s %s", bullet_l1, _("Count"));
+	stats_table_count_addf(table, refs->branches, "%s %s", bullet_l2, _("Branches"));
+	stats_table_count_addf(table, refs->tags, "%s %s", bullet_l2, _("Tags"));
+	stats_table_count_addf(table, refs->remotes, "%s %s", bullet_l2, _("Remotes"));
+	stats_table_count_addf(table, refs->others, "%s %s", bullet_l2, _("Others"));
 
 	object_count_total = get_total_object_values(&objects->type_counts);
 	stats_table_addf(table, "");
-	stats_table_addf(table, "* %s", _("Reachable objects"));
-	stats_table_count_addf(table, object_count_total, "  * %s", _("Count"));
+	stats_table_addf(table, "%s %s", bullet_l0, _("Reachable objects"));
+	stats_table_count_addf(table, object_count_total, "%s %s", bullet_l1, _("Count"));
 	stats_table_count_addf(table, objects->type_counts.commits,
-			       "    * %s", _("Commits"));
+			       "%s %s", bullet_l2, _("Commits"));
 	stats_table_count_addf(table, objects->type_counts.trees,
-			       "    * %s", _("Trees"));
+			       "%s %s", bullet_l2, _("Trees"));
 	stats_table_count_addf(table, objects->type_counts.blobs,
-			       "    * %s", _("Blobs"));
+			       "%s %s", bullet_l2, _("Blobs"));
 	stats_table_count_addf(table, objects->type_counts.tags,
-			       "    * %s", _("Tags"));
+			       "%s %s", bullet_l2, _("Tags"));
 
 	inflated_object_total = get_total_object_values(&objects->inflated_sizes);
 	stats_table_size_addf(table, inflated_object_total,
-			      "  * %s", _("Inflated size"));
+			      "%s %s", bullet_l1, _("Inflated size"));
 	stats_table_size_addf(table, objects->inflated_sizes.commits,
-			      "    * %s", _("Commits"));
+			      "%s %s", bullet_l2, _("Commits"));
 	stats_table_size_addf(table, objects->inflated_sizes.trees,
-			      "    * %s", _("Trees"));
+			      "%s %s", bullet_l2, _("Trees"));
 	stats_table_size_addf(table, objects->inflated_sizes.blobs,
-			      "    * %s", _("Blobs"));
+			      "%s %s", bullet_l2, _("Blobs"));
 	stats_table_size_addf(table, objects->inflated_sizes.tags,
-			      "    * %s", _("Tags"));
+			      "%s %s", bullet_l2, _("Tags"));
 
 	disk_object_total = get_total_object_values(&objects->disk_sizes);
 	stats_table_size_addf(table, disk_object_total,
-			      "  * %s", _("Disk size"));
+			      "%s %s", bullet_l1, _("Disk size"));
 	stats_table_size_addf(table, objects->disk_sizes.commits,
-			      "    * %s", _("Commits"));
+			      "%s %s", bullet_l2, _("Commits"));
 	stats_table_size_addf(table, objects->disk_sizes.trees,
-			      "    * %s", _("Trees"));
+			      "%s %s", bullet_l2, _("Trees"));
 	stats_table_size_addf(table, objects->disk_sizes.blobs,
-			      "    * %s", _("Blobs"));
+			      "%s %s", bullet_l2, _("Blobs"));
 	stats_table_size_addf(table, objects->disk_sizes.tags,
-			      "    * %s", _("Tags"));
+			      "%s %s", bullet_l2, _("Tags"));
 
 	stats_table_addf(table, "");
-	stats_table_addf(table, "* %s", _("Largest objects"));
-	stats_table_addf(table, "  * %s", _("Commits"));
+	stats_table_addf(table, "%s %s", bullet_l0, _("Largest objects"));
+	stats_table_addf(table, "%s %s", bullet_l1, _("Commits"));
 	stats_table_object_size_addf(table,
 				     &objects->largest.commit_size.oid,
 				     objects->largest.commit_size.value,
-				     "    * %s", _("Maximum size"));
+				     "%s %s", bullet_l2, _("Maximum size"));
 	stats_table_object_count_addf(table,
 				      &objects->largest.parent_count.oid,
 				      objects->largest.parent_count.value,
-				      "    * %s", _("Maximum parents"));
-	stats_table_addf(table, "  * %s", _("Trees"));
+				      "%s %s", bullet_l2, _("Maximum parents"));
+	stats_table_addf(table, "%s %s", bullet_l1, _("Trees"));
 	stats_table_object_size_addf(table,
 				     &objects->largest.tree_size.oid,
 				     objects->largest.tree_size.value,
-				     "    * %s", _("Maximum size"));
+				     "%s %s", bullet_l2, _("Maximum size"));
 	stats_table_object_count_addf(table,
 				      &objects->largest.tree_entries.oid,
 				      objects->largest.tree_entries.value,
-				      "    * %s", _("Maximum entries"));
-	stats_table_addf(table, "  * %s", _("Blobs"));
+				      "%s %s", bullet_l2, _("Maximum entries"));
+	stats_table_addf(table, "%s %s", bullet_l1, _("Blobs"));
 	stats_table_object_size_addf(table,
 				     &objects->largest.blob_size.oid,
 				     objects->largest.blob_size.value,
-				     "    * %s", _("Maximum size"));
-	stats_table_addf(table, "  * %s", _("Tags"));
+				     "%s %s", bullet_l2, _("Maximum size"));
+	stats_table_addf(table, "%s %s", bullet_l1, _("Tags"));
 	stats_table_object_size_addf(table,
 				     &objects->largest.tag_size.oid,
 				     objects->largest.tag_size.value,
-				     "    * %s", _("Maximum size"));
+				     "%s %s", bullet_l2, _("Maximum size"));
 }
 
 #define INDEX_WIDTH 4
@@ -589,28 +593,42 @@ static void stats_table_print_structure(const struct stats_table *table)
 	int unit_col_width = table->unit_col_width;
 	struct string_list_item *item;
 	struct strbuf buf = STRBUF_INIT;
+	const int utf8 = is_utf8_locale();
+	const char *border_left = utf8 ? "│ " : "| ";
+	const char *border_mid = utf8 ? " │ " : " | ";
+	const char *border_right = utf8 ? " │" : " |";
 
 	if (title_name_width > name_col_width)
 		name_col_width = title_name_width;
 	if (title_value_width > value_col_width + unit_col_width + 1)
 		value_col_width = title_value_width - unit_col_width;
 
-	strbuf_addstr(&buf, "| ");
+	strbuf_addstr(&buf, border_left);
 	strbuf_utf8_align(&buf, ALIGN_LEFT, name_col_width + INDEX_WIDTH,
 			  name_col_title);
-	strbuf_addstr(&buf, " | ");
+	strbuf_addstr(&buf, border_mid);
 	strbuf_utf8_align(&buf, ALIGN_LEFT,
 			  value_col_width + unit_col_width + 1, value_col_title);
-	strbuf_addstr(&buf, " |");
+	strbuf_addstr(&buf, border_right);
 	printf("%s\n", buf.buf);
 
-	printf("| ");
-	for (int i = 0; i < name_col_width + INDEX_WIDTH; i++)
-		putchar('-');
-	printf(" | ");
-	for (int i = 0; i < value_col_width + unit_col_width + 1; i++)
-		putchar('-');
-	printf(" |\n");
+	if (utf8) {
+		printf("├─");
+		for (int i = 0; i < name_col_width + INDEX_WIDTH; i++)
+			printf("─");
+		printf("─┼─");
+		for (int i = 0; i < value_col_width + unit_col_width + 1; i++)
+			printf("─");
+		printf("─┤\n");
+	} else {
+		printf("| ");
+		for (int i = 0; i < name_col_width + INDEX_WIDTH; i++)
+			putchar('-');
+		printf(" | ");
+		for (int i = 0; i < value_col_width + unit_col_width + 1; i++)
+			putchar('-');
+		printf(" |\n");
+	}
 
 	for_each_string_list_item(item, &table->rows) {
 		struct stats_table_entry *entry = item->util;
@@ -624,7 +642,7 @@ static void stats_table_print_structure(const struct stats_table *table)
 		}
 
 		strbuf_reset(&buf);
-		strbuf_addstr(&buf, "| ");
+		strbuf_addstr(&buf, border_left);
 		strbuf_utf8_align(&buf, ALIGN_LEFT, name_col_width, item->string);
 
 		if (entry && entry->oid)
@@ -633,11 +651,11 @@ static void stats_table_print_structure(const struct stats_table *table)
 		else
 			strbuf_addchars(&buf, ' ', INDEX_WIDTH);
 
-		strbuf_addstr(&buf, " | ");
+		strbuf_addstr(&buf, border_mid);
 		strbuf_utf8_align(&buf, ALIGN_RIGHT, value_col_width, value);
 		strbuf_addch(&buf, ' ');
 		strbuf_utf8_align(&buf, ALIGN_LEFT, unit_col_width, unit);
-		strbuf_addstr(&buf, " |");
+		strbuf_addstr(&buf, border_right);
 		printf("%s\n", buf.buf);
 	}
 
diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index 02cc2b594a..926db09175 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -4,6 +4,29 @@ test_description='test git repo structure'
 
 . ./test-lib.sh
 
+# Detect if a UTF-8 locale is available on the test system.
+if test -z "$GIT_TEST_UTF8_LOCALE"
+then
+	case "${LC_ALL:-$LANG}" in
+	*.[Uu][Tt][Ff]8 | *.[Uu][Tt][Ff]-8)
+		GIT_TEST_UTF8_LOCALE="${LC_ALL:-$LANG}"
+		;;
+	*)
+		if type locale >/dev/null 2>&1
+		then
+			GIT_TEST_UTF8_LOCALE=$(locale -a 2>/dev/null | sed -n '/\.[uU][tT][fF]-*8$/{
+				p
+				q
+			}')
+		fi
+		;;
+	esac
+fi
+if test -n "$GIT_TEST_UTF8_LOCALE"
+then
+	test_set_prereq UTF8_LOCALE
+fi
+
 object_type_disk_usage() {
 	disk_usage_opt="--disk-usage"
 
@@ -66,7 +89,10 @@ test_expect_success 'empty repository' '
 		|     * Maximum size        |    0 B |
 		EOF
 
-		git repo structure >out 2>err &&
+		# Force a non-UTF8 locale so this test always exercises the
+		# ASCII fallback formatting, regardless of what locale the
+		# runner defaults to.
+		LC_ALL=C git repo structure >out 2>err &&
 
 		test_cmp expect out &&
 		test_line_count = 0 err
@@ -137,7 +163,10 @@ test_expect_success SHA1 'repository with references and objects' '
 		[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
 		EOF
 
-		git repo structure >out 2>err &&
+		# Force a non-UTF8 locale so this test always exercises the
+		# ASCII fallback formatting, regardless of what locale the
+		# runner defaults to.
+		LC_ALL=C git repo structure >out 2>err &&
 
 		test_cmp expect out &&
 		test_line_count = 0 err
@@ -230,4 +259,112 @@ test_expect_success 'git repo structure -h shows only repo structure usage' '
 	test_grep ! "git repo info" actual
 '
 
+test_expect_success UTF8_LOCALE 'unicode output under UTF-8 locale with missing locale dir' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		cat >expect <<-\EOF &&
+		│ Repository structure      │ Value  │
+		├───────────────────────────┼────────┤
+		│ • References              │        │
+		│   • Count                 │    0   │
+		│     • Branches            │    0   │
+		│     • Tags                │    0   │
+		│     • Remotes             │    0   │
+		│     • Others              │    0   │
+		│                           │        │
+		│ • Reachable objects       │        │
+		│   • Count                 │    0   │
+		│     • Commits             │    0   │
+		│     • Trees               │    0   │
+		│     • Blobs               │    0   │
+		│     • Tags                │    0   │
+		│   • Inflated size         │    0 B │
+		│     • Commits             │    0 B │
+		│     • Trees               │    0 B │
+		│     • Blobs               │    0 B │
+		│     • Tags                │    0 B │
+		│   • Disk size             │    0 B │
+		│     • Commits             │    0 B │
+		│     • Trees               │    0 B │
+		│     • Blobs               │    0 B │
+		│     • Tags                │    0 B │
+		│                           │        │
+		│ • Largest objects         │        │
+		│   • Commits               │        │
+		│     • Maximum size        │    0 B │
+		│     • Maximum parents     │    0   │
+		│   • Trees                 │        │
+		│     • Maximum size        │    0 B │
+		│     • Maximum entries     │    0   │
+		│   • Blobs                 │        │
+		│     • Maximum size        │    0 B │
+		│   • Tags                  │        │
+		│     • Maximum size        │    0 B │
+		EOF
+		# Point GIT_TEXTDOMAINDIR at a nonexistent path so
+		# git_setup_gettext() takes its early-return path (its
+		# locale-directory check fails) and never populates the
+		# gettext-internal charset. This exercises the
+		# is_utf8_locale() fallback that derives the charset
+		# directly from LC_ALL/LC_CTYPE/LANG instead, regardless of
+		# whether gettext itself was able to initialize.
+		GIT_TEXTDOMAINDIR="$TRASH_DIRECTORY/nonexistent-locale-dir" \
+		LC_ALL="$GIT_TEST_UTF8_LOCALE" git repo structure >out 2>err &&
+		test_cmp expect out &&
+		test_line_count = 0 err
+	)
+'
+
+test_expect_success UTF8_LOCALE 'unicode output under UTF-8 locale' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		cat >expect <<-\EOF &&
+		│ Repository structure      │ Value  │
+		├───────────────────────────┼────────┤
+		│ • References              │        │
+		│   • Count                 │    0   │
+		│     • Branches            │    0   │
+		│     • Tags                │    0   │
+		│     • Remotes             │    0   │
+		│     • Others              │    0   │
+		│                           │        │
+		│ • Reachable objects       │        │
+		│   • Count                 │    0   │
+		│     • Commits             │    0   │
+		│     • Trees               │    0   │
+		│     • Blobs               │    0   │
+		│     • Tags                │    0   │
+		│   • Inflated size         │    0 B │
+		│     • Commits             │    0 B │
+		│     • Trees               │    0 B │
+		│     • Blobs               │    0 B │
+		│     • Tags                │    0 B │
+		│   • Disk size             │    0 B │
+		│     • Commits             │    0 B │
+		│     • Trees               │    0 B │
+		│     • Blobs               │    0 B │
+		│     • Tags                │    0 B │
+		│                           │        │
+		│ • Largest objects         │        │
+		│   • Commits               │        │
+		│     • Maximum size        │    0 B │
+		│     • Maximum parents     │    0   │
+		│   • Trees                 │        │
+		│     • Maximum size        │    0 B │
+		│     • Maximum entries     │    0   │
+		│   • Blobs                 │        │
+		│     • Maximum size        │    0 B │
+		│   • Tags                  │        │
+		│     • Maximum size        │    0 B │
+		EOF
+		LC_ALL="$GIT_TEST_UTF8_LOCALE" git repo structure >out 2>err &&
+		test_cmp expect out &&
+		test_line_count = 0 err
+	)
+'
+
 test_done
-- 
2.55.GIT


  parent reply	other threads:[~2026-08-21 13:55 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-21 13:53 [GSoC Patch 0/2] add unicode support to git repo structure K Jayatheerth
2026-08-21 13:53 ` [GSoC Patch 1/2] gettext: fall back to env-derived charset when unset K Jayatheerth
2026-08-21 13:53 ` K Jayatheerth [this message]
2026-08-21 16:59 ` [GSoC Patch 0/2] add unicode support to git repo structure Junio C Hamano
2026-08-23 13:19   ` Oswald Buddenhagen
2026-08-23 15:01     ` Junio C Hamano
2026-08-23 17:06     ` Tuomas Ahola

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260821135410.429698-3-jayatheerthkulkarni2005@gmail.com \
    --to=jayatheerthkulkarni2005@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=jltobler@gmail.com \
    --cc=lucasseikioshiro@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.