Subject: [PATCH 4/4] builtin/repo: add nul format for stats

From: Justin Tobler <[email-masked]>
Date: Mon, 22 Sep 2025 21:57:00 -0500

Introduce the `nul` mode for the --format option. When enabled, the
output is similar to the `keyvalue` mode, but key-values are delimited
by a NUL character instead of a newline. This allows stat values to
support special characters without having to cquote values.

Signed-off-by: Justin Tobler <[email-masked]>
---
 Documentation/git-repo.adoc | 10 ++++---
 builtin/repo.c              | 52 ++++++++++++++++++++-----------------
 t/t1901-repo-stats.sh       | 27 +++++++++++++++++++
 3 files changed, 62 insertions(+), 27 deletions(-)

diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 7d0341e4f1..57267064ea 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -9,7 +9,7 @@ SYNOPSIS
 --------
 [synopsis]
 git repo info [--format=(keyvalue|nul)] [-z] [<key>...]
-git repo stats [--format=(table|keyvalue)]
+git repo stats [--format=(table|keyvalue|nul)]
 
 DESCRIPTION
 -----------
@@ -44,12 +44,12 @@ supported:
 +
 `-z` is an alias for `--format=nul`.
 
-`stats [--format=(table|keyvalue)]`::
+`stats [--format=(table|keyvalue|nul)]`::
 	Retrieve stats about the current repository. All references and
 	reachable objects in the repository are categorized and counted
 	accordingly.
 +
-The output format can be chosen through the flag `--format`. Two formats are
+The output format can be chosen through the flag `--format`. Three formats are
 supported:
 +
 `table`:::
@@ -61,6 +61,10 @@ supported:
 	Each line of output contains a key-value pair of a repostiory stat. The
 	'=' character is used to delimit between the key and the value.
 
+`nul`:::
+	Similar to 'keyvalue', but uses a NUL character to delimit between
+	key-value pairs instead of a newline.
+
 INFO KEYS
 ---------
 In order to obtain a set of values from `git repo info`, you should provide
diff --git a/builtin/repo.c b/builtin/repo.c
index 4c16a68e4e..37034e6347 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -14,7 +14,7 @@
 
 static const char *const repo_usage[] = {
 	"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]",
-	"git repo stats [--format=(table|keyvalue)]",
+	"git repo stats [--format=(table|keyvalue|nul)]",
 	NULL
 };
 
@@ -291,27 +291,31 @@ static void stats_table_print(struct stats_table *table)
 	strbuf_release(&buf);
 }
 
-static void stats_print(struct stats *stats)
+static void stats_print(struct stats *stats, int nul_delim)
 {
 	struct strbuf buf = STRBUF_INIT;
-
-	strbuf_addf(&buf, "references.branches.count=%" PRIuMAX "\n",
-		    (uintmax_t)stats->refs.branches);
-	strbuf_addf(&buf, "references.tags.count=%" PRIuMAX "\n",
-		    (uintmax_t)stats->refs.tags);
-	strbuf_addf(&buf, "references.remotes.count=%" PRIuMAX "\n",
-		    (uintmax_t)stats->refs.remotes);
-	strbuf_addf(&buf, "references.others.count=%" PRIuMAX "\n",
-		    (uintmax_t)stats->refs.others);
-
-	strbuf_addf(&buf, "objects.commits.count=%" PRIuMAX "\n",
-		    (uintmax_t)stats->objects.commits);
-	strbuf_addf(&buf, "objects.trees.count=%" PRIuMAX "\n",
-		    (uintmax_t)stats->objects.trees);
-	strbuf_addf(&buf, "objects.blobs.count=%" PRIuMAX "\n",
-		    (uintmax_t)stats->objects.blobs);
-	strbuf_addf(&buf, "objects.tags.count=%" PRIuMAX "\n",
-		    (uintmax_t)stats->objects.tags);
+	char delim = '\n';
+
+	if (nul_delim)
+		delim = '\0';
+
+	strbuf_addf(&buf, "references.branches.count=%" PRIuMAX "%c",
+		    (uintmax_t)stats->refs.branches, delim);
+	strbuf_addf(&buf, "references.tags.count=%" PRIuMAX "%c",
+		    (uintmax_t)stats->refs.tags, delim);
+	strbuf_addf(&buf, "references.remotes.count=%" PRIuMAX "%c",
+		    (uintmax_t)stats->refs.remotes, delim);
+	strbuf_addf(&buf, "references.others.count=%" PRIuMAX "%c",
+		    (uintmax_t)stats->refs.others, delim);
+
+	strbuf_addf(&buf, "objects.commits.count=%" PRIuMAX "%c",
+		    (uintmax_t)stats->objects.commits, delim);
+	strbuf_addf(&buf, "objects.trees.count=%" PRIuMAX "%c",
+		    (uintmax_t)stats->objects.trees, delim);
+	strbuf_addf(&buf, "objects.blobs.count=%" PRIuMAX "%c",
+		    (uintmax_t)stats->objects.blobs, delim);
+	strbuf_addf(&buf, "objects.tags.count=%" PRIuMAX "%c",
+		    (uintmax_t)stats->objects.tags, delim);
 
 	fwrite(buf.buf, sizeof(char), buf.len, stdout);
 	strbuf_release(&buf);
@@ -408,9 +412,6 @@ static int repo_stats(int argc, const char **argv, const char *prefix,
 	struct rev_info revs;
 
 	parse_options(argc, argv, prefix, options, repo_usage, 0);
-	if (format == FORMAT_NUL_TERMINATED)
-		die(_("nul format not yet supported"));
-
 	repo_init_revisions(repo, &revs, prefix);
 	filter.name_patterns = ref_patterns.v;
 	filter_refs(&refs, &filter, FILTER_REFS_REGULAR);
@@ -424,7 +425,10 @@ static int repo_stats(int argc, const char **argv, const char *prefix,
 		stats_table_print(&table);
 		break;
 	case FORMAT_KEYVALUE:
-		stats_print(&stats);
+		stats_print(&stats, 0);
+		break;
+	case FORMAT_NUL_TERMINATED:
+		stats_print(&stats, 1);
 		break;
 	default:
 		BUG("not a valid output format: %d", format);
diff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh
index 5bc6d9d5c4..061b2fbbc1 100755
--- a/t/t1901-repo-stats.sh
+++ b/t/t1901-repo-stats.sh
@@ -127,4 +127,31 @@ test_expect_success 'repository stats with keyvalue format' '
 	)
 '
 
+test_expect_success 'repository stats with nul format' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit_bulk 42 &&
+		git tag -a foo -m bar &&
+		git repo stats --format=nul >out 2>err &&
+
+		cat >expect <<-EOF &&
+		references.branches.count=1
+		references.tags.count=1
+		references.remotes.count=0
+		references.others.count=0
+		objects.commits.count=42
+		objects.trees.count=42
+		objects.blobs.count=42
+		objects.tags.count=1
+		EOF
+
+		tr "\n" "\0" <expect >expect_null &&
+
+		test_cmp expect_null out &&
+		test_line_count = 0 err
+	)
+'
+
 test_done
-- 
2.51.0.193.g4975ec3473b