[GSoC Patch 2/2] repo: add Unicode support for `repo structure` output

K Jayatheerth <[email protected]>
Newsgroups org.kernel.vger.git
Message-ID <[email protected]>
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 <[email protected]>
Mentored-by: Lucas Seiki Oshiro <[email protected]>
Signed-off-by: K Jayatheerth <[email protected]>
---
 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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.