builtin/repo: add keyvalue and nul format for structure stats

All repository structure stats are outputted in a human-friendly table form. This format is not suitable for machine parsing. Add a --format option that supports three output modes: `table`, `keyvalue`, and `nul`. The `table` mode is the default format and prints the same table output as before. With the `keyvalue` mode, each line of output contains a key-value pair of a repository stat. The '=' character is used to delimit between keys and values. The `nul` mode is similar to `keyvalue`, but key-values are delimited by a NUL character instead of a newline. Also, instead of a '=' character to delimit between keys and values, a newline character is used. This allows stat values to support special characters without having to cquote them. These two new modes provides output that is more machine-friendly. Signed-off-by: Justin Tobler <jltobler@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Justin Tobler committed Oct 21, 2025 at 13:26 UTC 17215675b5a2c2eab54b295a7e92d953af2e8779
3 files changed +106 -7
Documentation/git-repo.adoc
+22 -3
@@ -9,7 +9,7 @@ SYNOPSIS
9 --------
10 [synopsis]
11 git repo info [--format=(keyvalue|nul)] [-z] [<key>...]
12 -git repo structure
12 +git repo structure [--format=(table|keyvalue|nul)]
13
14 DESCRIPTION
15 -----------
@@ -44,7 +44,7 @@ supported:
44 +
45 `-z` is an alias for `--format=nul`.
46
47 -`structure`::
47 +`structure [--format=(table|keyvalue|nul)]`::
48 Retrieve statistics about the current repository structure. The
49 following kinds of information are reported:
50 +
@@ -52,7 +52,26 @@ supported:
52 * Reachable object counts categorized by type
53
54 +
55 -The table output format may change and is not intended for machine parsing.
55 +The output format can be chosen through the flag `--format`. Three formats are
56 +supported:
57 ++
58 +`table`:::
59 + Outputs repository stats in a human-friendly table. This format may
60 + change and is not intended for machine parsing. This is the default
61 + format.
62 +
63 +`keyvalue`:::
64 + Each line of output contains a key-value pair for a repository stat.
65 + The '=' character is used to delimit between the key and the value.
66 + Values containing "unusual" characters are quoted as explained for the
67 + configuration variable `core.quotePath` (see linkgit:git-config[1]).
68 +
69 +`nul`:::
70 + Similar to `keyvalue`, but uses a NUL character to delimit between
71 + key-value pairs instead of a newline. Also uses a newline character as
72 + the delimiter between the key and value instead of '='. Unlike the
73 + `keyvalue` format, values containing "unusual" characters are never
74 + quoted.
75
76 INFO KEYS
77 ---------
builtin/repo.c
+51 -4
@@ -15,13 +15,14 @@
15
16 static const char *const repo_usage[] = {
17 "git repo info [--format=(keyvalue|nul)] [-z] [<key>...]",
18 - "git repo structure",
18 + "git repo structure [--format=(table|keyvalue|nul)]",
19 NULL
20 };
21
22 typedef int get_value_fn(struct repository *repo, struct strbuf *buf);
23
24 enum output_format {
25 + FORMAT_TABLE,
26 FORMAT_KEYVALUE,
27 FORMAT_NUL_TERMINATED,
28 };
@@ -136,6 +137,8 @@ static int parse_format_cb(const struct option *opt,
137 *format = FORMAT_NUL_TERMINATED;
138 else if (!strcmp(arg, "keyvalue"))
139 *format = FORMAT_KEYVALUE;
140 + else if (!strcmp(arg, "table"))
141 + *format = FORMAT_TABLE;
142 else
143 die(_("invalid format '%s'"), arg);
144
@@ -158,6 +161,8 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
161 };
162
163 argc = parse_options(argc, argv, prefix, options, repo_usage, 0);
164 + if (format != FORMAT_KEYVALUE && format != FORMAT_NUL_TERMINATED)
165 + die(_("unsupported output format"));
166
167 return print_fields(argc, argv, repo, format);
168 }
@@ -330,6 +335,30 @@ static void stats_table_clear(struct stats_table *table)
335 string_list_clear(&table->rows, 1);
336 }
337
338 +static void structure_keyvalue_print(struct repo_structure *stats,
339 + char key_delim, char value_delim)
340 +{
341 + printf("references.branches.count%c%" PRIuMAX "%c", key_delim,
342 + (uintmax_t)stats->refs.branches, value_delim);
343 + printf("references.tags.count%c%" PRIuMAX "%c", key_delim,
344 + (uintmax_t)stats->refs.tags, value_delim);
345 + printf("references.remotes.count%c%" PRIuMAX "%c", key_delim,
346 + (uintmax_t)stats->refs.remotes, value_delim);
347 + printf("references.others.count%c%" PRIuMAX "%c", key_delim,
348 + (uintmax_t)stats->refs.others, value_delim);
349 +
350 + printf("objects.commits.count%c%" PRIuMAX "%c", key_delim,
351 + (uintmax_t)stats->objects.commits, value_delim);
352 + printf("objects.trees.count%c%" PRIuMAX "%c", key_delim,
353 + (uintmax_t)stats->objects.trees, value_delim);
354 + printf("objects.blobs.count%c%" PRIuMAX "%c", key_delim,
355 + (uintmax_t)stats->objects.blobs, value_delim);
356 + printf("objects.tags.count%c%" PRIuMAX "%c", key_delim,
357 + (uintmax_t)stats->objects.tags, value_delim);
358 +
359 + fflush(stdout);
360 +}
361 +
362 struct count_references_data {
363 struct ref_stats *stats;
364 struct rev_info *revs;
@@ -426,9 +455,15 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
455 struct stats_table table = {
456 .rows = STRING_LIST_INIT_DUP,
457 };
458 + enum output_format format = FORMAT_TABLE;
459 struct repo_structure stats = { 0 };
460 struct rev_info revs;
431 - struct option options[] = { 0 };
461 + struct option options[] = {
462 + OPT_CALLBACK_F(0, "format", &format, N_("format"),
463 + N_("output format"),
464 + PARSE_OPT_NONEG, parse_format_cb),
465 + OPT_END()
466 + };
467
468 argc = parse_options(argc, argv, prefix, options, repo_usage, 0);
469 if (argc)
@@ -439,8 +474,20 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
474 structure_count_references(&stats.refs, &revs, repo);
475 structure_count_objects(&stats.objects, &revs);
476
442 - stats_table_setup_structure(&table, &stats);
443 - stats_table_print_structure(&table);
477 + switch (format) {
478 + case FORMAT_TABLE:
479 + stats_table_setup_structure(&table, &stats);
480 + stats_table_print_structure(&table);
481 + break;
482 + case FORMAT_KEYVALUE:
483 + structure_keyvalue_print(&stats, '=', '\n');
484 + break;
485 + case FORMAT_NUL_TERMINATED:
486 + structure_keyvalue_print(&stats, '\n', '\0');
487 + break;
488 + default:
489 + BUG("invalid output format");
490 + }
491
492 stats_table_clear(&table);
493 release_revisions(&revs);
t/t1901-repo-structure.sh
+33
@@ -73,4 +73,37 @@ test_expect_success 'repository with references and objects' '
73 )
74 '
75
76 +test_expect_success 'keyvalue and nul format' '
77 + test_when_finished "rm -rf repo" &&
78 + git init repo &&
79 + (
80 + cd repo &&
81 + test_commit_bulk 42 &&
82 + git tag -a foo -m bar &&
83 +
84 + cat >expect <<-\EOF &&
85 + references.branches.count=1
86 + references.tags.count=1
87 + references.remotes.count=0
88 + references.others.count=0
89 + objects.commits.count=42
90 + objects.trees.count=42
91 + objects.blobs.count=42
92 + objects.tags.count=1
93 + EOF
94 +
95 + git repo structure --format=keyvalue >out 2>err &&
96 +
97 + test_cmp expect out &&
98 + test_line_count = 0 err &&
99 +
100 + # Replace key and value delimiters for nul format.
101 + tr "\n=" "\0\n" <expect >expect_nul &&
102 + git repo structure --format=nul >out 2>err &&
103 +
104 + test_cmp expect_nul out &&
105 + test_line_count = 0 err
106 + )
107 +'
108 +
109 test_done