{"thread":{"id":"64187","subject":"[PATCH 0/4] builtin/repo: introduce stats subcommand","startedAt":"2025-09-23T02:57:06Z","lastAt":"2025-10-24T05:14:57Z","messageCount":92,"participants":["Justin Tobler","Patrick Steinhardt","Karthik Nayak","Eric Sunshine","Junio C Hamano","Lucas Seiki Oshiro"],"isPatch":true,"patchVersion":1,"patchTotal":4},"messages":[{"id":"527015","messageId":"20250923025700.3046260-1-jltobler@gmail.com","threadId":"64187","inReplyTo":null,"subject":"[PATCH 0/4] builtin/repo: introduce stats subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-23T02:56:56Z","receivedAt":"2025-09-23T02:57:06Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Greetings,\n\nThe shape of a repository's history can have huge impacts on the\nperformance and health of the repository itself. Currently, Git lacks a\nmeans to surface key stats/information regarding the shape of a\nrepository via a single command. Acquiring this information requires\nusers to be fairly knowledgeable about the structure of a Git repository\nand how to identify the relevant data points. To fill this gap,\nsupplemental tools such as git-sizer(1) have been developed.\n\nTo allow users to more readily identify potential issues for a\nrepository, introduce the \"stats\" subcommand in git-repo(1) to output\nstats for the repository that may be of interest to users. The goal of\nthis subcommand is to eventually provide similar functionality to\ngit-sizer(1), but in Git natively.\n\nIn this initial version, the \"stats\" subcommand only surfaces counts of\nthe various reference and object types in a repository. In a follow-up\nseries, I would like to introduce additional data points that are\npresent in git-sizer(1) such as largest objects, combined object sizes\nby type, and other general repository shape information.\n\nSome other general features that would be nice to introduce eventually:\n\n- A \"level of concern\" meter for reported stats. This could indicate to\n  users which stats may be worth looking into further.\n- Links to OIDs of interesting objects that correspond to certain stats.\n- Options to limit which references to use when evaluating the\n  repository.\n- A progress meter to provide better user feedback while the repository\n  is being evaluated.\n\nThanks,\n-Justin\n\nJustin Tobler (4):\n  builtin/repo: introduce stats subcommand\n  builtin/repo: add object counts in stats output\n  builtin/repo: add keyvalue format for stats\n  builtin/repo: add nul format for stats\n\n Documentation/git-repo.adoc |  22 +++\n builtin/repo.c              | 287 ++++++++++++++++++++++++++++++++++++\n t/meson.build               |   1 +\n t/t1901-repo-stats.sh       | 157 ++++++++++++++++++++\n 4 files changed, 467 insertions(+)\n create mode 100755 t/t1901-repo-stats.sh\n\n\nbase-commit: ca2559c1d630eb4f04cdee2328aaf1c768907a9e\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527016","messageId":"20250923025700.3046260-2-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250923025700.3046260-1-jltobler@gmail.com","subject":"[PATCH 1/4] builtin/repo: introduce stats subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-23T02:56:57Z","receivedAt":"2025-09-23T02:57:07Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The shape of a repository's history can have huge impacts on the\nperformance and health of the repository itself. Currently, Git lacks a\nmeans to surface key stats/information regarding the shape of a\nrepository via a single command. Acquiring this information requires\nusers to be fairly knowledgeable about the structure of a Git repository\nand how to identify the relevant data points. To fill this gap,\nsupplemental tools such as git-sizer(1) have been developed.\n\nTo allow users to more readily identify potential issues for a\nrepository, introduce the \"stats\" subcommand in git-repo(1) to output\nstats for the repository that may be of interest to users. The goal of\nthis subcommand is to eventually provide similar functionality to\ngit-sizer(1), but in Git natively.\n\nThe initial version of this command only iterates through all references\nin the repository and tracks the count of branches, tags, remotes, and\nother reference types. The corresponding information is displayed in a\nhuman-friendly table formatted in a very similar manner to git-sizer(1).\nThe width of each table column is adjusted automatically to satisfy the\nrequirements of the widest row contained.\n\nSubsequent commits will surface additional relevant data points to\noutput.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc |   7 ++\n builtin/repo.c              | 151 ++++++++++++++++++++++++++++++++++++\n t/meson.build               |   1 +\n t/t1901-repo-stats.sh       |  59 ++++++++++++++\n 4 files changed, 218 insertions(+)\n create mode 100755 t/t1901-repo-stats.sh\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 209afd1b61..7762329551 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -9,6 +9,7 @@ SYNOPSIS\n --------\n [synopsis]\n git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n+git repo stats\n \n DESCRIPTION\n -----------\n@@ -43,6 +44,12 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n+stats::\n+\tRetrieve stats about the current repository. All references in the\n+\trepository are categorized and counted accordingly.\n++\n+The table output format may change and is not intended for machine parsing.\n+\n INFO KEYS\n ---------\n In order to obtain a set of values from `git repo info`, you should provide\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex bbb0966f2d..15899dd74c 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -4,12 +4,15 @@\n #include \"environment.h\"\n #include \"parse-options.h\"\n #include \"quote.h\"\n+#include \"ref-filter.h\"\n #include \"refs.h\"\n #include \"strbuf.h\"\n+#include \"string-list.h\"\n #include \"shallow.h\"\n \n static const char *const repo_usage[] = {\n \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n+\t\"git repo stats\",\n \tNULL\n };\n \n@@ -156,12 +159,160 @@ static int repo_info(int argc, const char **argv, const char *prefix,\n \treturn print_fields(argc, argv, repo, format);\n }\n \n+struct stats {\n+\tsize_t branches;\n+\tsize_t remotes;\n+\tsize_t tags;\n+\tsize_t others;\n+};\n+\n+struct stats_table {\n+\tstruct string_list rows;\n+\n+\tint name_col_width;\n+\tint value_col_width;\n+};\n+\n+struct stats_table_entry {\n+\tchar *value;\n+};\n+\n+static void stats_table_add(struct stats_table *table, const char *name,\n+\t\t\t    struct stats_table_entry *entry)\n+{\n+\tint name_width = strlen(name);\n+\tstruct string_list_item *item;\n+\n+\titem = string_list_append(&table->rows, name);\n+\titem->util = entry;\n+\n+\tif (name_width > table->name_col_width)\n+\t\ttable->name_col_width = name_width;\n+\tif (entry) {\n+\t\tint value_width = strlen(entry->value);\n+\t\tif (value_width > table->value_col_width)\n+\t\t\ttable->value_col_width = value_width;\n+\t}\n+}\n+\n+static void stats_table_add_count(struct stats_table *table, const char *name,\n+\t\t\t\t  size_t value)\n+{\n+\tstruct stats_table_entry *entry;\n+\n+\tCALLOC_ARRAY(entry, 1);\n+\tentry->value = xstrfmt(\"%\" PRIuMAX, (uintmax_t)value);\n+\tstats_table_add(table, name, entry);\n+}\n+\n+static void stats_table_setup(struct stats_table *table, struct stats *stats)\n+{\n+\tsize_t ref_total;\n+\n+\tref_total = stats->branches + stats->remotes + stats->tags + stats->others;\n+\tstats_table_add(table, _(\"* References\"), NULL);\n+\tstats_table_add_count(table, _(\"  * Count\"), ref_total);\n+\tstats_table_add_count(table, _(\"    * Branches\"), stats->branches);\n+\tstats_table_add_count(table, _(\"    * Tags\"), stats->tags);\n+\tstats_table_add_count(table, _(\"    * Remotes\"), stats->remotes);\n+\tstats_table_add_count(table, _(\"    * Others\"), stats->others);\n+}\n+\n+static void stats_table_print(struct stats_table *table)\n+{\n+\tconst char *name_col_title = _(\"Repository stats\");\n+\tconst char *value_col_title = _(\"Value\");\n+\tint name_col_width = strlen(name_col_title);\n+\tint value_col_width = strlen(value_col_title);\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct string_list_item *item;\n+\n+\tif (table->name_col_width > name_col_width)\n+\t\tname_col_width = table->name_col_width;\n+\tif (table->value_col_width > value_col_width)\n+\t\tvalue_col_width = table->value_col_width;\n+\n+\tstrbuf_addf(&buf, \"| %-*s | %-*s |\\n\", name_col_width, name_col_title,\n+\t\t    value_col_width, value_col_title);\n+\tstrbuf_addstr(&buf, \"| \");\n+\tstrbuf_addchars(&buf, '-', name_col_width);\n+\tstrbuf_addstr(&buf, \" | \");\n+\tstrbuf_addchars(&buf, '-', value_col_width);\n+\tstrbuf_addstr(&buf, \" |\\n\");\n+\n+\tfor_each_string_list_item (item, &table->rows) {\n+\t\tstruct stats_table_entry *entry = item->util;\n+\t\tconst char *value = \"\";\n+\n+\t\tif (entry) {\n+\t\t\tstruct stats_table_entry *entry = item->util;\n+\t\t\tvalue = entry->value;\n+\t\t}\n+\n+\t\tstrbuf_addf(&buf, \"| %-*s | %*s |\\n\", name_col_width,\n+\t\t\t    item->string, value_col_width, value);\n+\n+\t\tif (entry)\n+\t\t\tfree(entry->value);\n+\t}\n+\n+\tfputs(buf.buf, stdout);\n+\tstrbuf_release(&buf);\n+}\n+\n+static void stats_count_references(struct stats *stats, struct ref_array *refs)\n+{\n+\tfor (int i = 0; i < refs->nr; i++) {\n+\t\tstruct ref_array_item *ref = refs->items[i];\n+\n+\t\tswitch (ref->kind) {\n+\t\tcase FILTER_REFS_BRANCHES:\n+\t\t\tstats->branches++;\n+\t\t\tbreak;\n+\t\tcase FILTER_REFS_REMOTES:\n+\t\t\tstats->remotes++;\n+\t\t\tbreak;\n+\t\tcase FILTER_REFS_TAGS:\n+\t\t\tstats->tags++;\n+\t\t\tbreak;\n+\t\tcase FILTER_REFS_OTHERS:\n+\t\t\tstats->others++;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+}\n+\n+static int repo_stats(int argc UNUSED, const char **argv UNUSED,\n+\t\t      const char *prefix UNUSED, struct repository *repo UNUSED)\n+{\n+\tstruct ref_filter filter = REF_FILTER_INIT;\n+\tstruct strvec ref_patterns = STRVEC_INIT;\n+\tstruct stats_table table = { 0 };\n+\tstruct ref_array refs = { 0 };\n+\tstruct stats stats = { 0 };\n+\n+\tfilter.name_patterns = ref_patterns.v;\n+\tfilter_refs(&refs, &filter, FILTER_REFS_REGULAR);\n+\n+\tstats_count_references(&stats, &refs);\n+\n+\tstats_table_setup(&table, &stats);\n+\tstats_table_print(&table);\n+\n+\tstring_list_clear(&table.rows, 1);\n+\tstrvec_clear(&ref_patterns);\n+\tref_array_clear(&refs);\n+\n+\treturn 0;\n+}\n+\n int cmd_repo(int argc, const char **argv, const char *prefix,\n \t     struct repository *repo)\n {\n \tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n \t\tOPT_SUBCOMMAND(\"info\", &fn, repo_info),\n+\t\tOPT_SUBCOMMAND(\"stats\", &fn, repo_stats),\n \t\tOPT_END()\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 7974795fe4..071d4a5112 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -236,6 +236,7 @@ integration_tests = [\n   't1701-racy-split-index.sh',\n   't1800-hook.sh',\n   't1900-repo.sh',\n+  't1901-repo-stats.sh',\n   't2000-conflict-when-checking-files-out.sh',\n   't2002-checkout-cache-u.sh',\n   't2003-checkout-cache-mkdir.sh',\ndiff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\nnew file mode 100755\nindex 0000000000..27c32ec45f\n--- /dev/null\n+++ b/t/t1901-repo-stats.sh\n@@ -0,0 +1,59 @@\n+#!/bin/sh\n+\n+test_description='test git repo stats'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'empty repository stats' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit repo stats >out 2>err &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\t| Repository stats | Value |\n+\t\t| ---------------- | ----- |\n+\t\t| * References     |       |\n+\t\t|   * Count        |     0 |\n+\t\t|     * Branches   |     0 |\n+\t\t|     * Tags       |     0 |\n+\t\t|     * Remotes    |     0 |\n+\t\t|     * Others     |     0 |\n+\t\tEOF\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n+test_expect_success 'repository stats with references' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit commit --allow-empty -m init &&\n+\t\toid=\"$(git rev-parse HEAD)\" &&\n+\t\tgit switch -c foo &&\n+\t\tgit tag init &&\n+\t\tgit update-ref refs/remotes/origin/foo \"$oid\" &&\n+\t\tgit notes add -m foo &&\n+\t\tgit repo stats >out 2>err &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\t| Repository stats | Value |\n+\t\t| ---------------- | ----- |\n+\t\t| * References     |       |\n+\t\t|   * Count        |     5 |\n+\t\t|     * Branches   |     2 |\n+\t\t|     * Tags       |     1 |\n+\t\t|     * Remotes    |     1 |\n+\t\t|     * Others     |     1 |\n+\t\tEOF\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n+test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527017","messageId":"20250923025700.3046260-3-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250923025700.3046260-1-jltobler@gmail.com","subject":"[PATCH 2/4] builtin/repo: add object counts in stats output","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-23T02:56:58Z","receivedAt":"2025-09-23T02:57:08Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The amount of objects in a repository can provide insight regarding its\nshape. To surface this information, use the path-walk API to count the\nnumber of reachable objects in the repository by object type. All\nregular references are used to determine the reachable set of objects.\nThe object counts are appended to the same table containing the\nreference information.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc |  5 +-\n builtin/repo.c              | 99 +++++++++++++++++++++++++++++++++----\n t/t1901-repo-stats.sh       | 46 +++++++++++++++++\n 3 files changed, 139 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 7762329551..2a67abfca8 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -45,8 +45,9 @@ supported:\n `-z` is an alias for `--format=nul`.\n \n stats::\n-\tRetrieve stats about the current repository. All references in the\n-\trepository are categorized and counted accordingly.\n+\tRetrieve stats about the current repository. All references and\n+\treachable objects in the repository are categorized and counted\n+\taccordingly.\n +\n The table output format may change and is not intended for machine parsing.\n \ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 15899dd74c..a24ea0e66b 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -3,9 +3,11 @@\n #include \"builtin.h\"\n #include \"environment.h\"\n #include \"parse-options.h\"\n+#include \"path-walk.h\"\n #include \"quote.h\"\n #include \"ref-filter.h\"\n #include \"refs.h\"\n+#include \"revision.h\"\n #include \"strbuf.h\"\n #include \"string-list.h\"\n #include \"shallow.h\"\n@@ -159,13 +161,25 @@ static int repo_info(int argc, const char **argv, const char *prefix,\n \treturn print_fields(argc, argv, repo, format);\n }\n \n-struct stats {\n+struct ref_stats {\n \tsize_t branches;\n \tsize_t remotes;\n \tsize_t tags;\n \tsize_t others;\n };\n \n+struct object_stats {\n+\tsize_t tags;\n+\tsize_t commits;\n+\tsize_t trees;\n+\tsize_t blobs;\n+};\n+\n+struct stats {\n+\tstruct ref_stats refs;\n+\tstruct object_stats objects;\n+};\n+\n struct stats_table {\n \tstruct string_list rows;\n \n@@ -207,15 +221,27 @@ static void stats_table_add_count(struct stats_table *table, const char *name,\n \n static void stats_table_setup(struct stats_table *table, struct stats *stats)\n {\n+\tstruct object_stats objects = stats->objects;\n+\tstruct ref_stats refs = stats->refs;\n+\tsize_t object_total;\n \tsize_t ref_total;\n \n-\tref_total = stats->branches + stats->remotes + stats->tags + stats->others;\n+\tref_total = refs.branches + refs.remotes + refs.tags + refs.others;\n \tstats_table_add(table, _(\"* References\"), NULL);\n \tstats_table_add_count(table, _(\"  * Count\"), ref_total);\n-\tstats_table_add_count(table, _(\"    * Branches\"), stats->branches);\n-\tstats_table_add_count(table, _(\"    * Tags\"), stats->tags);\n-\tstats_table_add_count(table, _(\"    * Remotes\"), stats->remotes);\n-\tstats_table_add_count(table, _(\"    * Others\"), stats->others);\n+\tstats_table_add_count(table, _(\"    * Branches\"), refs.branches);\n+\tstats_table_add_count(table, _(\"    * Tags\"), refs.tags);\n+\tstats_table_add_count(table, _(\"    * Remotes\"), refs.remotes);\n+\tstats_table_add_count(table, _(\"    * Others\"), refs.others);\n+\n+\tobject_total = objects.commits + objects.trees + objects.blobs + objects.tags;\n+\tstats_table_add(table, \"\", NULL);\n+\tstats_table_add(table, _(\"* Objects\"), NULL);\n+\tstats_table_add_count(table, _(\"  * Count\"), object_total);\n+\tstats_table_add_count(table, _(\"    * Commits\"), objects.commits);\n+\tstats_table_add_count(table, _(\"    * Trees\"), objects.trees);\n+\tstats_table_add_count(table, _(\"    * Blobs\"), objects.blobs);\n+\tstats_table_add_count(table, _(\"    * Tags\"), objects.tags);\n }\n \n static void stats_table_print(struct stats_table *table)\n@@ -260,7 +286,7 @@ static void stats_table_print(struct stats_table *table)\n \tstrbuf_release(&buf);\n }\n \n-static void stats_count_references(struct stats *stats, struct ref_array *refs)\n+static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n {\n \tfor (int i = 0; i < refs->nr; i++) {\n \t\tstruct ref_array_item *ref = refs->items[i];\n@@ -282,25 +308,80 @@ static void stats_count_references(struct stats *stats, struct ref_array *refs)\n \t}\n }\n \n+static int count_objects(const char *path UNUSED, struct oid_array *oids,\n+\t\t\t enum object_type type, void *data)\n+{\n+\tstruct object_stats *stats = data;\n+\n+\tswitch (type) {\n+\tcase OBJ_TAG:\n+\t\tstats->tags += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_COMMIT:\n+\t\tstats->commits += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_TREE:\n+\t\tstats->trees += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_BLOB:\n+\t\tstats->blobs += oids->nr;\n+\t\tbreak;\n+\tdefault:\n+\t\tbreak;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void stats_count_objects(struct object_stats *stats,\n+\t\t\t\tstruct ref_array *refs, struct rev_info *revs)\n+{\n+\tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n+\n+\tinfo.revs = revs;\n+\tinfo.path_fn = count_objects;\n+\tinfo.path_fn_data = stats;\n+\n+\tfor (int i = 0; i < refs->nr; i++) {\n+\t\tstruct ref_array_item *ref = refs->items[i];\n+\n+\t\tswitch (ref->kind) {\n+\t\tcase FILTER_REFS_BRANCHES:\n+\t\tcase FILTER_REFS_TAGS:\n+\t\tcase FILTER_REFS_REMOTES:\n+\t\tcase FILTER_REFS_OTHERS:\n+\t\t\tadd_pending_oid(revs, NULL, &ref->objectname, 0);\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\twalk_objects_by_path(&info);\n+\tpath_walk_info_clear(&info);\n+}\n+\n static int repo_stats(int argc UNUSED, const char **argv UNUSED,\n-\t\t      const char *prefix UNUSED, struct repository *repo UNUSED)\n+\t\t      const char *prefix, struct repository *repo)\n {\n \tstruct ref_filter filter = REF_FILTER_INIT;\n \tstruct strvec ref_patterns = STRVEC_INIT;\n \tstruct stats_table table = { 0 };\n \tstruct ref_array refs = { 0 };\n \tstruct stats stats = { 0 };\n+\tstruct rev_info revs;\n \n+\trepo_init_revisions(repo, &revs, prefix);\n \tfilter.name_patterns = ref_patterns.v;\n \tfilter_refs(&refs, &filter, FILTER_REFS_REGULAR);\n \n-\tstats_count_references(&stats, &refs);\n+\tstats_count_references(&stats.refs, &refs);\n+\tstats_count_objects(&stats.objects, &refs, &revs);\n \n \tstats_table_setup(&table, &stats);\n \tstats_table_print(&table);\n \n \tstring_list_clear(&table.rows, 1);\n \tstrvec_clear(&ref_patterns);\n+\trelease_revisions(&revs);\n \tref_array_clear(&refs);\n \n \treturn 0;\ndiff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\nindex 27c32ec45f..c6a7f08be5 100755\n--- a/t/t1901-repo-stats.sh\n+++ b/t/t1901-repo-stats.sh\n@@ -20,6 +20,13 @@ test_expect_success 'empty repository stats' '\n \t\t|     * Tags       |     0 |\n \t\t|     * Remotes    |     0 |\n \t\t|     * Others     |     0 |\n+\t\t|                  |       |\n+\t\t| * Objects        |       |\n+\t\t|   * Count        |     0 |\n+\t\t|     * Commits    |     0 |\n+\t\t|     * Trees      |     0 |\n+\t\t|     * Blobs      |     0 |\n+\t\t|     * Tags       |     0 |\n \t\tEOF\n \n \t\ttest_cmp expect out &&\n@@ -49,6 +56,45 @@ test_expect_success 'repository stats with references' '\n \t\t|     * Tags       |     1 |\n \t\t|     * Remotes    |     1 |\n \t\t|     * Others     |     1 |\n+\t\t|                  |       |\n+\t\t| * Objects        |       |\n+\t\t|   * Count        |     5 |\n+\t\t|     * Commits    |     2 |\n+\t\t|     * Trees      |     2 |\n+\t\t|     * Blobs      |     1 |\n+\t\t|     * Tags       |     0 |\n+\t\tEOF\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n+test_expect_success 'repository stats with objects' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit_bulk 42 &&\n+\t\tgit tag -a foo -m bar &&\n+\t\tgit repo stats >out 2>err &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\t| Repository stats | Value |\n+\t\t| ---------------- | ----- |\n+\t\t| * References     |       |\n+\t\t|   * Count        |     2 |\n+\t\t|     * Branches   |     1 |\n+\t\t|     * Tags       |     1 |\n+\t\t|     * Remotes    |     0 |\n+\t\t|     * Others     |     0 |\n+\t\t|                  |       |\n+\t\t| * Objects        |       |\n+\t\t|   * Count        |   127 |\n+\t\t|     * Commits    |    42 |\n+\t\t|     * Trees      |    42 |\n+\t\t|     * Blobs      |    42 |\n+\t\t|     * Tags       |     1 |\n \t\tEOF\n \n \t\ttest_cmp expect out &&\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527018","messageId":"20250923025700.3046260-4-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250923025700.3046260-1-jltobler@gmail.com","subject":"[PATCH 3/4] builtin/repo: add keyvalue format for stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-23T02:56:59Z","receivedAt":"2025-09-23T02:57:09Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"All repository stats are outputted in a human-friendly table form. This\nformat is not suitable for machine parsing. Add a --format option that\nsupports two output modes: `table` and `keyvalue`. The `table` mode is\nthe default format and prints the same table output as before. With the\n`keyvalue` mode, each line of output contains a key-value pair of a\nrepository stat. The '=' character is used to delimit between keys and\nvalues. This mode provides output that is more machine-friendly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc | 16 ++++++++--\n builtin/repo.c              | 61 ++++++++++++++++++++++++++++++++++---\n t/t1901-repo-stats.sh       | 25 +++++++++++++++\n 3 files changed, 94 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 2a67abfca8..7d0341e4f1 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -9,7 +9,7 @@ SYNOPSIS\n --------\n [synopsis]\n git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n-git repo stats\n+git repo stats [--format=(table|keyvalue)]\n \n DESCRIPTION\n -----------\n@@ -44,12 +44,22 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n-stats::\n+`stats [--format=(table|keyvalue)]`::\n \tRetrieve stats about the current repository. All references and\n \treachable objects in the repository are categorized and counted\n \taccordingly.\n +\n-The table output format may change and is not intended for machine parsing.\n+The output format can be chosen through the flag `--format`. Two formats are\n+supported:\n++\n+`table`:::\n+\tOutputs repository stats in a human-friendly table and is used by\n+\tdefault. This format may change and is not intended for machine\n+\tparsing.\n+\n+`keyvalue`:::\n+\tEach line of output contains a key-value pair of a repostiory stat. The\n+\t'=' character is used to delimit between the key and the value.\n \n INFO KEYS\n ---------\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex a24ea0e66b..4c16a68e4e 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -14,13 +14,14 @@\n \n static const char *const repo_usage[] = {\n \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n-\t\"git repo stats\",\n+\t\"git repo stats [--format=(table|keyvalue)]\",\n \tNULL\n };\n \n typedef int get_value_fn(struct repository *repo, struct strbuf *buf);\n \n enum output_format {\n+\tFORMAT_TABLE,\n \tFORMAT_KEYVALUE,\n \tFORMAT_NUL_TERMINATED,\n };\n@@ -135,6 +136,8 @@ static int parse_format_cb(const struct option *opt,\n \t\t*format = FORMAT_NUL_TERMINATED;\n \telse if (!strcmp(arg, \"keyvalue\"))\n \t\t*format = FORMAT_KEYVALUE;\n+\telse if (!strcmp(arg, \"table\"))\n+\t\t*format = FORMAT_TABLE;\n \telse\n \t\tdie(_(\"invalid format '%s'\"), arg);\n \n@@ -157,6 +160,8 @@ static int repo_info(int argc, const char **argv, const char *prefix,\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n+\tif (format == FORMAT_TABLE)\n+\t\tdie(_(\"table format not supported\"));\n \n \treturn print_fields(argc, argv, repo, format);\n }\n@@ -286,6 +291,32 @@ static void stats_table_print(struct stats_table *table)\n \tstrbuf_release(&buf);\n }\n \n+static void stats_print(struct stats *stats)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\n+\tstrbuf_addf(&buf, \"references.branches.count=%\" PRIuMAX \"\\n\",\n+\t\t    (uintmax_t)stats->refs.branches);\n+\tstrbuf_addf(&buf, \"references.tags.count=%\" PRIuMAX \"\\n\",\n+\t\t    (uintmax_t)stats->refs.tags);\n+\tstrbuf_addf(&buf, \"references.remotes.count=%\" PRIuMAX \"\\n\",\n+\t\t    (uintmax_t)stats->refs.remotes);\n+\tstrbuf_addf(&buf, \"references.others.count=%\" PRIuMAX \"\\n\",\n+\t\t    (uintmax_t)stats->refs.others);\n+\n+\tstrbuf_addf(&buf, \"objects.commits.count=%\" PRIuMAX \"\\n\",\n+\t\t    (uintmax_t)stats->objects.commits);\n+\tstrbuf_addf(&buf, \"objects.trees.count=%\" PRIuMAX \"\\n\",\n+\t\t    (uintmax_t)stats->objects.trees);\n+\tstrbuf_addf(&buf, \"objects.blobs.count=%\" PRIuMAX \"\\n\",\n+\t\t    (uintmax_t)stats->objects.blobs);\n+\tstrbuf_addf(&buf, \"objects.tags.count=%\" PRIuMAX \"\\n\",\n+\t\t    (uintmax_t)stats->objects.tags);\n+\n+\tfwrite(buf.buf, sizeof(char), buf.len, stdout);\n+\tstrbuf_release(&buf);\n+}\n+\n static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n {\n \tfor (int i = 0; i < refs->nr; i++) {\n@@ -359,9 +390,16 @@ static void stats_count_objects(struct object_stats *stats,\n \tpath_walk_info_clear(&info);\n }\n \n-static int repo_stats(int argc UNUSED, const char **argv UNUSED,\n-\t\t      const char *prefix, struct repository *repo)\n+static int repo_stats(int argc, const char **argv, const char *prefix,\n+\t\t      struct repository *repo)\n {\n+\tenum output_format format = FORMAT_TABLE;\n+\tstruct option options[] = {\n+\t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n+\t\t\t       N_(\"output format\"),\n+\t\t\t       PARSE_OPT_NONEG, parse_format_cb),\n+\t\tOPT_END()\n+\t};\n \tstruct ref_filter filter = REF_FILTER_INIT;\n \tstruct strvec ref_patterns = STRVEC_INIT;\n \tstruct stats_table table = { 0 };\n@@ -369,6 +407,10 @@ static int repo_stats(int argc UNUSED, const char **argv UNUSED,\n \tstruct stats stats = { 0 };\n \tstruct rev_info revs;\n \n+\tparse_options(argc, argv, prefix, options, repo_usage, 0);\n+\tif (format == FORMAT_NUL_TERMINATED)\n+\t\tdie(_(\"nul format not yet supported\"));\n+\n \trepo_init_revisions(repo, &revs, prefix);\n \tfilter.name_patterns = ref_patterns.v;\n \tfilter_refs(&refs, &filter, FILTER_REFS_REGULAR);\n@@ -376,8 +418,17 @@ static int repo_stats(int argc UNUSED, const char **argv UNUSED,\n \tstats_count_references(&stats.refs, &refs);\n \tstats_count_objects(&stats.objects, &refs, &revs);\n \n-\tstats_table_setup(&table, &stats);\n-\tstats_table_print(&table);\n+\tswitch (format) {\n+\tcase FORMAT_TABLE:\n+\t\tstats_table_setup(&table, &stats);\n+\t\tstats_table_print(&table);\n+\t\tbreak;\n+\tcase FORMAT_KEYVALUE:\n+\t\tstats_print(&stats);\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"not a valid output format: %d\", format);\n+\t}\n \n \tstring_list_clear(&table.rows, 1);\n \tstrvec_clear(&ref_patterns);\ndiff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\nindex c6a7f08be5..5bc6d9d5c4 100755\n--- a/t/t1901-repo-stats.sh\n+++ b/t/t1901-repo-stats.sh\n@@ -102,4 +102,29 @@ test_expect_success 'repository stats with objects' '\n \t)\n '\n \n+test_expect_success 'repository stats with keyvalue format' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit_bulk 42 &&\n+\t\tgit tag -a foo -m bar &&\n+\t\tgit repo stats --format=keyvalue >out 2>err &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\treferences.branches.count=1\n+\t\treferences.tags.count=1\n+\t\treferences.remotes.count=0\n+\t\treferences.others.count=0\n+\t\tobjects.commits.count=42\n+\t\tobjects.trees.count=42\n+\t\tobjects.blobs.count=42\n+\t\tobjects.tags.count=1\n+\t\tEOF\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527019","messageId":"20250923025700.3046260-5-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250923025700.3046260-1-jltobler@gmail.com","subject":"[PATCH 4/4] builtin/repo: add nul format for stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-23T02:57:00Z","receivedAt":"2025-09-23T02:57:10Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Introduce the `nul` mode for the --format option. When enabled, the\noutput is similar to the `keyvalue` mode, but key-values are delimited\nby a NUL character instead of a newline. This allows stat values to\nsupport special characters without having to cquote values.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc | 10 ++++---\n builtin/repo.c              | 52 ++++++++++++++++++++-----------------\n t/t1901-repo-stats.sh       | 27 +++++++++++++++++++\n 3 files changed, 62 insertions(+), 27 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 7d0341e4f1..57267064ea 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -9,7 +9,7 @@ SYNOPSIS\n --------\n [synopsis]\n git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n-git repo stats [--format=(table|keyvalue)]\n+git repo stats [--format=(table|keyvalue|nul)]\n \n DESCRIPTION\n -----------\n@@ -44,12 +44,12 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n-`stats [--format=(table|keyvalue)]`::\n+`stats [--format=(table|keyvalue|nul)]`::\n \tRetrieve stats about the current repository. All references and\n \treachable objects in the repository are categorized and counted\n \taccordingly.\n +\n-The output format can be chosen through the flag `--format`. Two formats are\n+The output format can be chosen through the flag `--format`. Three formats are\n supported:\n +\n `table`:::\n@@ -61,6 +61,10 @@ supported:\n \tEach line of output contains a key-value pair of a repostiory stat. The\n \t'=' character is used to delimit between the key and the value.\n \n+`nul`:::\n+\tSimilar to 'keyvalue', but uses a NUL character to delimit between\n+\tkey-value pairs instead of a newline.\n+\n INFO KEYS\n ---------\n In order to obtain a set of values from `git repo info`, you should provide\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 4c16a68e4e..37034e6347 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -14,7 +14,7 @@\n \n static const char *const repo_usage[] = {\n \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n-\t\"git repo stats [--format=(table|keyvalue)]\",\n+\t\"git repo stats [--format=(table|keyvalue|nul)]\",\n \tNULL\n };\n \n@@ -291,27 +291,31 @@ static void stats_table_print(struct stats_table *table)\n \tstrbuf_release(&buf);\n }\n \n-static void stats_print(struct stats *stats)\n+static void stats_print(struct stats *stats, int nul_delim)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n-\n-\tstrbuf_addf(&buf, \"references.branches.count=%\" PRIuMAX \"\\n\",\n-\t\t    (uintmax_t)stats->refs.branches);\n-\tstrbuf_addf(&buf, \"references.tags.count=%\" PRIuMAX \"\\n\",\n-\t\t    (uintmax_t)stats->refs.tags);\n-\tstrbuf_addf(&buf, \"references.remotes.count=%\" PRIuMAX \"\\n\",\n-\t\t    (uintmax_t)stats->refs.remotes);\n-\tstrbuf_addf(&buf, \"references.others.count=%\" PRIuMAX \"\\n\",\n-\t\t    (uintmax_t)stats->refs.others);\n-\n-\tstrbuf_addf(&buf, \"objects.commits.count=%\" PRIuMAX \"\\n\",\n-\t\t    (uintmax_t)stats->objects.commits);\n-\tstrbuf_addf(&buf, \"objects.trees.count=%\" PRIuMAX \"\\n\",\n-\t\t    (uintmax_t)stats->objects.trees);\n-\tstrbuf_addf(&buf, \"objects.blobs.count=%\" PRIuMAX \"\\n\",\n-\t\t    (uintmax_t)stats->objects.blobs);\n-\tstrbuf_addf(&buf, \"objects.tags.count=%\" PRIuMAX \"\\n\",\n-\t\t    (uintmax_t)stats->objects.tags);\n+\tchar delim = '\\n';\n+\n+\tif (nul_delim)\n+\t\tdelim = '\\0';\n+\n+\tstrbuf_addf(&buf, \"references.branches.count=%\" PRIuMAX \"%c\",\n+\t\t    (uintmax_t)stats->refs.branches, delim);\n+\tstrbuf_addf(&buf, \"references.tags.count=%\" PRIuMAX \"%c\",\n+\t\t    (uintmax_t)stats->refs.tags, delim);\n+\tstrbuf_addf(&buf, \"references.remotes.count=%\" PRIuMAX \"%c\",\n+\t\t    (uintmax_t)stats->refs.remotes, delim);\n+\tstrbuf_addf(&buf, \"references.others.count=%\" PRIuMAX \"%c\",\n+\t\t    (uintmax_t)stats->refs.others, delim);\n+\n+\tstrbuf_addf(&buf, \"objects.commits.count=%\" PRIuMAX \"%c\",\n+\t\t    (uintmax_t)stats->objects.commits, delim);\n+\tstrbuf_addf(&buf, \"objects.trees.count=%\" PRIuMAX \"%c\",\n+\t\t    (uintmax_t)stats->objects.trees, delim);\n+\tstrbuf_addf(&buf, \"objects.blobs.count=%\" PRIuMAX \"%c\",\n+\t\t    (uintmax_t)stats->objects.blobs, delim);\n+\tstrbuf_addf(&buf, \"objects.tags.count=%\" PRIuMAX \"%c\",\n+\t\t    (uintmax_t)stats->objects.tags, delim);\n \n \tfwrite(buf.buf, sizeof(char), buf.len, stdout);\n \tstrbuf_release(&buf);\n@@ -408,9 +412,6 @@ static int repo_stats(int argc, const char **argv, const char *prefix,\n \tstruct rev_info revs;\n \n \tparse_options(argc, argv, prefix, options, repo_usage, 0);\n-\tif (format == FORMAT_NUL_TERMINATED)\n-\t\tdie(_(\"nul format not yet supported\"));\n-\n \trepo_init_revisions(repo, &revs, prefix);\n \tfilter.name_patterns = ref_patterns.v;\n \tfilter_refs(&refs, &filter, FILTER_REFS_REGULAR);\n@@ -424,7 +425,10 @@ static int repo_stats(int argc, const char **argv, const char *prefix,\n \t\tstats_table_print(&table);\n \t\tbreak;\n \tcase FORMAT_KEYVALUE:\n-\t\tstats_print(&stats);\n+\t\tstats_print(&stats, 0);\n+\t\tbreak;\n+\tcase FORMAT_NUL_TERMINATED:\n+\t\tstats_print(&stats, 1);\n \t\tbreak;\n \tdefault:\n \t\tBUG(\"not a valid output format: %d\", format);\ndiff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\nindex 5bc6d9d5c4..061b2fbbc1 100755\n--- a/t/t1901-repo-stats.sh\n+++ b/t/t1901-repo-stats.sh\n@@ -127,4 +127,31 @@ test_expect_success 'repository stats with keyvalue format' '\n \t)\n '\n \n+test_expect_success 'repository stats with nul format' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit_bulk 42 &&\n+\t\tgit tag -a foo -m bar &&\n+\t\tgit repo stats --format=nul >out 2>err &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\treferences.branches.count=1\n+\t\treferences.tags.count=1\n+\t\treferences.remotes.count=0\n+\t\treferences.others.count=0\n+\t\tobjects.commits.count=42\n+\t\tobjects.trees.count=42\n+\t\tobjects.blobs.count=42\n+\t\tobjects.tags.count=1\n+\t\tEOF\n+\n+\t\ttr \"\\n\" \"\\0\" <expect >expect_null &&\n+\n+\t\ttest_cmp expect_null out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527069","messageId":"aNJ7_GoKT5ea4QJE@pks.im","threadId":"64187","inReplyTo":"20250923025700.3046260-2-jltobler@gmail.com","subject":"Re: [PATCH 1/4] builtin/repo: introduce stats subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-23T10:52:44Z","receivedAt":"2025-09-23T10:52:56Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Sep 22, 2025 at 09:56:57PM -0500, Justin Tobler wrote:\n> The shape of a repository's history can have huge impacts on the\n> performance and health of the repository itself. Currently, Git lacks a\n> means to surface key stats/information regarding the shape of a\n> repository via a single command. Acquiring this information requires\n> users to be fairly knowledgeable about the structure of a Git repository\n> and how to identify the relevant data points. To fill this gap,\n> supplemental tools such as git-sizer(1) have been developed.\n> \n> To allow users to more readily identify potential issues for a\n> repository, introduce the \"stats\" subcommand in git-repo(1) to output\n> stats for the repository that may be of interest to users. The goal of\n> this subcommand is to eventually provide similar functionality to\n> git-sizer(1), but in Git natively.\n\nNit: \"but natively in Git\" would read more natural.\n\n> The initial version of this command only iterates through all references\n> in the repository and tracks the count of branches, tags, remotes, and\n\ns/remotes/remote refs/\n\n> other reference types. The corresponding information is displayed in a\n> human-friendly table formatted in a very similar manner to git-sizer(1).\n> The width of each table column is adjusted automatically to satisfy the\n> requirements of the widest row contained.\n> \n> Subsequent commits will surface additional relevant data points to\n> output.\n> \n> Signed-off-by: Justin Tobler <jltobler@gmail.com>\n\nIs this command built on Derrick's git-survey(1)? If so, it would\nprobably be nice to add a \"Based-on-patch-by\" tag.\n\n> diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\n> index 209afd1b61..7762329551 100644\n> --- a/Documentation/git-repo.adoc\n> +++ b/Documentation/git-repo.adoc\n> @@ -43,6 +44,12 @@ supported:\n>  +\n>  `-z` is an alias for `--format=nul`.\n>  \n> +stats::\n> +\tRetrieve stats about the current repository. All references in the\n\ns/stats/statistics/\n\n> +\trepository are categorized and counted accordingly.\n> ++\n> +The table output format may change and is not intended for machine parsing.\n\nI guess we don't yet have a machine-parseable interface in this state,\nso we cannot ponit to it.\n\n> @@ -156,12 +159,160 @@ static int repo_info(int argc, const char **argv, const char *prefix,\n>  \treturn print_fields(argc, argv, repo, format);\n>  }\n>  \n> +struct stats {\n> +\tsize_t branches;\n> +\tsize_t remotes;\n> +\tsize_t tags;\n> +\tsize_t others;\n> +};\n> +\n> +struct stats_table {\n> +\tstruct string_list rows;\n> +\n> +\tint name_col_width;\n> +\tint value_col_width;\n\nYou assign the result from `strlen()` to these fields, so they should\nprobably be `size_t`.\n\n> +};\n> +\n> +struct stats_table_entry {\n> +\tchar *value;\n> +};\n> +\n> +static void stats_table_add(struct stats_table *table, const char *name,\n> +\t\t\t    struct stats_table_entry *entry)\n> +{\n> +\tint name_width = strlen(name);\n\n`strlen()` returns `size_t`.\n\n> +\tstruct string_list_item *item;\n> +\n> +\titem = string_list_append(&table->rows, name);\n> +\titem->util = entry;\n> +\n> +\tif (name_width > table->name_col_width)\n> +\t\ttable->name_col_width = name_width;\n> +\tif (entry) {\n> +\t\tint value_width = strlen(entry->value);\n> +\t\tif (value_width > table->value_col_width)\n> +\t\t\ttable->value_col_width = value_width;\n> +\t}\n\nI was wondering at first why you'd ever want to not pass an entry, but\nwe use that to have \"dividers\" in the table. Makes sense.\n\n> +}\n> +\n> +static void stats_table_add_count(struct stats_table *table, const char *name,\n> +\t\t\t\t  size_t value)\n> +{\n> +\tstruct stats_table_entry *entry;\n> +\n> +\tCALLOC_ARRAY(entry, 1);\n> +\tentry->value = xstrfmt(\"%\" PRIuMAX, (uintmax_t)value);\n> +\tstats_table_add(table, name, entry);\n> +}\n> +\n> +static void stats_table_setup(struct stats_table *table, struct stats *stats)\n> +{\n> +\tsize_t ref_total;\n> +\n> +\tref_total = stats->branches + stats->remotes + stats->tags + stats->others;\n> +\tstats_table_add(table, _(\"* References\"), NULL);\n> +\tstats_table_add_count(table, _(\"  * Count\"), ref_total);\n> +\tstats_table_add_count(table, _(\"    * Branches\"), stats->branches);\n> +\tstats_table_add_count(table, _(\"    * Tags\"), stats->tags);\n> +\tstats_table_add_count(table, _(\"    * Remotes\"), stats->remotes);\n> +\tstats_table_add_count(table, _(\"    * Others\"), stats->others);\n> +}\n\nWould it make sense to not translate the formatting directives, but only\nthe actual words?\n\n> +static void stats_table_print(struct stats_table *table)\n> +{\n> +\tconst char *name_col_title = _(\"Repository stats\");\n> +\tconst char *value_col_title = _(\"Value\");\n> +\tint name_col_width = strlen(name_col_title);\n> +\tint value_col_width = strlen(value_col_title);\n\nThese should both be `size_t`.\n\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\tstruct string_list_item *item;\n> +\n> +\tif (table->name_col_width > name_col_width)\n> +\t\tname_col_width = table->name_col_width;\n> +\tif (table->value_col_width > value_col_width)\n> +\t\tvalue_col_width = table->value_col_width;\n> +\n> +\tstrbuf_addf(&buf, \"| %-*s | %-*s |\\n\", name_col_width, name_col_title,\n> +\t\t    value_col_width, value_col_title);\n\nAha, that's why you went with `int`. You can use `cast_size_to_to_int()`\nto convert between the types.\n\n> +\tstrbuf_addstr(&buf, \"| \");\n> +\tstrbuf_addchars(&buf, '-', name_col_width);\n> +\tstrbuf_addstr(&buf, \" | \");\n> +\tstrbuf_addchars(&buf, '-', value_col_width);\n> +\tstrbuf_addstr(&buf, \" |\\n\");\n> +\n> +\tfor_each_string_list_item (item, &table->rows) {\n\nWe typically don't have a space after between the macro and its\narguments.\n\n> +\t\tstruct stats_table_entry *entry = item->util;\n> +\t\tconst char *value = \"\";\n> +\n> +\t\tif (entry) {\n> +\t\t\tstruct stats_table_entry *entry = item->util;\n> +\t\t\tvalue = entry->value;\n> +\t\t}\n> +\n> +\t\tstrbuf_addf(&buf, \"| %-*s | %*s |\\n\", name_col_width,\n> +\t\t\t    item->string, value_col_width, value);\n> +\n> +\t\tif (entry)\n> +\t\t\tfree(entry->value);\n\nIt's a bit weird that we free the values when we pretend to only print\ndata. Sure, we probably don't ever have a usecase where we want to print\ndata a second time. But I still think it would be nice to separate\nconcerns.\n\n> +\t}\n> +\n> +\tfputs(buf.buf, stdout);\n> +\tstrbuf_release(&buf);\n> +}\n> +\n> +static void stats_count_references(struct stats *stats, struct ref_array *refs)\n> +{\n> +\tfor (int i = 0; i < refs->nr; i++) {\n> +\t\tstruct ref_array_item *ref = refs->items[i];\n> +\n> +\t\tswitch (ref->kind) {\n> +\t\tcase FILTER_REFS_BRANCHES:\n> +\t\t\tstats->branches++;\n> +\t\t\tbreak;\n> +\t\tcase FILTER_REFS_REMOTES:\n> +\t\t\tstats->remotes++;\n> +\t\t\tbreak;\n> +\t\tcase FILTER_REFS_TAGS:\n> +\t\t\tstats->tags++;\n> +\t\t\tbreak;\n> +\t\tcase FILTER_REFS_OTHERS:\n> +\t\t\tstats->others++;\n> +\t\t\tbreak;\n\nDo we want to have a `default:` case where we `BUG()`? Otherwise we may\nnot notice that we undercount the overall number of refs.\n\n> +\t\t}\n> +\t}\n> +}\n> +\n> +static int repo_stats(int argc UNUSED, const char **argv UNUSED,\n> +\t\t      const char *prefix UNUSED, struct repository *repo UNUSED)\n\nNot a new issue, but I'd rather call this `cmd_repo_stats()` to note\nthat this is the entrypoint. We might as well adapt the other subcommand\nto follow that naming schema in a preparatory commit.\n\n> +{\n> +\tstruct ref_filter filter = REF_FILTER_INIT;\n> +\tstruct strvec ref_patterns = STRVEC_INIT;\n> +\tstruct stats_table table = { 0 };\n> +\tstruct ref_array refs = { 0 };\n> +\tstruct stats stats = { 0 };\n> +\n> +\tfilter.name_patterns = ref_patterns.v;\n> +\tfilter_refs(&refs, &filter, FILTER_REFS_REGULAR);\n\n`filter_refs()` may return an error code which we should probably\nhandle.\n\n> diff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\n> new file mode 100755\n> index 0000000000..27c32ec45f\n> --- /dev/null\n> +++ b/t/t1901-repo-stats.sh\n> @@ -0,0 +1,59 @@\n> +#!/bin/sh\n> +\n> +test_description='test git repo stats'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'empty repository stats' '\n\nNit: I don't think it's necessary to repeat \"repository stats\" in every\ntest name. That's already clear from the test suite.\n\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tgit repo stats >out 2>err &&\n> +\n> +\t\tcat >expect <<-EOF &&\n\ns/EOF/\\EOF/, as we don't need to expand any variables.\n\n> +\t\t| Repository stats | Value |\n> +\t\t| ---------------- | ----- |\n> +\t\t| * References     |       |\n> +\t\t|   * Count        |     0 |\n> +\t\t|     * Branches   |     0 |\n> +\t\t|     * Tags       |     0 |\n> +\t\t|     * Remotes    |     0 |\n> +\t\t|     * Others     |     0 |\n> +\t\tEOF\n> +\n> +\t\ttest_cmp expect out &&\n> +\t\ttest_line_count = 0 err\n> +\t)\n> +'\n> +\n> +test_expect_success 'repository stats with references' '\n\nSame comment regarding the name.\n\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tgit commit --allow-empty -m init &&\n> +\t\toid=\"$(git rev-parse HEAD)\" &&\n> +\t\tgit switch -c foo &&\n> +\t\tgit tag init &&\n> +\t\tgit update-ref refs/remotes/origin/foo \"$oid\" &&\n> +\t\tgit notes add -m foo &&\n> +\t\tgit repo stats >out 2>err &&\n> +\n> +\t\tcat >expect <<-EOF &&\n\nLikewise regarding \"\\EOF\".\n\n> +\t\t| Repository stats | Value |\n> +\t\t| ---------------- | ----- |\n> +\t\t| * References     |       |\n> +\t\t|   * Count        |     5 |\n> +\t\t|     * Branches   |     2 |\n> +\t\t|     * Tags       |     1 |\n> +\t\t|     * Remotes    |     1 |\n> +\t\t|     * Others     |     1 |\n> +\t\tEOF\n> +\n> +\t\ttest_cmp expect out &&\n> +\t\ttest_line_count = 0 err\n> +\t)\n> +'\n\nPatrick\n"},{"id":"527070","messageId":"aNJ8BvTZ_yNSrBA6@pks.im","threadId":"64187","inReplyTo":"20250923025700.3046260-3-jltobler@gmail.com","subject":"Re: [PATCH 2/4] builtin/repo: add object counts in stats output","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-23T10:52:54Z","receivedAt":"2025-09-23T10:52:59Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Sep 22, 2025 at 09:56:58PM -0500, Justin Tobler wrote:\n> diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\n> index 7762329551..2a67abfca8 100644\n> --- a/Documentation/git-repo.adoc\n> +++ b/Documentation/git-repo.adoc\n> @@ -45,8 +45,9 @@ supported:\n>  `-z` is an alias for `--format=nul`.\n>  \n>  stats::\n> -\tRetrieve stats about the current repository. All references in the\n> -\trepository are categorized and counted accordingly.\n> +\tRetrieve stats about the current repository. All references and\n> +\treachable objects in the repository are categorized and counted\n> +\taccordingly.\n>  +\n>  The table output format may change and is not intended for machine parsing.\n\nI already wanted to mention this on the first commit, but would it maybe\nmake sense if this was a bulleted list of information that we surface\nright from the start? Then we don't have to reflow the whole paragraph\nevery time we surface new information.\n\n> diff --git a/builtin/repo.c b/builtin/repo.c\n> index 15899dd74c..a24ea0e66b 100644\n> --- a/builtin/repo.c\n> +++ b/builtin/repo.c\n> @@ -159,13 +161,25 @@ static int repo_info(int argc, const char **argv, const char *prefix,\n>  \treturn print_fields(argc, argv, repo, format);\n>  }\n>  \n> -struct stats {\n> +struct ref_stats {\n\nNit: let's call it `ref_stats` right from the start instead of renaming.\n\n>  \tsize_t branches;\n>  \tsize_t remotes;\n>  \tsize_t tags;\n>  \tsize_t others;\n>  };\n>  \n> +struct object_stats {\n> +\tsize_t tags;\n> +\tsize_t commits;\n> +\tsize_t trees;\n> +\tsize_t blobs;\n> +};\n> +\n> +struct stats {\n\nI'd maybe call this `struct repo_stats`. `stats` feels quite generic and\nvery close to a collision with `struct stat`.\n\n> @@ -207,15 +221,27 @@ static void stats_table_add_count(struct stats_table *table, const char *name,\n>  \n>  static void stats_table_setup(struct stats_table *table, struct stats *stats)\n>  {\n> +\tstruct object_stats objects = stats->objects;\n> +\tstruct ref_stats refs = stats->refs;\n\nWe can avoid the copies by making these pointers. Not that it'd really\nmatter all that much.\n\n> +\tsize_t object_total;\n>  \tsize_t ref_total;\n>  \n> -\tref_total = stats->branches + stats->remotes + stats->tags + stats->others;\n> +\tref_total = refs.branches + refs.remotes + refs.tags + refs.others;\n>  \tstats_table_add(table, _(\"* References\"), NULL);\n>  \tstats_table_add_count(table, _(\"  * Count\"), ref_total);\n> -\tstats_table_add_count(table, _(\"    * Branches\"), stats->branches);\n> -\tstats_table_add_count(table, _(\"    * Tags\"), stats->tags);\n> -\tstats_table_add_count(table, _(\"    * Remotes\"), stats->remotes);\n> -\tstats_table_add_count(table, _(\"    * Others\"), stats->others);\n> +\tstats_table_add_count(table, _(\"    * Branches\"), refs.branches);\n> +\tstats_table_add_count(table, _(\"    * Tags\"), refs.tags);\n> +\tstats_table_add_count(table, _(\"    * Remotes\"), refs.remotes);\n> +\tstats_table_add_count(table, _(\"    * Others\"), refs.others);\n> +\n> +\tobject_total = objects.commits + objects.trees + objects.blobs + objects.tags;\n> +\tstats_table_add(table, \"\", NULL);\n> +\tstats_table_add(table, _(\"* Objects\"), NULL);\n\nShould we maybe say \"Reachable objects\" here to clarify that this\ndoesn't count unreachable ones?\n\n> @@ -282,25 +308,80 @@ static void stats_count_references(struct stats *stats, struct ref_array *refs)\n>  \t}\n>  }\n>  \n> +static int count_objects(const char *path UNUSED, struct oid_array *oids,\n> +\t\t\t enum object_type type, void *data)\n> +{\n> +\tstruct object_stats *stats = data;\n> +\n> +\tswitch (type) {\n> +\tcase OBJ_TAG:\n> +\t\tstats->tags += oids->nr;\n> +\t\tbreak;\n> +\tcase OBJ_COMMIT:\n> +\t\tstats->commits += oids->nr;\n> +\t\tbreak;\n> +\tcase OBJ_TREE:\n> +\t\tstats->trees += oids->nr;\n> +\t\tbreak;\n> +\tcase OBJ_BLOB:\n> +\t\tstats->blobs += oids->nr;\n> +\t\tbreak;\n> +\tdefault:\n\nLet's `BUG()` here. This case should never happen, and if it does\nsomething is seriously wrong.\n\n> +\t\tbreak;\n> +\t}\n> +\n> +\treturn 0;\n> +}\n> +\n> +static void stats_count_objects(struct object_stats *stats,\n> +\t\t\t\tstruct ref_array *refs, struct rev_info *revs)\n> +{\n> +\tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n> +\n> +\tinfo.revs = revs;\n> +\tinfo.path_fn = count_objects;\n> +\tinfo.path_fn_data = stats;\n> +\n> +\tfor (int i = 0; i < refs->nr; i++) {\n> +\t\tstruct ref_array_item *ref = refs->items[i];\n> +\n> +\t\tswitch (ref->kind) {\n> +\t\tcase FILTER_REFS_BRANCHES:\n> +\t\tcase FILTER_REFS_TAGS:\n> +\t\tcase FILTER_REFS_REMOTES:\n> +\t\tcase FILTER_REFS_OTHERS:\n> +\t\t\tadd_pending_oid(revs, NULL, &ref->objectname, 0);\n> +\t\t\tbreak;\n> +\t\t}\n> +\t}\n> +\n> +\twalk_objects_by_path(&info);\n> +\tpath_walk_info_clear(&info);\n> +}\n\nI guess this can take a while, so having a progress meter would be great\nto have to give the user some info what's happening. I guess it doesn't\nhave to be part of the first iteration thuogh as long as this is\nsomething we plan to add at a later point.\n\n> diff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\n> index 27c32ec45f..c6a7f08be5 100755\n> --- a/t/t1901-repo-stats.sh\n> +++ b/t/t1901-repo-stats.sh\n> @@ -20,6 +20,13 @@ test_expect_success 'empty repository stats' '\n>  \t\t|     * Tags       |     0 |\n>  \t\t|     * Remotes    |     0 |\n>  \t\t|     * Others     |     0 |\n> +\t\t|                  |       |\n> +\t\t| * Objects        |       |\n> +\t\t|   * Count        |     0 |\n> +\t\t|     * Commits    |     0 |\n> +\t\t|     * Trees      |     0 |\n> +\t\t|     * Blobs      |     0 |\n> +\t\t|     * Tags       |     0 |\n>  \t\tEOF\n>  \n>  \t\ttest_cmp expect out &&\n> @@ -49,6 +56,45 @@ test_expect_success 'repository stats with references' '\n>  \t\t|     * Tags       |     1 |\n>  \t\t|     * Remotes    |     1 |\n>  \t\t|     * Others     |     1 |\n> +\t\t|                  |       |\n> +\t\t| * Objects        |       |\n> +\t\t|   * Count        |     5 |\n> +\t\t|     * Commits    |     2 |\n> +\t\t|     * Trees      |     2 |\n> +\t\t|     * Blobs      |     1 |\n> +\t\t|     * Tags       |     0 |\n> +\t\tEOF\n> +\n> +\t\ttest_cmp expect out &&\n> +\t\ttest_line_count = 0 err\n> +\t)\n> +'\n> +\n> +test_expect_success 'repository stats with objects' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_commit_bulk 42 &&\n> +\t\tgit tag -a foo -m bar &&\n> +\t\tgit repo stats >out 2>err &&\n> +\n> +\t\tcat >expect <<-EOF &&\n> +\t\t| Repository stats | Value |\n> +\t\t| ---------------- | ----- |\n> +\t\t| * References     |       |\n> +\t\t|   * Count        |     2 |\n> +\t\t|     * Branches   |     1 |\n> +\t\t|     * Tags       |     1 |\n> +\t\t|     * Remotes    |     0 |\n> +\t\t|     * Others     |     0 |\n> +\t\t|                  |       |\n> +\t\t| * Objects        |       |\n> +\t\t|   * Count        |   127 |\n> +\t\t|     * Commits    |    42 |\n> +\t\t|     * Trees      |    42 |\n> +\t\t|     * Blobs      |    42 |\n> +\t\t|     * Tags       |     1 |\n>  \t\tEOF\n\nI quite like the output format, by the way. It's nice to read and makes\nit sufficiently clear that this is not expected to be parsed by a\nmachine.\n\nPatrick\n"},{"id":"527071","messageId":"aNJ8DIPI0v6Jhn5C@pks.im","threadId":"64187","inReplyTo":"20250923025700.3046260-4-jltobler@gmail.com","subject":"Re: [PATCH 3/4] builtin/repo: add keyvalue format for stats","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-23T10:53:00Z","receivedAt":"2025-09-23T10:53:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Sep 22, 2025 at 09:56:59PM -0500, Justin Tobler wrote:\n> @@ -157,6 +160,8 @@ static int repo_info(int argc, const char **argv, const char *prefix,\n>  \t};\n>  \n>  \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n> +\tif (format == FORMAT_TABLE)\n> +\t\tdie(_(\"table format not supported\"));\n\nWe can deduplicate this error message by saying \"format '%s' not\nsupported\".\n\nOther than that, let's fail closed and say `if (format !=\nFORMAT_KEYVALUE && format != FORMAT_NUL)`. Like this, we won't have to\nupdate the condition every time a new format is added, which is easy to\nforget.\n\n> @@ -286,6 +291,32 @@ static void stats_table_print(struct stats_table *table)\n>  \tstrbuf_release(&buf);\n>  }\n>  \n> +static void stats_print(struct stats *stats)\n\nI think it would make sense to call this `stats_keyvalue_print()` to\nclearly distinguish it from `stats_table_print()`.\n\n> @@ -359,9 +390,16 @@ static void stats_count_objects(struct object_stats *stats,\n>  \tpath_walk_info_clear(&info);\n>  }\n>  \n> -static int repo_stats(int argc UNUSED, const char **argv UNUSED,\n> -\t\t      const char *prefix, struct repository *repo)\n> +static int repo_stats(int argc, const char **argv, const char *prefix,\n> +\t\t      struct repository *repo)\n>  {\n> +\tenum output_format format = FORMAT_TABLE;\n> +\tstruct option options[] = {\n> +\t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n> +\t\t\t       N_(\"output format\"),\n> +\t\t\t       PARSE_OPT_NONEG, parse_format_cb),\n> +\t\tOPT_END()\n> +\t};\n>  \tstruct ref_filter filter = REF_FILTER_INIT;\n>  \tstruct strvec ref_patterns = STRVEC_INIT;\n>  \tstruct stats_table table = { 0 };\n\nNice that we can reuse the callback.\n\n> @@ -369,6 +407,10 @@ static int repo_stats(int argc UNUSED, const char **argv UNUSED,\n>  \tstruct stats stats = { 0 };\n>  \tstruct rev_info revs;\n>  \n> +\tparse_options(argc, argv, prefix, options, repo_usage, 0);\n> +\tif (format == FORMAT_NUL_TERMINATED)\n> +\t\tdie(_(\"nul format not yet supported\"));\n> +\n>  \trepo_init_revisions(repo, &revs, prefix);\n>  \tfilter.name_patterns = ref_patterns.v;\n>  \tfilter_refs(&refs, &filter, FILTER_REFS_REGULAR);\n\nSame comment here regarding failing in a closed way.\n\n> @@ -376,8 +418,17 @@ static int repo_stats(int argc UNUSED, const char **argv UNUSED,\n>  \tstats_count_references(&stats.refs, &refs);\n>  \tstats_count_objects(&stats.objects, &refs, &revs);\n>  \n> -\tstats_table_setup(&table, &stats);\n> -\tstats_table_print(&table);\n> +\tswitch (format) {\n> +\tcase FORMAT_TABLE:\n> +\t\tstats_table_setup(&table, &stats);\n> +\t\tstats_table_print(&table);\n> +\t\tbreak;\n> +\tcase FORMAT_KEYVALUE:\n> +\t\tstats_print(&stats);\n> +\t\tbreak;\n> +\tdefault:\n> +\t\tBUG(\"not a valid output format: %d\", format);\n\nNit: it may be valid, but definitely not supported.\n\nPatrick\n"},{"id":"527072","messageId":"aNJ8EUT_QVCqfFo7@pks.im","threadId":"64187","inReplyTo":"20250923025700.3046260-5-jltobler@gmail.com","subject":"Re: [PATCH 4/4] builtin/repo: add nul format for stats","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-23T10:53:05Z","receivedAt":"2025-09-23T10:53:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Sep 22, 2025 at 09:57:00PM -0500, Justin Tobler wrote:\n> diff --git a/builtin/repo.c b/builtin/repo.c\n> index 4c16a68e4e..37034e6347 100644\n> --- a/builtin/repo.c\n> +++ b/builtin/repo.c\n> @@ -291,27 +291,31 @@ static void stats_table_print(struct stats_table *table)\n>  \tstrbuf_release(&buf);\n>  }\n>  \n> -static void stats_print(struct stats *stats)\n> +static void stats_print(struct stats *stats, int nul_delim)\n\nInstead of passing a boolean-style option, can't we pass the expected\ndelimiter directly? Makes the callsite a bit more obvious.\n\n>  {\n>  \tstruct strbuf buf = STRBUF_INIT;\n> -\n> -\tstrbuf_addf(&buf, \"references.branches.count=%\" PRIuMAX \"\\n\",\n> -\t\t    (uintmax_t)stats->refs.branches);\n> -\tstrbuf_addf(&buf, \"references.tags.count=%\" PRIuMAX \"\\n\",\n> -\t\t    (uintmax_t)stats->refs.tags);\n> -\tstrbuf_addf(&buf, \"references.remotes.count=%\" PRIuMAX \"\\n\",\n> -\t\t    (uintmax_t)stats->refs.remotes);\n> -\tstrbuf_addf(&buf, \"references.others.count=%\" PRIuMAX \"\\n\",\n> -\t\t    (uintmax_t)stats->refs.others);\n> -\n> -\tstrbuf_addf(&buf, \"objects.commits.count=%\" PRIuMAX \"\\n\",\n> -\t\t    (uintmax_t)stats->objects.commits);\n> -\tstrbuf_addf(&buf, \"objects.trees.count=%\" PRIuMAX \"\\n\",\n> -\t\t    (uintmax_t)stats->objects.trees);\n> -\tstrbuf_addf(&buf, \"objects.blobs.count=%\" PRIuMAX \"\\n\",\n> -\t\t    (uintmax_t)stats->objects.blobs);\n> -\tstrbuf_addf(&buf, \"objects.tags.count=%\" PRIuMAX \"\\n\",\n> -\t\t    (uintmax_t)stats->objects.tags);\n> +\tchar delim = '\\n';\n> +\n> +\tif (nul_delim)\n> +\t\tdelim = '\\0';\n> +\n> +\tstrbuf_addf(&buf, \"references.branches.count=%\" PRIuMAX \"%c\",\n> +\t\t    (uintmax_t)stats->refs.branches, delim);\n> +\tstrbuf_addf(&buf, \"references.tags.count=%\" PRIuMAX \"%c\",\n> +\t\t    (uintmax_t)stats->refs.tags, delim);\n> +\tstrbuf_addf(&buf, \"references.remotes.count=%\" PRIuMAX \"%c\",\n> +\t\t    (uintmax_t)stats->refs.remotes, delim);\n> +\tstrbuf_addf(&buf, \"references.others.count=%\" PRIuMAX \"%c\",\n> +\t\t    (uintmax_t)stats->refs.others, delim);\n> +\n> +\tstrbuf_addf(&buf, \"objects.commits.count=%\" PRIuMAX \"%c\",\n> +\t\t    (uintmax_t)stats->objects.commits, delim);\n> +\tstrbuf_addf(&buf, \"objects.trees.count=%\" PRIuMAX \"%c\",\n> +\t\t    (uintmax_t)stats->objects.trees, delim);\n> +\tstrbuf_addf(&buf, \"objects.blobs.count=%\" PRIuMAX \"%c\",\n> +\t\t    (uintmax_t)stats->objects.blobs, delim);\n> +\tstrbuf_addf(&buf, \"objects.tags.count=%\" PRIuMAX \"%c\",\n> +\t\t    (uintmax_t)stats->objects.tags, delim);\n>  \n>  \tfwrite(buf.buf, sizeof(char), buf.len, stdout);\n>  \tstrbuf_release(&buf);\n\nIt's a bit unfortunate we have to rewrite most of the function. I'd\neither have the `delim` parameter right from the start or just squash\nthese two patches together.\n\n> diff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\n> index 5bc6d9d5c4..061b2fbbc1 100755\n> --- a/t/t1901-repo-stats.sh\n> +++ b/t/t1901-repo-stats.sh\n> @@ -127,4 +127,31 @@ test_expect_success 'repository stats with keyvalue format' '\n>  \t)\n>  '\n>  \n> +test_expect_success 'repository stats with nul format' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_commit_bulk 42 &&\n> +\t\tgit tag -a foo -m bar &&\n> +\t\tgit repo stats --format=nul >out 2>err &&\n> +\n> +\t\tcat >expect <<-EOF &&\n> +\t\treferences.branches.count=1\n> +\t\treferences.tags.count=1\n> +\t\treferences.remotes.count=0\n> +\t\treferences.others.count=0\n> +\t\tobjects.commits.count=42\n> +\t\tobjects.trees.count=42\n> +\t\tobjects.blobs.count=42\n> +\t\tobjects.tags.count=1\n> +\t\tEOF\n> +\n> +\t\ttr \"\\n\" \"\\0\" <expect >expect_null &&\n> +\n> +\t\ttest_cmp expect_null out &&\n> +\t\ttest_line_count = 0 err\n> +\t)\n> +'\n\nWe already have a test for the keyvalue format that looks mostly the\nsame, so we may just as well test both formats in a single test.\n\nPatrick\n"},{"id":"527084","messageId":"w4c752odnthf26stoiu7he2xk6fucocmsnxo5pa5vh5sg647g2@a6vphhukasgv","threadId":"64187","inReplyTo":"aNJ7_GoKT5ea4QJE@pks.im","subject":"Re: [PATCH 1/4] builtin/repo: introduce stats subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-23T15:10:50Z","receivedAt":"2025-09-23T15:10:52Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/09/23 12:52PM, Patrick Steinhardt wrote:\n> On Mon, Sep 22, 2025 at 09:56:57PM -0500, Justin Tobler wrote:\n[snip]\n> > Signed-off-by: Justin Tobler <jltobler@gmail.com>\n> \n> Is this command built on Derrick's git-survey(1)? If so, it would\n> probably be nice to add a \"Based-on-patch-by\" tag.\n\nThe git-survey(1) series sent by Derrick certainly served as inspiration\nfor this series. I didn't build this series from those patches, but\nthere is probably some overlap since I'm trying to accomplish something\nvery similar. I don't mind at all adding a \"Based-on-patch-by\" tag\nthough. Will do in the next version :)\n\n[snip]\n> > +\tstruct string_list_item *item;\n> > +\n> > +\titem = string_list_append(&table->rows, name);\n> > +\titem->util = entry;\n> > +\n> > +\tif (name_width > table->name_col_width)\n> > +\t\ttable->name_col_width = name_width;\n> > +\tif (entry) {\n> > +\t\tint value_width = strlen(entry->value);\n> > +\t\tif (value_width > table->value_col_width)\n> > +\t\t\ttable->value_col_width = value_width;\n> > +\t}\n> \n> I was wondering at first why you'd ever want to not pass an entry, but\n> we use that to have \"dividers\" in the table. Makes sense.\n\nYup. Also, some rows like \"* References\" may have a name, but no value.\n\n> > +}\n> > +\n> > +static void stats_table_add_count(struct stats_table *table, const char *name,\n> > +\t\t\t\t  size_t value)\n> > +{\n> > +\tstruct stats_table_entry *entry;\n> > +\n> > +\tCALLOC_ARRAY(entry, 1);\n> > +\tentry->value = xstrfmt(\"%\" PRIuMAX, (uintmax_t)value);\n> > +\tstats_table_add(table, name, entry);\n> > +}\n> > +\n> > +static void stats_table_setup(struct stats_table *table, struct stats *stats)\n> > +{\n> > +\tsize_t ref_total;\n> > +\n> > +\tref_total = stats->branches + stats->remotes + stats->tags + stats->others;\n> > +\tstats_table_add(table, _(\"* References\"), NULL);\n> > +\tstats_table_add_count(table, _(\"  * Count\"), ref_total);\n> > +\tstats_table_add_count(table, _(\"    * Branches\"), stats->branches);\n> > +\tstats_table_add_count(table, _(\"    * Tags\"), stats->tags);\n> > +\tstats_table_add_count(table, _(\"    * Remotes\"), stats->remotes);\n> > +\tstats_table_add_count(table, _(\"    * Others\"), stats->others);\n> > +}\n> \n> Would it make sense to not translate the formatting directives, but only\n> the actual words?\n\nFrom a simplicity stand point, it is quite nice to have the formatted\noffsets baked-in. It is probably better to separate out the\ntransalations though? I'll interate on this in the next version.\n\n> > +\tstruct strbuf buf = STRBUF_INIT;\n> > +\tstruct string_list_item *item;\n> > +\n> > +\tif (table->name_col_width > name_col_width)\n> > +\t\tname_col_width = table->name_col_width;\n> > +\tif (table->value_col_width > value_col_width)\n> > +\t\tvalue_col_width = table->value_col_width;\n> > +\n> > +\tstrbuf_addf(&buf, \"| %-*s | %-*s |\\n\", name_col_width, name_col_title,\n> > +\t\t    value_col_width, value_col_title);\n> \n> Aha, that's why you went with `int`. You can use `cast_size_to_to_int()`\n> to convert between the types.\n\nYep :) I'll adapt following your suggestion in the next version.\n\n> > +\tstrbuf_addstr(&buf, \"| \");\n> > +\tstrbuf_addchars(&buf, '-', name_col_width);\n> > +\tstrbuf_addstr(&buf, \" | \");\n> > +\tstrbuf_addchars(&buf, '-', value_col_width);\n> > +\tstrbuf_addstr(&buf, \" |\\n\");\n> > +\n> > +\tfor_each_string_list_item (item, &table->rows) {\n> \n> We typically don't have a space after between the macro and its\n> arguments.\n\nYour right! I seem to recall the style linter wanted me to have it this\nway, but I'll change it back to be consistent.\n\n> > +\t\tstruct stats_table_entry *entry = item->util;\n> > +\t\tconst char *value = \"\";\n> > +\n> > +\t\tif (entry) {\n> > +\t\t\tstruct stats_table_entry *entry = item->util;\n> > +\t\t\tvalue = entry->value;\n> > +\t\t}\n> > +\n> > +\t\tstrbuf_addf(&buf, \"| %-*s | %*s |\\n\", name_col_width,\n> > +\t\t\t    item->string, value_col_width, value);\n> > +\n> > +\t\tif (entry)\n> > +\t\t\tfree(entry->value);\n> \n> It's a bit weird that we free the values when we pretend to only print\n> data. Sure, we probably don't ever have a usecase where we want to print\n> data a second time. But I still think it would be nice to separate\n> concerns.\n\nThat's fair. I'll probably add a stats_table_clear() in the next\nversion.\n\n> > +\t}\n> > +\n> > +\tfputs(buf.buf, stdout);\n> > +\tstrbuf_release(&buf);\n> > +}\n> > +\n> > +static void stats_count_references(struct stats *stats, struct ref_array *refs)\n> > +{\n> > +\tfor (int i = 0; i < refs->nr; i++) {\n> > +\t\tstruct ref_array_item *ref = refs->items[i];\n> > +\n> > +\t\tswitch (ref->kind) {\n> > +\t\tcase FILTER_REFS_BRANCHES:\n> > +\t\t\tstats->branches++;\n> > +\t\t\tbreak;\n> > +\t\tcase FILTER_REFS_REMOTES:\n> > +\t\t\tstats->remotes++;\n> > +\t\t\tbreak;\n> > +\t\tcase FILTER_REFS_TAGS:\n> > +\t\t\tstats->tags++;\n> > +\t\t\tbreak;\n> > +\t\tcase FILTER_REFS_OTHERS:\n> > +\t\t\tstats->others++;\n> > +\t\t\tbreak;\n> \n> Do we want to have a `default:` case where we `BUG()`? Otherwise we may\n> not notice that we undercount the overall number of refs.\n\nSince filter_refs() is only checking regular references, we shouldn't\never encounter other types, but it doesn't hurt to BUG() here if it were\nto happen. Will do.\n\n> > +\t\t}\n> > +\t}\n> > +}\n> > +\n> > +static int repo_stats(int argc UNUSED, const char **argv UNUSED,\n> > +\t\t      const char *prefix UNUSED, struct repository *repo UNUSED)\n> \n> Not a new issue, but I'd rather call this `cmd_repo_stats()` to note\n> that this is the entrypoint. We might as well adapt the other subcommand\n> to follow that naming schema in a preparatory commit.\n\nMake sense. I'll update repo_info() in a preparatory commit as\nsuggested.\n\n> > +{\n> > +\tstruct ref_filter filter = REF_FILTER_INIT;\n> > +\tstruct strvec ref_patterns = STRVEC_INIT;\n> > +\tstruct stats_table table = { 0 };\n> > +\tstruct ref_array refs = { 0 };\n> > +\tstruct stats stats = { 0 };\n> > +\n> > +\tfilter.name_patterns = ref_patterns.v;\n> > +\tfilter_refs(&refs, &filter, FILTER_REFS_REGULAR);\n> \n> `filter_refs()` may return an error code which we should probably\n> handle.\n\nWill do.\n\n> > diff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\n> > new file mode 100755\n> > index 0000000000..27c32ec45f\n> > --- /dev/null\n> > +++ b/t/t1901-repo-stats.sh\n> > @@ -0,0 +1,59 @@\n> > +#!/bin/sh\n> > +\n> > +test_description='test git repo stats'\n> > +\n> > +. ./test-lib.sh\n> > +\n> > +test_expect_success 'empty repository stats' '\n> \n> Nit: I don't think it's necessary to repeat \"repository stats\" in every\n> test name. That's already clear from the test suite.\n\nThat's fair. Will adapt the tests accordingly.\n\nThanks for the review,\n-Justin\n"},{"id":"527085","messageId":"rarmjytj4aci2uwewga3firgh3py65cd4nuu3dq52bxvjejp2a@hg2i3fpzjcir","threadId":"64187","inReplyTo":"aNJ8BvTZ_yNSrBA6@pks.im","subject":"Re: [PATCH 2/4] builtin/repo: add object counts in stats output","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-23T15:19:51Z","receivedAt":"2025-09-23T15:19:53Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/09/23 12:52PM, Patrick Steinhardt wrote:\n> On Mon, Sep 22, 2025 at 09:56:58PM -0500, Justin Tobler wrote:\n> > diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\n> > index 7762329551..2a67abfca8 100644\n> > --- a/Documentation/git-repo.adoc\n> > +++ b/Documentation/git-repo.adoc\n> > @@ -45,8 +45,9 @@ supported:\n> >  `-z` is an alias for `--format=nul`.\n> >  \n> >  stats::\n> > -\tRetrieve stats about the current repository. All references in the\n> > -\trepository are categorized and counted accordingly.\n> > +\tRetrieve stats about the current repository. All references and\n> > +\treachable objects in the repository are categorized and counted\n> > +\taccordingly.\n> >  +\n> >  The table output format may change and is not intended for machine parsing.\n> \n> I already wanted to mention this on the first commit, but would it maybe\n> make sense if this was a bulleted list of information that we surface\n> right from the start? Then we don't have to reflow the whole paragraph\n> every time we surface new information.\n\nMakes sense.\n\n> > diff --git a/builtin/repo.c b/builtin/repo.c\n> > index 15899dd74c..a24ea0e66b 100644\n> > --- a/builtin/repo.c\n> > +++ b/builtin/repo.c\n> > @@ -159,13 +161,25 @@ static int repo_info(int argc, const char **argv, const char *prefix,\n> >  \treturn print_fields(argc, argv, repo, format);\n> >  }\n> >  \n> > -struct stats {\n> > +struct ref_stats {\n> \n> Nit: let's call it `ref_stats` right from the start instead of renaming.\n\nWill do.\n\n> >  \tsize_t branches;\n> >  \tsize_t remotes;\n> >  \tsize_t tags;\n> >  \tsize_t others;\n> >  };\n> >  \n> > +struct object_stats {\n> > +\tsize_t tags;\n> > +\tsize_t commits;\n> > +\tsize_t trees;\n> > +\tsize_t blobs;\n> > +};\n> > +\n> > +struct stats {\n> \n> I'd maybe call this `struct repo_stats`. `stats` feels quite generic and\n> very close to a collision with `struct stat`.\n\nThat's fair. I quite like the name `repo_stats`. I'll use that instead.\nThanks\n\n> \n> > @@ -207,15 +221,27 @@ static void stats_table_add_count(struct stats_table *table, const char *name,\n> >  \n> >  static void stats_table_setup(struct stats_table *table, struct stats *stats)\n> >  {\n> > +\tstruct object_stats objects = stats->objects;\n> > +\tstruct ref_stats refs = stats->refs;\n> \n> We can avoid the copies by making these pointers. Not that it'd really\n> matter all that much.\n\nYa, I'll change this in the next version.\n\n> > +\tsize_t object_total;\n> >  \tsize_t ref_total;\n> >  \n> > -\tref_total = stats->branches + stats->remotes + stats->tags + stats->others;\n> > +\tref_total = refs.branches + refs.remotes + refs.tags + refs.others;\n> >  \tstats_table_add(table, _(\"* References\"), NULL);\n> >  \tstats_table_add_count(table, _(\"  * Count\"), ref_total);\n> > -\tstats_table_add_count(table, _(\"    * Branches\"), stats->branches);\n> > -\tstats_table_add_count(table, _(\"    * Tags\"), stats->tags);\n> > -\tstats_table_add_count(table, _(\"    * Remotes\"), stats->remotes);\n> > -\tstats_table_add_count(table, _(\"    * Others\"), stats->others);\n> > +\tstats_table_add_count(table, _(\"    * Branches\"), refs.branches);\n> > +\tstats_table_add_count(table, _(\"    * Tags\"), refs.tags);\n> > +\tstats_table_add_count(table, _(\"    * Remotes\"), refs.remotes);\n> > +\tstats_table_add_count(table, _(\"    * Others\"), refs.others);\n> > +\n> > +\tobject_total = objects.commits + objects.trees + objects.blobs + objects.tags;\n> > +\tstats_table_add(table, \"\", NULL);\n> > +\tstats_table_add(table, _(\"* Objects\"), NULL);\n> \n> Should we maybe say \"Reachable objects\" here to clarify that this\n> doesn't count unreachable ones?\n\nGood suggestion. Will update.\n\n> > @@ -282,25 +308,80 @@ static void stats_count_references(struct stats *stats, struct ref_array *refs)\n> >  \t}\n> >  }\n> >  \n> > +static int count_objects(const char *path UNUSED, struct oid_array *oids,\n> > +\t\t\t enum object_type type, void *data)\n> > +{\n> > +\tstruct object_stats *stats = data;\n> > +\n> > +\tswitch (type) {\n> > +\tcase OBJ_TAG:\n> > +\t\tstats->tags += oids->nr;\n> > +\t\tbreak;\n> > +\tcase OBJ_COMMIT:\n> > +\t\tstats->commits += oids->nr;\n> > +\t\tbreak;\n> > +\tcase OBJ_TREE:\n> > +\t\tstats->trees += oids->nr;\n> > +\t\tbreak;\n> > +\tcase OBJ_BLOB:\n> > +\t\tstats->blobs += oids->nr;\n> > +\t\tbreak;\n> > +\tdefault:\n> \n> Let's `BUG()` here. This case should never happen, and if it does\n> something is seriously wrong.\n\nI agree it doesn't hurt to be more defensive here. I'll update in the\nnext version.\n\n> > +\t\tbreak;\n> > +\t}\n> > +\n> > +\treturn 0;\n> > +}\n> > +\n> > +static void stats_count_objects(struct object_stats *stats,\n> > +\t\t\t\tstruct ref_array *refs, struct rev_info *revs)\n> > +{\n> > +\tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n> > +\n> > +\tinfo.revs = revs;\n> > +\tinfo.path_fn = count_objects;\n> > +\tinfo.path_fn_data = stats;\n> > +\n> > +\tfor (int i = 0; i < refs->nr; i++) {\n> > +\t\tstruct ref_array_item *ref = refs->items[i];\n> > +\n> > +\t\tswitch (ref->kind) {\n> > +\t\tcase FILTER_REFS_BRANCHES:\n> > +\t\tcase FILTER_REFS_TAGS:\n> > +\t\tcase FILTER_REFS_REMOTES:\n> > +\t\tcase FILTER_REFS_OTHERS:\n> > +\t\t\tadd_pending_oid(revs, NULL, &ref->objectname, 0);\n> > +\t\t\tbreak;\n> > +\t\t}\n> > +\t}\n> > +\n> > +\twalk_objects_by_path(&info);\n> > +\tpath_walk_info_clear(&info);\n> > +}\n> \n> I guess this can take a while, so having a progress meter would be great\n> to have to give the user some info what's happening. I guess it doesn't\n> have to be part of the first iteration thuogh as long as this is\n> something we plan to add at a later point.\n\nYa, I was planning on adding a progress meter in the future. It may not\nbe too much to add it as part of this series though. I'll take a look.\n\n-Justin\n"},{"id":"527086","messageId":"CAOLa=ZQHU8m-BLzD=uWx6e-FVcs-aDmrUp9qrU72_5E=CNQRNg@mail.gmail.com","threadId":"64187","inReplyTo":"20250923025700.3046260-2-jltobler@gmail.com","subject":"Re: [PATCH 1/4] builtin/repo: introduce stats subcommand","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-09-23T15:22:17Z","receivedAt":"2025-09-23T15:22:19Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n[snip]\n\n> diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\n> index 209afd1b61..7762329551 100644\n> --- a/Documentation/git-repo.adoc\n> +++ b/Documentation/git-repo.adoc\n> @@ -9,6 +9,7 @@ SYNOPSIS\n>  --------\n>  [synopsis]\n>  git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n> +git repo stats\n>\n>  DESCRIPTION\n>  -----------\n> @@ -43,6 +44,12 @@ supported:\n>  +\n>  `-z` is an alias for `--format=nul`.\n>\n> +stats::\n> +\tRetrieve stats about the current repository. All references in the\n> +\trepository are categorized and counted accordingly.\n> ++\n\nNit: Should we use bullet points for the information provided? As of\nthis commit it is only references, but with time, this will grow, so\nwould be nice to have a set of bullet points to understand the different\ninformation retrieved.\n\n> +The table output format may change and is not intended for machine parsing.\n> +\n>  INFO KEYS\n>  ---------\n>  In order to obtain a set of values from `git repo info`, you should provide\n> diff --git a/builtin/repo.c b/builtin/repo.c\n> index bbb0966f2d..15899dd74c 100644\n> --- a/builtin/repo.c\n> +++ b/builtin/repo.c\n> @@ -4,12 +4,15 @@\n>  #include \"environment.h\"\n>  #include \"parse-options.h\"\n>  #include \"quote.h\"\n> +#include \"ref-filter.h\"\n>  #include \"refs.h\"\n>  #include \"strbuf.h\"\n> +#include \"string-list.h\"\n>  #include \"shallow.h\"\n>\n>  static const char *const repo_usage[] = {\n>  \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n> +\t\"git repo stats\",\n>  \tNULL\n>  };\n>\n> @@ -156,12 +159,160 @@ static int repo_info(int argc, const char **argv, const char *prefix,\n>  \treturn print_fields(argc, argv, repo, format);\n>  }\n>\n> +struct stats {\n> +\tsize_t branches;\n> +\tsize_t remotes;\n> +\tsize_t tags;\n> +\tsize_t others;\n\nMaybe we can use a nested structure here, which would reflect the output\ntable. That would be much nicer, as the code would grow, currently we\nknow 'others' here refers to references, but with other fields coming\nin, this would no longer be obvious.\n\n> +};\n> +\n> +struct stats_table {\n> +\tstruct string_list rows;\n> +\n> +\tint name_col_width;\n> +\tint value_col_width;\n\nCan these be negative?\n\n> +};\n> +\n\nNit: Could we add a comment about what this structure is and what it\nholds? I think it is sort of obvious, but would be nice to clarify.\n\n\n> +struct stats_table_entry {\n> +\tchar *value;\n> +};\n> +\n> +static void stats_table_add(struct stats_table *table, const char *name,\n> +\t\t\t    struct stats_table_entry *entry)\n> +{\n> +\tint name_width = strlen(name);\n> +\tstruct string_list_item *item;\n> +\n> +\titem = string_list_append(&table->rows, name);\n> +\titem->util = entry;\n> +\n> +\tif (name_width > table->name_col_width)\n> +\t\ttable->name_col_width = name_width;\n> +\tif (entry) {\n> +\t\tint value_width = strlen(entry->value);\n> +\t\tif (value_width > table->value_col_width)\n> +\t\t\ttable->value_col_width = value_width;\n> +\t}\n> +}\n> +\n> +static void stats_table_add_count(struct stats_table *table, const char *name,\n> +\t\t\t\t  size_t value)\n> +{\n> +\tstruct stats_table_entry *entry;\n> +\n> +\tCALLOC_ARRAY(entry, 1);\n> +\tentry->value = xstrfmt(\"%\" PRIuMAX, (uintmax_t)value);\n> +\tstats_table_add(table, name, entry);\n> +}\n> +\n> +static void stats_table_setup(struct stats_table *table, struct stats *stats)\n> +{\n> +\tsize_t ref_total;\n> +\n> +\tref_total = stats->branches + stats->remotes + stats->tags + stats->others;\n> +\tstats_table_add(table, _(\"* References\"), NULL);\n> +\tstats_table_add_count(table, _(\"  * Count\"), ref_total);\n> +\tstats_table_add_count(table, _(\"    * Branches\"), stats->branches);\n> +\tstats_table_add_count(table, _(\"    * Tags\"), stats->tags);\n> +\tstats_table_add_count(table, _(\"    * Remotes\"), stats->remotes);\n> +\tstats_table_add_count(table, _(\"    * Others\"), stats->others);\n> +}\n> +\n> +static void stats_table_print(struct stats_table *table)\n> +{\n> +\tconst char *name_col_title = _(\"Repository stats\");\n> +\tconst char *value_col_title = _(\"Value\");\n> +\tint name_col_width = strlen(name_col_title);\n> +\tint value_col_width = strlen(value_col_title);\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\tstruct string_list_item *item;\n> +\n> +\tif (table->name_col_width > name_col_width)\n> +\t\tname_col_width = table->name_col_width;\n> +\tif (table->value_col_width > value_col_width)\n> +\t\tvalue_col_width = table->value_col_width;\n> +\n> +\tstrbuf_addf(&buf, \"| %-*s | %-*s |\\n\", name_col_width, name_col_title,\n> +\t\t    value_col_width, value_col_title);\n> +\tstrbuf_addstr(&buf, \"| \");\n> +\tstrbuf_addchars(&buf, '-', name_col_width);\n> +\tstrbuf_addstr(&buf, \" | \");\n> +\tstrbuf_addchars(&buf, '-', value_col_width);\n> +\tstrbuf_addstr(&buf, \" |\\n\");\n> +\n> +\tfor_each_string_list_item (item, &table->rows) {\n> +\t\tstruct stats_table_entry *entry = item->util;\n> +\t\tconst char *value = \"\";\n> +\n> +\t\tif (entry) {\n> +\t\t\tstruct stats_table_entry *entry = item->util;\n> +\t\t\tvalue = entry->value;\n> +\t\t}\n> +\n> +\t\tstrbuf_addf(&buf, \"| %-*s | %*s |\\n\", name_col_width,\n> +\t\t\t    item->string, value_col_width, value);\n> +\n> +\t\tif (entry)\n> +\t\t\tfree(entry->value);\n> +\t}\n> +\n> +\tfputs(buf.buf, stdout);\n> +\tstrbuf_release(&buf);\n> +}\n> +\n> +static void stats_count_references(struct stats *stats, struct ref_array *refs)\n> +{\n> +\tfor (int i = 0; i < refs->nr; i++) {\n> +\t\tstruct ref_array_item *ref = refs->items[i];\n> +\n> +\t\tswitch (ref->kind) {\n> +\t\tcase FILTER_REFS_BRANCHES:\n> +\t\t\tstats->branches++;\n> +\t\t\tbreak;\n> +\t\tcase FILTER_REFS_REMOTES:\n> +\t\t\tstats->remotes++;\n> +\t\t\tbreak;\n> +\t\tcase FILTER_REFS_TAGS:\n> +\t\t\tstats->tags++;\n> +\t\t\tbreak;\n> +\t\tcase FILTER_REFS_OTHERS:\n> +\t\t\tstats->others++;\n> +\t\t\tbreak;\n> +\t\t}\n> +\t}\n> +}\n> +\n> +static int repo_stats(int argc UNUSED, const char **argv UNUSED,\n> +\t\t      const char *prefix UNUSED, struct repository *repo UNUSED)\n> +{\n> +\tstruct ref_filter filter = REF_FILTER_INIT;\n> +\tstruct strvec ref_patterns = STRVEC_INIT;\n> +\tstruct stats_table table = { 0 };\n> +\tstruct ref_array refs = { 0 };\n> +\tstruct stats stats = { 0 };\n> +\n> +\tfilter.name_patterns = ref_patterns.v;\n> +\tfilter_refs(&refs, &filter, FILTER_REFS_REGULAR);\n> +\n\nI was wondering why we need the filter mechanism here, but seems like\nthis is to obtain the type of the reference. This is setup automatically\nby the filter mechanism and so it's okay.\n\nWe could replicate the same using the ref iterator code. This would be a\nlittle more involved, but would remove the need to store all refs in a\n'ref_array' and also the need to loop over references twice. But it\ndoesn't really matter in this usecase I assume.\n\n> +\tstats_count_references(&stats, &refs);\n> +\n> +\tstats_table_setup(&table, &stats);\n> +\tstats_table_print(&table);\n> +\n> +\tstring_list_clear(&table.rows, 1);\n> +\tstrvec_clear(&ref_patterns);\n\nHuh. So `ref_patterns` is simply a dummy variable, I was wondering why\nwe simply can't set `filter.name_patterns = NULL`. I see that it is\nbecause in `filter_pattern_match()` we do `if\n(!*filter->name_patterns)`. This is not a great interface for\nref-filter. Perhaps we could add a precursor commit like:\n\ndiff --git a/ref-filter.c b/ref-filter.c\nindex 520d2539c9..20284b5918 100644\n--- a/ref-filter.c\n+++ b/ref-filter.c\n@@ -2664,7 +2664,7 @@ static int match_name_as_path(const char\n**pattern, const char *refname,\n /* Return 1 if the refname matches one of the patterns, otherwise 0. */\n static int filter_pattern_match(struct ref_filter *filter, const char *refname)\n {\n-\tif (!*filter->name_patterns)\n+\tif (!filter->name_patterns || !*filter->name_patterns)\n \t\treturn 1; /* No pattern always matches */\n \tif (filter->match_as_path)\n \t\treturn match_name_as_path(filter->name_patterns, refname,\ndiff --git a/ref-filter.h b/ref-filter.h\nindex f22ca94b49..44d9b481ad 100644\n--- a/ref-filter.h\n+++ b/ref-filter.h\n@@ -109,6 +109,7 @@ struct ref_format {\n #define REF_FILTER_INIT { \\\n \t.points_at = OID_ARRAY_INIT, \\\n \t.exclude = STRVEC_INIT, \\\n+\t.name_pattern = NULL, \\\n }\n #define REF_FORMAT_INIT {             \\\n \t.use_color = -1,              \\\n\n\nand then we could modify this commit:\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 15899dd74c..09733b8df7 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -286,12 +286,11 @@ static int repo_stats(int argc UNUSED, const\nchar **argv UNUSED,\n \t\t      const char *prefix UNUSED, struct repository *repo UNUSED)\n {\n \tstruct ref_filter filter = REF_FILTER_INIT;\n-\tstruct strvec ref_patterns = STRVEC_INIT;\n \tstruct stats_table table = { 0 };\n \tstruct ref_array refs = { 0 };\n \tstruct stats stats = { 0 };\n\n-\tfilter.name_patterns = ref_patterns.v;\n \tfilter_refs(&refs, &filter, FILTER_REFS_REGULAR);\n\n \tstats_count_references(&stats, &refs);\n@@ -300,7 +299,6 @@ static int repo_stats(int argc UNUSED, const char\n**argv UNUSED,\n \tstats_table_print(&table);\n\n \tstring_list_clear(&table.rows, 1);\n-\tstrvec_clear(&ref_patterns);\n \tref_array_clear(&refs);\n\n \treturn 0;\n\n> +\tref_array_clear(&refs);\n> +\n> +\treturn 0;\n> +}\n> +\n>  int cmd_repo(int argc, const char **argv, const char *prefix,\n>  \t     struct repository *repo)\n>  {\n>  \tparse_opt_subcommand_fn *fn = NULL;\n>  \tstruct option options[] = {\n>  \t\tOPT_SUBCOMMAND(\"info\", &fn, repo_info),\n> +\t\tOPT_SUBCOMMAND(\"stats\", &fn, repo_stats),\n>  \t\tOPT_END()\n>  \t};\n>\n\n[snip]\n"},{"id":"527088","messageId":"aNK8KujMRfV93nag@pks.im","threadId":"64187","inReplyTo":"w4c752odnthf26stoiu7he2xk6fucocmsnxo5pa5vh5sg647g2@a6vphhukasgv","subject":"Re: [PATCH 1/4] builtin/repo: introduce stats subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-23T15:26:34Z","receivedAt":"2025-09-23T15:26:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Sep 23, 2025 at 10:10:50AM -0500, Justin Tobler wrote:\n> On 25/09/23 12:52PM, Patrick Steinhardt wrote:\n> > On Mon, Sep 22, 2025 at 09:56:57PM -0500, Justin Tobler wrote:\n[snip]\n> > > +static void stats_table_setup(struct stats_table *table, struct stats *stats)\n> > > +{\n> > > +\tsize_t ref_total;\n> > > +\n> > > +\tref_total = stats->branches + stats->remotes + stats->tags + stats->others;\n> > > +\tstats_table_add(table, _(\"* References\"), NULL);\n> > > +\tstats_table_add_count(table, _(\"  * Count\"), ref_total);\n> > > +\tstats_table_add_count(table, _(\"    * Branches\"), stats->branches);\n> > > +\tstats_table_add_count(table, _(\"    * Tags\"), stats->tags);\n> > > +\tstats_table_add_count(table, _(\"    * Remotes\"), stats->remotes);\n> > > +\tstats_table_add_count(table, _(\"    * Others\"), stats->others);\n> > > +}\n> > \n> > Would it make sense to not translate the formatting directives, but only\n> > the actual words?\n> \n> From a simplicity stand point, it is quite nice to have the formatted\n> offsets baked-in. It is probably better to separate out the\n> transalations though? I'll interate on this in the next version.\n\nYeah, I don't mind the baked-in offsets. We could allow formatting\ndirectives here and then pass the translated nouns as varargs.\n\nPatrick\n"},{"id":"527089","messageId":"5kwuxyttcnhe57hej3xfmaunbjmpu7og52ft2bb4uawjqq4lvb@s3zub5742v4e","threadId":"64187","inReplyTo":"aNJ8DIPI0v6Jhn5C@pks.im","subject":"Re: [PATCH 3/4] builtin/repo: add keyvalue format for stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-23T15:26:46Z","receivedAt":"2025-09-23T15:26:48Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/09/23 12:53PM, Patrick Steinhardt wrote:\n> On Mon, Sep 22, 2025 at 09:56:59PM -0500, Justin Tobler wrote:\n> > @@ -157,6 +160,8 @@ static int repo_info(int argc, const char **argv, const char *prefix,\n> >  \t};\n> >  \n> >  \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n> > +\tif (format == FORMAT_TABLE)\n> > +\t\tdie(_(\"table format not supported\"));\n> \n> We can deduplicate this error message by saying \"format '%s' not\n> supported\".\n> \n> Other than that, let's fail closed and say `if (format !=\n> FORMAT_KEYVALUE && format != FORMAT_NUL)`. Like this, we won't have to\n> update the condition every time a new format is added, which is easy to\n> forget.\n\nThat's fair. Will update.\n\n> > @@ -286,6 +291,32 @@ static void stats_table_print(struct stats_table *table)\n> >  \tstrbuf_release(&buf);\n> >  }\n> >  \n> > +static void stats_print(struct stats *stats)\n> \n> I think it would make sense to call this `stats_keyvalue_print()` to\n> clearly distinguish it from `stats_table_print()`.\n\nI adapt this same function in the next version to also print the NUL\nformat which is why I kept it generic. The NUL format is really still\njust a key-format just with different delimiters. I'll change this in\nthe next version.\n\n[snip]\n> > @@ -376,8 +418,17 @@ static int repo_stats(int argc UNUSED, const char **argv UNUSED,\n> >  \tstats_count_references(&stats.refs, &refs);\n> >  \tstats_count_objects(&stats.objects, &refs, &revs);\n> >  \n> > -\tstats_table_setup(&table, &stats);\n> > -\tstats_table_print(&table);\n> > +\tswitch (format) {\n> > +\tcase FORMAT_TABLE:\n> > +\t\tstats_table_setup(&table, &stats);\n> > +\t\tstats_table_print(&table);\n> > +\t\tbreak;\n> > +\tcase FORMAT_KEYVALUE:\n> > +\t\tstats_print(&stats);\n> > +\t\tbreak;\n> > +\tdefault:\n> > +\t\tBUG(\"not a valid output format: %d\", format);\n> \n> Nit: it may be valid, but definitely not supported.\n\nAt this point, the only other valid option was handled above. This would\nleave only invalid options. I think in the next version I'm just going\nto combine patches 3 and 4 though to avoid this issue altogether though.\n\n-Justin\n"},{"id":"527091","messageId":"CAOLa=ZTihUt0d-3_LOydX4GxWPSquAOWkzw3+0XzM-zp6wFZ+w@mail.gmail.com","threadId":"64187","inReplyTo":"20250923025700.3046260-3-jltobler@gmail.com","subject":"Re: [PATCH 2/4] builtin/repo: add object counts in stats output","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-09-23T15:30:06Z","receivedAt":"2025-09-23T15:30:09Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n[snip]\n\n> @@ -159,13 +161,25 @@ static int repo_info(int argc, const char **argv, const char *prefix,\n>  \treturn print_fields(argc, argv, repo, format);\n>  }\n>\n> -struct stats {\n> +struct ref_stats {\n>  \tsize_t branches;\n>  \tsize_t remotes;\n>  \tsize_t tags;\n>  \tsize_t others;\n>  };\n>\n> +struct object_stats {\n> +\tsize_t tags;\n> +\tsize_t commits;\n> +\tsize_t trees;\n> +\tsize_t blobs;\n> +};\n> +\n> +struct stats {\n> +\tstruct ref_stats refs;\n> +\tstruct object_stats objects;\n> +};\n> +\n\nOkay so we do use nested structs, which I mentioned in the previous\ncommit, so we're all good.\n\nNit: A lot of the diff in this commit is because we don't make the\nrequired structs in the previous commit. Perhaps we could start with the\nnested structs in the previous commit? That way this patch would be much\nsimpler.\n\n[snip]\n"},{"id":"527092","messageId":"r2rt5aq64x232gmpj5wzzchut2w2axnkyfzg7wpthwuf2w3j4f@teydueqvon5d","threadId":"64187","inReplyTo":"aNJ8EUT_QVCqfFo7@pks.im","subject":"Re: [PATCH 4/4] builtin/repo: add nul format for stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-23T15:33:45Z","receivedAt":"2025-09-23T15:33:46Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/09/23 12:53PM, Patrick Steinhardt wrote:\n> On Mon, Sep 22, 2025 at 09:57:00PM -0500, Justin Tobler wrote:\n> > diff --git a/builtin/repo.c b/builtin/repo.c\n> > index 4c16a68e4e..37034e6347 100644\n> > --- a/builtin/repo.c\n> > +++ b/builtin/repo.c\n> > @@ -291,27 +291,31 @@ static void stats_table_print(struct stats_table *table)\n> >  \tstrbuf_release(&buf);\n> >  }\n> >  \n> > -static void stats_print(struct stats *stats)\n> > +static void stats_print(struct stats *stats, int nul_delim)\n> \n> Instead of passing a boolean-style option, can't we pass the expected\n> delimiter directly? Makes the callsite a bit more obvious.\n\nYa, we could do that here instead. Something I just noticed is that the\nNUL format in `git repo info` also replaces the '=' delimiter with a\nnewline. I'm not sure if it would be best to match the same behavior\nhere?\n\nIf so, we would have to either pass both delimiters as arguments to the\nfunction, or just keep the boolean toggle for the mode.\n\n> >  {\n> >  \tstruct strbuf buf = STRBUF_INIT;\n> > -\n> > -\tstrbuf_addf(&buf, \"references.branches.count=%\" PRIuMAX \"\\n\",\n> > -\t\t    (uintmax_t)stats->refs.branches);\n> > -\tstrbuf_addf(&buf, \"references.tags.count=%\" PRIuMAX \"\\n\",\n> > -\t\t    (uintmax_t)stats->refs.tags);\n> > -\tstrbuf_addf(&buf, \"references.remotes.count=%\" PRIuMAX \"\\n\",\n> > -\t\t    (uintmax_t)stats->refs.remotes);\n> > -\tstrbuf_addf(&buf, \"references.others.count=%\" PRIuMAX \"\\n\",\n> > -\t\t    (uintmax_t)stats->refs.others);\n> > -\n> > -\tstrbuf_addf(&buf, \"objects.commits.count=%\" PRIuMAX \"\\n\",\n> > -\t\t    (uintmax_t)stats->objects.commits);\n> > -\tstrbuf_addf(&buf, \"objects.trees.count=%\" PRIuMAX \"\\n\",\n> > -\t\t    (uintmax_t)stats->objects.trees);\n> > -\tstrbuf_addf(&buf, \"objects.blobs.count=%\" PRIuMAX \"\\n\",\n> > -\t\t    (uintmax_t)stats->objects.blobs);\n> > -\tstrbuf_addf(&buf, \"objects.tags.count=%\" PRIuMAX \"\\n\",\n> > -\t\t    (uintmax_t)stats->objects.tags);\n> > +\tchar delim = '\\n';\n> > +\n> > +\tif (nul_delim)\n> > +\t\tdelim = '\\0';\n> > +\n> > +\tstrbuf_addf(&buf, \"references.branches.count=%\" PRIuMAX \"%c\",\n> > +\t\t    (uintmax_t)stats->refs.branches, delim);\n> > +\tstrbuf_addf(&buf, \"references.tags.count=%\" PRIuMAX \"%c\",\n> > +\t\t    (uintmax_t)stats->refs.tags, delim);\n> > +\tstrbuf_addf(&buf, \"references.remotes.count=%\" PRIuMAX \"%c\",\n> > +\t\t    (uintmax_t)stats->refs.remotes, delim);\n> > +\tstrbuf_addf(&buf, \"references.others.count=%\" PRIuMAX \"%c\",\n> > +\t\t    (uintmax_t)stats->refs.others, delim);\n> > +\n> > +\tstrbuf_addf(&buf, \"objects.commits.count=%\" PRIuMAX \"%c\",\n> > +\t\t    (uintmax_t)stats->objects.commits, delim);\n> > +\tstrbuf_addf(&buf, \"objects.trees.count=%\" PRIuMAX \"%c\",\n> > +\t\t    (uintmax_t)stats->objects.trees, delim);\n> > +\tstrbuf_addf(&buf, \"objects.blobs.count=%\" PRIuMAX \"%c\",\n> > +\t\t    (uintmax_t)stats->objects.blobs, delim);\n> > +\tstrbuf_addf(&buf, \"objects.tags.count=%\" PRIuMAX \"%c\",\n> > +\t\t    (uintmax_t)stats->objects.tags, delim);\n> >  \n> >  \tfwrite(buf.buf, sizeof(char), buf.len, stdout);\n> >  \tstrbuf_release(&buf);\n> \n> It's a bit unfortunate we have to rewrite most of the function. I'd\n> either have the `delim` parameter right from the start or just squash\n> these two patches together.\n\nI'll just squash these two patches together.\n\n> > diff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\n> > index 5bc6d9d5c4..061b2fbbc1 100755\n> > --- a/t/t1901-repo-stats.sh\n> > +++ b/t/t1901-repo-stats.sh\n> > @@ -127,4 +127,31 @@ test_expect_success 'repository stats with keyvalue format' '\n> >  \t)\n> >  '\n> >  \n> > +test_expect_success 'repository stats with nul format' '\n> > +\ttest_when_finished \"rm -rf repo\" &&\n> > +\tgit init repo &&\n> > +\t(\n> > +\t\tcd repo &&\n> > +\t\ttest_commit_bulk 42 &&\n> > +\t\tgit tag -a foo -m bar &&\n> > +\t\tgit repo stats --format=nul >out 2>err &&\n> > +\n> > +\t\tcat >expect <<-EOF &&\n> > +\t\treferences.branches.count=1\n> > +\t\treferences.tags.count=1\n> > +\t\treferences.remotes.count=0\n> > +\t\treferences.others.count=0\n> > +\t\tobjects.commits.count=42\n> > +\t\tobjects.trees.count=42\n> > +\t\tobjects.blobs.count=42\n> > +\t\tobjects.tags.count=1\n> > +\t\tEOF\n> > +\n> > +\t\ttr \"\\n\" \"\\0\" <expect >expect_null &&\n> > +\n> > +\t\ttest_cmp expect_null out &&\n> > +\t\ttest_line_count = 0 err\n> > +\t)\n> > +'\n> \n> We already have a test for the keyvalue format that looks mostly the\n> same, so we may just as well test both formats in a single test.\n\nGood suggestion. Will update.\n\n-Justin\n"},{"id":"527093","messageId":"CAOLa=ZT0y-WOOFz8FdLm40AqwEATzNsg2fzvnJKPzx2mkPiz-g@mail.gmail.com","threadId":"64187","inReplyTo":"20250923025700.3046260-4-jltobler@gmail.com","subject":"Re: [PATCH 3/4] builtin/repo: add keyvalue format for stats","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-09-23T15:39:08Z","receivedAt":"2025-09-23T15:39:10Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n[snip]\n\n> diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\n> index 2a67abfca8..7d0341e4f1 100644\n> --- a/Documentation/git-repo.adoc\n> +++ b/Documentation/git-repo.adoc\n> @@ -9,7 +9,7 @@ SYNOPSIS\n>  --------\n>  [synopsis]\n>  git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n> -git repo stats\n> +git repo stats [--format=(table|keyvalue)]\n>\n>  DESCRIPTION\n>  -----------\n> @@ -44,12 +44,22 @@ supported:\n>  +\n>  `-z` is an alias for `--format=nul`.\n>\n> -stats::\n> +`stats [--format=(table|keyvalue)]`::\n>  \tRetrieve stats about the current repository. All references and\n>  \treachable objects in the repository are categorized and counted\n>  \taccordingly.\n>  +\n> -The table output format may change and is not intended for machine parsing.\n> +The output format can be chosen through the flag `--format`. Two formats are\n> +supported:\n> ++\n> +`table`:::\n> +\tOutputs repository stats in a human-friendly table and is used by\n> +\tdefault. This format may change and is not intended for machine\n> +\tparsing.\n> +\n> +`keyvalue`:::\n> +\tEach line of output contains a key-value pair of a repostiory stat. The\n\ns/repostiory/repository\n\n> +\t'=' character is used to delimit between the key and the value.\n>\n\nDoes each value end with a newline or with a NUL? We should mention\nthat here.\n\n[snip]\n\n> +static void stats_print(struct stats *stats)\n> +{\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\n> +\tstrbuf_addf(&buf, \"references.branches.count=%\" PRIuMAX \"\\n\",\n> +\t\t    (uintmax_t)stats->refs.branches);\n> +\tstrbuf_addf(&buf, \"references.tags.count=%\" PRIuMAX \"\\n\",\n> +\t\t    (uintmax_t)stats->refs.tags);\n> +\tstrbuf_addf(&buf, \"references.remotes.count=%\" PRIuMAX \"\\n\",\n> +\t\t    (uintmax_t)stats->refs.remotes);\n> +\tstrbuf_addf(&buf, \"references.others.count=%\" PRIuMAX \"\\n\",\n> +\t\t    (uintmax_t)stats->refs.others);\n> +\n> +\tstrbuf_addf(&buf, \"objects.commits.count=%\" PRIuMAX \"\\n\",\n> +\t\t    (uintmax_t)stats->objects.commits);\n> +\tstrbuf_addf(&buf, \"objects.trees.count=%\" PRIuMAX \"\\n\",\n> +\t\t    (uintmax_t)stats->objects.trees);\n> +\tstrbuf_addf(&buf, \"objects.blobs.count=%\" PRIuMAX \"\\n\",\n> +\t\t    (uintmax_t)stats->objects.blobs);\n> +\tstrbuf_addf(&buf, \"objects.tags.count=%\" PRIuMAX \"\\n\",\n> +\t\t    (uintmax_t)stats->objects.tags);\n> +\n> +\tfwrite(buf.buf, sizeof(char), buf.len, stdout);\n> +\tstrbuf_release(&buf);\n> +}\n> +\n\nOkay so newline delimeted, similar to 'git repo info'.\n\n>  static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n>  {\n>  \tfor (int i = 0; i < refs->nr; i++) {\n> @@ -359,9 +390,16 @@ static void stats_count_objects(struct object_stats *stats,\n>  \tpath_walk_info_clear(&info);\n>  }\n>\n> -static int repo_stats(int argc UNUSED, const char **argv UNUSED,\n> -\t\t      const char *prefix, struct repository *repo)\n> +static int repo_stats(int argc, const char **argv, const char *prefix,\n> +\t\t      struct repository *repo)\n>  {\n> +\tenum output_format format = FORMAT_TABLE;\n> +\tstruct option options[] = {\n> +\t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n> +\t\t\t       N_(\"output format\"),\n> +\t\t\t       PARSE_OPT_NONEG, parse_format_cb),\n> +\t\tOPT_END()\n> +\t};\n>  \tstruct ref_filter filter = REF_FILTER_INIT;\n>  \tstruct strvec ref_patterns = STRVEC_INIT;\n>  \tstruct stats_table table = { 0 };\n> @@ -369,6 +407,10 @@ static int repo_stats(int argc UNUSED, const char **argv UNUSED,\n>  \tstruct stats stats = { 0 };\n>  \tstruct rev_info revs;\n>\n> +\tparse_options(argc, argv, prefix, options, repo_usage, 0);\n> +\tif (format == FORMAT_NUL_TERMINATED)\n> +\t\tdie(_(\"nul format not yet supported\"));\n> +\n\nOkay I forsee the next patch adding this support.\n\nThe patch looks good!\n\n[snip]\n"},{"id":"527094","messageId":"CAOLa=ZSyXFAE+=ApxscikcmYjtz2-EzrOdORZn3KfiU5Rg_RgA@mail.gmail.com","threadId":"64187","inReplyTo":"20250923025700.3046260-5-jltobler@gmail.com","subject":"Re: [PATCH 4/4] builtin/repo: add nul format for stats","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-09-23T15:41:44Z","receivedAt":"2025-09-23T15:41:48Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n[snip]\n\n> @@ -291,27 +291,31 @@ static void stats_table_print(struct stats_table *table)\n>  \tstrbuf_release(&buf);\n>  }\n>\n> -static void stats_print(struct stats *stats)\n> +static void stats_print(struct stats *stats, int nul_delim)\n>  {\n>\n\nNit: we can use 'bool' variable types now.\n\n>  \tstruct strbuf buf = STRBUF_INIT;\n> -\n> -\tstrbuf_addf(&buf, \"references.branches.count=%\" PRIuMAX \"\\n\",\n> -\t\t    (uintmax_t)stats->refs.branches);\n> -\tstrbuf_addf(&buf, \"references.tags.count=%\" PRIuMAX \"\\n\",\n> -\t\t    (uintmax_t)stats->refs.tags);\n> -\tstrbuf_addf(&buf, \"references.remotes.count=%\" PRIuMAX \"\\n\",\n> -\t\t    (uintmax_t)stats->refs.remotes);\n> -\tstrbuf_addf(&buf, \"references.others.count=%\" PRIuMAX \"\\n\",\n> -\t\t    (uintmax_t)stats->refs.others);\n> -\n> -\tstrbuf_addf(&buf, \"objects.commits.count=%\" PRIuMAX \"\\n\",\n> -\t\t    (uintmax_t)stats->objects.commits);\n> -\tstrbuf_addf(&buf, \"objects.trees.count=%\" PRIuMAX \"\\n\",\n> -\t\t    (uintmax_t)stats->objects.trees);\n> -\tstrbuf_addf(&buf, \"objects.blobs.count=%\" PRIuMAX \"\\n\",\n> -\t\t    (uintmax_t)stats->objects.blobs);\n> -\tstrbuf_addf(&buf, \"objects.tags.count=%\" PRIuMAX \"\\n\",\n> -\t\t    (uintmax_t)stats->objects.tags);\n> +\tchar delim = '\\n';\n> +\n> +\tif (nul_delim)\n> +\t\tdelim = '\\0';\n> +\n> +\tstrbuf_addf(&buf, \"references.branches.count=%\" PRIuMAX \"%c\",\n> +\t\t    (uintmax_t)stats->refs.branches, delim);\n> +\tstrbuf_addf(&buf, \"references.tags.count=%\" PRIuMAX \"%c\",\n> +\t\t    (uintmax_t)stats->refs.tags, delim);\n> +\tstrbuf_addf(&buf, \"references.remotes.count=%\" PRIuMAX \"%c\",\n> +\t\t    (uintmax_t)stats->refs.remotes, delim);\n> +\tstrbuf_addf(&buf, \"references.others.count=%\" PRIuMAX \"%c\",\n> +\t\t    (uintmax_t)stats->refs.others, delim);\n> +\n> +\tstrbuf_addf(&buf, \"objects.commits.count=%\" PRIuMAX \"%c\",\n> +\t\t    (uintmax_t)stats->objects.commits, delim);\n> +\tstrbuf_addf(&buf, \"objects.trees.count=%\" PRIuMAX \"%c\",\n> +\t\t    (uintmax_t)stats->objects.trees, delim);\n> +\tstrbuf_addf(&buf, \"objects.blobs.count=%\" PRIuMAX \"%c\",\n> +\t\t    (uintmax_t)stats->objects.blobs, delim);\n> +\tstrbuf_addf(&buf, \"objects.tags.count=%\" PRIuMAX \"%c\",\n> +\t\t    (uintmax_t)stats->objects.tags, delim);\n>\n>  \tfwrite(buf.buf, sizeof(char), buf.len, stdout);\n>  \tstrbuf_release(&buf);\n\nThe rest looks good to me! :)\n"},{"id":"527097","messageId":"k6szjxs2rcthy3kj7h5rgzkmeezldjdkuewcqfu3yhuxcbnkw5@a32ezi6ytqgm","threadId":"64187","inReplyTo":"CAOLa=ZQHU8m-BLzD=uWx6e-FVcs-aDmrUp9qrU72_5E=CNQRNg@mail.gmail.com","subject":"Re: [PATCH 1/4] builtin/repo: introduce stats subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-23T15:55:27Z","receivedAt":"2025-09-23T15:55:29Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/09/23 11:22AM, Karthik Nayak wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> [snip]\n> \n> > diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\n> > index 209afd1b61..7762329551 100644\n> > --- a/Documentation/git-repo.adoc\n> > +++ b/Documentation/git-repo.adoc\n> > @@ -9,6 +9,7 @@ SYNOPSIS\n> >  --------\n> >  [synopsis]\n> >  git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n> > +git repo stats\n> >\n> >  DESCRIPTION\n> >  -----------\n> > @@ -43,6 +44,12 @@ supported:\n> >  +\n> >  `-z` is an alias for `--format=nul`.\n> >\n> > +stats::\n> > +\tRetrieve stats about the current repository. All references in the\n> > +\trepository are categorized and counted accordingly.\n> > ++\n> \n> Nit: Should we use bullet points for the information provided? As of\n> this commit it is only references, but with time, this will grow, so\n> would be nice to have a set of bullet points to understand the different\n> information retrieved.\n\nYa, it was also suggested else where to do the same. Will update.\n\n> > +The table output format may change and is not intended for machine parsing.\n> > +\n> >  INFO KEYS\n> >  ---------\n> >  In order to obtain a set of values from `git repo info`, you should provide\n> > diff --git a/builtin/repo.c b/builtin/repo.c\n> > index bbb0966f2d..15899dd74c 100644\n> > --- a/builtin/repo.c\n> > +++ b/builtin/repo.c\n> > @@ -4,12 +4,15 @@\n> >  #include \"environment.h\"\n> >  #include \"parse-options.h\"\n> >  #include \"quote.h\"\n> > +#include \"ref-filter.h\"\n> >  #include \"refs.h\"\n> >  #include \"strbuf.h\"\n> > +#include \"string-list.h\"\n> >  #include \"shallow.h\"\n> >\n> >  static const char *const repo_usage[] = {\n> >  \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n> > +\t\"git repo stats\",\n> >  \tNULL\n> >  };\n> >\n> > @@ -156,12 +159,160 @@ static int repo_info(int argc, const char **argv, const char *prefix,\n> >  \treturn print_fields(argc, argv, repo, format);\n> >  }\n> >\n> > +struct stats {\n> > +\tsize_t branches;\n> > +\tsize_t remotes;\n> > +\tsize_t tags;\n> > +\tsize_t others;\n> \n> Maybe we can use a nested structure here, which would reflect the output\n> table. That would be much nicer, as the code would grow, currently we\n> know 'others' here refers to references, but with other fields coming\n> in, this would no longer be obvious.\n\nWill do.\n\n> > +};\n> > +\n> > +struct stats_table {\n> > +\tstruct string_list rows;\n> > +\n> > +\tint name_col_width;\n> > +\tint value_col_width;\n> \n> Can these be negative?\n\nNo, but the only usage of these values is in the format string used to\nprint each row and it requires an int. I'm not sure it makes sense to\ncast in this situation.\n\n> > +};\n> > +\n> \n> Nit: Could we add a comment about what this structure is and what it\n> holds? I think it is sort of obvious, but would be nice to clarify.\n\nAdding a comment doesn't hurt. Will do.\n\n[snip]\n> > +static int repo_stats(int argc UNUSED, const char **argv UNUSED,\n> > +\t\t      const char *prefix UNUSED, struct repository *repo UNUSED)\n> > +{\n> > +\tstruct ref_filter filter = REF_FILTER_INIT;\n> > +\tstruct strvec ref_patterns = STRVEC_INIT;\n> > +\tstruct stats_table table = { 0 };\n> > +\tstruct ref_array refs = { 0 };\n> > +\tstruct stats stats = { 0 };\n> > +\n> > +\tfilter.name_patterns = ref_patterns.v;\n> > +\tfilter_refs(&refs, &filter, FILTER_REFS_REGULAR);\n> > +\n> \n> I was wondering why we need the filter mechanism here, but seems like\n> this is to obtain the type of the reference. This is setup automatically\n> by the filter mechanism and so it's okay.\n> \n> We could replicate the same using the ref iterator code. This would be a\n> little more involved, but would remove the need to store all refs in a\n> 'ref_array' and also the need to loop over references twice. But it\n> doesn't really matter in this usecase I assume.\n\nIn a future series, I would like to add options that restrict the set of\nreferences used when evaluating the repository. For this reason, I think\nit makes sense to stick with filter_refs() here.\n\n> > +\tstats_count_references(&stats, &refs);\n> > +\n> > +\tstats_table_setup(&table, &stats);\n> > +\tstats_table_print(&table);\n> > +\n> > +\tstring_list_clear(&table.rows, 1);\n> > +\tstrvec_clear(&ref_patterns);\n> \n> Huh. So `ref_patterns` is simply a dummy variable, I was wondering why\n> we simply can't set `filter.name_patterns = NULL`. I see that it is\n> because in `filter_pattern_match()` we do `if\n> (!*filter->name_patterns)`. This is not a great interface for\n> ref-filter. Perhaps we could add a precursor commit like:\n> \n> diff --git a/ref-filter.c b/ref-filter.c\n> index 520d2539c9..20284b5918 100644\n> --- a/ref-filter.c\n> +++ b/ref-filter.c\n> @@ -2664,7 +2664,7 @@ static int match_name_as_path(const char\n> **pattern, const char *refname,\n>  /* Return 1 if the refname matches one of the patterns, otherwise 0. */\n>  static int filter_pattern_match(struct ref_filter *filter, const char *refname)\n>  {\n> -\tif (!*filter->name_patterns)\n> +\tif (!filter->name_patterns || !*filter->name_patterns)\n>  \t\treturn 1; /* No pattern always matches */\n>  \tif (filter->match_as_path)\n>  \t\treturn match_name_as_path(filter->name_patterns, refname,\n> diff --git a/ref-filter.h b/ref-filter.h\n> index f22ca94b49..44d9b481ad 100644\n> --- a/ref-filter.h\n> +++ b/ref-filter.h\n> @@ -109,6 +109,7 @@ struct ref_format {\n>  #define REF_FILTER_INIT { \\\n>  \t.points_at = OID_ARRAY_INIT, \\\n>  \t.exclude = STRVEC_INIT, \\\n> +\t.name_pattern = NULL, \\\n>  }\n>  #define REF_FORMAT_INIT {             \\\n>  \t.use_color = -1,              \\\n\nYa, I actually already have this exact patch locally. :)\n\nI considered sending it as part of this series, but opted to just use\nthe dummy vairable instead. It probably makes sense though to improve\nthe ref-filter interface though. I'll do so in the next version. \n\nThanks for the review,\n-Justin\n"},{"id":"527098","messageId":"jp2pzjazi6w6pm4jv7bk2c3gywksezpaoufd6aulcdxr4tbqi4@k3fjsgvejp5m","threadId":"64187","inReplyTo":"CAOLa=ZTihUt0d-3_LOydX4GxWPSquAOWkzw3+0XzM-zp6wFZ+w@mail.gmail.com","subject":"Re: [PATCH 2/4] builtin/repo: add object counts in stats output","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-23T15:56:25Z","receivedAt":"2025-09-23T15:56:27Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/09/23 10:30AM, Karthik Nayak wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> [snip]\n> \n> > @@ -159,13 +161,25 @@ static int repo_info(int argc, const char **argv, const char *prefix,\n> >  \treturn print_fields(argc, argv, repo, format);\n> >  }\n> >\n> > -struct stats {\n> > +struct ref_stats {\n> >  \tsize_t branches;\n> >  \tsize_t remotes;\n> >  \tsize_t tags;\n> >  \tsize_t others;\n> >  };\n> >\n> > +struct object_stats {\n> > +\tsize_t tags;\n> > +\tsize_t commits;\n> > +\tsize_t trees;\n> > +\tsize_t blobs;\n> > +};\n> > +\n> > +struct stats {\n> > +\tstruct ref_stats refs;\n> > +\tstruct object_stats objects;\n> > +};\n> > +\n> \n> Okay so we do use nested structs, which I mentioned in the previous\n> commit, so we're all good.\n> \n> Nit: A lot of the diff in this commit is because we don't make the\n> required structs in the previous commit. Perhaps we could start with the\n> nested structs in the previous commit? That way this patch would be much\n> simpler.\n\nYa, I clean this up in the next version.\n\n-Justin\n"},{"id":"527099","messageId":"hapuro77uciopotqmkjrapnomyojsys7mfmurkuvecu4fy6il5@echgeotkgcmk","threadId":"64187","inReplyTo":"CAOLa=ZT0y-WOOFz8FdLm40AqwEATzNsg2fzvnJKPzx2mkPiz-g@mail.gmail.com","subject":"Re: [PATCH 3/4] builtin/repo: add keyvalue format for stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-23T15:59:22Z","receivedAt":"2025-09-23T15:59:24Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/09/23 11:39AM, Karthik Nayak wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> [snip]\n> \n> > diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\n> > index 2a67abfca8..7d0341e4f1 100644\n> > --- a/Documentation/git-repo.adoc\n> > +++ b/Documentation/git-repo.adoc\n> > @@ -9,7 +9,7 @@ SYNOPSIS\n> >  --------\n> >  [synopsis]\n> >  git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n> > -git repo stats\n> > +git repo stats [--format=(table|keyvalue)]\n> >\n> >  DESCRIPTION\n> >  -----------\n> > @@ -44,12 +44,22 @@ supported:\n> >  +\n> >  `-z` is an alias for `--format=nul`.\n> >\n> > -stats::\n> > +`stats [--format=(table|keyvalue)]`::\n> >  \tRetrieve stats about the current repository. All references and\n> >  \treachable objects in the repository are categorized and counted\n> >  \taccordingly.\n> >  +\n> > -The table output format may change and is not intended for machine parsing.\n> > +The output format can be chosen through the flag `--format`. Two formats are\n> > +supported:\n> > ++\n> > +`table`:::\n> > +\tOutputs repository stats in a human-friendly table and is used by\n> > +\tdefault. This format may change and is not intended for machine\n> > +\tparsing.\n> > +\n> > +`keyvalue`:::\n> > +\tEach line of output contains a key-value pair of a repostiory stat. The\n> \n> s/repostiory/repository\n\nWill fix.\n\n> > +\t'=' character is used to delimit between the key and the value.\n> >\n> \n> Does each value end with a newline or with a NUL? We should mention\n> that here.\n\nI was thinking that this would be implied by the prior sentence, but it\nis probably better to be more explicit here. Will update. :)\n\n-Justin\n"},{"id":"527100","messageId":"xa3araqtucuf4mwm3bll2moq2tlnkyq7hw6dsyma53g7xpv7tj@nu3of2vy6geo","threadId":"64187","inReplyTo":"CAOLa=ZSyXFAE+=ApxscikcmYjtz2-EzrOdORZn3KfiU5Rg_RgA@mail.gmail.com","subject":"Re: [PATCH 4/4] builtin/repo: add nul format for stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-23T16:02:58Z","receivedAt":"2025-09-23T16:03:01Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/09/23 11:41AM, Karthik Nayak wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> > -static void stats_print(struct stats *stats)\n> > +static void stats_print(struct stats *stats, int nul_delim)\n> >  {\n> >\n> \n> Nit: we can use 'bool' variable types now.\n\nIt was suggested elsewhere to just pass the delimiter character here\ndirectly instead. I will probably do that. :)\n\n-Justin\n"},{"id":"527169","messageId":"aNN4Mk9Nh_mF1dJ6@pks.im","threadId":"64187","inReplyTo":"r2rt5aq64x232gmpj5wzzchut2w2axnkyfzg7wpthwuf2w3j4f@teydueqvon5d","subject":"Re: [PATCH 4/4] builtin/repo: add nul format for stats","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-24T04:48:50Z","receivedAt":"2025-09-24T04:48:57Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Sep 23, 2025 at 10:33:45AM -0500, Justin Tobler wrote:\n> On 25/09/23 12:53PM, Patrick Steinhardt wrote:\n> > On Mon, Sep 22, 2025 at 09:57:00PM -0500, Justin Tobler wrote:\n> > > diff --git a/builtin/repo.c b/builtin/repo.c\n> > > index 4c16a68e4e..37034e6347 100644\n> > > --- a/builtin/repo.c\n> > > +++ b/builtin/repo.c\n> > > @@ -291,27 +291,31 @@ static void stats_table_print(struct stats_table *table)\n> > >  \tstrbuf_release(&buf);\n> > >  }\n> > >  \n> > > -static void stats_print(struct stats *stats)\n> > > +static void stats_print(struct stats *stats, int nul_delim)\n> > \n> > Instead of passing a boolean-style option, can't we pass the expected\n> > delimiter directly? Makes the callsite a bit more obvious.\n> \n> Ya, we could do that here instead. Something I just noticed is that the\n> NUL format in `git repo info` also replaces the '=' delimiter with a\n> newline. I'm not sure if it would be best to match the same behavior\n> here?\n> \n> If so, we would have to either pass both delimiters as arguments to the\n> function, or just keep the boolean toggle for the mode.\n\nI think being consistent would be nice, yes. I'd personally lean towards\npassing both delimiters as arguments in that case. Makes the callsites\nway easier to read.\n\nPatrick\n"},{"id":"527262","messageId":"20250924212426.2930029-1-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250923025700.3046260-1-jltobler@gmail.com","subject":"[PATCH v2 0/6] builtin/repo: introduce stats subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-24T21:24:20Z","receivedAt":"2025-09-24T21:24:31Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Greetings,\n\nThe shape of a repository's history can have huge impacts on the\nperformance and health of the repository itself. Currently, Git lacks a\nmeans to surface key stats/information regarding the shape of a\nrepository via a single command. Acquiring this information requires\nusers to be fairly knowledgeable about the structure of a Git repository\nand how to identify the relevant data points. To fill this gap,\nsupplemental tools such as git-sizer(1) have been developed.\n\nTo allow users to more readily identify potential issues for a\nrepository, introduce the \"stats\" subcommand in git-repo(1) to output\nstats for the repository that may be of interest to users. The goal of\nthis subcommand is to eventually provide similar functionality to\ngit-sizer(1), but in Git natively.\n\nIn this initial version, the \"stats\" subcommand only surfaces counts of\nthe various reference and object types in a repository. In a follow-up\nseries, I would like to introduce additional data points that are\npresent in git-sizer(1) such as largest objects, combined object sizes\nby type, and other general repository shape information.\n\nSome other general features that would be nice to introduce eventually:\n\n- A \"level of concern\" meter for reported stats. This could indicate to\n  users which stats may be worth looking into further.\n- Links to OIDs of interesting objects that correspond to certain stats.\n- Options to limit which references to use when evaluating the\n  repository.\n\nChanges since V1:\n\n- Translatable terms displayed in the table have formatting separated\n  out.\n- Squashed the `keyvalue` and `nul` output format patches into one.\n- Added a progress meter to provide users with more feedback.\n- Updated docs to outline to outline reported data in a bulleted list.\n- Combined similar tests together to reduce repetitive setup.\n- Added patch to improve ref-filter interface so we don't have to create\n  a dummy patterns array.\n- Many other renames and cleanups to improve patch clarity.\n\nThanks,\n-Justin\n\nJustin Tobler (6):\n  builtin/repo: rename repo_info() to cmd_repo_info()\n  ref-filter: allow NULL filter pattern\n  builtin/repo: introduce stats subcommand\n  builtin/repo: add object counts in stats output\n  builtin/repo: add keyvalue and nul format for stats\n  builtin/repo: add progress meter for stats\n\n Documentation/git-repo.adoc |  30 +++\n builtin/repo.c              | 354 +++++++++++++++++++++++++++++++++++-\n ref-filter.c                |   4 +-\n t/meson.build               |   1 +\n t/t1901-repo-stats.sh       | 109 +++++++++++\n 5 files changed, 493 insertions(+), 5 deletions(-)\n create mode 100755 t/t1901-repo-stats.sh\n\n\nbase-commit: ca2559c1d630eb4f04cdee2328aaf1c768907a9e\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527263","messageId":"20250924212426.2930029-2-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250924212426.2930029-1-jltobler@gmail.com","subject":"[PATCH v2 1/6] builtin/repo: rename repo_info() to cmd_repo_info()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-24T21:24:21Z","receivedAt":"2025-09-24T21:24:32Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Subcommand functions are often prefixed with `cmd_` to denote that they\nare an entrypoint. Rename repo_info() to cmd_repo_info() accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/repo.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex bbb0966f2d..eeeab8fbd2 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -136,8 +136,8 @@ static int parse_format_cb(const struct option *opt,\n \treturn 0;\n }\n \n-static int repo_info(int argc, const char **argv, const char *prefix,\n-\t\t     struct repository *repo)\n+static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n+\t\t\t struct repository *repo)\n {\n \tenum output_format format = FORMAT_KEYVALUE;\n \tstruct option options[] = {\n@@ -161,7 +161,7 @@ int cmd_repo(int argc, const char **argv, const char *prefix,\n {\n \tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n-\t\tOPT_SUBCOMMAND(\"info\", &fn, repo_info),\n+\t\tOPT_SUBCOMMAND(\"info\", &fn, cmd_repo_info),\n \t\tOPT_END()\n \t};\n \n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527264","messageId":"20250924212426.2930029-3-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250924212426.2930029-1-jltobler@gmail.com","subject":"[PATCH v2 2/6] ref-filter: allow NULL filter pattern","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-24T21:24:22Z","receivedAt":"2025-09-24T21:24:33Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"When setting up `struct ref_filter` for filter_refs(), the\n`name_patterns` field must point to an array of pattern strings even if\nno patterns are required. To improve this interface, treat a NULL\n`name_patterns` field the same as when it points to an empty array.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n ref-filter.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/ref-filter.c b/ref-filter.c\nindex 520d2539c9..2cb5a166d6 100644\n--- a/ref-filter.c\n+++ b/ref-filter.c\n@@ -2664,7 +2664,7 @@ static int match_name_as_path(const char **pattern, const char *refname,\n /* Return 1 if the refname matches one of the patterns, otherwise 0. */\n static int filter_pattern_match(struct ref_filter *filter, const char *refname)\n {\n-\tif (!*filter->name_patterns)\n+\tif (!filter->name_patterns || !*filter->name_patterns)\n \t\treturn 1; /* No pattern always matches */\n \tif (filter->match_as_path)\n \t\treturn match_name_as_path(filter->name_patterns, refname,\n@@ -2751,7 +2751,7 @@ static int for_each_fullref_in_pattern(struct ref_filter *filter,\n \t\treturn for_each_fullref_with_seek(filter, cb, cb_data, 0);\n \t}\n \n-\tif (!filter->name_patterns[0]) {\n+\tif (!filter->name_patterns || !filter->name_patterns[0]) {\n \t\t/* no patterns; we have to look at everything */\n \t\treturn for_each_fullref_with_seek(filter, cb, cb_data, 0);\n \t}\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527265","messageId":"20250924212426.2930029-4-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250924212426.2930029-1-jltobler@gmail.com","subject":"[PATCH v2 3/6] builtin/repo: introduce stats subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-24T21:24:23Z","receivedAt":"2025-09-24T21:24:34Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The shape of a repository's history can have huge impacts on the\nperformance and health of the repository itself. Currently, Git lacks a\nmeans to surface key stats/information regarding the shape of a\nrepository via a single command. Acquiring this information requires\nusers to be fairly knowledgeable about the structure of a Git repository\nand how to identify the relevant data points. To fill this gap,\nsupplemental tools such as git-sizer(1) have been developed.\n\nTo allow users to more readily identify potential issues for a\nrepository, introduce the \"stats\" subcommand in git-repo(1) to output\nstats for the repository that may be of interest to users. The goal of\nthis subcommand is to eventually provide similar functionality to\ngit-sizer(1), but natively in Git.\n\nThe initial version of this command only iterates through all references\nin the repository and tracks the count of branches, tags, remote refs,\nand other reference types. The corresponding information is displayed in\na human-friendly table formatted in a very similar manner to\ngit-sizer(1). The width of each table column is adjusted automatically\nto satisfy the requirements of the widest row contained.\n\nSubsequent commits will surface additional relevant data points to\noutput.\n\nBased-on-patch-by: Derrick Stolee <stolee@gmail.com>\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc |  10 ++\n builtin/repo.c              | 179 ++++++++++++++++++++++++++++++++++++\n t/meson.build               |   1 +\n t/t1901-repo-stats.sh       |  61 ++++++++++++\n 4 files changed, 251 insertions(+)\n create mode 100755 t/t1901-repo-stats.sh\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 209afd1b61..a009bf8cf1 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -9,6 +9,7 @@ SYNOPSIS\n --------\n [synopsis]\n git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n+git repo stats\n \n DESCRIPTION\n -----------\n@@ -43,6 +44,15 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n+`stats`::\n+\tRetrieve statistics about the current repository. The following kinds\n+\tof information are reported:\n++\n+* Reference counts categorized by type\n+\n++\n+The table output format may change and is not intended for machine parsing.\n+\n INFO KEYS\n ---------\n In order to obtain a set of values from `git repo info`, you should provide\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex eeeab8fbd2..32ddf2350e 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -4,12 +4,15 @@\n #include \"environment.h\"\n #include \"parse-options.h\"\n #include \"quote.h\"\n+#include \"ref-filter.h\"\n #include \"refs.h\"\n #include \"strbuf.h\"\n+#include \"string-list.h\"\n #include \"shallow.h\"\n \n static const char *const repo_usage[] = {\n \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n+\t\"git repo stats\",\n \tNULL\n };\n \n@@ -156,12 +159,188 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \treturn print_fields(argc, argv, repo, format);\n }\n \n+struct ref_stats {\n+\tsize_t branches;\n+\tsize_t remotes;\n+\tsize_t tags;\n+\tsize_t others;\n+};\n+\n+struct stats_table {\n+\tstruct string_list rows;\n+\n+\tsize_t name_col_width;\n+\tsize_t value_col_width;\n+};\n+\n+/*\n+ * Holds column data that gets stored for each row.\n+ */\n+struct stats_table_entry {\n+\tchar *value;\n+};\n+\n+static void stats_table_add(struct stats_table *table, const char *format,\n+\t\t\t    const char *name, struct stats_table_entry *entry)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct string_list_item *item;\n+\tchar *formatted_name;\n+\tsize_t name_width;\n+\n+\tstrbuf_addf(&buf, format, name);\n+\tformatted_name = strbuf_detach(&buf, &name_width);\n+\n+\titem = string_list_append_nodup(&table->rows, formatted_name);\n+\titem->util = entry;\n+\n+\tif (name_width > table->name_col_width)\n+\t\ttable->name_col_width = name_width;\n+\tif (entry) {\n+\t\tsize_t value_width = strlen(entry->value);\n+\t\tif (value_width > table->value_col_width)\n+\t\t\ttable->value_col_width = value_width;\n+\t}\n+}\n+\n+static void stats_table_add_count(struct stats_table *table, const char *format,\n+\t\t\t\t  const char *name, size_t value)\n+{\n+\tstruct stats_table_entry *entry;\n+\n+\tCALLOC_ARRAY(entry, 1);\n+\tentry->value = xstrfmt(\"%\" PRIuMAX, (uintmax_t)value);\n+\tstats_table_add(table, format, name, entry);\n+}\n+\n+static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n+{\n+\tsize_t ref_total;\n+\n+\tref_total = refs->branches + refs->remotes + refs->tags + refs->others;\n+\tstats_table_add(table, \"* %s\", _(\"References\"), NULL);\n+\tstats_table_add_count(table, \"  * %s\", _(\"Count\"), ref_total);\n+\tstats_table_add_count(table, \"    * %s\", _(\"Branches\"), refs->branches);\n+\tstats_table_add_count(table, \"    * %s\", _(\"Tags\"), refs->tags);\n+\tstats_table_add_count(table, \"    * %s\", _(\"Remotes\"), refs->remotes);\n+\tstats_table_add_count(table, \"    * %s\", _(\"Others\"), refs->others);\n+}\n+\n+static inline size_t max_size_t(size_t a, size_t b)\n+{\n+\treturn (a > b) ? a : b;\n+}\n+\n+static void stats_table_print(struct stats_table *table)\n+{\n+\tconst char *name_col_title = _(\"Repository stats\");\n+\tconst char *value_col_title = _(\"Value\");\n+\tsize_t name_title_len = strlen(name_col_title);\n+\tsize_t value_title_len = strlen(value_col_title);\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct string_list_item *item;\n+\tint name_col_width;\n+\tint value_col_width;\n+\n+\tname_col_width = cast_size_t_to_int(\n+\t\tmax_size_t(table->name_col_width, name_title_len));\n+\tvalue_col_width = cast_size_t_to_int(\n+\t\tmax_size_t(table->value_col_width, value_title_len));\n+\n+\tstrbuf_addf(&buf, \"| %-*s | %-*s |\\n\", name_col_width, name_col_title,\n+\t\t    value_col_width, value_col_title);\n+\tstrbuf_addstr(&buf, \"| \");\n+\tstrbuf_addchars(&buf, '-', name_col_width);\n+\tstrbuf_addstr(&buf, \" | \");\n+\tstrbuf_addchars(&buf, '-', value_col_width);\n+\tstrbuf_addstr(&buf, \" |\\n\");\n+\n+\tfor_each_string_list_item(item, &table->rows) {\n+\t\tstruct stats_table_entry *entry = item->util;\n+\t\tconst char *value = \"\";\n+\n+\t\tif (entry) {\n+\t\t\tstruct stats_table_entry *entry = item->util;\n+\t\t\tvalue = entry->value;\n+\t\t}\n+\n+\t\tstrbuf_addf(&buf, \"| %-*s | %*s |\\n\", name_col_width,\n+\t\t\t    item->string, value_col_width, value);\n+\t}\n+\n+\tfputs(buf.buf, stdout);\n+\tstrbuf_release(&buf);\n+}\n+\n+static void stats_table_clear(struct stats_table *table)\n+{\n+\tstruct stats_table_entry *entry;\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, &table->rows) {\n+\t\tentry = item->util;\n+\t\tif (entry)\n+\t\t\tfree(entry->value);\n+\t}\n+\n+\tstring_list_clear(&table->rows, 1);\n+}\n+\n+static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n+{\n+\tfor (int i = 0; i < refs->nr; i++) {\n+\t\tstruct ref_array_item *ref = refs->items[i];\n+\n+\t\tswitch (ref->kind) {\n+\t\tcase FILTER_REFS_BRANCHES:\n+\t\t\tstats->branches++;\n+\t\t\tbreak;\n+\t\tcase FILTER_REFS_REMOTES:\n+\t\t\tstats->remotes++;\n+\t\t\tbreak;\n+\t\tcase FILTER_REFS_TAGS:\n+\t\t\tstats->tags++;\n+\t\t\tbreak;\n+\t\tcase FILTER_REFS_OTHERS:\n+\t\t\tstats->others++;\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tBUG(\"unexpected reference type\");\n+\t\t}\n+\t}\n+}\n+\n+static int cmd_repo_stats(int argc UNUSED, const char **argv UNUSED,\n+\t\t\t  const char *prefix UNUSED, struct repository *repo UNUSED)\n+{\n+\tstruct ref_filter filter = REF_FILTER_INIT;\n+\tstruct stats_table table = {\n+\t\t.rows = STRING_LIST_INIT_DUP,\n+\t};\n+\tstruct ref_stats stats = { 0 };\n+\tstruct ref_array refs = { 0 };\n+\n+\tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n+\t\tdie(_(\"unable to filter refs\"));\n+\n+\tstats_count_references(&stats, &refs);\n+\n+\tstats_table_setup(&table, &stats);\n+\tstats_table_print(&table);\n+\n+\tstats_table_clear(&table);\n+\tref_array_clear(&refs);\n+\n+\treturn 0;\n+}\n+\n int cmd_repo(int argc, const char **argv, const char *prefix,\n \t     struct repository *repo)\n {\n \tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n \t\tOPT_SUBCOMMAND(\"info\", &fn, cmd_repo_info),\n+\t\tOPT_SUBCOMMAND(\"stats\", &fn, cmd_repo_stats),\n \t\tOPT_END()\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 7974795fe4..071d4a5112 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -236,6 +236,7 @@ integration_tests = [\n   't1701-racy-split-index.sh',\n   't1800-hook.sh',\n   't1900-repo.sh',\n+  't1901-repo-stats.sh',\n   't2000-conflict-when-checking-files-out.sh',\n   't2002-checkout-cache-u.sh',\n   't2003-checkout-cache-mkdir.sh',\ndiff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\nnew file mode 100755\nindex 0000000000..535ac511dd\n--- /dev/null\n+++ b/t/t1901-repo-stats.sh\n@@ -0,0 +1,61 @@\n+#!/bin/sh\n+\n+test_description='test git repo stats'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'empty repository' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\t| Repository stats | Value |\n+\t\t| ---------------- | ----- |\n+\t\t| * References     |       |\n+\t\t|   * Count        |     0 |\n+\t\t|     * Branches   |     0 |\n+\t\t|     * Tags       |     0 |\n+\t\t|     * Remotes    |     0 |\n+\t\t|     * Others     |     0 |\n+\t\tEOF\n+\n+\t\tgit repo stats >out 2>err &&\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n+test_expect_success 'repository with references' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit commit --allow-empty -m init &&\n+\t\tgit tag -a foo -m bar &&\n+\n+\t\toid=\"$(git rev-parse HEAD)\" &&\n+\t\tgit update-ref refs/remotes/origin/foo \"$oid\" &&\n+\n+\t\tgit notes add -m foo &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\t\t| Repository stats | Value |\n+\t\t| ---------------- | ----- |\n+\t\t| * References     |       |\n+\t\t|   * Count        |     4 |\n+\t\t|     * Branches   |     1 |\n+\t\t|     * Tags       |     1 |\n+\t\t|     * Remotes    |     1 |\n+\t\t|     * Others     |     1 |\n+\t\tEOF\n+\n+\t\tgit repo stats >out 2>err &&\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n+test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527266","messageId":"20250924212426.2930029-5-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250924212426.2930029-1-jltobler@gmail.com","subject":"[PATCH v2 4/6] builtin/repo: add object counts in stats output","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-24T21:24:24Z","receivedAt":"2025-09-24T21:24:35Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The amount of objects in a repository can provide insight regarding its\nshape. To surface this information, use the path-walk API to count the\nnumber of reachable objects in the repository by object type. All\nregular references are used to determine the reachable set of objects.\nThe object counts are appended to the same table containing the\nreference information.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc |  1 +\n builtin/repo.c              | 91 +++++++++++++++++++++++++++++++++++--\n t/t1901-repo-stats.sh       | 51 +++++++++++++--------\n 3 files changed, 121 insertions(+), 22 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex a009bf8cf1..0b8d74ed3e 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -49,6 +49,7 @@ supported:\n \tof information are reported:\n +\n * Reference counts categorized by type\n+* Reachable object counts categorized by type\n \n +\n The table output format may change and is not intended for machine parsing.\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 32ddf2350e..8f130bca66 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -3,9 +3,11 @@\n #include \"builtin.h\"\n #include \"environment.h\"\n #include \"parse-options.h\"\n+#include \"path-walk.h\"\n #include \"quote.h\"\n #include \"ref-filter.h\"\n #include \"refs.h\"\n+#include \"revision.h\"\n #include \"strbuf.h\"\n #include \"string-list.h\"\n #include \"shallow.h\"\n@@ -166,6 +168,18 @@ struct ref_stats {\n \tsize_t others;\n };\n \n+struct object_stats {\n+\tsize_t tags;\n+\tsize_t commits;\n+\tsize_t trees;\n+\tsize_t blobs;\n+};\n+\n+struct repo_stats {\n+\tstruct ref_stats refs;\n+\tstruct object_stats objects;\n+};\n+\n struct stats_table {\n \tstruct string_list rows;\n \n@@ -213,8 +227,11 @@ static void stats_table_add_count(struct stats_table *table, const char *format,\n \tstats_table_add(table, format, name, entry);\n }\n \n-static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n+static void stats_table_setup(struct stats_table *table, struct repo_stats *stats)\n {\n+\tstruct object_stats *objects = &stats->objects;\n+\tstruct ref_stats *refs = &stats->refs;\n+\tsize_t object_total;\n \tsize_t ref_total;\n \n \tref_total = refs->branches + refs->remotes + refs->tags + refs->others;\n@@ -224,6 +241,15 @@ static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n \tstats_table_add_count(table, \"    * %s\", _(\"Tags\"), refs->tags);\n \tstats_table_add_count(table, \"    * %s\", _(\"Remotes\"), refs->remotes);\n \tstats_table_add_count(table, \"    * %s\", _(\"Others\"), refs->others);\n+\n+\tobject_total = objects->commits + objects->trees + objects->blobs + objects->tags;\n+\tstats_table_add(table, \"%s\", \"\", NULL);\n+\tstats_table_add(table, \"* %s\", _(\"Reachable objects\"), NULL);\n+\tstats_table_add_count(table, \"  * %s\", _(\"Count\"), object_total);\n+\tstats_table_add_count(table, \"    * %s\", _(\"Commits\"), objects->commits);\n+\tstats_table_add_count(table, \"    * %s\", _(\"Trees\"), objects->trees);\n+\tstats_table_add_count(table, \"    * %s\", _(\"Blobs\"), objects->blobs);\n+\tstats_table_add_count(table, \"    * %s\", _(\"Tags\"), objects->tags);\n }\n \n static inline size_t max_size_t(size_t a, size_t b)\n@@ -310,25 +336,82 @@ static void stats_count_references(struct ref_stats *stats, struct ref_array *re\n \t}\n }\n \n+static int count_objects(const char *path UNUSED, struct oid_array *oids,\n+\t\t\t enum object_type type, void *cb_data)\n+{\n+\tstruct object_stats *stats = cb_data;\n+\n+\tswitch (type) {\n+\tcase OBJ_TAG:\n+\t\tstats->tags += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_COMMIT:\n+\t\tstats->commits += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_TREE:\n+\t\tstats->trees += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_BLOB:\n+\t\tstats->blobs += oids->nr;\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"invalid object type\");\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void stats_count_objects(struct object_stats *stats,\n+\t\t\t\tstruct ref_array *refs, struct rev_info *revs)\n+{\n+\tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n+\n+\tinfo.revs = revs;\n+\tinfo.path_fn = count_objects;\n+\tinfo.path_fn_data = stats;\n+\n+\tfor (int i = 0; i < refs->nr; i++) {\n+\t\tstruct ref_array_item *ref = refs->items[i];\n+\n+\t\tswitch (ref->kind) {\n+\t\tcase FILTER_REFS_BRANCHES:\n+\t\tcase FILTER_REFS_TAGS:\n+\t\tcase FILTER_REFS_REMOTES:\n+\t\tcase FILTER_REFS_OTHERS:\n+\t\t\tadd_pending_oid(revs, NULL, &ref->objectname, 0);\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tBUG(\"unexpected reference type\");\n+\t\t}\n+\t}\n+\n+\twalk_objects_by_path(&info);\n+\tpath_walk_info_clear(&info);\n+}\n+\n static int cmd_repo_stats(int argc UNUSED, const char **argv UNUSED,\n-\t\t\t  const char *prefix UNUSED, struct repository *repo UNUSED)\n+\t\t\t  const char *prefix, struct repository *repo)\n {\n \tstruct ref_filter filter = REF_FILTER_INIT;\n \tstruct stats_table table = {\n \t\t.rows = STRING_LIST_INIT_DUP,\n \t};\n-\tstruct ref_stats stats = { 0 };\n+\tstruct repo_stats stats = { 0 };\n \tstruct ref_array refs = { 0 };\n+\tstruct rev_info revs;\n \n+\trepo_init_revisions(repo, &revs, prefix);\n \tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n \t\tdie(_(\"unable to filter refs\"));\n \n-\tstats_count_references(&stats, &refs);\n+\tstats_count_references(&stats.refs, &refs);\n+\tstats_count_objects(&stats.objects, &refs, &revs);\n \n \tstats_table_setup(&table, &stats);\n \tstats_table_print(&table);\n \n \tstats_table_clear(&table);\n+\trelease_revisions(&revs);\n \tref_array_clear(&refs);\n \n \treturn 0;\ndiff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\nindex 535ac511dd..315b9e1767 100755\n--- a/t/t1901-repo-stats.sh\n+++ b/t/t1901-repo-stats.sh\n@@ -10,14 +10,21 @@ test_expect_success 'empty repository' '\n \t(\n \t\tcd repo &&\n \t\tcat >expect <<-\\EOF &&\n-\t\t| Repository stats | Value |\n-\t\t| ---------------- | ----- |\n-\t\t| * References     |       |\n-\t\t|   * Count        |     0 |\n-\t\t|     * Branches   |     0 |\n-\t\t|     * Tags       |     0 |\n-\t\t|     * Remotes    |     0 |\n-\t\t|     * Others     |     0 |\n+\t\t| Repository stats    | Value |\n+\t\t| ------------------- | ----- |\n+\t\t| * References        |       |\n+\t\t|   * Count           |     0 |\n+\t\t|     * Branches      |     0 |\n+\t\t|     * Tags          |     0 |\n+\t\t|     * Remotes       |     0 |\n+\t\t|     * Others        |     0 |\n+\t\t|                     |       |\n+\t\t| * Reachable objects |       |\n+\t\t|   * Count           |     0 |\n+\t\t|     * Commits       |     0 |\n+\t\t|     * Trees         |     0 |\n+\t\t|     * Blobs         |     0 |\n+\t\t|     * Tags          |     0 |\n \t\tEOF\n \n \t\tgit repo stats >out 2>err &&\n@@ -27,28 +34,36 @@ test_expect_success 'empty repository' '\n \t)\n '\n \n-test_expect_success 'repository with references' '\n+test_expect_success 'repository with references and objects' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n \t(\n \t\tcd repo &&\n-\t\tgit commit --allow-empty -m init &&\n+\t\ttest_commit_bulk 42 &&\n \t\tgit tag -a foo -m bar &&\n \n \t\toid=\"$(git rev-parse HEAD)\" &&\n \t\tgit update-ref refs/remotes/origin/foo \"$oid\" &&\n \n+\t\t# Also creates a commit, tree, and blob.\n \t\tgit notes add -m foo &&\n \n \t\tcat >expect <<-\\EOF &&\n-\t\t| Repository stats | Value |\n-\t\t| ---------------- | ----- |\n-\t\t| * References     |       |\n-\t\t|   * Count        |     4 |\n-\t\t|     * Branches   |     1 |\n-\t\t|     * Tags       |     1 |\n-\t\t|     * Remotes    |     1 |\n-\t\t|     * Others     |     1 |\n+\t\t| Repository stats    | Value |\n+\t\t| ------------------- | ----- |\n+\t\t| * References        |       |\n+\t\t|   * Count           |     4 |\n+\t\t|     * Branches      |     1 |\n+\t\t|     * Tags          |     1 |\n+\t\t|     * Remotes       |     1 |\n+\t\t|     * Others        |     1 |\n+\t\t|                     |       |\n+\t\t| * Reachable objects |       |\n+\t\t|   * Count           |   130 |\n+\t\t|     * Commits       |    43 |\n+\t\t|     * Trees         |    43 |\n+\t\t|     * Blobs         |    43 |\n+\t\t|     * Tags          |     1 |\n \t\tEOF\n \n \t\tgit repo stats >out 2>err &&\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527267","messageId":"20250924212426.2930029-6-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250924212426.2930029-1-jltobler@gmail.com","subject":"[PATCH v2 5/6] builtin/repo: add keyvalue and nul format for stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-24T21:24:25Z","receivedAt":"2025-09-24T21:24:36Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"All repository stats are outputted in a human-friendly table form. This\nformat is not suitable for machine parsing. Add a --format option that\nsupports three output modes: `table`, `keyvalue`, and `nul`. The `table`\nmode is the default format and prints the same table output as before.\n\nWith the `keyvalue` mode, each line of output contains a key-value pair\nof a repository stat. The '=' character is used to delimit between keys\nand values. The `nul` mode is similar to `keyvalue`, but key-values are\ndelimited by a NUL character instead of a newline. Also, instead of a\n'=' character to delimit between keys and values, a newline character is\nused. This allows stat values to support special characters without\nhaving to cquote them. These two new modes provides output that is more\nmachine-friendly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc | 25 +++++++++++++--\n builtin/repo.c              | 62 ++++++++++++++++++++++++++++++++++---\n t/t1901-repo-stats.sh       | 33 ++++++++++++++++++++\n 3 files changed, 112 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 0b8d74ed3e..db21b75522 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -9,7 +9,7 @@ SYNOPSIS\n --------\n [synopsis]\n git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n-git repo stats\n+git repo stats [--format=(table|keyvalue|nul)]\n \n DESCRIPTION\n -----------\n@@ -44,7 +44,7 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n-`stats`::\n+`stats [--format=(table|keyvalue|nul)]`::\n \tRetrieve statistics about the current repository. The following kinds\n \tof information are reported:\n +\n@@ -52,7 +52,26 @@ supported:\n * Reachable object counts categorized by type\n \n +\n-The table output format may change and is not intended for machine parsing.\n+The output format can be chosen through the flag `--format`. Three formats are\n+supported:\n++\n+`table`:::\n+\tOutputs repository stats in a human-friendly table and is used by\n+\tdefault. This format may change and is not intended for machine\n+\tparsing.\n+\n+`keyvalue`:::\n+\tEach line of output contains a key-value pair for a repository stat.\n+\tThe '=' character is used to delimit between the key and the value.\n+\tValues containing \"unusual\" characters are quoted as explained for the\n+\tconfiguration variable `core.quotePath` (see linkgit:git-config[1]).\n+\n+`nul`:::\n+\tSimilar to `keyvalue`, but uses a NUL character to delimit between\n+\tkey-value pairs instead of a newline. Also uses a newline character as\n+\tthe delimiter between the key and value instead of '='. Unlike the\n+\t`keyvalue` format, values containing \"unusual\" characters are never\n+\tquoted.\n \n INFO KEYS\n ---------\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 8f130bca66..fe7d43f78e 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -14,13 +14,14 @@\n \n static const char *const repo_usage[] = {\n \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n-\t\"git repo stats\",\n+\t\"git repo stats [--format=(table|keyvalue|nul)]\",\n \tNULL\n };\n \n typedef int get_value_fn(struct repository *repo, struct strbuf *buf);\n \n enum output_format {\n+\tFORMAT_TABLE,\n \tFORMAT_KEYVALUE,\n \tFORMAT_NUL_TERMINATED,\n };\n@@ -135,6 +136,8 @@ static int parse_format_cb(const struct option *opt,\n \t\t*format = FORMAT_NUL_TERMINATED;\n \telse if (!strcmp(arg, \"keyvalue\"))\n \t\t*format = FORMAT_KEYVALUE;\n+\telse if (!strcmp(arg, \"table\"))\n+\t\t*format = FORMAT_TABLE;\n \telse\n \t\tdie(_(\"invalid format '%s'\"), arg);\n \n@@ -157,6 +160,8 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n+\tif (format != FORMAT_KEYVALUE && format != FORMAT_NUL_TERMINATED)\n+\t\tdie(_(\"unsupported output format\"));\n \n \treturn print_fields(argc, argv, repo, format);\n }\n@@ -312,6 +317,33 @@ static void stats_table_clear(struct stats_table *table)\n \tstring_list_clear(&table->rows, 1);\n }\n \n+static void stats_keyvalue_print(struct repo_stats *stats, char key_delim,\n+\t\t\t\t char value_delim)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\n+\tstrbuf_addf(&buf, \"references.branches.count%c%\" PRIuMAX \"%c\",\n+\t\t    key_delim, (uintmax_t)stats->refs.branches, value_delim);\n+\tstrbuf_addf(&buf, \"references.tags.count%c%\" PRIuMAX \"%c\",\n+\t\t    key_delim, (uintmax_t)stats->refs.tags, value_delim);\n+\tstrbuf_addf(&buf, \"references.remotes.count%c%\" PRIuMAX \"%c\",\n+\t\t    key_delim, (uintmax_t)stats->refs.remotes, value_delim);\n+\tstrbuf_addf(&buf, \"references.others.count%c%\" PRIuMAX \"%c\",\n+\t\t    key_delim, (uintmax_t)stats->refs.others, value_delim);\n+\n+\tstrbuf_addf(&buf, \"objects.commits.count%c%\" PRIuMAX \"%c\",\n+\t\t    key_delim, (uintmax_t)stats->objects.commits, value_delim);\n+\tstrbuf_addf(&buf, \"objects.trees.count%c%\" PRIuMAX \"%c\",\n+\t\t    key_delim, (uintmax_t)stats->objects.trees, value_delim);\n+\tstrbuf_addf(&buf, \"objects.blobs.count%c%\" PRIuMAX \"%c\",\n+\t\t    key_delim, (uintmax_t)stats->objects.blobs, value_delim);\n+\tstrbuf_addf(&buf, \"objects.tags.count%c%\" PRIuMAX \"%c\",\n+\t\t    key_delim, (uintmax_t)stats->objects.tags, value_delim);\n+\n+\tfwrite(buf.buf, sizeof(char), buf.len, stdout);\n+\tstrbuf_release(&buf);\n+}\n+\n static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n {\n \tfor (int i = 0; i < refs->nr; i++) {\n@@ -389,17 +421,25 @@ static void stats_count_objects(struct object_stats *stats,\n \tpath_walk_info_clear(&info);\n }\n \n-static int cmd_repo_stats(int argc UNUSED, const char **argv UNUSED,\n-\t\t\t  const char *prefix, struct repository *repo)\n+static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n+\t\t\t  struct repository *repo)\n {\n \tstruct ref_filter filter = REF_FILTER_INIT;\n \tstruct stats_table table = {\n \t\t.rows = STRING_LIST_INIT_DUP,\n \t};\n+\tenum output_format format = FORMAT_TABLE;\n \tstruct repo_stats stats = { 0 };\n \tstruct ref_array refs = { 0 };\n \tstruct rev_info revs;\n+\tstruct option options[] = {\n+\t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n+\t\t\t       N_(\"output format\"),\n+\t\t\t       PARSE_OPT_NONEG, parse_format_cb),\n+\t\tOPT_END()\n+\t};\n \n+\tparse_options(argc, argv, prefix, options, repo_usage, 0);\n \trepo_init_revisions(repo, &revs, prefix);\n \tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n \t\tdie(_(\"unable to filter refs\"));\n@@ -407,8 +447,20 @@ static int cmd_repo_stats(int argc UNUSED, const char **argv UNUSED,\n \tstats_count_references(&stats.refs, &refs);\n \tstats_count_objects(&stats.objects, &refs, &revs);\n \n-\tstats_table_setup(&table, &stats);\n-\tstats_table_print(&table);\n+\tswitch (format) {\n+\tcase FORMAT_TABLE:\n+\t\tstats_table_setup(&table, &stats);\n+\t\tstats_table_print(&table);\n+\t\tbreak;\n+\tcase FORMAT_KEYVALUE:\n+\t\tstats_keyvalue_print(&stats, '=', '\\n');\n+\t\tbreak;\n+\tcase FORMAT_NUL_TERMINATED:\n+\t\tstats_keyvalue_print(&stats, '\\n', '\\0');\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"invalid output format\");\n+\t}\n \n \tstats_table_clear(&table);\n \trelease_revisions(&revs);\ndiff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\nindex 315b9e1767..d2c1b6e307 100755\n--- a/t/t1901-repo-stats.sh\n+++ b/t/t1901-repo-stats.sh\n@@ -73,4 +73,37 @@ test_expect_success 'repository with references and objects' '\n \t)\n '\n \n+test_expect_success 'repository stats with keyvalue and nul format' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit_bulk 42 &&\n+\t\tgit tag -a foo -m bar &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\t\treferences.branches.count=1\n+\t\treferences.tags.count=1\n+\t\treferences.remotes.count=0\n+\t\treferences.others.count=0\n+\t\tobjects.commits.count=42\n+\t\tobjects.trees.count=42\n+\t\tobjects.blobs.count=42\n+\t\tobjects.tags.count=1\n+\t\tEOF\n+\n+\t\tgit repo stats --format=keyvalue >out 2>err &&\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err &&\n+\n+\t\t# Replace key and value delimiters for nul format.\n+\t\ttr \"\\n\" \"\\0\" <expect | tr \"=\" \"\\n\" >expect_null &&\n+\t\tgit repo stats --format=nul >out 2>err &&\n+\n+\t\ttest_cmp expect_null out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527268","messageId":"20250924212426.2930029-7-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250924212426.2930029-1-jltobler@gmail.com","subject":"[PATCH v2 6/6] builtin/repo: add progress meter for stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-24T21:24:26Z","receivedAt":"2025-09-24T21:24:37Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"When using the stats subcommand for git-repo(1), evaluating a repository\nmay take some time depending on its shape. Add a progress meter to\nprovide feedback to the user about what is happening. The progress meter\nis enabled by default when the command is executed from a tty. It can\nalso be explicitly enabled/disabled via the --[no-]progress option.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/repo.c | 46 ++++++++++++++++++++++++++++++++++++++++------\n 1 file changed, 40 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex fe7d43f78e..fdc8af92dc 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -4,6 +4,7 @@\n #include \"environment.h\"\n #include \"parse-options.h\"\n #include \"path-walk.h\"\n+#include \"progress.h\"\n #include \"quote.h\"\n #include \"ref-filter.h\"\n #include \"refs.h\"\n@@ -344,8 +345,14 @@ static void stats_keyvalue_print(struct repo_stats *stats, char key_delim,\n \tstrbuf_release(&buf);\n }\n \n-static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n+static void stats_count_references(struct ref_stats *stats, struct ref_array *refs,\n+\t\t\t\t   struct repository *repo, int show_progress)\n {\n+\tstruct progress *progress = NULL;\n+\n+\tif (show_progress)\n+\t\tprogress = start_progress(repo, _(\"Counting references\"), refs->nr);\n+\n \tfor (int i = 0; i < refs->nr; i++) {\n \t\tstruct ref_array_item *ref = refs->items[i];\n \n@@ -365,13 +372,24 @@ static void stats_count_references(struct ref_stats *stats, struct ref_array *re\n \t\tdefault:\n \t\t\tBUG(\"unexpected reference type\");\n \t\t}\n+\n+\t\tdisplay_progress(progress, i + 1);\n \t}\n+\n+\tstop_progress(&progress);\n }\n \n+struct count_objects_data {\n+\tstruct object_stats *stats;\n+\tstruct progress *progress;\n+};\n+\n static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\t enum object_type type, void *cb_data)\n {\n-\tstruct object_stats *stats = cb_data;\n+\tstruct count_objects_data *data = cb_data;\n+\tstruct object_stats *stats = data->stats;\n+\tsize_t object_count;\n \n \tswitch (type) {\n \tcase OBJ_TAG:\n@@ -390,17 +408,24 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\tBUG(\"invalid object type\");\n \t}\n \n+\tobject_count = stats->tags + stats->commits + stats->trees + stats->blobs;\n+\tdisplay_progress(data->progress, object_count);\n+\n \treturn 0;\n }\n \n static void stats_count_objects(struct object_stats *stats,\n-\t\t\t\tstruct ref_array *refs, struct rev_info *revs)\n+\t\t\t\tstruct ref_array *refs, struct rev_info *revs,\n+\t\t\t\tstruct repository *repo, int show_progress)\n {\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n+\tstruct count_objects_data data = {\n+\t\t.stats = stats,\n+\t};\n \n \tinfo.revs = revs;\n \tinfo.path_fn = count_objects;\n-\tinfo.path_fn_data = stats;\n+\tinfo.path_fn_data = &data;\n \n \tfor (int i = 0; i < refs->nr; i++) {\n \t\tstruct ref_array_item *ref = refs->items[i];\n@@ -417,8 +442,12 @@ static void stats_count_objects(struct object_stats *stats,\n \t\t}\n \t}\n \n+\tif (show_progress)\n+\t\tdata.progress = start_progress(repo, _(\"Counting Objects\"), 0);\n+\n \twalk_objects_by_path(&info);\n \tpath_walk_info_clear(&info);\n+\tstop_progress(&data.progress);\n }\n \n static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n@@ -432,10 +461,12 @@ static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n \tstruct repo_stats stats = { 0 };\n \tstruct ref_array refs = { 0 };\n \tstruct rev_info revs;\n+\tint show_progress = -1;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n \t\t\t       N_(\"output format\"),\n \t\t\t       PARSE_OPT_NONEG, parse_format_cb),\n+\t\tOPT_BOOL(0, \"progress\", &show_progress, N_(\"show progress\")),\n \t\tOPT_END()\n \t};\n \n@@ -444,8 +475,11 @@ static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n \tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n \t\tdie(_(\"unable to filter refs\"));\n \n-\tstats_count_references(&stats.refs, &refs);\n-\tstats_count_objects(&stats.objects, &refs, &revs);\n+\tif (show_progress < 0)\n+\t\tshow_progress = isatty(2);\n+\n+\tstats_count_references(&stats.refs, &refs, repo, show_progress);\n+\tstats_count_objects(&stats.objects, &refs, &revs, repo, show_progress);\n \n \tswitch (format) {\n \tcase FORMAT_TABLE:\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527280","messageId":"aNTVa3-RtYNPlcHc@pks.im","threadId":"64187","inReplyTo":"20250924212426.2930029-4-jltobler@gmail.com","subject":"Re: [PATCH v2 3/6] builtin/repo: introduce stats subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-25T05:38:51Z","receivedAt":"2025-09-25T05:39:02Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Sep 24, 2025 at 04:24:23PM -0500, Justin Tobler wrote:\n> diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\n> index 209afd1b61..a009bf8cf1 100644\n> --- a/Documentation/git-repo.adoc\n> +++ b/Documentation/git-repo.adoc\n> @@ -156,12 +159,188 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n>  \treturn print_fields(argc, argv, repo, format);\n>  }\n>  \n> +struct ref_stats {\n> +\tsize_t branches;\n> +\tsize_t remotes;\n> +\tsize_t tags;\n> +\tsize_t others;\n> +};\n> +\n> +struct stats_table {\n> +\tstruct string_list rows;\n> +\n> +\tsize_t name_col_width;\n> +\tsize_t value_col_width;\n> +};\n> +\n> +/*\n> + * Holds column data that gets stored for each row.\n> + */\n> +struct stats_table_entry {\n> +\tchar *value;\n> +};\n> +\n> +static void stats_table_add(struct stats_table *table, const char *format,\n> +\t\t\t    const char *name, struct stats_table_entry *entry)\n\nWe could of course accept varargs right from the start and thus allow\nthe caller to pass arbitrary formatting directives. But I guess we don't\nneed it now, so it's fine to not do it.\n\n[snip]\n> +static void stats_table_print(struct stats_table *table)\n\nNit: The table can be marked as `const` as we don't modify it.\n\n> +{\n> +\tconst char *name_col_title = _(\"Repository stats\");\n> +\tconst char *value_col_title = _(\"Value\");\n> +\tsize_t name_title_len = strlen(name_col_title);\n> +\tsize_t value_title_len = strlen(value_col_title);\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\tstruct string_list_item *item;\n> +\tint name_col_width;\n> +\tint value_col_width;\n> +\n> +\tname_col_width = cast_size_t_to_int(\n> +\t\tmax_size_t(table->name_col_width, name_title_len));\n> +\tvalue_col_width = cast_size_t_to_int(\n> +\t\tmax_size_t(table->value_col_width, value_title_len));\n> +\n> +\tstrbuf_addf(&buf, \"| %-*s | %-*s |\\n\", name_col_width, name_col_title,\n> +\t\t    value_col_width, value_col_title);\n> +\tstrbuf_addstr(&buf, \"| \");\n> +\tstrbuf_addchars(&buf, '-', name_col_width);\n> +\tstrbuf_addstr(&buf, \" | \");\n> +\tstrbuf_addchars(&buf, '-', value_col_width);\n> +\tstrbuf_addstr(&buf, \" |\\n\");\n> +\n> +\tfor_each_string_list_item(item, &table->rows) {\n> +\t\tstruct stats_table_entry *entry = item->util;\n> +\t\tconst char *value = \"\";\n> +\n> +\t\tif (entry) {\n> +\t\t\tstruct stats_table_entry *entry = item->util;\n> +\t\t\tvalue = entry->value;\n> +\t\t}\n> +\n> +\t\tstrbuf_addf(&buf, \"| %-*s | %*s |\\n\", name_col_width,\n> +\t\t\t    item->string, value_col_width, value);\n> +\t}\n> +\n> +\tfputs(buf.buf, stdout);\n> +\tstrbuf_release(&buf);\n> +}\n\nBy the way, is there any specific reason we do the detour via the strbuf\ninstead of printing the data to stdout directly?\n\n> +static void stats_table_clear(struct stats_table *table)\n> +{\n> +\tstruct stats_table_entry *entry;\n> +\tstruct string_list_item *item;\n> +\n> +\tfor_each_string_list_item(item, &table->rows) {\n> +\t\tentry = item->util;\n> +\t\tif (entry)\n> +\t\t\tfree(entry->value);\n> +\t}\n> +\n> +\tstring_list_clear(&table->rows, 1);\n> +}\n\nYeah, this is much nicer now.\n\n> +static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n> +{\n> +\tfor (int i = 0; i < refs->nr; i++) {\n> +\t\tstruct ref_array_item *ref = refs->items[i];\n> +\n> +\t\tswitch (ref->kind) {\n> +\t\tcase FILTER_REFS_BRANCHES:\n> +\t\t\tstats->branches++;\n> +\t\t\tbreak;\n> +\t\tcase FILTER_REFS_REMOTES:\n> +\t\t\tstats->remotes++;\n> +\t\t\tbreak;\n> +\t\tcase FILTER_REFS_TAGS:\n> +\t\t\tstats->tags++;\n> +\t\t\tbreak;\n> +\t\tcase FILTER_REFS_OTHERS:\n> +\t\t\tstats->others++;\n> +\t\t\tbreak;\n> +\t\tdefault:\n> +\t\t\tBUG(\"unexpected reference type\");\n> +\t\t}\n> +\t}\n> +}\n\nHere we're now being defensive. Good.\n\nPatrick\n"},{"id":"527281","messageId":"aNTVdy4hhLDlMpVT@pks.im","threadId":"64187","inReplyTo":"20250924212426.2930029-6-jltobler@gmail.com","subject":"Re: [PATCH v2 5/6] builtin/repo: add keyvalue and nul format for stats","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-25T05:39:03Z","receivedAt":"2025-09-25T05:39:09Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Sep 24, 2025 at 04:24:25PM -0500, Justin Tobler wrote:\n> diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\n> index 0b8d74ed3e..db21b75522 100644\n> --- a/Documentation/git-repo.adoc\n> +++ b/Documentation/git-repo.adoc\n> @@ -52,7 +52,26 @@ supported:\n>  * Reachable object counts categorized by type\n>  \n>  +\n> -The table output format may change and is not intended for machine parsing.\n> +The output format can be chosen through the flag `--format`. Three formats are\n> +supported:\n> ++\n> +`table`:::\n> +\tOutputs repository stats in a human-friendly table and is used by\n> +\tdefault. This format may change and is not intended for machine\n> +\tparsing.\n\nLet's mention that this is the default format.\n\n> +`keyvalue`:::\n> +\tEach line of output contains a key-value pair for a repository stat.\n> +\tThe '=' character is used to delimit between the key and the value.\n> +\tValues containing \"unusual\" characters are quoted as explained for the\n> +\tconfiguration variable `core.quotePath` (see linkgit:git-config[1]).\n\nIn the current state there is never any quoting, so this statement here\nis a bit misleading. Should we maybe drop that part?\n\n> +`nul`:::\n> +\tSimilar to `keyvalue`, but uses a NUL character to delimit between\n> +\tkey-value pairs instead of a newline. Also uses a newline character as\n> +\tthe delimiter between the key and value instead of '='. Unlike the\n> +\t`keyvalue` format, values containing \"unusual\" characters are never\n> +\tquoted.\n\nLikewise.\n\n> diff --git a/builtin/repo.c b/builtin/repo.c\n> index 8f130bca66..fe7d43f78e 100644\n> --- a/builtin/repo.c\n> +++ b/builtin/repo.c\n> @@ -312,6 +317,33 @@ static void stats_table_clear(struct stats_table *table)\n>  \tstring_list_clear(&table->rows, 1);\n>  }\n>  \n> +static void stats_keyvalue_print(struct repo_stats *stats, char key_delim,\n> +\t\t\t\t char value_delim)\n> +{\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\n> +\tstrbuf_addf(&buf, \"references.branches.count%c%\" PRIuMAX \"%c\",\n> +\t\t    key_delim, (uintmax_t)stats->refs.branches, value_delim);\n> +\tstrbuf_addf(&buf, \"references.tags.count%c%\" PRIuMAX \"%c\",\n> +\t\t    key_delim, (uintmax_t)stats->refs.tags, value_delim);\n> +\tstrbuf_addf(&buf, \"references.remotes.count%c%\" PRIuMAX \"%c\",\n> +\t\t    key_delim, (uintmax_t)stats->refs.remotes, value_delim);\n> +\tstrbuf_addf(&buf, \"references.others.count%c%\" PRIuMAX \"%c\",\n> +\t\t    key_delim, (uintmax_t)stats->refs.others, value_delim);\n> +\n> +\tstrbuf_addf(&buf, \"objects.commits.count%c%\" PRIuMAX \"%c\",\n> +\t\t    key_delim, (uintmax_t)stats->objects.commits, value_delim);\n> +\tstrbuf_addf(&buf, \"objects.trees.count%c%\" PRIuMAX \"%c\",\n> +\t\t    key_delim, (uintmax_t)stats->objects.trees, value_delim);\n> +\tstrbuf_addf(&buf, \"objects.blobs.count%c%\" PRIuMAX \"%c\",\n> +\t\t    key_delim, (uintmax_t)stats->objects.blobs, value_delim);\n> +\tstrbuf_addf(&buf, \"objects.tags.count%c%\" PRIuMAX \"%c\",\n> +\t\t    key_delim, (uintmax_t)stats->objects.tags, value_delim);\n> +\n> +\tfwrite(buf.buf, sizeof(char), buf.len, stdout);\n> +\tstrbuf_release(&buf);\n> +}\n\nSame question here regarding the buffering. Can't we print to stdout\ndirectly, or is there a reason not to?\n\n> @@ -389,17 +421,25 @@ static void stats_count_objects(struct object_stats *stats,\n>  \tpath_walk_info_clear(&info);\n>  }\n>  \n> -static int cmd_repo_stats(int argc UNUSED, const char **argv UNUSED,\n> -\t\t\t  const char *prefix, struct repository *repo)\n> +static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n> +\t\t\t  struct repository *repo)\n>  {\n>  \tstruct ref_filter filter = REF_FILTER_INIT;\n>  \tstruct stats_table table = {\n>  \t\t.rows = STRING_LIST_INIT_DUP,\n>  \t};\n> +\tenum output_format format = FORMAT_TABLE;\n>  \tstruct repo_stats stats = { 0 };\n>  \tstruct ref_array refs = { 0 };\n>  \tstruct rev_info revs;\n> +\tstruct option options[] = {\n> +\t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n> +\t\t\t       N_(\"output format\"),\n> +\t\t\t       PARSE_OPT_NONEG, parse_format_cb),\n> +\t\tOPT_END()\n> +\t};\n>  \n> +\tparse_options(argc, argv, prefix, options, repo_usage, 0);\n\nI think it would be sensible to introduce this call to `parse_options()`\nright in the first commit that wires up the new subcommand. If we don't\ndo that we otherwise accept arbitrary arguments without raising any\nerror, and neither do we know to output help.\n\nSo we should move the addition to a previous commit and probably do the\nfollowing:\n\n        argc = parse_options(...);\n        if (argc)\n            usagef(\"too many arguments\");\n\n> @@ -407,8 +447,20 @@ static int cmd_repo_stats(int argc UNUSED, const char **argv UNUSED,\n>  \tstats_count_references(&stats.refs, &refs);\n>  \tstats_count_objects(&stats.objects, &refs, &revs);\n>  \n> -\tstats_table_setup(&table, &stats);\n> -\tstats_table_print(&table);\n> +\tswitch (format) {\n> +\tcase FORMAT_TABLE:\n> +\t\tstats_table_setup(&table, &stats);\n> +\t\tstats_table_print(&table);\n> +\t\tbreak;\n> +\tcase FORMAT_KEYVALUE:\n> +\t\tstats_keyvalue_print(&stats, '=', '\\n');\n> +\t\tbreak;\n> +\tcase FORMAT_NUL_TERMINATED:\n> +\t\tstats_keyvalue_print(&stats, '\\n', '\\0');\n> +\t\tbreak;\n\nThis reads much nicer now. The newline as key-value delimiter is a\ncurious choice, but you simply do what we already do in `git repo info`.\n\n> diff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\n> index 315b9e1767..d2c1b6e307 100755\n> --- a/t/t1901-repo-stats.sh\n> +++ b/t/t1901-repo-stats.sh\n> @@ -73,4 +73,37 @@ test_expect_success 'repository with references and objects' '\n>  \t)\n>  '\n>  \n> +test_expect_success 'repository stats with keyvalue and nul format' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_commit_bulk 42 &&\n> +\t\tgit tag -a foo -m bar &&\n> +\n> +\t\tcat >expect <<-\\EOF &&\n> +\t\treferences.branches.count=1\n> +\t\treferences.tags.count=1\n> +\t\treferences.remotes.count=0\n> +\t\treferences.others.count=0\n> +\t\tobjects.commits.count=42\n> +\t\tobjects.trees.count=42\n> +\t\tobjects.blobs.count=42\n> +\t\tobjects.tags.count=1\n> +\t\tEOF\n> +\n> +\t\tgit repo stats --format=keyvalue >out 2>err &&\n> +\n> +\t\ttest_cmp expect out &&\n> +\t\ttest_line_count = 0 err &&\n> +\n> +\t\t# Replace key and value delimiters for nul format.\n> +\t\ttr \"\\n\" \"\\0\" <expect | tr \"=\" \"\\n\" >expect_null &&\n\nYou can do this without the pipe:\n\n    tr \"\\n=\" \"\\0\\n\" <expect >expect_nul\n\nAlso, let's call the file `expect_nul` (with a single 'l') to match the\nformat.\n\nPatrick\n"},{"id":"527282","messageId":"aNTVgnEGE2qUwZwK@pks.im","threadId":"64187","inReplyTo":"20250924212426.2930029-7-jltobler@gmail.com","subject":"Re: [PATCH v2 6/6] builtin/repo: add progress meter for stats","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-25T05:39:14Z","receivedAt":"2025-09-25T05:39:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Sep 24, 2025 at 04:24:26PM -0500, Justin Tobler wrote:\n> diff --git a/builtin/repo.c b/builtin/repo.c\n> index fe7d43f78e..fdc8af92dc 100644\n> --- a/builtin/repo.c\n> +++ b/builtin/repo.c\n> @@ -344,8 +345,14 @@ static void stats_keyvalue_print(struct repo_stats *stats, char key_delim,\n>  \tstrbuf_release(&buf);\n>  }\n>  \n> -static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n> +static void stats_count_references(struct ref_stats *stats, struct ref_array *refs,\n> +\t\t\t\t   struct repository *repo, int show_progress)\n>  {\n> +\tstruct progress *progress = NULL;\n> +\n> +\tif (show_progress)\n> +\t\tprogress = start_progress(repo, _(\"Counting references\"), refs->nr);\n\nWe tend to use `start_delayed_progress()` so that the progress meter is\nnot displayed when the action takes less than a second. The delay can be\ndisabled in our tests by using `GIT_PROGRESS_DELAY=0`.\n\n> @@ -365,13 +372,24 @@ static void stats_count_references(struct ref_stats *stats, struct ref_array *re\n>  \t\tdefault:\n>  \t\t\tBUG(\"unexpected reference type\");\n>  \t\t}\n> +\n> +\t\tdisplay_progress(progress, i + 1);\n>  \t}\n> +\n> +\tstop_progress(&progress);\n>  }\n>  \n> +struct count_objects_data {\n> +\tstruct object_stats *stats;\n> +\tstruct progress *progress;\n> +};\n> +\n>  static int count_objects(const char *path UNUSED, struct oid_array *oids,\n>  \t\t\t enum object_type type, void *cb_data)\n>  {\n> -\tstruct object_stats *stats = cb_data;\n> +\tstruct count_objects_data *data = cb_data;\n> +\tstruct object_stats *stats = data->stats;\n> +\tsize_t object_count;\n>  \n>  \tswitch (type) {\n>  \tcase OBJ_TAG:\n> @@ -390,17 +408,24 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n>  \t\tBUG(\"invalid object type\");\n>  \t}\n>  \n> +\tobject_count = stats->tags + stats->commits + stats->trees + stats->blobs;\n\nWe have this computation in two locations now. Maybe we should\ndeduplicate it via something like:\n\n    static inline size_t stats_get_total_object_count()\n    {\n        return stats->tags + stats->commits + stats->trees + stats->blobs;\n    }\n\n> @@ -417,8 +442,12 @@ static void stats_count_objects(struct object_stats *stats,\n>  \t\t}\n>  \t}\n>  \n> +\tif (show_progress)\n> +\t\tdata.progress = start_progress(repo, _(\"Counting Objects\"), 0);\n\ns/Objects/objects/\n\n> @@ -432,10 +461,12 @@ static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n>  \tstruct repo_stats stats = { 0 };\n>  \tstruct ref_array refs = { 0 };\n>  \tstruct rev_info revs;\n> +\tint show_progress = -1;\n>  \tstruct option options[] = {\n>  \t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n>  \t\t\t       N_(\"output format\"),\n>  \t\t\t       PARSE_OPT_NONEG, parse_format_cb),\n> +\t\tOPT_BOOL(0, \"progress\", &show_progress, N_(\"show progress\")),\n>  \t\tOPT_END()\n>  \t};\n>  \n> @@ -444,8 +475,11 @@ static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n>  \tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n>  \t\tdie(_(\"unable to filter refs\"));\n>  \n> -\tstats_count_references(&stats.refs, &refs);\n> -\tstats_count_objects(&stats.objects, &refs, &revs);\n> +\tif (show_progress < 0)\n> +\t\tshow_progress = isatty(2);\n\nMakes sense.\n\n> +\tstats_count_references(&stats.refs, &refs, repo, show_progress);\n> +\tstats_count_objects(&stats.objects, &refs, &revs, repo, show_progress);\n>  \n>  \tswitch (format) {\n>  \tcase FORMAT_TABLE:\n\nShould our tests be updated to verify that we know to print progress\ndepending on whether or not `--progress` is passed?\n\nPatrick\n"},{"id":"527321","messageId":"a3qgun7qrabs6wplbv5p75cokmr2flklnvyhzteytiu356b2pp@nruhrm4d2hhk","threadId":"64187","inReplyTo":"aNTVa3-RtYNPlcHc@pks.im","subject":"Re: [PATCH v2 3/6] builtin/repo: introduce stats subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-25T13:01:20Z","receivedAt":"2025-09-25T13:01:22Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/09/25 07:38AM, Patrick Steinhardt wrote:\n> On Wed, Sep 24, 2025 at 04:24:23PM -0500, Justin Tobler wrote:\n[snip]\n> > +static void stats_table_add(struct stats_table *table, const char *format,\n> > +\t\t\t    const char *name, struct stats_table_entry *entry)\n> \n> We could of course accept varargs right from the start and thus allow\n> the caller to pass arbitrary formatting directives. But I guess we don't\n> need it now, so it's fine to not do it.\n\nI was on the fence about using varargs from the start. I figured since\nwe don't have a need right now for multiple formatting directives, we\ncould just pass the one. I might just go ahead and implement varargs\nhere in the next version though.\n\n> [snip]\n> > +static void stats_table_print(struct stats_table *table)\n> \n> Nit: The table can be marked as `const` as we don't modify it.\n\nWill update.\n\n> > +{\n> > +\tconst char *name_col_title = _(\"Repository stats\");\n> > +\tconst char *value_col_title = _(\"Value\");\n> > +\tsize_t name_title_len = strlen(name_col_title);\n> > +\tsize_t value_title_len = strlen(value_col_title);\n> > +\tstruct strbuf buf = STRBUF_INIT;\n> > +\tstruct string_list_item *item;\n> > +\tint name_col_width;\n> > +\tint value_col_width;\n> > +\n> > +\tname_col_width = cast_size_t_to_int(\n> > +\t\tmax_size_t(table->name_col_width, name_title_len));\n> > +\tvalue_col_width = cast_size_t_to_int(\n> > +\t\tmax_size_t(table->value_col_width, value_title_len));\n> > +\n> > +\tstrbuf_addf(&buf, \"| %-*s | %-*s |\\n\", name_col_width, name_col_title,\n> > +\t\t    value_col_width, value_col_title);\n> > +\tstrbuf_addstr(&buf, \"| \");\n> > +\tstrbuf_addchars(&buf, '-', name_col_width);\n> > +\tstrbuf_addstr(&buf, \" | \");\n> > +\tstrbuf_addchars(&buf, '-', value_col_width);\n> > +\tstrbuf_addstr(&buf, \" |\\n\");\n> > +\n> > +\tfor_each_string_list_item(item, &table->rows) {\n> > +\t\tstruct stats_table_entry *entry = item->util;\n> > +\t\tconst char *value = \"\";\n> > +\n> > +\t\tif (entry) {\n> > +\t\t\tstruct stats_table_entry *entry = item->util;\n> > +\t\t\tvalue = entry->value;\n> > +\t\t}\n> > +\n> > +\t\tstrbuf_addf(&buf, \"| %-*s | %*s |\\n\", name_col_width,\n> > +\t\t\t    item->string, value_col_width, value);\n> > +\t}\n> > +\n> > +\tfputs(buf.buf, stdout);\n> > +\tstrbuf_release(&buf);\n> > +}\n> \n> By the way, is there any specific reason we do the detour via the strbuf\n> instead of printing the data to stdout directly?\n\nNot really. I think it's just a holdover from a previous implementation\nthat was passing around a strbuf before printing. I'll go ahead and just\nprint directly to stdout via printf in the next version.\n\n-Justin\n"},{"id":"527322","messageId":"lqzdrznf6kynr35eo74zodbfeofj6hnmskm2wqk6ty3o7wwaci@nyytil34eyzq","threadId":"64187","inReplyTo":"aNTVdy4hhLDlMpVT@pks.im","subject":"Re: [PATCH v2 5/6] builtin/repo: add keyvalue and nul format for stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-25T13:16:06Z","receivedAt":"2025-09-25T13:16:08Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/09/25 07:39AM, Patrick Steinhardt wrote:\n> On Wed, Sep 24, 2025 at 04:24:25PM -0500, Justin Tobler wrote:\n> > diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\n> > index 0b8d74ed3e..db21b75522 100644\n> > --- a/Documentation/git-repo.adoc\n> > +++ b/Documentation/git-repo.adoc\n> > @@ -52,7 +52,26 @@ supported:\n> >  * Reachable object counts categorized by type\n> >  \n> >  +\n> > -The table output format may change and is not intended for machine parsing.\n> > +The output format can be chosen through the flag `--format`. Three formats are\n> > +supported:\n> > ++\n> > +`table`:::\n> > +\tOutputs repository stats in a human-friendly table and is used by\n> > +\tdefault. This format may change and is not intended for machine\n> > +\tparsing.\n> \n> Let's mention that this is the default format.\n\nI didn't mention that it is \"used by default\", but I think the wording\ncould be more clear here. Will improve in the next version.\n\n> > +`keyvalue`:::\n> > +\tEach line of output contains a key-value pair for a repository stat.\n> > +\tThe '=' character is used to delimit between the key and the value.\n> > +\tValues containing \"unusual\" characters are quoted as explained for the\n> > +\tconfiguration variable `core.quotePath` (see linkgit:git-config[1]).\n> \n> In the current state there is never any quoting, so this statement here\n> is a bit misleading. Should we maybe drop that part?\n\nWhile there are currently not any values in the output that would\nrequire quoting, I'm inclined to leave this note in the documentation.\nThat way we set the expectation regarding how parsers should handle the\noutput from the start.\n\n[snip]\n> > +static void stats_keyvalue_print(struct repo_stats *stats, char key_delim,\n> > +\t\t\t\t char value_delim)\n> > +{\n> > +\tstruct strbuf buf = STRBUF_INIT;\n> > +\n> > +\tstrbuf_addf(&buf, \"references.branches.count%c%\" PRIuMAX \"%c\",\n> > +\t\t    key_delim, (uintmax_t)stats->refs.branches, value_delim);\n> > +\tstrbuf_addf(&buf, \"references.tags.count%c%\" PRIuMAX \"%c\",\n> > +\t\t    key_delim, (uintmax_t)stats->refs.tags, value_delim);\n> > +\tstrbuf_addf(&buf, \"references.remotes.count%c%\" PRIuMAX \"%c\",\n> > +\t\t    key_delim, (uintmax_t)stats->refs.remotes, value_delim);\n> > +\tstrbuf_addf(&buf, \"references.others.count%c%\" PRIuMAX \"%c\",\n> > +\t\t    key_delim, (uintmax_t)stats->refs.others, value_delim);\n> > +\n> > +\tstrbuf_addf(&buf, \"objects.commits.count%c%\" PRIuMAX \"%c\",\n> > +\t\t    key_delim, (uintmax_t)stats->objects.commits, value_delim);\n> > +\tstrbuf_addf(&buf, \"objects.trees.count%c%\" PRIuMAX \"%c\",\n> > +\t\t    key_delim, (uintmax_t)stats->objects.trees, value_delim);\n> > +\tstrbuf_addf(&buf, \"objects.blobs.count%c%\" PRIuMAX \"%c\",\n> > +\t\t    key_delim, (uintmax_t)stats->objects.blobs, value_delim);\n> > +\tstrbuf_addf(&buf, \"objects.tags.count%c%\" PRIuMAX \"%c\",\n> > +\t\t    key_delim, (uintmax_t)stats->objects.tags, value_delim);\n> > +\n> > +\tfwrite(buf.buf, sizeof(char), buf.len, stdout);\n> > +\tstrbuf_release(&buf);\n> > +}\n> \n> Same question here regarding the buffering. Can't we print to stdout\n> directly, or is there a reason not to?\n\nYa, we can just write to stdout directly. Will update.\n\n> > @@ -389,17 +421,25 @@ static void stats_count_objects(struct object_stats *stats,\n> >  \tpath_walk_info_clear(&info);\n> >  }\n> >  \n> > -static int cmd_repo_stats(int argc UNUSED, const char **argv UNUSED,\n> > -\t\t\t  const char *prefix, struct repository *repo)\n> > +static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n> > +\t\t\t  struct repository *repo)\n> >  {\n> >  \tstruct ref_filter filter = REF_FILTER_INIT;\n> >  \tstruct stats_table table = {\n> >  \t\t.rows = STRING_LIST_INIT_DUP,\n> >  \t};\n> > +\tenum output_format format = FORMAT_TABLE;\n> >  \tstruct repo_stats stats = { 0 };\n> >  \tstruct ref_array refs = { 0 };\n> >  \tstruct rev_info revs;\n> > +\tstruct option options[] = {\n> > +\t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n> > +\t\t\t       N_(\"output format\"),\n> > +\t\t\t       PARSE_OPT_NONEG, parse_format_cb),\n> > +\t\tOPT_END()\n> > +\t};\n> >  \n> > +\tparse_options(argc, argv, prefix, options, repo_usage, 0);\n> \n> I think it would be sensible to introduce this call to `parse_options()`\n> right in the first commit that wires up the new subcommand. If we don't\n> do that we otherwise accept arbitrary arguments without raising any\n> error, and neither do we know to output help.\n> \n> So we should move the addition to a previous commit and probably do the\n> following:\n> \n>         argc = parse_options(...);\n>         if (argc)\n>             usagef(\"too many arguments\");\n\nGood point. I'll add this in the next version.\n\n> > @@ -407,8 +447,20 @@ static int cmd_repo_stats(int argc UNUSED, const char **argv UNUSED,\n> >  \tstats_count_references(&stats.refs, &refs);\n> >  \tstats_count_objects(&stats.objects, &refs, &revs);\n> >  \n> > -\tstats_table_setup(&table, &stats);\n> > -\tstats_table_print(&table);\n> > +\tswitch (format) {\n> > +\tcase FORMAT_TABLE:\n> > +\t\tstats_table_setup(&table, &stats);\n> > +\t\tstats_table_print(&table);\n> > +\t\tbreak;\n> > +\tcase FORMAT_KEYVALUE:\n> > +\t\tstats_keyvalue_print(&stats, '=', '\\n');\n> > +\t\tbreak;\n> > +\tcase FORMAT_NUL_TERMINATED:\n> > +\t\tstats_keyvalue_print(&stats, '\\n', '\\0');\n> > +\t\tbreak;\n> \n> This reads much nicer now. The newline as key-value delimiter is a\n> curious choice, but you simply do what we already do in `git repo info`.\n\nI agree that the key-value delimiter chosen is a bit strange. The\ncommand is still experimental so we could maybe change it if want. Not\nsure if it would be worth it though.\n\n> > diff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\n> > index 315b9e1767..d2c1b6e307 100755\n> > --- a/t/t1901-repo-stats.sh\n> > +++ b/t/t1901-repo-stats.sh\n> > @@ -73,4 +73,37 @@ test_expect_success 'repository with references and objects' '\n> >  \t)\n> >  '\n> >  \n> > +test_expect_success 'repository stats with keyvalue and nul format' '\n> > +\ttest_when_finished \"rm -rf repo\" &&\n> > +\tgit init repo &&\n> > +\t(\n> > +\t\tcd repo &&\n> > +\t\ttest_commit_bulk 42 &&\n> > +\t\tgit tag -a foo -m bar &&\n> > +\n> > +\t\tcat >expect <<-\\EOF &&\n> > +\t\treferences.branches.count=1\n> > +\t\treferences.tags.count=1\n> > +\t\treferences.remotes.count=0\n> > +\t\treferences.others.count=0\n> > +\t\tobjects.commits.count=42\n> > +\t\tobjects.trees.count=42\n> > +\t\tobjects.blobs.count=42\n> > +\t\tobjects.tags.count=1\n> > +\t\tEOF\n> > +\n> > +\t\tgit repo stats --format=keyvalue >out 2>err &&\n> > +\n> > +\t\ttest_cmp expect out &&\n> > +\t\ttest_line_count = 0 err &&\n> > +\n> > +\t\t# Replace key and value delimiters for nul format.\n> > +\t\ttr \"\\n\" \"\\0\" <expect | tr \"=\" \"\\n\" >expect_null &&\n> \n> You can do this without the pipe:\n> \n>     tr \"\\n=\" \"\\0\\n\" <expect >expect_nul\n> \n> Also, let's call the file `expect_nul` (with a single 'l') to match the\n> format.\n\nThanks. Will update.\n\n-Justin\n"},{"id":"527323","messageId":"ehjxy2qvwhrtmyvmjv6hu5ej2pslomjn476mlkkodmbxwutel5@yx6an6nj75p4","threadId":"64187","inReplyTo":"aNTVgnEGE2qUwZwK@pks.im","subject":"Re: [PATCH v2 6/6] builtin/repo: add progress meter for stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-25T13:20:48Z","receivedAt":"2025-09-25T13:20:50Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/09/25 07:39AM, Patrick Steinhardt wrote:\n> On Wed, Sep 24, 2025 at 04:24:26PM -0500, Justin Tobler wrote:\n> > diff --git a/builtin/repo.c b/builtin/repo.c\n> > index fe7d43f78e..fdc8af92dc 100644\n> > --- a/builtin/repo.c\n> > +++ b/builtin/repo.c\n> > @@ -344,8 +345,14 @@ static void stats_keyvalue_print(struct repo_stats *stats, char key_delim,\n> >  \tstrbuf_release(&buf);\n> >  }\n> >  \n> > -static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n> > +static void stats_count_references(struct ref_stats *stats, struct ref_array *refs,\n> > +\t\t\t\t   struct repository *repo, int show_progress)\n> >  {\n> > +\tstruct progress *progress = NULL;\n> > +\n> > +\tif (show_progress)\n> > +\t\tprogress = start_progress(repo, _(\"Counting references\"), refs->nr);\n> \n> We tend to use `start_delayed_progress()` so that the progress meter is\n> not displayed when the action takes less than a second. The delay can be\n> disabled in our tests by using `GIT_PROGRESS_DELAY=0`.\n\nGood to know. I'll update in the next version.\n\n[snip]\n> > +\tobject_count = stats->tags + stats->commits + stats->trees + stats->blobs;\n> \n> We have this computation in two locations now. Maybe we should\n> deduplicate it via something like:\n> \n>     static inline size_t stats_get_total_object_count()\n>     {\n>         return stats->tags + stats->commits + stats->trees + stats->blobs;\n>     }\n\nMake sense. Will add.\n\n> \n> > @@ -417,8 +442,12 @@ static void stats_count_objects(struct object_stats *stats,\n> >  \t\t}\n> >  \t}\n> >  \n> > +\tif (show_progress)\n> > +\t\tdata.progress = start_progress(repo, _(\"Counting Objects\"), 0);\n> \n> s/Objects/objects/\n\nWill fix in the next version.\n\n[snip]\n> > +\tstats_count_references(&stats.refs, &refs, repo, show_progress);\n> > +\tstats_count_objects(&stats.objects, &refs, &revs, repo, show_progress);\n> >  \n> >  \tswitch (format) {\n> >  \tcase FORMAT_TABLE:\n> \n> Should our tests be updated to verify that we know to print progress\n> depending on whether or not `--progress` is passed?\n\nThat seems reasonable to me. I'll add a test for this in the next\nversion.\n\n-Justin\n"},{"id":"527327","messageId":"aNVKki1hXoYi8qCV@pks.im","threadId":"64187","inReplyTo":"lqzdrznf6kynr35eo74zodbfeofj6hnmskm2wqk6ty3o7wwaci@nyytil34eyzq","subject":"Re: [PATCH v2 5/6] builtin/repo: add keyvalue and nul format for stats","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-25T13:58:42Z","receivedAt":"2025-09-25T13:58:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Sep 25, 2025 at 08:16:06AM -0500, Justin Tobler wrote:\n> On 25/09/25 07:39AM, Patrick Steinhardt wrote:\n> > On Wed, Sep 24, 2025 at 04:24:25PM -0500, Justin Tobler wrote:\n> > > diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\n> > > index 0b8d74ed3e..db21b75522 100644\n> > > --- a/Documentation/git-repo.adoc\n> > > +++ b/Documentation/git-repo.adoc\n> > > @@ -52,7 +52,26 @@ supported:\n> > >  * Reachable object counts categorized by type\n> > >  \n> > >  +\n> > > -The table output format may change and is not intended for machine parsing.\n> > > +The output format can be chosen through the flag `--format`. Three formats are\n> > > +supported:\n> > > ++\n> > > +`table`:::\n> > > +\tOutputs repository stats in a human-friendly table and is used by\n> > > +\tdefault. This format may change and is not intended for machine\n> > > +\tparsing.\n> > \n> > Let's mention that this is the default format.\n> \n> I didn't mention that it is \"used by default\", but I think the wording\n> could be more clear here. Will improve in the next version.\n\nOh, I completely missed this. I guess I was looking for a sentence like\n\"This is the default.\" at the end of this paragraph, which is what we\noften use in other parts.\n\n> > > +`keyvalue`:::\n> > > +\tEach line of output contains a key-value pair for a repository stat.\n> > > +\tThe '=' character is used to delimit between the key and the value.\n> > > +\tValues containing \"unusual\" characters are quoted as explained for the\n> > > +\tconfiguration variable `core.quotePath` (see linkgit:git-config[1]).\n> > \n> > In the current state there is never any quoting, so this statement here\n> > is a bit misleading. Should we maybe drop that part?\n> \n> While there are currently not any values in the output that would\n> require quoting, I'm inclined to leave this note in the documentation.\n> That way we set the expectation regarding how parsers should handle the\n> output from the start.\n\nFair enough.\n\n> > > @@ -407,8 +447,20 @@ static int cmd_repo_stats(int argc UNUSED, const char **argv UNUSED,\n> > >  \tstats_count_references(&stats.refs, &refs);\n> > >  \tstats_count_objects(&stats.objects, &refs, &revs);\n> > >  \n> > > -\tstats_table_setup(&table, &stats);\n> > > -\tstats_table_print(&table);\n> > > +\tswitch (format) {\n> > > +\tcase FORMAT_TABLE:\n> > > +\t\tstats_table_setup(&table, &stats);\n> > > +\t\tstats_table_print(&table);\n> > > +\t\tbreak;\n> > > +\tcase FORMAT_KEYVALUE:\n> > > +\t\tstats_keyvalue_print(&stats, '=', '\\n');\n> > > +\t\tbreak;\n> > > +\tcase FORMAT_NUL_TERMINATED:\n> > > +\t\tstats_keyvalue_print(&stats, '\\n', '\\0');\n> > > +\t\tbreak;\n> > \n> > This reads much nicer now. The newline as key-value delimiter is a\n> > curious choice, but you simply do what we already do in `git repo info`.\n> \n> I agree that the key-value delimiter chosen is a bit strange. The\n> command is still experimental so we could maybe change it if want. Not\n> sure if it would be worth it though.\n\nDunno. If we wanted to do it I'd do it in a follow-up patch series\nanyway.\n\nPatrick\n"},{"id":"527372","messageId":"20250925232928.3846-1-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250924212426.2930029-1-jltobler@gmail.com","subject":"[PATCH v3 0/7] builtin/repo: introduce stats subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-25T23:29:21Z","receivedAt":"2025-09-25T23:29:35Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Greetings,\n\nThe shape of a repository's history can have huge impacts on the\nperformance and health of the repository itself. Currently, Git lacks a\nmeans to surface key stats/information regarding the shape of a\nrepository via a single command. Acquiring this information requires\nusers to be fairly knowledgeable about the structure of a Git repository\nand how to identify the relevant data points. To fill this gap,\nsupplemental tools such as git-sizer(1) have been developed.\n\nTo allow users to more readily identify potential issues for a\nrepository, introduce the \"stats\" subcommand in git-repo(1) to output\nstats for the repository that may be of interest to users. The goal of\nthis subcommand is to eventually provide similar functionality to\ngit-sizer(1), but in Git natively.\n\nIn this initial version, the \"stats\" subcommand only surfaces counts of\nthe various reference and object types in a repository. In a follow-up\nseries, I would like to introduce additional data points that are\npresent in git-sizer(1) such as largest objects, combined object sizes\nby type, and other general repository shape information.\n\nSome other general features that would be nice to introduce eventually:\n\n- A \"level of concern\" meter for reported stats. This could indicate to\n  users which stats may be worth looking into further.\n- Links to OIDs of interesting objects that correspond to certain stats.\n- Options to limit which references to use when evaluating the\n  repository.\n\nChanges since V2:\n\n- Added clang-format patch to address false postive triggered in this\n  series.\n- Use varargs for stats_table_add() family of functions.\n- Print to stdout directly instead of using strbuf.\n- Add parse_option() earlier in the series.\n- Use start_delayed_progress() instead of start_progress().\n- Add test to validate --[no-]progress options.\n- Some other small fixes.\n\nChanges since V1:\n\n- Translatable terms displayed in the table have formatting separated\n  out.\n- Squashed the `keyvalue` and `nul` output format patches into one.\n- Added a progress meter to provide users with more feedback.\n- Updated docs to outline to outline reported data in a bulleted list.\n- Combined similar tests together to reduce repetitive setup.\n- Added patch to improve ref-filter interface so we don't have to create\n  a dummy patterns array.\n- Many other renames and cleanups to improve patch clarity.\n\nThanks,\n-Justin\n\nJustin Tobler (7):\n  builtin/repo: rename repo_info() to cmd_repo_info()\n  ref-filter: allow NULL filter pattern\n  clang-format: exclude control macros from SpaceBeforeParens\n  builtin/repo: introduce stats subcommand\n  builtin/repo: add object counts in stats output\n  builtin/repo: add keyvalue and nul format for stats\n  builtin/repo: add progress meter for stats\n\n .clang-format               |   2 +-\n Documentation/git-repo.adoc |  30 +++\n builtin/repo.c              | 372 +++++++++++++++++++++++++++++++++++-\n ref-filter.c                |   4 +-\n t/meson.build               |   1 +\n t/t1901-repo-stats.sh       | 129 +++++++++++++\n 6 files changed, 532 insertions(+), 6 deletions(-)\n create mode 100755 t/t1901-repo-stats.sh\n\nRange-diff against v2:\n1:  ed04168562 = 1:  ed04168562 builtin/repo: rename repo_info() to cmd_repo_info()\n2:  6aa76d1323 = 2:  6aa76d1323 ref-filter: allow NULL filter pattern\n-:  ---------- > 3:  02a3fcc5fb clang-format: exclude control macros from SpaceBeforeParens\n3:  dc06ca98fb ! 4:  12cfbdc464 builtin/repo: introduce stats subcommand\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\tchar *value;\n     +};\n     +\n    -+static void stats_table_add(struct stats_table *table, const char *format,\n    -+\t\t\t    const char *name, struct stats_table_entry *entry)\n    ++static void stats_table_vaddf(struct stats_table *table,\n    ++\t\t\t      struct stats_table_entry *entry,\n    ++\t\t\t      const char *format, va_list ap)\n     +{\n     +\tstruct strbuf buf = STRBUF_INIT;\n     +\tstruct string_list_item *item;\n     +\tchar *formatted_name;\n     +\tsize_t name_width;\n     +\n    -+\tstrbuf_addf(&buf, format, name);\n    ++\tstrbuf_vaddf(&buf, format, ap);\n     +\tformatted_name = strbuf_detach(&buf, &name_width);\n     +\n     +\titem = string_list_append_nodup(&table->rows, formatted_name);\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\t}\n     +}\n     +\n    -+static void stats_table_add_count(struct stats_table *table, const char *format,\n    -+\t\t\t\t  const char *name, size_t value)\n    ++static void stats_table_addf(struct stats_table *table, const char *format, ...)\n    ++{\n    ++\tva_list ap;\n    ++\n    ++\tva_start(ap, format);\n    ++\tstats_table_vaddf(table, NULL, format, ap);\n    ++\tva_end(ap);\n    ++}\n    ++\n    ++static void stats_table_count_addf(struct stats_table *table, size_t value,\n    ++\t\t\t\t   const char *format, ...)\n     +{\n     +\tstruct stats_table_entry *entry;\n    ++\tva_list ap;\n     +\n     +\tCALLOC_ARRAY(entry, 1);\n     +\tentry->value = xstrfmt(\"%\" PRIuMAX, (uintmax_t)value);\n    -+\tstats_table_add(table, format, name, entry);\n    ++\n    ++\tva_start(ap, format);\n    ++\tstats_table_vaddf(table, entry, format, ap);\n    ++\tva_end(ap);\n     +}\n     +\n     +static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\tsize_t ref_total;\n     +\n     +\tref_total = refs->branches + refs->remotes + refs->tags + refs->others;\n    -+\tstats_table_add(table, \"* %s\", _(\"References\"), NULL);\n    -+\tstats_table_add_count(table, \"  * %s\", _(\"Count\"), ref_total);\n    -+\tstats_table_add_count(table, \"    * %s\", _(\"Branches\"), refs->branches);\n    -+\tstats_table_add_count(table, \"    * %s\", _(\"Tags\"), refs->tags);\n    -+\tstats_table_add_count(table, \"    * %s\", _(\"Remotes\"), refs->remotes);\n    -+\tstats_table_add_count(table, \"    * %s\", _(\"Others\"), refs->others);\n    ++\tstats_table_addf(table, \"* %s\", _(\"References\"));\n    ++\tstats_table_count_addf(table, ref_total, \"  * %s\", _(\"Count\"));\n    ++\tstats_table_count_addf(table, refs->branches, \"    * %s\", _(\"Branches\"));\n    ++\tstats_table_count_addf(table, refs->tags, \"    * %s\", _(\"Tags\"));\n    ++\tstats_table_count_addf(table, refs->remotes, \"    * %s\", _(\"Remotes\"));\n    ++\tstats_table_count_addf(table, refs->others, \"    * %s\", _(\"Others\"));\n     +}\n     +\n     +static inline size_t max_size_t(size_t a, size_t b)\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\treturn (a > b) ? a : b;\n     +}\n     +\n    -+static void stats_table_print(struct stats_table *table)\n    ++static void stats_table_print(const struct stats_table *table)\n     +{\n     +\tconst char *name_col_title = _(\"Repository stats\");\n     +\tconst char *value_col_title = _(\"Value\");\n     +\tsize_t name_title_len = strlen(name_col_title);\n     +\tsize_t value_title_len = strlen(value_col_title);\n    -+\tstruct strbuf buf = STRBUF_INIT;\n     +\tstruct string_list_item *item;\n     +\tint name_col_width;\n     +\tint value_col_width;\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\tvalue_col_width = cast_size_t_to_int(\n     +\t\tmax_size_t(table->value_col_width, value_title_len));\n     +\n    -+\tstrbuf_addf(&buf, \"| %-*s | %-*s |\\n\", name_col_width, name_col_title,\n    -+\t\t    value_col_width, value_col_title);\n    -+\tstrbuf_addstr(&buf, \"| \");\n    -+\tstrbuf_addchars(&buf, '-', name_col_width);\n    -+\tstrbuf_addstr(&buf, \" | \");\n    -+\tstrbuf_addchars(&buf, '-', value_col_width);\n    -+\tstrbuf_addstr(&buf, \" |\\n\");\n    ++\tprintf(\"| %-*s | %-*s |\\n\", name_col_width, name_col_title,\n    ++\t       value_col_width, value_col_title);\n    ++\tprintf(\"| \");\n    ++\tfor (int i = 0; i < name_col_width; i++)\n    ++\t\tputchar('-');\n    ++\tprintf(\" | \");\n    ++\tfor (int i = 0; i < value_col_width; i++)\n    ++\t\tputchar('-');\n    ++\tprintf(\" |\\n\");\n     +\n     +\tfor_each_string_list_item(item, &table->rows) {\n     +\t\tstruct stats_table_entry *entry = item->util;\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\t\t\tvalue = entry->value;\n     +\t\t}\n     +\n    -+\t\tstrbuf_addf(&buf, \"| %-*s | %*s |\\n\", name_col_width,\n    -+\t\t\t    item->string, value_col_width, value);\n    ++\t\tprintf(\"| %-*s | %*s |\\n\", name_col_width, item->string,\n    ++\t\t       value_col_width, value);\n     +\t}\n    -+\n    -+\tfputs(buf.buf, stdout);\n    -+\tstrbuf_release(&buf);\n     +}\n     +\n     +static void stats_table_clear(struct stats_table *table)\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\t}\n     +}\n     +\n    -+static int cmd_repo_stats(int argc UNUSED, const char **argv UNUSED,\n    -+\t\t\t  const char *prefix UNUSED, struct repository *repo UNUSED)\n    ++static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n    ++\t\t\t  struct repository *repo UNUSED)\n     +{\n     +\tstruct ref_filter filter = REF_FILTER_INIT;\n     +\tstruct stats_table table = {\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\t};\n     +\tstruct ref_stats stats = { 0 };\n     +\tstruct ref_array refs = { 0 };\n    ++\tstruct option options[] = { 0 };\n    ++\n    ++\targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n    ++\tif (argc)\n    ++\t\tusage(_(\"too many arguments\"));\n     +\n     +\tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n     +\t\tdie(_(\"unable to filter refs\"));\n4:  2832670e82 ! 5:  ab27340d58 builtin/repo: add object counts in stats output\n    @@ builtin/repo.c: struct ref_stats {\n      struct stats_table {\n      \tstruct string_list rows;\n      \n    -@@ builtin/repo.c: static void stats_table_add_count(struct stats_table *table, const char *format,\n    - \tstats_table_add(table, format, name, entry);\n    +@@ builtin/repo.c: static void stats_table_count_addf(struct stats_table *table, size_t value,\n    + \tva_end(ap);\n      }\n      \n     -static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n    -+static void stats_table_setup(struct stats_table *table, struct repo_stats *stats)\n    ++static inline size_t get_total_object_count(struct object_stats *stats)\n      {\n    ++\treturn stats->tags + stats->commits + stats->trees + stats->blobs;\n    ++}\n    ++\n    ++static void stats_table_setup(struct stats_table *table, struct repo_stats *stats)\n    ++{\n     +\tstruct object_stats *objects = &stats->objects;\n     +\tstruct ref_stats *refs = &stats->refs;\n     +\tsize_t object_total;\n    @@ builtin/repo.c: static void stats_table_add_count(struct stats_table *table, con\n      \n      \tref_total = refs->branches + refs->remotes + refs->tags + refs->others;\n     @@ builtin/repo.c: static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n    - \tstats_table_add_count(table, \"    * %s\", _(\"Tags\"), refs->tags);\n    - \tstats_table_add_count(table, \"    * %s\", _(\"Remotes\"), refs->remotes);\n    - \tstats_table_add_count(table, \"    * %s\", _(\"Others\"), refs->others);\n    + \tstats_table_count_addf(table, refs->tags, \"    * %s\", _(\"Tags\"));\n    + \tstats_table_count_addf(table, refs->remotes, \"    * %s\", _(\"Remotes\"));\n    + \tstats_table_count_addf(table, refs->others, \"    * %s\", _(\"Others\"));\n     +\n    -+\tobject_total = objects->commits + objects->trees + objects->blobs + objects->tags;\n    -+\tstats_table_add(table, \"%s\", \"\", NULL);\n    -+\tstats_table_add(table, \"* %s\", _(\"Reachable objects\"), NULL);\n    -+\tstats_table_add_count(table, \"  * %s\", _(\"Count\"), object_total);\n    -+\tstats_table_add_count(table, \"    * %s\", _(\"Commits\"), objects->commits);\n    -+\tstats_table_add_count(table, \"    * %s\", _(\"Trees\"), objects->trees);\n    -+\tstats_table_add_count(table, \"    * %s\", _(\"Blobs\"), objects->blobs);\n    -+\tstats_table_add_count(table, \"    * %s\", _(\"Tags\"), objects->tags);\n    ++\tobject_total = get_total_object_count(objects);\n    ++\tstats_table_addf(table, \"\");\n    ++\tstats_table_addf(table, \"* %s\", _(\"Reachable objects\"));\n    ++\tstats_table_count_addf(table, object_total, \"  * %s\", _(\"Count\"));\n    ++\tstats_table_count_addf(table, objects->commits, \"    * %s\", _(\"Commits\"));\n    ++\tstats_table_count_addf(table, objects->trees, \"    * %s\", _(\"Trees\"));\n    ++\tstats_table_count_addf(table, objects->blobs, \"    * %s\", _(\"Blobs\"));\n    ++\tstats_table_count_addf(table, objects->tags, \"    * %s\", _(\"Tags\"));\n      }\n      \n      static inline size_t max_size_t(size_t a, size_t b)\n    @@ builtin/repo.c: static void stats_count_references(struct ref_stats *stats, stru\n     +\tpath_walk_info_clear(&info);\n     +}\n     +\n    - static int cmd_repo_stats(int argc UNUSED, const char **argv UNUSED,\n    --\t\t\t  const char *prefix UNUSED, struct repository *repo UNUSED)\n    -+\t\t\t  const char *prefix, struct repository *repo)\n    + static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n    +-\t\t\t  struct repository *repo UNUSED)\n    ++\t\t\t  struct repository *repo)\n      {\n      \tstruct ref_filter filter = REF_FILTER_INIT;\n      \tstruct stats_table table = {\n    @@ builtin/repo.c: static void stats_count_references(struct ref_stats *stats, stru\n     +\tstruct repo_stats stats = { 0 };\n      \tstruct ref_array refs = { 0 };\n     +\tstruct rev_info revs;\n    + \tstruct option options[] = { 0 };\n    + \n    + \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n    + \tif (argc)\n    + \t\tusage(_(\"too many arguments\"));\n      \n     +\trepo_init_revisions(repo, &revs, prefix);\n      \tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n5:  5d1adf7905 ! 6:  f69110224d builtin/repo: add keyvalue and nul format for stats\n    @@ Documentation/git-repo.adoc: supported:\n     +supported:\n     ++\n     +`table`:::\n    -+\tOutputs repository stats in a human-friendly table and is used by\n    -+\tdefault. This format may change and is not intended for machine\n    -+\tparsing.\n    ++\tOutputs repository stats in a human-friendly table. This format may\n    ++\tchange and is not intended for machine parsing. This is the default\n    ++\tformat.\n     +\n     +`keyvalue`:::\n     +\tEach line of output contains a key-value pair for a repository stat.\n    @@ builtin/repo.c: static void stats_table_clear(struct stats_table *table)\n     +static void stats_keyvalue_print(struct repo_stats *stats, char key_delim,\n     +\t\t\t\t char value_delim)\n     +{\n    -+\tstruct strbuf buf = STRBUF_INIT;\n    ++\tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n    ++\t       (uintmax_t)stats->refs.branches, value_delim);\n    ++\tprintf(\"references.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n    ++\t       (uintmax_t)stats->refs.tags, value_delim);\n    ++\tprintf(\"references.remotes.count%c%\" PRIuMAX \"%c\", key_delim,\n    ++\t       (uintmax_t)stats->refs.remotes, value_delim);\n    ++\tprintf(\"references.others.count%c%\" PRIuMAX \"%c\", key_delim,\n    ++\t       (uintmax_t)stats->refs.others, value_delim);\n     +\n    -+\tstrbuf_addf(&buf, \"references.branches.count%c%\" PRIuMAX \"%c\",\n    -+\t\t    key_delim, (uintmax_t)stats->refs.branches, value_delim);\n    -+\tstrbuf_addf(&buf, \"references.tags.count%c%\" PRIuMAX \"%c\",\n    -+\t\t    key_delim, (uintmax_t)stats->refs.tags, value_delim);\n    -+\tstrbuf_addf(&buf, \"references.remotes.count%c%\" PRIuMAX \"%c\",\n    -+\t\t    key_delim, (uintmax_t)stats->refs.remotes, value_delim);\n    -+\tstrbuf_addf(&buf, \"references.others.count%c%\" PRIuMAX \"%c\",\n    -+\t\t    key_delim, (uintmax_t)stats->refs.others, value_delim);\n    ++\tprintf(\"objects.commits.count%c%\" PRIuMAX \"%c\", key_delim,\n    ++\t       (uintmax_t)stats->objects.commits, value_delim);\n    ++\tprintf(\"objects.trees.count%c%\" PRIuMAX \"%c\", key_delim,\n    ++\t       (uintmax_t)stats->objects.trees, value_delim);\n    ++\tprintf(\"objects.blobs.count%c%\" PRIuMAX \"%c\", key_delim,\n    ++\t       (uintmax_t)stats->objects.blobs, value_delim);\n    ++\tprintf(\"objects.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n    ++\t       (uintmax_t)stats->objects.tags, value_delim);\n     +\n    -+\tstrbuf_addf(&buf, \"objects.commits.count%c%\" PRIuMAX \"%c\",\n    -+\t\t    key_delim, (uintmax_t)stats->objects.commits, value_delim);\n    -+\tstrbuf_addf(&buf, \"objects.trees.count%c%\" PRIuMAX \"%c\",\n    -+\t\t    key_delim, (uintmax_t)stats->objects.trees, value_delim);\n    -+\tstrbuf_addf(&buf, \"objects.blobs.count%c%\" PRIuMAX \"%c\",\n    -+\t\t    key_delim, (uintmax_t)stats->objects.blobs, value_delim);\n    -+\tstrbuf_addf(&buf, \"objects.tags.count%c%\" PRIuMAX \"%c\",\n    -+\t\t    key_delim, (uintmax_t)stats->objects.tags, value_delim);\n    -+\n    -+\tfwrite(buf.buf, sizeof(char), buf.len, stdout);\n    -+\tstrbuf_release(&buf);\n    ++\tfflush(stdout);\n     +}\n     +\n      static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n      {\n      \tfor (int i = 0; i < refs->nr; i++) {\n    -@@ builtin/repo.c: static void stats_count_objects(struct object_stats *stats,\n    - \tpath_walk_info_clear(&info);\n    - }\n    - \n    --static int cmd_repo_stats(int argc UNUSED, const char **argv UNUSED,\n    --\t\t\t  const char *prefix, struct repository *repo)\n    -+static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n    -+\t\t\t  struct repository *repo)\n    - {\n    - \tstruct ref_filter filter = REF_FILTER_INIT;\n    +@@ builtin/repo.c: static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n      \tstruct stats_table table = {\n      \t\t.rows = STRING_LIST_INIT_DUP,\n      \t};\n    @@ builtin/repo.c: static void stats_count_objects(struct object_stats *stats,\n      \tstruct repo_stats stats = { 0 };\n      \tstruct ref_array refs = { 0 };\n      \tstruct rev_info revs;\n    +-\tstruct option options[] = { 0 };\n     +\tstruct option options[] = {\n     +\t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n     +\t\t\t       N_(\"output format\"),\n    @@ builtin/repo.c: static void stats_count_objects(struct object_stats *stats,\n     +\t\tOPT_END()\n     +\t};\n      \n    -+\tparse_options(argc, argv, prefix, options, repo_usage, 0);\n    - \trepo_init_revisions(repo, &revs, prefix);\n    - \tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n    - \t\tdie(_(\"unable to filter refs\"));\n    -@@ builtin/repo.c: static int cmd_repo_stats(int argc UNUSED, const char **argv UNUSED,\n    + \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n    + \tif (argc)\n    +@@ builtin/repo.c: static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n      \tstats_count_references(&stats.refs, &refs);\n      \tstats_count_objects(&stats.objects, &refs, &revs);\n      \n    @@ t/t1901-repo-stats.sh: test_expect_success 'repository with references and objec\n      \t)\n      '\n      \n    -+test_expect_success 'repository stats with keyvalue and nul format' '\n    ++test_expect_success 'keyvalue and nul format' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tgit init repo &&\n     +\t(\n    @@ t/t1901-repo-stats.sh: test_expect_success 'repository with references and objec\n     +\t\ttest_line_count = 0 err &&\n     +\n     +\t\t# Replace key and value delimiters for nul format.\n    -+\t\ttr \"\\n\" \"\\0\" <expect | tr \"=\" \"\\n\" >expect_null &&\n    ++\t\ttr \"\\n=\" \"\\0\\n\" <expect >expect_nul &&\n     +\t\tgit repo stats --format=nul >out 2>err &&\n     +\n    -+\t\ttest_cmp expect_null out &&\n    ++\t\ttest_cmp expect_nul out &&\n     +\t\ttest_line_count = 0 err\n     +\t)\n     +'\n6:  2a745c8417 ! 7:  cff5e183bb builtin/repo: add progress meter for stats\n    @@ builtin/repo.c\n      #include \"ref-filter.h\"\n      #include \"refs.h\"\n     @@ builtin/repo.c: static void stats_keyvalue_print(struct repo_stats *stats, char key_delim,\n    - \tstrbuf_release(&buf);\n    + \tfflush(stdout);\n      }\n      \n     -static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n    @@ builtin/repo.c: static void stats_keyvalue_print(struct repo_stats *stats, char\n     +\tstruct progress *progress = NULL;\n     +\n     +\tif (show_progress)\n    -+\t\tprogress = start_progress(repo, _(\"Counting references\"), refs->nr);\n    ++\t\tprogress = start_delayed_progress(repo, _(\"Counting references\"),\n    ++\t\t\t\t\t\t  refs->nr);\n     +\n      \tfor (int i = 0; i < refs->nr; i++) {\n      \t\tstruct ref_array_item *ref = refs->items[i];\n    @@ builtin/repo.c: static int count_objects(const char *path UNUSED, struct oid_arr\n      \t\tBUG(\"invalid object type\");\n      \t}\n      \n    -+\tobject_count = stats->tags + stats->commits + stats->trees + stats->blobs;\n    ++\tobject_count = get_total_object_count(stats);\n     +\tdisplay_progress(data->progress, object_count);\n     +\n      \treturn 0;\n    @@ builtin/repo.c: static void stats_count_objects(struct object_stats *stats,\n      \t}\n      \n     +\tif (show_progress)\n    -+\t\tdata.progress = start_progress(repo, _(\"Counting Objects\"), 0);\n    ++\t\tdata.progress = start_delayed_progress(repo, _(\"Counting objects\"), 0);\n     +\n      \twalk_objects_by_path(&info);\n      \tpath_walk_info_clear(&info);\n    @@ builtin/repo.c: static int cmd_repo_stats(int argc, const char **argv, const cha\n      \n      \tswitch (format) {\n      \tcase FORMAT_TABLE:\n    +\n    + ## t/t1901-repo-stats.sh ##\n    +@@ t/t1901-repo-stats.sh: test_expect_success 'keyvalue and nul format' '\n    + \t)\n    + '\n    + \n    ++test_expect_success 'progress meter option' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tgit init repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\ttest_commit foo &&\n    ++\n    ++\t\tGIT_PROGRESS_DELAY=0 git repo stats --progress >out 2>err &&\n    ++\n    ++\t\ttest_file_not_empty out &&\n    ++\t\ttest_grep \"Counting references: 100% (2/2), done.\" err &&\n    ++\t\ttest_grep \"Counting objects: 3, done.\" err &&\n    ++\n    ++\t\tGIT_PROGRESS_DELAY=0 git repo stats --no-progress >out 2>err &&\n    ++\n    ++\t\ttest_file_not_empty out &&\n    ++\t\ttest_line_count = 0 err\n    ++\t)\n    ++'\n    ++\n    + test_done\n\nbase-commit: ca2559c1d630eb4f04cdee2328aaf1c768907a9e\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527373","messageId":"20250925232928.3846-2-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250925232928.3846-1-jltobler@gmail.com","subject":"[PATCH v3 1/7] builtin/repo: rename repo_info() to cmd_repo_info()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-25T23:29:22Z","receivedAt":"2025-09-25T23:29:36Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Subcommand functions are often prefixed with `cmd_` to denote that they\nare an entrypoint. Rename repo_info() to cmd_repo_info() accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/repo.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex bbb0966f2d..eeeab8fbd2 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -136,8 +136,8 @@ static int parse_format_cb(const struct option *opt,\n \treturn 0;\n }\n \n-static int repo_info(int argc, const char **argv, const char *prefix,\n-\t\t     struct repository *repo)\n+static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n+\t\t\t struct repository *repo)\n {\n \tenum output_format format = FORMAT_KEYVALUE;\n \tstruct option options[] = {\n@@ -161,7 +161,7 @@ int cmd_repo(int argc, const char **argv, const char *prefix,\n {\n \tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n-\t\tOPT_SUBCOMMAND(\"info\", &fn, repo_info),\n+\t\tOPT_SUBCOMMAND(\"info\", &fn, cmd_repo_info),\n \t\tOPT_END()\n \t};\n \n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527374","messageId":"20250925232928.3846-3-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250925232928.3846-1-jltobler@gmail.com","subject":"[PATCH v3 2/7] ref-filter: allow NULL filter pattern","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-25T23:29:23Z","receivedAt":"2025-09-25T23:29:37Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"When setting up `struct ref_filter` for filter_refs(), the\n`name_patterns` field must point to an array of pattern strings even if\nno patterns are required. To improve this interface, treat a NULL\n`name_patterns` field the same as when it points to an empty array.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n ref-filter.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/ref-filter.c b/ref-filter.c\nindex 520d2539c9..2cb5a166d6 100644\n--- a/ref-filter.c\n+++ b/ref-filter.c\n@@ -2664,7 +2664,7 @@ static int match_name_as_path(const char **pattern, const char *refname,\n /* Return 1 if the refname matches one of the patterns, otherwise 0. */\n static int filter_pattern_match(struct ref_filter *filter, const char *refname)\n {\n-\tif (!*filter->name_patterns)\n+\tif (!filter->name_patterns || !*filter->name_patterns)\n \t\treturn 1; /* No pattern always matches */\n \tif (filter->match_as_path)\n \t\treturn match_name_as_path(filter->name_patterns, refname,\n@@ -2751,7 +2751,7 @@ static int for_each_fullref_in_pattern(struct ref_filter *filter,\n \t\treturn for_each_fullref_with_seek(filter, cb, cb_data, 0);\n \t}\n \n-\tif (!filter->name_patterns[0]) {\n+\tif (!filter->name_patterns || !filter->name_patterns[0]) {\n \t\t/* no patterns; we have to look at everything */\n \t\treturn for_each_fullref_with_seek(filter, cb, cb_data, 0);\n \t}\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527375","messageId":"20250925232928.3846-4-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250925232928.3846-1-jltobler@gmail.com","subject":"[PATCH v3 3/7] clang-format: exclude control macros from SpaceBeforeParens","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-25T23:29:24Z","receivedAt":"2025-09-25T23:29:38Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The formatter currently suggests adding a space between a control macro\nand parentheses. In the Git project, this is not typically expected. Set\n`SpaceBeforeParens` to `ControlStatementsExceptControlMacros`\naccordingly.\n\nHelped-by: Karthik Nayak <karthik.188@gmail.com>\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n .clang-format | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/.clang-format b/.clang-format\nindex dcfd0aad60..86b4fe33e5 100644\n--- a/.clang-format\n+++ b/.clang-format\n@@ -149,7 +149,7 @@ SpaceBeforeCaseColon: false\n #     f();\n #   }\n # }\n-SpaceBeforeParens: ControlStatements\n+SpaceBeforeParens: ControlStatementsExceptControlMacros\n \n # Don't insert spaces inside empty '()'\n SpaceInEmptyParentheses: false\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527376","messageId":"20250925232928.3846-5-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250925232928.3846-1-jltobler@gmail.com","subject":"[PATCH v3 4/7] builtin/repo: introduce stats subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-25T23:29:25Z","receivedAt":"2025-09-25T23:29:39Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The shape of a repository's history can have huge impacts on the\nperformance and health of the repository itself. Currently, Git lacks a\nmeans to surface key stats/information regarding the shape of a\nrepository via a single command. Acquiring this information requires\nusers to be fairly knowledgeable about the structure of a Git repository\nand how to identify the relevant data points. To fill this gap,\nsupplemental tools such as git-sizer(1) have been developed.\n\nTo allow users to more readily identify potential issues for a\nrepository, introduce the \"stats\" subcommand in git-repo(1) to output\nstats for the repository that may be of interest to users. The goal of\nthis subcommand is to eventually provide similar functionality to\ngit-sizer(1), but natively in Git.\n\nThe initial version of this command only iterates through all references\nin the repository and tracks the count of branches, tags, remote refs,\nand other reference types. The corresponding information is displayed in\na human-friendly table formatted in a very similar manner to\ngit-sizer(1). The width of each table column is adjusted automatically\nto satisfy the requirements of the widest row contained.\n\nSubsequent commits will surface additional relevant data points to\noutput.\n\nBased-on-patch-by: Derrick Stolee <stolee@gmail.com>\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc |  10 ++\n builtin/repo.c              | 196 ++++++++++++++++++++++++++++++++++++\n t/meson.build               |   1 +\n t/t1901-repo-stats.sh       |  61 +++++++++++\n 4 files changed, 268 insertions(+)\n create mode 100755 t/t1901-repo-stats.sh\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 209afd1b61..a009bf8cf1 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -9,6 +9,7 @@ SYNOPSIS\n --------\n [synopsis]\n git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n+git repo stats\n \n DESCRIPTION\n -----------\n@@ -43,6 +44,15 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n+`stats`::\n+\tRetrieve statistics about the current repository. The following kinds\n+\tof information are reported:\n++\n+* Reference counts categorized by type\n+\n++\n+The table output format may change and is not intended for machine parsing.\n+\n INFO KEYS\n ---------\n In order to obtain a set of values from `git repo info`, you should provide\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex eeeab8fbd2..0b7dd636e5 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -4,12 +4,15 @@\n #include \"environment.h\"\n #include \"parse-options.h\"\n #include \"quote.h\"\n+#include \"ref-filter.h\"\n #include \"refs.h\"\n #include \"strbuf.h\"\n+#include \"string-list.h\"\n #include \"shallow.h\"\n \n static const char *const repo_usage[] = {\n \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n+\t\"git repo stats\",\n \tNULL\n };\n \n@@ -156,12 +159,205 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \treturn print_fields(argc, argv, repo, format);\n }\n \n+struct ref_stats {\n+\tsize_t branches;\n+\tsize_t remotes;\n+\tsize_t tags;\n+\tsize_t others;\n+};\n+\n+struct stats_table {\n+\tstruct string_list rows;\n+\n+\tsize_t name_col_width;\n+\tsize_t value_col_width;\n+};\n+\n+/*\n+ * Holds column data that gets stored for each row.\n+ */\n+struct stats_table_entry {\n+\tchar *value;\n+};\n+\n+static void stats_table_vaddf(struct stats_table *table,\n+\t\t\t      struct stats_table_entry *entry,\n+\t\t\t      const char *format, va_list ap)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct string_list_item *item;\n+\tchar *formatted_name;\n+\tsize_t name_width;\n+\n+\tstrbuf_vaddf(&buf, format, ap);\n+\tformatted_name = strbuf_detach(&buf, &name_width);\n+\n+\titem = string_list_append_nodup(&table->rows, formatted_name);\n+\titem->util = entry;\n+\n+\tif (name_width > table->name_col_width)\n+\t\ttable->name_col_width = name_width;\n+\tif (entry) {\n+\t\tsize_t value_width = strlen(entry->value);\n+\t\tif (value_width > table->value_col_width)\n+\t\t\ttable->value_col_width = value_width;\n+\t}\n+}\n+\n+static void stats_table_addf(struct stats_table *table, const char *format, ...)\n+{\n+\tva_list ap;\n+\n+\tva_start(ap, format);\n+\tstats_table_vaddf(table, NULL, format, ap);\n+\tva_end(ap);\n+}\n+\n+static void stats_table_count_addf(struct stats_table *table, size_t value,\n+\t\t\t\t   const char *format, ...)\n+{\n+\tstruct stats_table_entry *entry;\n+\tva_list ap;\n+\n+\tCALLOC_ARRAY(entry, 1);\n+\tentry->value = xstrfmt(\"%\" PRIuMAX, (uintmax_t)value);\n+\n+\tva_start(ap, format);\n+\tstats_table_vaddf(table, entry, format, ap);\n+\tva_end(ap);\n+}\n+\n+static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n+{\n+\tsize_t ref_total;\n+\n+\tref_total = refs->branches + refs->remotes + refs->tags + refs->others;\n+\tstats_table_addf(table, \"* %s\", _(\"References\"));\n+\tstats_table_count_addf(table, ref_total, \"  * %s\", _(\"Count\"));\n+\tstats_table_count_addf(table, refs->branches, \"    * %s\", _(\"Branches\"));\n+\tstats_table_count_addf(table, refs->tags, \"    * %s\", _(\"Tags\"));\n+\tstats_table_count_addf(table, refs->remotes, \"    * %s\", _(\"Remotes\"));\n+\tstats_table_count_addf(table, refs->others, \"    * %s\", _(\"Others\"));\n+}\n+\n+static inline size_t max_size_t(size_t a, size_t b)\n+{\n+\treturn (a > b) ? a : b;\n+}\n+\n+static void stats_table_print(const struct stats_table *table)\n+{\n+\tconst char *name_col_title = _(\"Repository stats\");\n+\tconst char *value_col_title = _(\"Value\");\n+\tsize_t name_title_len = strlen(name_col_title);\n+\tsize_t value_title_len = strlen(value_col_title);\n+\tstruct string_list_item *item;\n+\tint name_col_width;\n+\tint value_col_width;\n+\n+\tname_col_width = cast_size_t_to_int(\n+\t\tmax_size_t(table->name_col_width, name_title_len));\n+\tvalue_col_width = cast_size_t_to_int(\n+\t\tmax_size_t(table->value_col_width, value_title_len));\n+\n+\tprintf(\"| %-*s | %-*s |\\n\", name_col_width, name_col_title,\n+\t       value_col_width, value_col_title);\n+\tprintf(\"| \");\n+\tfor (int i = 0; i < name_col_width; i++)\n+\t\tputchar('-');\n+\tprintf(\" | \");\n+\tfor (int i = 0; i < value_col_width; i++)\n+\t\tputchar('-');\n+\tprintf(\" |\\n\");\n+\n+\tfor_each_string_list_item(item, &table->rows) {\n+\t\tstruct stats_table_entry *entry = item->util;\n+\t\tconst char *value = \"\";\n+\n+\t\tif (entry) {\n+\t\t\tstruct stats_table_entry *entry = item->util;\n+\t\t\tvalue = entry->value;\n+\t\t}\n+\n+\t\tprintf(\"| %-*s | %*s |\\n\", name_col_width, item->string,\n+\t\t       value_col_width, value);\n+\t}\n+}\n+\n+static void stats_table_clear(struct stats_table *table)\n+{\n+\tstruct stats_table_entry *entry;\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, &table->rows) {\n+\t\tentry = item->util;\n+\t\tif (entry)\n+\t\t\tfree(entry->value);\n+\t}\n+\n+\tstring_list_clear(&table->rows, 1);\n+}\n+\n+static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n+{\n+\tfor (int i = 0; i < refs->nr; i++) {\n+\t\tstruct ref_array_item *ref = refs->items[i];\n+\n+\t\tswitch (ref->kind) {\n+\t\tcase FILTER_REFS_BRANCHES:\n+\t\t\tstats->branches++;\n+\t\t\tbreak;\n+\t\tcase FILTER_REFS_REMOTES:\n+\t\t\tstats->remotes++;\n+\t\t\tbreak;\n+\t\tcase FILTER_REFS_TAGS:\n+\t\t\tstats->tags++;\n+\t\t\tbreak;\n+\t\tcase FILTER_REFS_OTHERS:\n+\t\t\tstats->others++;\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tBUG(\"unexpected reference type\");\n+\t\t}\n+\t}\n+}\n+\n+static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n+\t\t\t  struct repository *repo UNUSED)\n+{\n+\tstruct ref_filter filter = REF_FILTER_INIT;\n+\tstruct stats_table table = {\n+\t\t.rows = STRING_LIST_INIT_DUP,\n+\t};\n+\tstruct ref_stats stats = { 0 };\n+\tstruct ref_array refs = { 0 };\n+\tstruct option options[] = { 0 };\n+\n+\targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n+\tif (argc)\n+\t\tusage(_(\"too many arguments\"));\n+\n+\tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n+\t\tdie(_(\"unable to filter refs\"));\n+\n+\tstats_count_references(&stats, &refs);\n+\n+\tstats_table_setup(&table, &stats);\n+\tstats_table_print(&table);\n+\n+\tstats_table_clear(&table);\n+\tref_array_clear(&refs);\n+\n+\treturn 0;\n+}\n+\n int cmd_repo(int argc, const char **argv, const char *prefix,\n \t     struct repository *repo)\n {\n \tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n \t\tOPT_SUBCOMMAND(\"info\", &fn, cmd_repo_info),\n+\t\tOPT_SUBCOMMAND(\"stats\", &fn, cmd_repo_stats),\n \t\tOPT_END()\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 7974795fe4..071d4a5112 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -236,6 +236,7 @@ integration_tests = [\n   't1701-racy-split-index.sh',\n   't1800-hook.sh',\n   't1900-repo.sh',\n+  't1901-repo-stats.sh',\n   't2000-conflict-when-checking-files-out.sh',\n   't2002-checkout-cache-u.sh',\n   't2003-checkout-cache-mkdir.sh',\ndiff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\nnew file mode 100755\nindex 0000000000..535ac511dd\n--- /dev/null\n+++ b/t/t1901-repo-stats.sh\n@@ -0,0 +1,61 @@\n+#!/bin/sh\n+\n+test_description='test git repo stats'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'empty repository' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\t| Repository stats | Value |\n+\t\t| ---------------- | ----- |\n+\t\t| * References     |       |\n+\t\t|   * Count        |     0 |\n+\t\t|     * Branches   |     0 |\n+\t\t|     * Tags       |     0 |\n+\t\t|     * Remotes    |     0 |\n+\t\t|     * Others     |     0 |\n+\t\tEOF\n+\n+\t\tgit repo stats >out 2>err &&\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n+test_expect_success 'repository with references' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit commit --allow-empty -m init &&\n+\t\tgit tag -a foo -m bar &&\n+\n+\t\toid=\"$(git rev-parse HEAD)\" &&\n+\t\tgit update-ref refs/remotes/origin/foo \"$oid\" &&\n+\n+\t\tgit notes add -m foo &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\t\t| Repository stats | Value |\n+\t\t| ---------------- | ----- |\n+\t\t| * References     |       |\n+\t\t|   * Count        |     4 |\n+\t\t|     * Branches   |     1 |\n+\t\t|     * Tags       |     1 |\n+\t\t|     * Remotes    |     1 |\n+\t\t|     * Others     |     1 |\n+\t\tEOF\n+\n+\t\tgit repo stats >out 2>err &&\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n+test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527377","messageId":"20250925232928.3846-6-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250925232928.3846-1-jltobler@gmail.com","subject":"[PATCH v3 5/7] builtin/repo: add object counts in stats output","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-25T23:29:26Z","receivedAt":"2025-09-25T23:29:40Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The amount of objects in a repository can provide insight regarding its\nshape. To surface this information, use the path-walk API to count the\nnumber of reachable objects in the repository by object type. All\nregular references are used to determine the reachable set of objects.\nThe object counts are appended to the same table containing the\nreference information.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc |  1 +\n builtin/repo.c              | 96 +++++++++++++++++++++++++++++++++++--\n t/t1901-repo-stats.sh       | 51 +++++++++++++-------\n 3 files changed, 126 insertions(+), 22 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex a009bf8cf1..0b8d74ed3e 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -49,6 +49,7 @@ supported:\n \tof information are reported:\n +\n * Reference counts categorized by type\n+* Reachable object counts categorized by type\n \n +\n The table output format may change and is not intended for machine parsing.\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 0b7dd636e5..43cd6b1b38 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -3,9 +3,11 @@\n #include \"builtin.h\"\n #include \"environment.h\"\n #include \"parse-options.h\"\n+#include \"path-walk.h\"\n #include \"quote.h\"\n #include \"ref-filter.h\"\n #include \"refs.h\"\n+#include \"revision.h\"\n #include \"strbuf.h\"\n #include \"string-list.h\"\n #include \"shallow.h\"\n@@ -166,6 +168,18 @@ struct ref_stats {\n \tsize_t others;\n };\n \n+struct object_stats {\n+\tsize_t tags;\n+\tsize_t commits;\n+\tsize_t trees;\n+\tsize_t blobs;\n+};\n+\n+struct repo_stats {\n+\tstruct ref_stats refs;\n+\tstruct object_stats objects;\n+};\n+\n struct stats_table {\n \tstruct string_list rows;\n \n@@ -227,8 +241,16 @@ static void stats_table_count_addf(struct stats_table *table, size_t value,\n \tva_end(ap);\n }\n \n-static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n+static inline size_t get_total_object_count(struct object_stats *stats)\n {\n+\treturn stats->tags + stats->commits + stats->trees + stats->blobs;\n+}\n+\n+static void stats_table_setup(struct stats_table *table, struct repo_stats *stats)\n+{\n+\tstruct object_stats *objects = &stats->objects;\n+\tstruct ref_stats *refs = &stats->refs;\n+\tsize_t object_total;\n \tsize_t ref_total;\n \n \tref_total = refs->branches + refs->remotes + refs->tags + refs->others;\n@@ -238,6 +260,15 @@ static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n \tstats_table_count_addf(table, refs->tags, \"    * %s\", _(\"Tags\"));\n \tstats_table_count_addf(table, refs->remotes, \"    * %s\", _(\"Remotes\"));\n \tstats_table_count_addf(table, refs->others, \"    * %s\", _(\"Others\"));\n+\n+\tobject_total = get_total_object_count(objects);\n+\tstats_table_addf(table, \"\");\n+\tstats_table_addf(table, \"* %s\", _(\"Reachable objects\"));\n+\tstats_table_count_addf(table, object_total, \"  * %s\", _(\"Count\"));\n+\tstats_table_count_addf(table, objects->commits, \"    * %s\", _(\"Commits\"));\n+\tstats_table_count_addf(table, objects->trees, \"    * %s\", _(\"Trees\"));\n+\tstats_table_count_addf(table, objects->blobs, \"    * %s\", _(\"Blobs\"));\n+\tstats_table_count_addf(table, objects->tags, \"    * %s\", _(\"Tags\"));\n }\n \n static inline size_t max_size_t(size_t a, size_t b)\n@@ -322,30 +353,87 @@ static void stats_count_references(struct ref_stats *stats, struct ref_array *re\n \t}\n }\n \n+static int count_objects(const char *path UNUSED, struct oid_array *oids,\n+\t\t\t enum object_type type, void *cb_data)\n+{\n+\tstruct object_stats *stats = cb_data;\n+\n+\tswitch (type) {\n+\tcase OBJ_TAG:\n+\t\tstats->tags += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_COMMIT:\n+\t\tstats->commits += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_TREE:\n+\t\tstats->trees += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_BLOB:\n+\t\tstats->blobs += oids->nr;\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"invalid object type\");\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void stats_count_objects(struct object_stats *stats,\n+\t\t\t\tstruct ref_array *refs, struct rev_info *revs)\n+{\n+\tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n+\n+\tinfo.revs = revs;\n+\tinfo.path_fn = count_objects;\n+\tinfo.path_fn_data = stats;\n+\n+\tfor (int i = 0; i < refs->nr; i++) {\n+\t\tstruct ref_array_item *ref = refs->items[i];\n+\n+\t\tswitch (ref->kind) {\n+\t\tcase FILTER_REFS_BRANCHES:\n+\t\tcase FILTER_REFS_TAGS:\n+\t\tcase FILTER_REFS_REMOTES:\n+\t\tcase FILTER_REFS_OTHERS:\n+\t\t\tadd_pending_oid(revs, NULL, &ref->objectname, 0);\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tBUG(\"unexpected reference type\");\n+\t\t}\n+\t}\n+\n+\twalk_objects_by_path(&info);\n+\tpath_walk_info_clear(&info);\n+}\n+\n static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n-\t\t\t  struct repository *repo UNUSED)\n+\t\t\t  struct repository *repo)\n {\n \tstruct ref_filter filter = REF_FILTER_INIT;\n \tstruct stats_table table = {\n \t\t.rows = STRING_LIST_INIT_DUP,\n \t};\n-\tstruct ref_stats stats = { 0 };\n+\tstruct repo_stats stats = { 0 };\n \tstruct ref_array refs = { 0 };\n+\tstruct rev_info revs;\n \tstruct option options[] = { 0 };\n \n \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n \tif (argc)\n \t\tusage(_(\"too many arguments\"));\n \n+\trepo_init_revisions(repo, &revs, prefix);\n \tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n \t\tdie(_(\"unable to filter refs\"));\n \n-\tstats_count_references(&stats, &refs);\n+\tstats_count_references(&stats.refs, &refs);\n+\tstats_count_objects(&stats.objects, &refs, &revs);\n \n \tstats_table_setup(&table, &stats);\n \tstats_table_print(&table);\n \n \tstats_table_clear(&table);\n+\trelease_revisions(&revs);\n \tref_array_clear(&refs);\n \n \treturn 0;\ndiff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\nindex 535ac511dd..315b9e1767 100755\n--- a/t/t1901-repo-stats.sh\n+++ b/t/t1901-repo-stats.sh\n@@ -10,14 +10,21 @@ test_expect_success 'empty repository' '\n \t(\n \t\tcd repo &&\n \t\tcat >expect <<-\\EOF &&\n-\t\t| Repository stats | Value |\n-\t\t| ---------------- | ----- |\n-\t\t| * References     |       |\n-\t\t|   * Count        |     0 |\n-\t\t|     * Branches   |     0 |\n-\t\t|     * Tags       |     0 |\n-\t\t|     * Remotes    |     0 |\n-\t\t|     * Others     |     0 |\n+\t\t| Repository stats    | Value |\n+\t\t| ------------------- | ----- |\n+\t\t| * References        |       |\n+\t\t|   * Count           |     0 |\n+\t\t|     * Branches      |     0 |\n+\t\t|     * Tags          |     0 |\n+\t\t|     * Remotes       |     0 |\n+\t\t|     * Others        |     0 |\n+\t\t|                     |       |\n+\t\t| * Reachable objects |       |\n+\t\t|   * Count           |     0 |\n+\t\t|     * Commits       |     0 |\n+\t\t|     * Trees         |     0 |\n+\t\t|     * Blobs         |     0 |\n+\t\t|     * Tags          |     0 |\n \t\tEOF\n \n \t\tgit repo stats >out 2>err &&\n@@ -27,28 +34,36 @@ test_expect_success 'empty repository' '\n \t)\n '\n \n-test_expect_success 'repository with references' '\n+test_expect_success 'repository with references and objects' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n \t(\n \t\tcd repo &&\n-\t\tgit commit --allow-empty -m init &&\n+\t\ttest_commit_bulk 42 &&\n \t\tgit tag -a foo -m bar &&\n \n \t\toid=\"$(git rev-parse HEAD)\" &&\n \t\tgit update-ref refs/remotes/origin/foo \"$oid\" &&\n \n+\t\t# Also creates a commit, tree, and blob.\n \t\tgit notes add -m foo &&\n \n \t\tcat >expect <<-\\EOF &&\n-\t\t| Repository stats | Value |\n-\t\t| ---------------- | ----- |\n-\t\t| * References     |       |\n-\t\t|   * Count        |     4 |\n-\t\t|     * Branches   |     1 |\n-\t\t|     * Tags       |     1 |\n-\t\t|     * Remotes    |     1 |\n-\t\t|     * Others     |     1 |\n+\t\t| Repository stats    | Value |\n+\t\t| ------------------- | ----- |\n+\t\t| * References        |       |\n+\t\t|   * Count           |     4 |\n+\t\t|     * Branches      |     1 |\n+\t\t|     * Tags          |     1 |\n+\t\t|     * Remotes       |     1 |\n+\t\t|     * Others        |     1 |\n+\t\t|                     |       |\n+\t\t| * Reachable objects |       |\n+\t\t|   * Count           |   130 |\n+\t\t|     * Commits       |    43 |\n+\t\t|     * Trees         |    43 |\n+\t\t|     * Blobs         |    43 |\n+\t\t|     * Tags          |     1 |\n \t\tEOF\n \n \t\tgit repo stats >out 2>err &&\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527378","messageId":"20250925232928.3846-7-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250925232928.3846-1-jltobler@gmail.com","subject":"[PATCH v3 6/7] builtin/repo: add keyvalue and nul format for stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-25T23:29:27Z","receivedAt":"2025-09-25T23:29:41Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"All repository stats are outputted in a human-friendly table form. This\nformat is not suitable for machine parsing. Add a --format option that\nsupports three output modes: `table`, `keyvalue`, and `nul`. The `table`\nmode is the default format and prints the same table output as before.\n\nWith the `keyvalue` mode, each line of output contains a key-value pair\nof a repository stat. The '=' character is used to delimit between keys\nand values. The `nul` mode is similar to `keyvalue`, but key-values are\ndelimited by a NUL character instead of a newline. Also, instead of a\n'=' character to delimit between keys and values, a newline character is\nused. This allows stat values to support special characters without\nhaving to cquote them. These two new modes provides output that is more\nmachine-friendly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc | 25 +++++++++++++++--\n builtin/repo.c              | 55 ++++++++++++++++++++++++++++++++++---\n t/t1901-repo-stats.sh       | 33 ++++++++++++++++++++++\n 3 files changed, 106 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 0b8d74ed3e..3fbce0b88c 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -9,7 +9,7 @@ SYNOPSIS\n --------\n [synopsis]\n git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n-git repo stats\n+git repo stats [--format=(table|keyvalue|nul)]\n \n DESCRIPTION\n -----------\n@@ -44,7 +44,7 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n-`stats`::\n+`stats [--format=(table|keyvalue|nul)]`::\n \tRetrieve statistics about the current repository. The following kinds\n \tof information are reported:\n +\n@@ -52,7 +52,26 @@ supported:\n * Reachable object counts categorized by type\n \n +\n-The table output format may change and is not intended for machine parsing.\n+The output format can be chosen through the flag `--format`. Three formats are\n+supported:\n++\n+`table`:::\n+\tOutputs repository stats in a human-friendly table. This format may\n+\tchange and is not intended for machine parsing. This is the default\n+\tformat.\n+\n+`keyvalue`:::\n+\tEach line of output contains a key-value pair for a repository stat.\n+\tThe '=' character is used to delimit between the key and the value.\n+\tValues containing \"unusual\" characters are quoted as explained for the\n+\tconfiguration variable `core.quotePath` (see linkgit:git-config[1]).\n+\n+`nul`:::\n+\tSimilar to `keyvalue`, but uses a NUL character to delimit between\n+\tkey-value pairs instead of a newline. Also uses a newline character as\n+\tthe delimiter between the key and value instead of '='. Unlike the\n+\t`keyvalue` format, values containing \"unusual\" characters are never\n+\tquoted.\n \n INFO KEYS\n ---------\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 43cd6b1b38..e8a02c950b 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -14,13 +14,14 @@\n \n static const char *const repo_usage[] = {\n \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n-\t\"git repo stats\",\n+\t\"git repo stats [--format=(table|keyvalue|nul)]\",\n \tNULL\n };\n \n typedef int get_value_fn(struct repository *repo, struct strbuf *buf);\n \n enum output_format {\n+\tFORMAT_TABLE,\n \tFORMAT_KEYVALUE,\n \tFORMAT_NUL_TERMINATED,\n };\n@@ -135,6 +136,8 @@ static int parse_format_cb(const struct option *opt,\n \t\t*format = FORMAT_NUL_TERMINATED;\n \telse if (!strcmp(arg, \"keyvalue\"))\n \t\t*format = FORMAT_KEYVALUE;\n+\telse if (!strcmp(arg, \"table\"))\n+\t\t*format = FORMAT_TABLE;\n \telse\n \t\tdie(_(\"invalid format '%s'\"), arg);\n \n@@ -157,6 +160,8 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n+\tif (format != FORMAT_KEYVALUE && format != FORMAT_NUL_TERMINATED)\n+\t\tdie(_(\"unsupported output format\"));\n \n \treturn print_fields(argc, argv, repo, format);\n }\n@@ -329,6 +334,30 @@ static void stats_table_clear(struct stats_table *table)\n \tstring_list_clear(&table->rows, 1);\n }\n \n+static void stats_keyvalue_print(struct repo_stats *stats, char key_delim,\n+\t\t\t\t char value_delim)\n+{\n+\tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->refs.branches, value_delim);\n+\tprintf(\"references.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->refs.tags, value_delim);\n+\tprintf(\"references.remotes.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->refs.remotes, value_delim);\n+\tprintf(\"references.others.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->refs.others, value_delim);\n+\n+\tprintf(\"objects.commits.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.commits, value_delim);\n+\tprintf(\"objects.trees.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.trees, value_delim);\n+\tprintf(\"objects.blobs.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.blobs, value_delim);\n+\tprintf(\"objects.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.tags, value_delim);\n+\n+\tfflush(stdout);\n+}\n+\n static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n {\n \tfor (int i = 0; i < refs->nr; i++) {\n@@ -413,10 +442,16 @@ static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n \tstruct stats_table table = {\n \t\t.rows = STRING_LIST_INIT_DUP,\n \t};\n+\tenum output_format format = FORMAT_TABLE;\n \tstruct repo_stats stats = { 0 };\n \tstruct ref_array refs = { 0 };\n \tstruct rev_info revs;\n-\tstruct option options[] = { 0 };\n+\tstruct option options[] = {\n+\t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n+\t\t\t       N_(\"output format\"),\n+\t\t\t       PARSE_OPT_NONEG, parse_format_cb),\n+\t\tOPT_END()\n+\t};\n \n \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n \tif (argc)\n@@ -429,8 +464,20 @@ static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n \tstats_count_references(&stats.refs, &refs);\n \tstats_count_objects(&stats.objects, &refs, &revs);\n \n-\tstats_table_setup(&table, &stats);\n-\tstats_table_print(&table);\n+\tswitch (format) {\n+\tcase FORMAT_TABLE:\n+\t\tstats_table_setup(&table, &stats);\n+\t\tstats_table_print(&table);\n+\t\tbreak;\n+\tcase FORMAT_KEYVALUE:\n+\t\tstats_keyvalue_print(&stats, '=', '\\n');\n+\t\tbreak;\n+\tcase FORMAT_NUL_TERMINATED:\n+\t\tstats_keyvalue_print(&stats, '\\n', '\\0');\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"invalid output format\");\n+\t}\n \n \tstats_table_clear(&table);\n \trelease_revisions(&revs);\ndiff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\nindex 315b9e1767..2409edae4f 100755\n--- a/t/t1901-repo-stats.sh\n+++ b/t/t1901-repo-stats.sh\n@@ -73,4 +73,37 @@ test_expect_success 'repository with references and objects' '\n \t)\n '\n \n+test_expect_success 'keyvalue and nul format' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit_bulk 42 &&\n+\t\tgit tag -a foo -m bar &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\t\treferences.branches.count=1\n+\t\treferences.tags.count=1\n+\t\treferences.remotes.count=0\n+\t\treferences.others.count=0\n+\t\tobjects.commits.count=42\n+\t\tobjects.trees.count=42\n+\t\tobjects.blobs.count=42\n+\t\tobjects.tags.count=1\n+\t\tEOF\n+\n+\t\tgit repo stats --format=keyvalue >out 2>err &&\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err &&\n+\n+\t\t# Replace key and value delimiters for nul format.\n+\t\ttr \"\\n=\" \"\\0\\n\" <expect >expect_nul &&\n+\t\tgit repo stats --format=nul >out 2>err &&\n+\n+\t\ttest_cmp expect_nul out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527379","messageId":"20250925232928.3846-8-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250925232928.3846-1-jltobler@gmail.com","subject":"[PATCH v3 7/7] builtin/repo: add progress meter for stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-25T23:29:28Z","receivedAt":"2025-09-25T23:29:42Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"When using the stats subcommand for git-repo(1), evaluating a repository\nmay take some time depending on its shape. Add a progress meter to\nprovide feedback to the user about what is happening. The progress meter\nis enabled by default when the command is executed from a tty. It can\nalso be explicitly enabled/disabled via the --[no-]progress option.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/repo.c        | 47 +++++++++++++++++++++++++++++++++++++------\n t/t1901-repo-stats.sh | 20 ++++++++++++++++++\n 2 files changed, 61 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex e8a02c950b..e553d7aa28 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -4,6 +4,7 @@\n #include \"environment.h\"\n #include \"parse-options.h\"\n #include \"path-walk.h\"\n+#include \"progress.h\"\n #include \"quote.h\"\n #include \"ref-filter.h\"\n #include \"refs.h\"\n@@ -358,8 +359,15 @@ static void stats_keyvalue_print(struct repo_stats *stats, char key_delim,\n \tfflush(stdout);\n }\n \n-static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n+static void stats_count_references(struct ref_stats *stats, struct ref_array *refs,\n+\t\t\t\t   struct repository *repo, int show_progress)\n {\n+\tstruct progress *progress = NULL;\n+\n+\tif (show_progress)\n+\t\tprogress = start_delayed_progress(repo, _(\"Counting references\"),\n+\t\t\t\t\t\t  refs->nr);\n+\n \tfor (int i = 0; i < refs->nr; i++) {\n \t\tstruct ref_array_item *ref = refs->items[i];\n \n@@ -379,13 +387,24 @@ static void stats_count_references(struct ref_stats *stats, struct ref_array *re\n \t\tdefault:\n \t\t\tBUG(\"unexpected reference type\");\n \t\t}\n+\n+\t\tdisplay_progress(progress, i + 1);\n \t}\n+\n+\tstop_progress(&progress);\n }\n \n+struct count_objects_data {\n+\tstruct object_stats *stats;\n+\tstruct progress *progress;\n+};\n+\n static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\t enum object_type type, void *cb_data)\n {\n-\tstruct object_stats *stats = cb_data;\n+\tstruct count_objects_data *data = cb_data;\n+\tstruct object_stats *stats = data->stats;\n+\tsize_t object_count;\n \n \tswitch (type) {\n \tcase OBJ_TAG:\n@@ -404,17 +423,24 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\tBUG(\"invalid object type\");\n \t}\n \n+\tobject_count = get_total_object_count(stats);\n+\tdisplay_progress(data->progress, object_count);\n+\n \treturn 0;\n }\n \n static void stats_count_objects(struct object_stats *stats,\n-\t\t\t\tstruct ref_array *refs, struct rev_info *revs)\n+\t\t\t\tstruct ref_array *refs, struct rev_info *revs,\n+\t\t\t\tstruct repository *repo, int show_progress)\n {\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n+\tstruct count_objects_data data = {\n+\t\t.stats = stats,\n+\t};\n \n \tinfo.revs = revs;\n \tinfo.path_fn = count_objects;\n-\tinfo.path_fn_data = stats;\n+\tinfo.path_fn_data = &data;\n \n \tfor (int i = 0; i < refs->nr; i++) {\n \t\tstruct ref_array_item *ref = refs->items[i];\n@@ -431,8 +457,12 @@ static void stats_count_objects(struct object_stats *stats,\n \t\t}\n \t}\n \n+\tif (show_progress)\n+\t\tdata.progress = start_delayed_progress(repo, _(\"Counting objects\"), 0);\n+\n \twalk_objects_by_path(&info);\n \tpath_walk_info_clear(&info);\n+\tstop_progress(&data.progress);\n }\n \n static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n@@ -446,10 +476,12 @@ static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n \tstruct repo_stats stats = { 0 };\n \tstruct ref_array refs = { 0 };\n \tstruct rev_info revs;\n+\tint show_progress = -1;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n \t\t\t       N_(\"output format\"),\n \t\t\t       PARSE_OPT_NONEG, parse_format_cb),\n+\t\tOPT_BOOL(0, \"progress\", &show_progress, N_(\"show progress\")),\n \t\tOPT_END()\n \t};\n \n@@ -461,8 +493,11 @@ static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n \tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n \t\tdie(_(\"unable to filter refs\"));\n \n-\tstats_count_references(&stats.refs, &refs);\n-\tstats_count_objects(&stats.objects, &refs, &revs);\n+\tif (show_progress < 0)\n+\t\tshow_progress = isatty(2);\n+\n+\tstats_count_references(&stats.refs, &refs, repo, show_progress);\n+\tstats_count_objects(&stats.objects, &refs, &revs, repo, show_progress);\n \n \tswitch (format) {\n \tcase FORMAT_TABLE:\ndiff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\nindex 2409edae4f..03d6db479f 100755\n--- a/t/t1901-repo-stats.sh\n+++ b/t/t1901-repo-stats.sh\n@@ -106,4 +106,24 @@ test_expect_success 'keyvalue and nul format' '\n \t)\n '\n \n+test_expect_success 'progress meter option' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit foo &&\n+\n+\t\tGIT_PROGRESS_DELAY=0 git repo stats --progress >out 2>err &&\n+\n+\t\ttest_file_not_empty out &&\n+\t\ttest_grep \"Counting references: 100% (2/2), done.\" err &&\n+\t\ttest_grep \"Counting objects: 3, done.\" err &&\n+\n+\t\tGIT_PROGRESS_DELAY=0 git repo stats --no-progress >out 2>err &&\n+\n+\t\ttest_file_not_empty out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527380","messageId":"CAPig+cTSpq132SZQHUzYHJNqY-nOYizFW71Lu3o6a6m3jDNVJQ@mail.gmail.com","threadId":"64187","inReplyTo":"20250925232928.3846-5-jltobler@gmail.com","subject":"Re: [PATCH v3 4/7] builtin/repo: introduce stats subcommand","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2025-09-25T23:51:20Z","receivedAt":"2025-09-25T23:51:33Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Thu, Sep 25, 2025 at 7:30 PM Justin Tobler <jltobler@gmail.com> wrote:\n> The shape of a repository's history can have huge impacts on the\n> performance and health of the repository itself. Currently, Git lacks a\n> means to surface key stats/information regarding the shape of a\n> repository via a single command. Acquiring this information requires\n> users to be fairly knowledgeable about the structure of a Git repository\n> and how to identify the relevant data points. To fill this gap,\n> supplemental tools such as git-sizer(1) have been developed.\n> [...]\n> Signed-off-by: Justin Tobler <jltobler@gmail.com>\n> ---\n> diff --git a/builtin/repo.c b/builtin/repo.c\n> @@ -156,12 +159,205 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n> +static void stats_table_vaddf(struct stats_table *table,\n> +                             struct stats_table_entry *entry,\n> +                             const char *format, va_list ap)\n> +{\n> +       size_t name_width;\n> +\n> +       strbuf_vaddf(&buf, format, ap);\n> +       formatted_name = strbuf_detach(&buf, &name_width);\n> +       [...]\n> +       if (name_width > table->name_col_width)\n> +               table->name_col_width = name_width;\n\nHere, you're using the byte length of the composed string to compute\nthe table width which you will use for alignment purposes when\nrendering the table...\n\n> +static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n> +{\n> +       size_t ref_total;\n> +\n> +       ref_total = refs->branches + refs->remotes + refs->tags + refs->others;\n> +       stats_table_addf(table, \"* %s\", _(\"References\"));\n> +       stats_table_count_addf(table, ref_total, \"  * %s\", _(\"Count\"));\n> +       stats_table_count_addf(table, refs->branches, \"    * %s\", _(\"Branches\"));\n> +       stats_table_count_addf(table, refs->tags, \"    * %s\", _(\"Tags\"));\n> +       stats_table_count_addf(table, refs->remotes, \"    * %s\", _(\"Remotes\"));\n> +       stats_table_count_addf(table, refs->others, \"    * %s\", _(\"Others\"));\n> +}\n\n...however, here you feed the function translatable strings, which\nmeans that the display length of the composed string is not guaranteed\nto be the same as the byte length.\n\nTo resolve this, you probably want to investigate Git's utf8.h header,\nin particular, the utf8_strwidth() function.\n"},{"id":"527389","messageId":"wugagxpikclvhv5ajxzmeb42rneuv3hoi3bd23lqt5uysdotdg@gxvmipzrajdu","threadId":"64187","inReplyTo":"CAPig+cTSpq132SZQHUzYHJNqY-nOYizFW71Lu3o6a6m3jDNVJQ@mail.gmail.com","subject":"Re: [PATCH v3 4/7] builtin/repo: introduce stats subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-26T01:38:10Z","receivedAt":"2025-09-26T01:38:13Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/09/25 07:51PM, Eric Sunshine wrote:\n> On Thu, Sep 25, 2025 at 7:30 PM Justin Tobler <jltobler@gmail.com> wrote:\n[snip]\n> > +       strbuf_vaddf(&buf, format, ap);\n> > +       formatted_name = strbuf_detach(&buf, &name_width);\n> > +       [...]\n> > +       if (name_width > table->name_col_width)\n> > +               table->name_col_width = name_width;\n> \n> Here, you're using the byte length of the composed string to compute\n> the table width which you will use for alignment purposes when\n> rendering the table...\n> \n> > +static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n> > +{\n> > +       size_t ref_total;\n> > +\n> > +       ref_total = refs->branches + refs->remotes + refs->tags + refs->others;\n> > +       stats_table_addf(table, \"* %s\", _(\"References\"));\n> > +       stats_table_count_addf(table, ref_total, \"  * %s\", _(\"Count\"));\n> > +       stats_table_count_addf(table, refs->branches, \"    * %s\", _(\"Branches\"));\n> > +       stats_table_count_addf(table, refs->tags, \"    * %s\", _(\"Tags\"));\n> > +       stats_table_count_addf(table, refs->remotes, \"    * %s\", _(\"Remotes\"));\n> > +       stats_table_count_addf(table, refs->others, \"    * %s\", _(\"Others\"));\n> > +}\n> \n> ...however, here you feed the function translatable strings, which\n> means that the display length of the composed string is not guaranteed\n> to be the same as the byte length.\n> \n> To resolve this, you probably want to investigate Git's utf8.h header,\n> in particular, the utf8_strwidth() function.\n\nThanks for raising this. I hadn't considered the fact that the\ncharacters in translated strings could occupy more than one byte. I'll\naddress this in the next version by using utf8_strwidth() as you\nmentioned. :)\n\nThanks,\n-Justin\n"},{"id":"527480","messageId":"20250927145049.723341-1-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250925232928.3846-1-jltobler@gmail.com","subject":"[PATCH v4 0/7] builtin/repo: introduce stats subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-27T14:50:42Z","receivedAt":"2025-09-27T14:50:55Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Greetings,\n\nThe shape of a repository's history can have huge impacts on the\nperformance and health of the repository itself. Currently, Git lacks a\nmeans to surface key stats/information regarding the shape of a\nrepository via a single command. Acquiring this information requires\nusers to be fairly knowledgeable about the structure of a Git repository\nand how to identify the relevant data points. To fill this gap,\nsupplemental tools such as git-sizer(1) have been developed.\n\nTo allow users to more readily identify potential issues for a\nrepository, introduce the \"stats\" subcommand in git-repo(1) to output\nstats for the repository that may be of interest to users. The goal of\nthis subcommand is to eventually provide similar functionality to\ngit-sizer(1), but in Git natively.\n\nIn this initial version, the \"stats\" subcommand only surfaces counts of\nthe various reference and object types in a repository. In a follow-up\nseries, I would like to introduce additional data points that are\npresent in git-sizer(1) such as largest objects, combined object sizes\nby type, and other general repository shape information.\n\nSome other general features that would be nice to introduce eventually:\n\n- A \"level of concern\" meter for reported stats. This could indicate to\n  users which stats may be worth looking into further.\n- Links to OIDs of interesting objects that correspond to certain stats.\n- Options to limit which references to use when evaluating the\n  repository.\n\nChanges since V3:\n\n- Changed from using strlen() to utf8_strlen() to take into\n  consideration that translatable strings may have characters that are\n  more than one byte.\n\nChanges since V2:\n\n- Added clang-format patch to address false postive triggered in this\n  series.\n- Use varargs for stats_table_add() family of functions.\n- Print to stdout directly instead of using strbuf.\n- Add parse_option() earlier in the series.\n- Use start_delayed_progress() instead of start_progress().\n- Add test to validate --[no-]progress options.\n- Some other small fixes.\n\nChanges since V1:\n\n- Translatable terms displayed in the table have formatting separated\n  out.\n- Squashed the `keyvalue` and `nul` output format patches into one.\n- Added a progress meter to provide users with more feedback.\n- Updated docs to outline to outline reported data in a bulleted list.\n- Combined similar tests together to reduce repetitive setup.\n- Added patch to improve ref-filter interface so we don't have to create\n  a dummy patterns array.\n- Many other renames and cleanups to improve patch clarity.\n\nThanks,\n-Justin\n\nJustin Tobler (7):\n  builtin/repo: rename repo_info() to cmd_repo_info()\n  ref-filter: allow NULL filter pattern\n  clang-format: exclude control macros from SpaceBeforeParens\n  builtin/repo: introduce stats subcommand\n  builtin/repo: add object counts in stats output\n  builtin/repo: add keyvalue and nul format for stats\n  builtin/repo: add progress meter for stats\n\n .clang-format               |   2 +-\n Documentation/git-repo.adoc |  30 +++\n builtin/repo.c              | 374 +++++++++++++++++++++++++++++++++++-\n ref-filter.c                |   4 +-\n t/meson.build               |   1 +\n t/t1901-repo-stats.sh       | 129 +++++++++++++\n 6 files changed, 534 insertions(+), 6 deletions(-)\n create mode 100755 t/t1901-repo-stats.sh\n\nRange-diff against v3:\n1:  ed04168562 = 1:  ed04168562 builtin/repo: rename repo_info() to cmd_repo_info()\n2:  6aa76d1323 = 2:  6aa76d1323 ref-filter: allow NULL filter pattern\n3:  02a3fcc5fb = 3:  02a3fcc5fb clang-format: exclude control macros from SpaceBeforeParens\n4:  12cfbdc464 ! 4:  8ec9914886 builtin/repo: introduce stats subcommand\n    @@ builtin/repo.c\n      #include \"strbuf.h\"\n     +#include \"string-list.h\"\n      #include \"shallow.h\"\n    ++#include \"utf8.h\"\n      \n      static const char *const repo_usage[] = {\n      \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\tsize_t name_width;\n     +\n     +\tstrbuf_vaddf(&buf, format, ap);\n    -+\tformatted_name = strbuf_detach(&buf, &name_width);\n    ++\tformatted_name = strbuf_detach(&buf, NULL);\n    ++\tname_width = utf8_strwidth(formatted_name);\n     +\n     +\titem = string_list_append_nodup(&table->rows, formatted_name);\n     +\titem->util = entry;\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\tif (name_width > table->name_col_width)\n     +\t\ttable->name_col_width = name_width;\n     +\tif (entry) {\n    -+\t\tsize_t value_width = strlen(entry->value);\n    ++\t\tsize_t value_width = utf8_strwidth(entry->value);\n     +\t\tif (value_width > table->value_col_width)\n     +\t\t\ttable->value_col_width = value_width;\n     +\t}\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +{\n     +\tconst char *name_col_title = _(\"Repository stats\");\n     +\tconst char *value_col_title = _(\"Value\");\n    -+\tsize_t name_title_len = strlen(name_col_title);\n    -+\tsize_t value_title_len = strlen(value_col_title);\n    ++\tsize_t name_title_len = utf8_strwidth(name_col_title);\n    ++\tsize_t value_title_len = utf8_strwidth(value_col_title);\n     +\tstruct string_list_item *item;\n     +\tint name_col_width;\n     +\tint value_col_width;\n5:  ab27340d58 = 5:  584d35f2c7 builtin/repo: add object counts in stats output\n6:  f69110224d = 6:  76975b2eab builtin/repo: add keyvalue and nul format for stats\n7:  cff5e183bb = 7:  1105346a3c builtin/repo: add progress meter for stats\n\nbase-commit: ca2559c1d630eb4f04cdee2328aaf1c768907a9e\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527481","messageId":"20250927145049.723341-2-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250927145049.723341-1-jltobler@gmail.com","subject":"[PATCH v4 1/7] builtin/repo: rename repo_info() to cmd_repo_info()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-27T14:50:43Z","receivedAt":"2025-09-27T14:50:56Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Subcommand functions are often prefixed with `cmd_` to denote that they\nare an entrypoint. Rename repo_info() to cmd_repo_info() accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/repo.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex bbb0966f2d..eeeab8fbd2 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -136,8 +136,8 @@ static int parse_format_cb(const struct option *opt,\n \treturn 0;\n }\n \n-static int repo_info(int argc, const char **argv, const char *prefix,\n-\t\t     struct repository *repo)\n+static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n+\t\t\t struct repository *repo)\n {\n \tenum output_format format = FORMAT_KEYVALUE;\n \tstruct option options[] = {\n@@ -161,7 +161,7 @@ int cmd_repo(int argc, const char **argv, const char *prefix,\n {\n \tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n-\t\tOPT_SUBCOMMAND(\"info\", &fn, repo_info),\n+\t\tOPT_SUBCOMMAND(\"info\", &fn, cmd_repo_info),\n \t\tOPT_END()\n \t};\n \n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527482","messageId":"20250927145049.723341-3-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250927145049.723341-1-jltobler@gmail.com","subject":"[PATCH v4 2/7] ref-filter: allow NULL filter pattern","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-27T14:50:44Z","receivedAt":"2025-09-27T14:50:57Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"When setting up `struct ref_filter` for filter_refs(), the\n`name_patterns` field must point to an array of pattern strings even if\nno patterns are required. To improve this interface, treat a NULL\n`name_patterns` field the same as when it points to an empty array.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n ref-filter.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/ref-filter.c b/ref-filter.c\nindex 520d2539c9..2cb5a166d6 100644\n--- a/ref-filter.c\n+++ b/ref-filter.c\n@@ -2664,7 +2664,7 @@ static int match_name_as_path(const char **pattern, const char *refname,\n /* Return 1 if the refname matches one of the patterns, otherwise 0. */\n static int filter_pattern_match(struct ref_filter *filter, const char *refname)\n {\n-\tif (!*filter->name_patterns)\n+\tif (!filter->name_patterns || !*filter->name_patterns)\n \t\treturn 1; /* No pattern always matches */\n \tif (filter->match_as_path)\n \t\treturn match_name_as_path(filter->name_patterns, refname,\n@@ -2751,7 +2751,7 @@ static int for_each_fullref_in_pattern(struct ref_filter *filter,\n \t\treturn for_each_fullref_with_seek(filter, cb, cb_data, 0);\n \t}\n \n-\tif (!filter->name_patterns[0]) {\n+\tif (!filter->name_patterns || !filter->name_patterns[0]) {\n \t\t/* no patterns; we have to look at everything */\n \t\treturn for_each_fullref_with_seek(filter, cb, cb_data, 0);\n \t}\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527483","messageId":"20250927145049.723341-4-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250927145049.723341-1-jltobler@gmail.com","subject":"[PATCH v4 3/7] clang-format: exclude control macros from SpaceBeforeParens","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-27T14:50:45Z","receivedAt":"2025-09-27T14:50:58Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The formatter currently suggests adding a space between a control macro\nand parentheses. In the Git project, this is not typically expected. Set\n`SpaceBeforeParens` to `ControlStatementsExceptControlMacros`\naccordingly.\n\nHelped-by: Karthik Nayak <karthik.188@gmail.com>\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n .clang-format | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/.clang-format b/.clang-format\nindex dcfd0aad60..86b4fe33e5 100644\n--- a/.clang-format\n+++ b/.clang-format\n@@ -149,7 +149,7 @@ SpaceBeforeCaseColon: false\n #     f();\n #   }\n # }\n-SpaceBeforeParens: ControlStatements\n+SpaceBeforeParens: ControlStatementsExceptControlMacros\n \n # Don't insert spaces inside empty '()'\n SpaceInEmptyParentheses: false\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527484","messageId":"20250927145049.723341-5-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250927145049.723341-1-jltobler@gmail.com","subject":"[PATCH v4 4/7] builtin/repo: introduce stats subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-27T14:50:46Z","receivedAt":"2025-09-27T14:50:59Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The shape of a repository's history can have huge impacts on the\nperformance and health of the repository itself. Currently, Git lacks a\nmeans to surface key stats/information regarding the shape of a\nrepository via a single command. Acquiring this information requires\nusers to be fairly knowledgeable about the structure of a Git repository\nand how to identify the relevant data points. To fill this gap,\nsupplemental tools such as git-sizer(1) have been developed.\n\nTo allow users to more readily identify potential issues for a\nrepository, introduce the \"stats\" subcommand in git-repo(1) to output\nstats for the repository that may be of interest to users. The goal of\nthis subcommand is to eventually provide similar functionality to\ngit-sizer(1), but natively in Git.\n\nThe initial version of this command only iterates through all references\nin the repository and tracks the count of branches, tags, remote refs,\nand other reference types. The corresponding information is displayed in\na human-friendly table formatted in a very similar manner to\ngit-sizer(1). The width of each table column is adjusted automatically\nto satisfy the requirements of the widest row contained.\n\nSubsequent commits will surface additional relevant data points to\noutput.\n\nBased-on-patch-by: Derrick Stolee <stolee@gmail.com>\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc |  10 ++\n builtin/repo.c              | 198 ++++++++++++++++++++++++++++++++++++\n t/meson.build               |   1 +\n t/t1901-repo-stats.sh       |  61 +++++++++++\n 4 files changed, 270 insertions(+)\n create mode 100755 t/t1901-repo-stats.sh\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 209afd1b61..a009bf8cf1 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -9,6 +9,7 @@ SYNOPSIS\n --------\n [synopsis]\n git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n+git repo stats\n \n DESCRIPTION\n -----------\n@@ -43,6 +44,15 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n+`stats`::\n+\tRetrieve statistics about the current repository. The following kinds\n+\tof information are reported:\n++\n+* Reference counts categorized by type\n+\n++\n+The table output format may change and is not intended for machine parsing.\n+\n INFO KEYS\n ---------\n In order to obtain a set of values from `git repo info`, you should provide\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex eeeab8fbd2..889e344f15 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -4,12 +4,16 @@\n #include \"environment.h\"\n #include \"parse-options.h\"\n #include \"quote.h\"\n+#include \"ref-filter.h\"\n #include \"refs.h\"\n #include \"strbuf.h\"\n+#include \"string-list.h\"\n #include \"shallow.h\"\n+#include \"utf8.h\"\n \n static const char *const repo_usage[] = {\n \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n+\t\"git repo stats\",\n \tNULL\n };\n \n@@ -156,12 +160,206 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \treturn print_fields(argc, argv, repo, format);\n }\n \n+struct ref_stats {\n+\tsize_t branches;\n+\tsize_t remotes;\n+\tsize_t tags;\n+\tsize_t others;\n+};\n+\n+struct stats_table {\n+\tstruct string_list rows;\n+\n+\tsize_t name_col_width;\n+\tsize_t value_col_width;\n+};\n+\n+/*\n+ * Holds column data that gets stored for each row.\n+ */\n+struct stats_table_entry {\n+\tchar *value;\n+};\n+\n+static void stats_table_vaddf(struct stats_table *table,\n+\t\t\t      struct stats_table_entry *entry,\n+\t\t\t      const char *format, va_list ap)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct string_list_item *item;\n+\tchar *formatted_name;\n+\tsize_t name_width;\n+\n+\tstrbuf_vaddf(&buf, format, ap);\n+\tformatted_name = strbuf_detach(&buf, NULL);\n+\tname_width = utf8_strwidth(formatted_name);\n+\n+\titem = string_list_append_nodup(&table->rows, formatted_name);\n+\titem->util = entry;\n+\n+\tif (name_width > table->name_col_width)\n+\t\ttable->name_col_width = name_width;\n+\tif (entry) {\n+\t\tsize_t value_width = utf8_strwidth(entry->value);\n+\t\tif (value_width > table->value_col_width)\n+\t\t\ttable->value_col_width = value_width;\n+\t}\n+}\n+\n+static void stats_table_addf(struct stats_table *table, const char *format, ...)\n+{\n+\tva_list ap;\n+\n+\tva_start(ap, format);\n+\tstats_table_vaddf(table, NULL, format, ap);\n+\tva_end(ap);\n+}\n+\n+static void stats_table_count_addf(struct stats_table *table, size_t value,\n+\t\t\t\t   const char *format, ...)\n+{\n+\tstruct stats_table_entry *entry;\n+\tva_list ap;\n+\n+\tCALLOC_ARRAY(entry, 1);\n+\tentry->value = xstrfmt(\"%\" PRIuMAX, (uintmax_t)value);\n+\n+\tva_start(ap, format);\n+\tstats_table_vaddf(table, entry, format, ap);\n+\tva_end(ap);\n+}\n+\n+static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n+{\n+\tsize_t ref_total;\n+\n+\tref_total = refs->branches + refs->remotes + refs->tags + refs->others;\n+\tstats_table_addf(table, \"* %s\", _(\"References\"));\n+\tstats_table_count_addf(table, ref_total, \"  * %s\", _(\"Count\"));\n+\tstats_table_count_addf(table, refs->branches, \"    * %s\", _(\"Branches\"));\n+\tstats_table_count_addf(table, refs->tags, \"    * %s\", _(\"Tags\"));\n+\tstats_table_count_addf(table, refs->remotes, \"    * %s\", _(\"Remotes\"));\n+\tstats_table_count_addf(table, refs->others, \"    * %s\", _(\"Others\"));\n+}\n+\n+static inline size_t max_size_t(size_t a, size_t b)\n+{\n+\treturn (a > b) ? a : b;\n+}\n+\n+static void stats_table_print(const struct stats_table *table)\n+{\n+\tconst char *name_col_title = _(\"Repository stats\");\n+\tconst char *value_col_title = _(\"Value\");\n+\tsize_t name_title_len = utf8_strwidth(name_col_title);\n+\tsize_t value_title_len = utf8_strwidth(value_col_title);\n+\tstruct string_list_item *item;\n+\tint name_col_width;\n+\tint value_col_width;\n+\n+\tname_col_width = cast_size_t_to_int(\n+\t\tmax_size_t(table->name_col_width, name_title_len));\n+\tvalue_col_width = cast_size_t_to_int(\n+\t\tmax_size_t(table->value_col_width, value_title_len));\n+\n+\tprintf(\"| %-*s | %-*s |\\n\", name_col_width, name_col_title,\n+\t       value_col_width, value_col_title);\n+\tprintf(\"| \");\n+\tfor (int i = 0; i < name_col_width; i++)\n+\t\tputchar('-');\n+\tprintf(\" | \");\n+\tfor (int i = 0; i < value_col_width; i++)\n+\t\tputchar('-');\n+\tprintf(\" |\\n\");\n+\n+\tfor_each_string_list_item(item, &table->rows) {\n+\t\tstruct stats_table_entry *entry = item->util;\n+\t\tconst char *value = \"\";\n+\n+\t\tif (entry) {\n+\t\t\tstruct stats_table_entry *entry = item->util;\n+\t\t\tvalue = entry->value;\n+\t\t}\n+\n+\t\tprintf(\"| %-*s | %*s |\\n\", name_col_width, item->string,\n+\t\t       value_col_width, value);\n+\t}\n+}\n+\n+static void stats_table_clear(struct stats_table *table)\n+{\n+\tstruct stats_table_entry *entry;\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, &table->rows) {\n+\t\tentry = item->util;\n+\t\tif (entry)\n+\t\t\tfree(entry->value);\n+\t}\n+\n+\tstring_list_clear(&table->rows, 1);\n+}\n+\n+static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n+{\n+\tfor (int i = 0; i < refs->nr; i++) {\n+\t\tstruct ref_array_item *ref = refs->items[i];\n+\n+\t\tswitch (ref->kind) {\n+\t\tcase FILTER_REFS_BRANCHES:\n+\t\t\tstats->branches++;\n+\t\t\tbreak;\n+\t\tcase FILTER_REFS_REMOTES:\n+\t\t\tstats->remotes++;\n+\t\t\tbreak;\n+\t\tcase FILTER_REFS_TAGS:\n+\t\t\tstats->tags++;\n+\t\t\tbreak;\n+\t\tcase FILTER_REFS_OTHERS:\n+\t\t\tstats->others++;\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tBUG(\"unexpected reference type\");\n+\t\t}\n+\t}\n+}\n+\n+static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n+\t\t\t  struct repository *repo UNUSED)\n+{\n+\tstruct ref_filter filter = REF_FILTER_INIT;\n+\tstruct stats_table table = {\n+\t\t.rows = STRING_LIST_INIT_DUP,\n+\t};\n+\tstruct ref_stats stats = { 0 };\n+\tstruct ref_array refs = { 0 };\n+\tstruct option options[] = { 0 };\n+\n+\targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n+\tif (argc)\n+\t\tusage(_(\"too many arguments\"));\n+\n+\tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n+\t\tdie(_(\"unable to filter refs\"));\n+\n+\tstats_count_references(&stats, &refs);\n+\n+\tstats_table_setup(&table, &stats);\n+\tstats_table_print(&table);\n+\n+\tstats_table_clear(&table);\n+\tref_array_clear(&refs);\n+\n+\treturn 0;\n+}\n+\n int cmd_repo(int argc, const char **argv, const char *prefix,\n \t     struct repository *repo)\n {\n \tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n \t\tOPT_SUBCOMMAND(\"info\", &fn, cmd_repo_info),\n+\t\tOPT_SUBCOMMAND(\"stats\", &fn, cmd_repo_stats),\n \t\tOPT_END()\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 7974795fe4..071d4a5112 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -236,6 +236,7 @@ integration_tests = [\n   't1701-racy-split-index.sh',\n   't1800-hook.sh',\n   't1900-repo.sh',\n+  't1901-repo-stats.sh',\n   't2000-conflict-when-checking-files-out.sh',\n   't2002-checkout-cache-u.sh',\n   't2003-checkout-cache-mkdir.sh',\ndiff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\nnew file mode 100755\nindex 0000000000..535ac511dd\n--- /dev/null\n+++ b/t/t1901-repo-stats.sh\n@@ -0,0 +1,61 @@\n+#!/bin/sh\n+\n+test_description='test git repo stats'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'empty repository' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\t| Repository stats | Value |\n+\t\t| ---------------- | ----- |\n+\t\t| * References     |       |\n+\t\t|   * Count        |     0 |\n+\t\t|     * Branches   |     0 |\n+\t\t|     * Tags       |     0 |\n+\t\t|     * Remotes    |     0 |\n+\t\t|     * Others     |     0 |\n+\t\tEOF\n+\n+\t\tgit repo stats >out 2>err &&\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n+test_expect_success 'repository with references' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit commit --allow-empty -m init &&\n+\t\tgit tag -a foo -m bar &&\n+\n+\t\toid=\"$(git rev-parse HEAD)\" &&\n+\t\tgit update-ref refs/remotes/origin/foo \"$oid\" &&\n+\n+\t\tgit notes add -m foo &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\t\t| Repository stats | Value |\n+\t\t| ---------------- | ----- |\n+\t\t| * References     |       |\n+\t\t|   * Count        |     4 |\n+\t\t|     * Branches   |     1 |\n+\t\t|     * Tags       |     1 |\n+\t\t|     * Remotes    |     1 |\n+\t\t|     * Others     |     1 |\n+\t\tEOF\n+\n+\t\tgit repo stats >out 2>err &&\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n+test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527485","messageId":"20250927145049.723341-7-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250927145049.723341-1-jltobler@gmail.com","subject":"[PATCH v4 6/7] builtin/repo: add keyvalue and nul format for stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-27T14:50:48Z","receivedAt":"2025-09-27T14:51:01Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"All repository stats are outputted in a human-friendly table form. This\nformat is not suitable for machine parsing. Add a --format option that\nsupports three output modes: `table`, `keyvalue`, and `nul`. The `table`\nmode is the default format and prints the same table output as before.\n\nWith the `keyvalue` mode, each line of output contains a key-value pair\nof a repository stat. The '=' character is used to delimit between keys\nand values. The `nul` mode is similar to `keyvalue`, but key-values are\ndelimited by a NUL character instead of a newline. Also, instead of a\n'=' character to delimit between keys and values, a newline character is\nused. This allows stat values to support special characters without\nhaving to cquote them. These two new modes provides output that is more\nmachine-friendly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc | 25 +++++++++++++++--\n builtin/repo.c              | 55 ++++++++++++++++++++++++++++++++++---\n t/t1901-repo-stats.sh       | 33 ++++++++++++++++++++++\n 3 files changed, 106 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 0b8d74ed3e..3fbce0b88c 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -9,7 +9,7 @@ SYNOPSIS\n --------\n [synopsis]\n git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n-git repo stats\n+git repo stats [--format=(table|keyvalue|nul)]\n \n DESCRIPTION\n -----------\n@@ -44,7 +44,7 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n-`stats`::\n+`stats [--format=(table|keyvalue|nul)]`::\n \tRetrieve statistics about the current repository. The following kinds\n \tof information are reported:\n +\n@@ -52,7 +52,26 @@ supported:\n * Reachable object counts categorized by type\n \n +\n-The table output format may change and is not intended for machine parsing.\n+The output format can be chosen through the flag `--format`. Three formats are\n+supported:\n++\n+`table`:::\n+\tOutputs repository stats in a human-friendly table. This format may\n+\tchange and is not intended for machine parsing. This is the default\n+\tformat.\n+\n+`keyvalue`:::\n+\tEach line of output contains a key-value pair for a repository stat.\n+\tThe '=' character is used to delimit between the key and the value.\n+\tValues containing \"unusual\" characters are quoted as explained for the\n+\tconfiguration variable `core.quotePath` (see linkgit:git-config[1]).\n+\n+`nul`:::\n+\tSimilar to `keyvalue`, but uses a NUL character to delimit between\n+\tkey-value pairs instead of a newline. Also uses a newline character as\n+\tthe delimiter between the key and value instead of '='. Unlike the\n+\t`keyvalue` format, values containing \"unusual\" characters are never\n+\tquoted.\n \n INFO KEYS\n ---------\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 3eefbeddba..6f41c9ada2 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -15,13 +15,14 @@\n \n static const char *const repo_usage[] = {\n \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n-\t\"git repo stats\",\n+\t\"git repo stats [--format=(table|keyvalue|nul)]\",\n \tNULL\n };\n \n typedef int get_value_fn(struct repository *repo, struct strbuf *buf);\n \n enum output_format {\n+\tFORMAT_TABLE,\n \tFORMAT_KEYVALUE,\n \tFORMAT_NUL_TERMINATED,\n };\n@@ -136,6 +137,8 @@ static int parse_format_cb(const struct option *opt,\n \t\t*format = FORMAT_NUL_TERMINATED;\n \telse if (!strcmp(arg, \"keyvalue\"))\n \t\t*format = FORMAT_KEYVALUE;\n+\telse if (!strcmp(arg, \"table\"))\n+\t\t*format = FORMAT_TABLE;\n \telse\n \t\tdie(_(\"invalid format '%s'\"), arg);\n \n@@ -158,6 +161,8 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n+\tif (format != FORMAT_KEYVALUE && format != FORMAT_NUL_TERMINATED)\n+\t\tdie(_(\"unsupported output format\"));\n \n \treturn print_fields(argc, argv, repo, format);\n }\n@@ -331,6 +336,30 @@ static void stats_table_clear(struct stats_table *table)\n \tstring_list_clear(&table->rows, 1);\n }\n \n+static void stats_keyvalue_print(struct repo_stats *stats, char key_delim,\n+\t\t\t\t char value_delim)\n+{\n+\tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->refs.branches, value_delim);\n+\tprintf(\"references.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->refs.tags, value_delim);\n+\tprintf(\"references.remotes.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->refs.remotes, value_delim);\n+\tprintf(\"references.others.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->refs.others, value_delim);\n+\n+\tprintf(\"objects.commits.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.commits, value_delim);\n+\tprintf(\"objects.trees.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.trees, value_delim);\n+\tprintf(\"objects.blobs.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.blobs, value_delim);\n+\tprintf(\"objects.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.tags, value_delim);\n+\n+\tfflush(stdout);\n+}\n+\n static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n {\n \tfor (int i = 0; i < refs->nr; i++) {\n@@ -415,10 +444,16 @@ static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n \tstruct stats_table table = {\n \t\t.rows = STRING_LIST_INIT_DUP,\n \t};\n+\tenum output_format format = FORMAT_TABLE;\n \tstruct repo_stats stats = { 0 };\n \tstruct ref_array refs = { 0 };\n \tstruct rev_info revs;\n-\tstruct option options[] = { 0 };\n+\tstruct option options[] = {\n+\t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n+\t\t\t       N_(\"output format\"),\n+\t\t\t       PARSE_OPT_NONEG, parse_format_cb),\n+\t\tOPT_END()\n+\t};\n \n \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n \tif (argc)\n@@ -431,8 +466,20 @@ static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n \tstats_count_references(&stats.refs, &refs);\n \tstats_count_objects(&stats.objects, &refs, &revs);\n \n-\tstats_table_setup(&table, &stats);\n-\tstats_table_print(&table);\n+\tswitch (format) {\n+\tcase FORMAT_TABLE:\n+\t\tstats_table_setup(&table, &stats);\n+\t\tstats_table_print(&table);\n+\t\tbreak;\n+\tcase FORMAT_KEYVALUE:\n+\t\tstats_keyvalue_print(&stats, '=', '\\n');\n+\t\tbreak;\n+\tcase FORMAT_NUL_TERMINATED:\n+\t\tstats_keyvalue_print(&stats, '\\n', '\\0');\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"invalid output format\");\n+\t}\n \n \tstats_table_clear(&table);\n \trelease_revisions(&revs);\ndiff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\nindex 315b9e1767..2409edae4f 100755\n--- a/t/t1901-repo-stats.sh\n+++ b/t/t1901-repo-stats.sh\n@@ -73,4 +73,37 @@ test_expect_success 'repository with references and objects' '\n \t)\n '\n \n+test_expect_success 'keyvalue and nul format' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit_bulk 42 &&\n+\t\tgit tag -a foo -m bar &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\t\treferences.branches.count=1\n+\t\treferences.tags.count=1\n+\t\treferences.remotes.count=0\n+\t\treferences.others.count=0\n+\t\tobjects.commits.count=42\n+\t\tobjects.trees.count=42\n+\t\tobjects.blobs.count=42\n+\t\tobjects.tags.count=1\n+\t\tEOF\n+\n+\t\tgit repo stats --format=keyvalue >out 2>err &&\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err &&\n+\n+\t\t# Replace key and value delimiters for nul format.\n+\t\ttr \"\\n=\" \"\\0\\n\" <expect >expect_nul &&\n+\t\tgit repo stats --format=nul >out 2>err &&\n+\n+\t\ttest_cmp expect_nul out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527486","messageId":"20250927145049.723341-6-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250927145049.723341-1-jltobler@gmail.com","subject":"[PATCH v4 5/7] builtin/repo: add object counts in stats output","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-27T14:50:47Z","receivedAt":"2025-09-27T14:51:01Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The amount of objects in a repository can provide insight regarding its\nshape. To surface this information, use the path-walk API to count the\nnumber of reachable objects in the repository by object type. All\nregular references are used to determine the reachable set of objects.\nThe object counts are appended to the same table containing the\nreference information.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc |  1 +\n builtin/repo.c              | 96 +++++++++++++++++++++++++++++++++++--\n t/t1901-repo-stats.sh       | 51 +++++++++++++-------\n 3 files changed, 126 insertions(+), 22 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex a009bf8cf1..0b8d74ed3e 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -49,6 +49,7 @@ supported:\n \tof information are reported:\n +\n * Reference counts categorized by type\n+* Reachable object counts categorized by type\n \n +\n The table output format may change and is not intended for machine parsing.\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 889e344f15..3eefbeddba 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -3,9 +3,11 @@\n #include \"builtin.h\"\n #include \"environment.h\"\n #include \"parse-options.h\"\n+#include \"path-walk.h\"\n #include \"quote.h\"\n #include \"ref-filter.h\"\n #include \"refs.h\"\n+#include \"revision.h\"\n #include \"strbuf.h\"\n #include \"string-list.h\"\n #include \"shallow.h\"\n@@ -167,6 +169,18 @@ struct ref_stats {\n \tsize_t others;\n };\n \n+struct object_stats {\n+\tsize_t tags;\n+\tsize_t commits;\n+\tsize_t trees;\n+\tsize_t blobs;\n+};\n+\n+struct repo_stats {\n+\tstruct ref_stats refs;\n+\tstruct object_stats objects;\n+};\n+\n struct stats_table {\n \tstruct string_list rows;\n \n@@ -229,8 +243,16 @@ static void stats_table_count_addf(struct stats_table *table, size_t value,\n \tva_end(ap);\n }\n \n-static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n+static inline size_t get_total_object_count(struct object_stats *stats)\n {\n+\treturn stats->tags + stats->commits + stats->trees + stats->blobs;\n+}\n+\n+static void stats_table_setup(struct stats_table *table, struct repo_stats *stats)\n+{\n+\tstruct object_stats *objects = &stats->objects;\n+\tstruct ref_stats *refs = &stats->refs;\n+\tsize_t object_total;\n \tsize_t ref_total;\n \n \tref_total = refs->branches + refs->remotes + refs->tags + refs->others;\n@@ -240,6 +262,15 @@ static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n \tstats_table_count_addf(table, refs->tags, \"    * %s\", _(\"Tags\"));\n \tstats_table_count_addf(table, refs->remotes, \"    * %s\", _(\"Remotes\"));\n \tstats_table_count_addf(table, refs->others, \"    * %s\", _(\"Others\"));\n+\n+\tobject_total = get_total_object_count(objects);\n+\tstats_table_addf(table, \"\");\n+\tstats_table_addf(table, \"* %s\", _(\"Reachable objects\"));\n+\tstats_table_count_addf(table, object_total, \"  * %s\", _(\"Count\"));\n+\tstats_table_count_addf(table, objects->commits, \"    * %s\", _(\"Commits\"));\n+\tstats_table_count_addf(table, objects->trees, \"    * %s\", _(\"Trees\"));\n+\tstats_table_count_addf(table, objects->blobs, \"    * %s\", _(\"Blobs\"));\n+\tstats_table_count_addf(table, objects->tags, \"    * %s\", _(\"Tags\"));\n }\n \n static inline size_t max_size_t(size_t a, size_t b)\n@@ -324,30 +355,87 @@ static void stats_count_references(struct ref_stats *stats, struct ref_array *re\n \t}\n }\n \n+static int count_objects(const char *path UNUSED, struct oid_array *oids,\n+\t\t\t enum object_type type, void *cb_data)\n+{\n+\tstruct object_stats *stats = cb_data;\n+\n+\tswitch (type) {\n+\tcase OBJ_TAG:\n+\t\tstats->tags += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_COMMIT:\n+\t\tstats->commits += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_TREE:\n+\t\tstats->trees += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_BLOB:\n+\t\tstats->blobs += oids->nr;\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"invalid object type\");\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void stats_count_objects(struct object_stats *stats,\n+\t\t\t\tstruct ref_array *refs, struct rev_info *revs)\n+{\n+\tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n+\n+\tinfo.revs = revs;\n+\tinfo.path_fn = count_objects;\n+\tinfo.path_fn_data = stats;\n+\n+\tfor (int i = 0; i < refs->nr; i++) {\n+\t\tstruct ref_array_item *ref = refs->items[i];\n+\n+\t\tswitch (ref->kind) {\n+\t\tcase FILTER_REFS_BRANCHES:\n+\t\tcase FILTER_REFS_TAGS:\n+\t\tcase FILTER_REFS_REMOTES:\n+\t\tcase FILTER_REFS_OTHERS:\n+\t\t\tadd_pending_oid(revs, NULL, &ref->objectname, 0);\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tBUG(\"unexpected reference type\");\n+\t\t}\n+\t}\n+\n+\twalk_objects_by_path(&info);\n+\tpath_walk_info_clear(&info);\n+}\n+\n static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n-\t\t\t  struct repository *repo UNUSED)\n+\t\t\t  struct repository *repo)\n {\n \tstruct ref_filter filter = REF_FILTER_INIT;\n \tstruct stats_table table = {\n \t\t.rows = STRING_LIST_INIT_DUP,\n \t};\n-\tstruct ref_stats stats = { 0 };\n+\tstruct repo_stats stats = { 0 };\n \tstruct ref_array refs = { 0 };\n+\tstruct rev_info revs;\n \tstruct option options[] = { 0 };\n \n \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n \tif (argc)\n \t\tusage(_(\"too many arguments\"));\n \n+\trepo_init_revisions(repo, &revs, prefix);\n \tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n \t\tdie(_(\"unable to filter refs\"));\n \n-\tstats_count_references(&stats, &refs);\n+\tstats_count_references(&stats.refs, &refs);\n+\tstats_count_objects(&stats.objects, &refs, &revs);\n \n \tstats_table_setup(&table, &stats);\n \tstats_table_print(&table);\n \n \tstats_table_clear(&table);\n+\trelease_revisions(&revs);\n \tref_array_clear(&refs);\n \n \treturn 0;\ndiff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\nindex 535ac511dd..315b9e1767 100755\n--- a/t/t1901-repo-stats.sh\n+++ b/t/t1901-repo-stats.sh\n@@ -10,14 +10,21 @@ test_expect_success 'empty repository' '\n \t(\n \t\tcd repo &&\n \t\tcat >expect <<-\\EOF &&\n-\t\t| Repository stats | Value |\n-\t\t| ---------------- | ----- |\n-\t\t| * References     |       |\n-\t\t|   * Count        |     0 |\n-\t\t|     * Branches   |     0 |\n-\t\t|     * Tags       |     0 |\n-\t\t|     * Remotes    |     0 |\n-\t\t|     * Others     |     0 |\n+\t\t| Repository stats    | Value |\n+\t\t| ------------------- | ----- |\n+\t\t| * References        |       |\n+\t\t|   * Count           |     0 |\n+\t\t|     * Branches      |     0 |\n+\t\t|     * Tags          |     0 |\n+\t\t|     * Remotes       |     0 |\n+\t\t|     * Others        |     0 |\n+\t\t|                     |       |\n+\t\t| * Reachable objects |       |\n+\t\t|   * Count           |     0 |\n+\t\t|     * Commits       |     0 |\n+\t\t|     * Trees         |     0 |\n+\t\t|     * Blobs         |     0 |\n+\t\t|     * Tags          |     0 |\n \t\tEOF\n \n \t\tgit repo stats >out 2>err &&\n@@ -27,28 +34,36 @@ test_expect_success 'empty repository' '\n \t)\n '\n \n-test_expect_success 'repository with references' '\n+test_expect_success 'repository with references and objects' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n \t(\n \t\tcd repo &&\n-\t\tgit commit --allow-empty -m init &&\n+\t\ttest_commit_bulk 42 &&\n \t\tgit tag -a foo -m bar &&\n \n \t\toid=\"$(git rev-parse HEAD)\" &&\n \t\tgit update-ref refs/remotes/origin/foo \"$oid\" &&\n \n+\t\t# Also creates a commit, tree, and blob.\n \t\tgit notes add -m foo &&\n \n \t\tcat >expect <<-\\EOF &&\n-\t\t| Repository stats | Value |\n-\t\t| ---------------- | ----- |\n-\t\t| * References     |       |\n-\t\t|   * Count        |     4 |\n-\t\t|     * Branches   |     1 |\n-\t\t|     * Tags       |     1 |\n-\t\t|     * Remotes    |     1 |\n-\t\t|     * Others     |     1 |\n+\t\t| Repository stats    | Value |\n+\t\t| ------------------- | ----- |\n+\t\t| * References        |       |\n+\t\t|   * Count           |     4 |\n+\t\t|     * Branches      |     1 |\n+\t\t|     * Tags          |     1 |\n+\t\t|     * Remotes       |     1 |\n+\t\t|     * Others        |     1 |\n+\t\t|                     |       |\n+\t\t| * Reachable objects |       |\n+\t\t|   * Count           |   130 |\n+\t\t|     * Commits       |    43 |\n+\t\t|     * Trees         |    43 |\n+\t\t|     * Blobs         |    43 |\n+\t\t|     * Tags          |     1 |\n \t\tEOF\n \n \t\tgit repo stats >out 2>err &&\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527487","messageId":"20250927145049.723341-8-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250927145049.723341-1-jltobler@gmail.com","subject":"[PATCH v4 7/7] builtin/repo: add progress meter for stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-27T14:50:49Z","receivedAt":"2025-09-27T14:51:03Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"When using the stats subcommand for git-repo(1), evaluating a repository\nmay take some time depending on its shape. Add a progress meter to\nprovide feedback to the user about what is happening. The progress meter\nis enabled by default when the command is executed from a tty. It can\nalso be explicitly enabled/disabled via the --[no-]progress option.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/repo.c        | 47 +++++++++++++++++++++++++++++++++++++------\n t/t1901-repo-stats.sh | 20 ++++++++++++++++++\n 2 files changed, 61 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 6f41c9ada2..c5fe9901ec 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -4,6 +4,7 @@\n #include \"environment.h\"\n #include \"parse-options.h\"\n #include \"path-walk.h\"\n+#include \"progress.h\"\n #include \"quote.h\"\n #include \"ref-filter.h\"\n #include \"refs.h\"\n@@ -360,8 +361,15 @@ static void stats_keyvalue_print(struct repo_stats *stats, char key_delim,\n \tfflush(stdout);\n }\n \n-static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n+static void stats_count_references(struct ref_stats *stats, struct ref_array *refs,\n+\t\t\t\t   struct repository *repo, int show_progress)\n {\n+\tstruct progress *progress = NULL;\n+\n+\tif (show_progress)\n+\t\tprogress = start_delayed_progress(repo, _(\"Counting references\"),\n+\t\t\t\t\t\t  refs->nr);\n+\n \tfor (int i = 0; i < refs->nr; i++) {\n \t\tstruct ref_array_item *ref = refs->items[i];\n \n@@ -381,13 +389,24 @@ static void stats_count_references(struct ref_stats *stats, struct ref_array *re\n \t\tdefault:\n \t\t\tBUG(\"unexpected reference type\");\n \t\t}\n+\n+\t\tdisplay_progress(progress, i + 1);\n \t}\n+\n+\tstop_progress(&progress);\n }\n \n+struct count_objects_data {\n+\tstruct object_stats *stats;\n+\tstruct progress *progress;\n+};\n+\n static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\t enum object_type type, void *cb_data)\n {\n-\tstruct object_stats *stats = cb_data;\n+\tstruct count_objects_data *data = cb_data;\n+\tstruct object_stats *stats = data->stats;\n+\tsize_t object_count;\n \n \tswitch (type) {\n \tcase OBJ_TAG:\n@@ -406,17 +425,24 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\tBUG(\"invalid object type\");\n \t}\n \n+\tobject_count = get_total_object_count(stats);\n+\tdisplay_progress(data->progress, object_count);\n+\n \treturn 0;\n }\n \n static void stats_count_objects(struct object_stats *stats,\n-\t\t\t\tstruct ref_array *refs, struct rev_info *revs)\n+\t\t\t\tstruct ref_array *refs, struct rev_info *revs,\n+\t\t\t\tstruct repository *repo, int show_progress)\n {\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n+\tstruct count_objects_data data = {\n+\t\t.stats = stats,\n+\t};\n \n \tinfo.revs = revs;\n \tinfo.path_fn = count_objects;\n-\tinfo.path_fn_data = stats;\n+\tinfo.path_fn_data = &data;\n \n \tfor (int i = 0; i < refs->nr; i++) {\n \t\tstruct ref_array_item *ref = refs->items[i];\n@@ -433,8 +459,12 @@ static void stats_count_objects(struct object_stats *stats,\n \t\t}\n \t}\n \n+\tif (show_progress)\n+\t\tdata.progress = start_delayed_progress(repo, _(\"Counting objects\"), 0);\n+\n \twalk_objects_by_path(&info);\n \tpath_walk_info_clear(&info);\n+\tstop_progress(&data.progress);\n }\n \n static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n@@ -448,10 +478,12 @@ static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n \tstruct repo_stats stats = { 0 };\n \tstruct ref_array refs = { 0 };\n \tstruct rev_info revs;\n+\tint show_progress = -1;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n \t\t\t       N_(\"output format\"),\n \t\t\t       PARSE_OPT_NONEG, parse_format_cb),\n+\t\tOPT_BOOL(0, \"progress\", &show_progress, N_(\"show progress\")),\n \t\tOPT_END()\n \t};\n \n@@ -463,8 +495,11 @@ static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n \tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n \t\tdie(_(\"unable to filter refs\"));\n \n-\tstats_count_references(&stats.refs, &refs);\n-\tstats_count_objects(&stats.objects, &refs, &revs);\n+\tif (show_progress < 0)\n+\t\tshow_progress = isatty(2);\n+\n+\tstats_count_references(&stats.refs, &refs, repo, show_progress);\n+\tstats_count_objects(&stats.objects, &refs, &revs, repo, show_progress);\n \n \tswitch (format) {\n \tcase FORMAT_TABLE:\ndiff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh\nindex 2409edae4f..03d6db479f 100755\n--- a/t/t1901-repo-stats.sh\n+++ b/t/t1901-repo-stats.sh\n@@ -106,4 +106,24 @@ test_expect_success 'keyvalue and nul format' '\n \t)\n '\n \n+test_expect_success 'progress meter option' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit foo &&\n+\n+\t\tGIT_PROGRESS_DELAY=0 git repo stats --progress >out 2>err &&\n+\n+\t\ttest_file_not_empty out &&\n+\t\ttest_grep \"Counting references: 100% (2/2), done.\" err &&\n+\t\ttest_grep \"Counting objects: 3, done.\" err &&\n+\n+\t\tGIT_PROGRESS_DELAY=0 git repo stats --no-progress >out 2>err &&\n+\n+\t\ttest_file_not_empty out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"527488","messageId":"xmqqv7l39a5c.fsf@gitster.g","threadId":"64187","inReplyTo":"20250927145049.723341-4-jltobler@gmail.com","subject":"Re: [PATCH v4 3/7] clang-format: exclude control macros from SpaceBeforeParens","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-09-27T15:40:15Z","receivedAt":"2025-09-27T15:40:21Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> The formatter currently suggests adding a space between a control macro\n> and parentheses. In the Git project, this is not typically expected. Set\n> `SpaceBeforeParens` to `ControlStatementsExceptControlMacros`\n> accordingly.\n>\n> Helped-by: Karthik Nayak <karthik.188@gmail.com>\n> Signed-off-by: Justin Tobler <jltobler@gmail.com>\n> ---\n>  .clang-format | 2 +-\n>  1 file changed, 1 insertion(+), 1 deletion(-)\n\nWhile this may be a welcome addition, I somehow do not think it\nbelongs as [3/7] to this series, whose theme is about \"git repo\nstats\".\n\nPerhaps make it a separate topic and have it graduate sooner?\n\n> diff --git a/.clang-format b/.clang-format\n> index dcfd0aad60..86b4fe33e5 100644\n> --- a/.clang-format\n> +++ b/.clang-format\n> @@ -149,7 +149,7 @@ SpaceBeforeCaseColon: false\n>  #     f();\n>  #   }\n>  # }\n> -SpaceBeforeParens: ControlStatements\n> +SpaceBeforeParens: ControlStatementsExceptControlMacros\n>  \n>  # Don't insert spaces inside empty '()'\n>  SpaceInEmptyParentheses: false\n"},{"id":"527489","messageId":"5g4ysjfqrjltnqr35xh4efhzmqmzpmut44pherfsjzqinfbpmo@6ax5lzob7sox","threadId":"64187","inReplyTo":"xmqqv7l39a5c.fsf@gitster.g","subject":"Re: [PATCH v4 3/7] clang-format: exclude control macros from SpaceBeforeParens","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-09-27T15:51:57Z","receivedAt":"2025-09-27T15:52:00Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/09/27 08:40AM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > The formatter currently suggests adding a space between a control macro\n> > and parentheses. In the Git project, this is not typically expected. Set\n> > `SpaceBeforeParens` to `ControlStatementsExceptControlMacros`\n> > accordingly.\n> >\n> > Helped-by: Karthik Nayak <karthik.188@gmail.com>\n> > Signed-off-by: Justin Tobler <jltobler@gmail.com>\n> > ---\n> >  .clang-format | 2 +-\n> >  1 file changed, 1 insertion(+), 1 deletion(-)\n> \n> While this may be a welcome addition, I somehow do not think it\n> belongs as [3/7] to this series, whose theme is about \"git repo\n> stats\".\n\nThat's completely fair. I noticed the formatter flagged this issue while\nworking on this series, but I was also on the fence as to whether it\nshould be submitted separately.\n\n> Perhaps make it a separate topic and have it graduate sooner?\n\nI'll send this patch as a separate topic and can send another version of\nthis series without this patch.\n\nThanks,\n-Justin\n"},{"id":"527490","messageId":"xmqqfrc797pk.fsf@gitster.g","threadId":"64187","inReplyTo":"20250927145049.723341-5-jltobler@gmail.com","subject":"Re: [PATCH v4 4/7] builtin/repo: introduce stats subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-09-27T16:32:55Z","receivedAt":"2025-09-27T16:32:58Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> The shape of a repository's history can have huge impacts on the\n> performance and health of the repository itself. Currently, Git lacks a\n> means to surface key stats/information regarding the shape of a\n> repository via a single command.\n\nTalking about \"shape of a repository's *history*\" may negatively\naffect your goal here.  If a project is overly mergy with many\noctopus merges, it would have huge impacts on the performance to run\n\"git bisect\" over its history, so it may be interesting to know the\nratio of the merge commits in the total commits, and also the\naverage number of merge parents.  But after you obtain such numbers,\nyou cannot do anything about it, as you cannot afford to rewrite its\nhistory only to improve the \"performance and health\".\n\nAnd that is what makes \"key stats\" relative to your goal.  If your\ngoal is to give stats on the things you can control (e.g., how long\na typical delta chain is, how many loose objects there are that can\nbe moved to a packfile, how small would your object database would\nbecome if you prune all the unreachable objects), that would cut off\nsome stats that may still be interesting but may not contribute to\naddress \"huge impacts on the performance and health\".\n\nWith Devil's advocate hat on, a single command that gives a set of\nstats that are \"key\" to a goal of a single use case may not be as\nuseful as a collection of commands, each of which gives stats on one\naspect of the repository, that can be combined to help you address\nvarious different goals.\n\n> To allow users to more readily identify potential issues for a\n> repository, introduce the \"stats\" subcommand in git-repo(1) to output\n> stats for the repository that may be of interest to users. The goal of\n> this subcommand is to eventually provide similar functionality to\n> git-sizer(1), but natively in Git.\n\nSo, it is needless to say that the kind of \"stats\" obtained by such\na single tool needs to be chosen carefully, but more importantly,\nits output should give users actionable output, as whoever designed\nsuch a tool and chose what \"key stats\" are has a clear idea on\nvarious aspects of repository.  \"stats\" measure the health of the\nrepository against certain yardstick, but it should come with a\nclear instruction to make use of that measurement.  The tool may say\n\"the stats indicate that you have commits that touch too many paths\nat the same time\".  The users need to be know what consequence of\nthat finding is, and what they can do about it.\n\nFor example, what would the user do with the new knowledge that the\nrepository has 100x as many local branches as there are\nremote-tracking branches?  Without breaking down these numerous\nlocal branches into those that are still used in active development\n(hint: peek into their reflog), kept as historical landmarks, past\ndevelopment that has already been merged (hint: \"git branch --list\n--merged origin/master\"), or abandoned cruft that hasn't been\ntouched with some changes that are not merged anywhere, the users\nwould not know what to do.\n\n> +`stats`::\n> +\tRetrieve statistics about the current repository. The following kinds\n> +\tof information are reported:\n> ++\n> +* Reference counts categorized by type\n> +\n> ++\n> +The table output format may change and is not intended for machine parsing.\n\nDo we eventually want to give another format that is intended for\nmachine parsing?\n\nIn a format meant for human consumption, is it still sensible to\ntarget fixed-column terminals these days?  Rather, would they want\nprettier-formatted html, or csv that they can easily import to\nspreadsheet?  (these are not objections but genuine questions).\n\n> +static void stats_table_vaddf(struct stats_table *table,\n> +\t\t\t      struct stats_table_entry *entry,\n> +\t\t\t      const char *format, va_list ap)\n> +{\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\tstruct string_list_item *item;\n> +\tchar *formatted_name;\n> +\tsize_t name_width;\n> +\n> +\tstrbuf_vaddf(&buf, format, ap);\n> +\tformatted_name = strbuf_detach(&buf, NULL);\n> +\tname_width = utf8_strwidth(formatted_name);\n> +\n> +\titem = string_list_append_nodup(&table->rows, formatted_name);\n> +\titem->util = entry;\n> +\n> +\tif (name_width > table->name_col_width)\n> +\t\ttable->name_col_width = name_width;\n> +\tif (entry) {\n> +\t\tsize_t value_width = utf8_strwidth(entry->value);\n> +\t\tif (value_width > table->value_col_width)\n> +\t\t\ttable->value_col_width = value_width;\n> +\t}\n> +}\n\nOK, accumulate while measuring, so that you can compute the max\nwidth of these things before writing them out, which is quite\nbog-standard way to collect data.\n\n> +static inline size_t max_size_t(size_t a, size_t b)\n> +{\n> +\treturn (a > b) ? a : b;\n> +}\n\nHeh.\n\n> +static void stats_table_print(const struct stats_table *table)\n> +{\n> +\tconst char *name_col_title = _(\"Repository stats\");\n> +\tconst char *value_col_title = _(\"Value\");\n> +\tsize_t name_title_len = utf8_strwidth(name_col_title);\n> +\tsize_t value_title_len = utf8_strwidth(value_col_title);\n> +\tstruct string_list_item *item;\n> +\tint name_col_width;\n> +\tint value_col_width;\n> +\n> +\tname_col_width = cast_size_t_to_int(\n> +\t\tmax_size_t(table->name_col_width, name_title_len));\n> +\tvalue_col_width = cast_size_t_to_int(\n> +\t\tmax_size_t(table->value_col_width, value_title_len));\n\nIf table->name_col_width and table->value_col_width were int to\nbegin with, none of these casts would have been necessary.  Aren't\nwe overusing size_t to count things that are not memory allocations?\n\n> +\tprintf(\"| %-*s | %-*s |\\n\", name_col_width, name_col_title,\n> +\t       value_col_width, value_col_title);\n> +\tprintf(\"| \");\n> +\tfor (int i = 0; i < name_col_width; i++)\n> +\t\tputchar('-');\n> +\tprintf(\" | \");\n> +\tfor (int i = 0; i < value_col_width; i++)\n> +\t\tputchar('-');\n> +\tprintf(\" |\\n\");\n\nI wonder if people want to use unicode \"Box Drawing\" block and other\nfancier things, as we assume utf8 for names and values, in which\ncase these printf would need to be \"translatable\", but locale\nadministrators should not have more say than others what kind of\nline drawing elements are to be used, so perhaps the above is good\nenough at least for now.\n"},{"id":"527491","messageId":"xmqqa52f97p1.fsf@gitster.g","threadId":"64187","inReplyTo":"20250927145049.723341-1-jltobler@gmail.com","subject":"Re: [PATCH v4 0/7] builtin/repo: introduce stats subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-09-27T16:33:14Z","receivedAt":"2025-09-27T16:33:16Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> Changes since V3:\n>\n> - Changed from using strlen() to utf8_strlen() to take into\n>   consideration that translatable strings may have characters that are\n>   more than one byte.\n\nIf you are truncating a string, chopping the tail end of it, you do\nhave to take into account the fact that a single character can be\nmore than one byte long and avoid chopping in the middle of one\ncharacter.\n\nBut your use of <utf8.h> to measure the display columns for a\nformatted name in stats table needs a different care.  You are using\nutf8_strwidth() to account for the fact that the number of display\ncolumns consumed to show a non-ASCII string may be different from\nthe number of bytes in the string.\n\nNo need to resend, for the obvious reason that the cover letter\nmaterial would not make into the etched-in-stone \"git log\" history.\nI just thought I should mention the above to unconfuse potential\nreaders.\n"},{"id":"527499","messageId":"xmqqzfaf78xp.fsf@gitster.g","threadId":"64187","inReplyTo":"5g4ysjfqrjltnqr35xh4efhzmqmzpmut44pherfsjzqinfbpmo@6ax5lzob7sox","subject":"Re: [PATCH v4 3/7] clang-format: exclude control macros from SpaceBeforeParens","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-09-27T23:49:22Z","receivedAt":"2025-09-27T23:49:25Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n>> Perhaps make it a separate topic and have it graduate sooner?\n>\n> I'll send this patch as a separate topic and can send another version of\n> this series without this patch.\n\nNah, let's save one iteration by having me queue them on two\nseparate topics.  If you need update to the \"repo\" topic, you can\njust send it as a series without this step.\n\nThanks.\n"},{"id":"528425","messageId":"qdot36lscj2jzuej2zixdlgocubiimh54dkxzl2xgcy3g2n42m@gpdi7jwc2oyd","threadId":"64187","inReplyTo":"xmqqfrc797pk.fsf@gitster.g","subject":"Re: [PATCH v4 4/7] builtin/repo: introduce stats subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-09T22:09:45Z","receivedAt":"2025-10-09T22:09:47Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/09/27 09:32AM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > The shape of a repository's history can have huge impacts on the\n> > performance and health of the repository itself. Currently, Git lacks a\n> > means to surface key stats/information regarding the shape of a\n> > repository via a single command.\n> \n> Talking about \"shape of a repository's *history*\" may negatively\n> affect your goal here.  If a project is overly mergy with many\n> octopus merges, it would have huge impacts on the performance to run\n> \"git bisect\" over its history, so it may be interesting to know the\n> ratio of the merge commits in the total commits, and also the\n> average number of merge parents.  But after you obtain such numbers,\n> you cannot do anything about it, as you cannot afford to rewrite its\n> history only to improve the \"performance and health\".\n\nThe above example is actually something I would like to add in a future\nseries. From my perspective, if a certain repository operation is\nperforming poorly, it is still valuable to have insight into the reason\nwhy, regardless of whether it is realistically actionable or not for the\nuser.\n\nI will try to better clarify the command's intent in the log message.\n\n> And that is what makes \"key stats\" relative to your goal.  If your\n> goal is to give stats on the things you can control (e.g., how long\n> a typical delta chain is, how many loose objects there are that can\n> be moved to a packfile, how small would your object database would\n> become if you prune all the unreachable objects), that would cut off\n> some stats that may still be interesting but may not contribute to\n> address \"huge impacts on the performance and health\".\n\nI would say the main goal of this command is to surface interesting\ninformation about the repository and its object graph structure.\nSomething that may make a stat \"interesting\" is if its value could be a\npotential indicator of poor repository performance. Like the max number of\nparents a commit has, or the max number of entries a tree has. I don't\nthink the actual stat value needs to be concerning itself for it to be\ndisplayed though.\n\nIf there is actionable recourse a user can take to remediate a\nconcerning stat that would be ideal, but I see the primarly goal being\nto just surface the information regardless.\n\n> With Devil's advocate hat on, a single command that gives a set of\n> stats that are \"key\" to a goal of a single use case may not be as\n> useful as a collection of commands, each of which gives stats on one\n> aspect of the repository, that can be combined to help you address\n> various different goals.\n\nGood points. From my perspective, the benefit of having a single command\nhere is to provide a simple means to generate a report of the general\nrepository shape. In this context, \"key stats\" reflect certain\ncharactistics about the repository that may be concerning performance\nwise for typical repository operations or just of interest in general.\n\nOne of the motivations here is to enable a user to easily generate such\na report and be able to share it with others that may not have access to\nthe underlying repository.\n\nI think this still could leave room for more fine-grained commands that\ncan surface more targeted information about a repository with other\ngoals in mind in the future though.\n\n> > To allow users to more readily identify potential issues for a\n> > repository, introduce the \"stats\" subcommand in git-repo(1) to output\n> > stats for the repository that may be of interest to users. The goal of\n> > this subcommand is to eventually provide similar functionality to\n> > git-sizer(1), but natively in Git.\n> \n> So, it is needless to say that the kind of \"stats\" obtained by such\n> a single tool needs to be chosen carefully, but more importantly,\n> its output should give users actionable output, as whoever designed\n> such a tool and chose what \"key stats\" are has a clear idea on\n> various aspects of repository.  \"stats\" measure the health of the\n> repository against certain yardstick, but it should come with a\n> clear instruction to make use of that measurement.  The tool may say\n> \"the stats indicate that you have commits that touch too many paths\n> at the same time\".  The users need to be know what consequence of\n> that finding is, and what they can do about it.\n\nAs mentioned above, from my perspective, the git-repo-stats command\nitself is not about specifically targeting and diagnoising actionable\nissues that a repository has. It's primary focus should be to provide\ninsight about the repository structure that may be helpful when trying\nto understand certain repository performance characteristics in general.\n\nIn the next version I'll rework this log message to better clarify the\nintent of this command.\n\n> For example, what would the user do with the new knowledge that the\n> repository has 100x as many local branches as there are\n> remote-tracking branches?  Without breaking down these numerous\n> local branches into those that are still used in active development\n> (hint: peek into their reflog), kept as historical landmarks, past\n> development that has already been merged (hint: \"git branch --list\n> --merged origin/master\"), or abandoned cruft that hasn't been\n> touched with some changes that are not merged anywhere, the users\n> would not know what to do.\n\nIn a future series, I would like to introduce a \"level of concern\" meter\nfor the outputted stats. As you mentioned earlier, this could provide a\nmeasure of health for a repository stat against a certain yardstick. At\nthat point in time, I think it would also make sense to provide\ndocumentation on actions that a user could potentially take to address\nspecific stats that are marked with a higher level of concern. Certain\nstats that get identified as concerning may not be realistically\nactionable though.\n\nFor now, I think it's fine to omit this though because the outputted\nstats are presented agnostically without any concern level.\n\n> > +`stats`::\n> > +\tRetrieve statistics about the current repository. The following kinds\n> > +\tof information are reported:\n> > ++\n> > +* Reference counts categorized by type\n> > +\n> > ++\n> > +The table output format may change and is not intended for machine parsing.\n> \n> Do we eventually want to give another format that is intended for\n> machine parsing?\n\nYes, and we introduce a key-value and NUL format later in this series. I\nwill mention this in the log message.\n\n> In a format meant for human consumption, is it still sensible to\n> target fixed-column terminals these days?  Rather, would they want\n> prettier-formatted html, or csv that they can easily import to\n> spreadsheet?  (these are not objections but genuine questions).\n\nFrom my perspective, having an output format that can be immediately\nviewed in the same place the command is run is still quite valuable and\nis still common in similar tooling.\n\nI think there may also be value in additional formats, such as the ones\nmentioned above, but I think those should be implemented as a separate\nseries if demand presents itself.\n\n> > +static void stats_table_print(const struct stats_table *table)\n> > +{\n> > +\tconst char *name_col_title = _(\"Repository stats\");\n> > +\tconst char *value_col_title = _(\"Value\");\n> > +\tsize_t name_title_len = utf8_strwidth(name_col_title);\n> > +\tsize_t value_title_len = utf8_strwidth(value_col_title);\n> > +\tstruct string_list_item *item;\n> > +\tint name_col_width;\n> > +\tint value_col_width;\n> > +\n> > +\tname_col_width = cast_size_t_to_int(\n> > +\t\tmax_size_t(table->name_col_width, name_title_len));\n> > +\tvalue_col_width = cast_size_t_to_int(\n> > +\t\tmax_size_t(table->value_col_width, value_title_len));\n> \n> If table->name_col_width and table->value_col_width were int to\n> begin with, none of these casts would have been necessary.  Aren't\n> we overusing size_t to count things that are not memory allocations?\n\nYes. Storing the column widths as size_t in this scenario doesn't make\nmuch sense because they need to be an int for the format string anyways.\nFurthermore, the number of columns will always be a relatively small\nnumber.\n\nI'll fix this in the next version. :)\n\n> > +\tprintf(\"| %-*s | %-*s |\\n\", name_col_width, name_col_title,\n> > +\t       value_col_width, value_col_title);\n> > +\tprintf(\"| \");\n> > +\tfor (int i = 0; i < name_col_width; i++)\n> > +\t\tputchar('-');\n> > +\tprintf(\" | \");\n> > +\tfor (int i = 0; i < value_col_width; i++)\n> > +\t\tputchar('-');\n> > +\tprintf(\" |\\n\");\n> \n> I wonder if people want to use unicode \"Box Drawing\" block and other\n> fancier things, as we assume utf8 for names and values, in which\n> case these printf would need to be \"translatable\", but locale\n> administrators should not have more say than others what kind of\n> line drawing elements are to be used, so perhaps the above is good\n> enough at least for now.\n\nI think the current table is probably sufficient for now. I do forsee\niteration on this format in future series though.\n\n-Justin\n"},{"id":"528440","messageId":"gzjaa3nmv6s44dszregri7kcnfxg6optkycg2wggtib6bxvdgq@elearr6dishf","threadId":"64187","inReplyTo":"qdot36lscj2jzuej2zixdlgocubiimh54dkxzl2xgcy3g2n42m@gpdi7jwc2oyd","subject":"Re: [PATCH v4 4/7] builtin/repo: introduce stats subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-10T00:42:40Z","receivedAt":"2025-10-10T00:42:42Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/10/09 05:09PM, Justin Tobler wrote:\n> On 25/09/27 09:32AM, Junio C Hamano wrote:\n> > With Devil's advocate hat on, a single command that gives a set of\n> > stats that are \"key\" to a goal of a single use case may not be as\n> > useful as a collection of commands, each of which gives stats on one\n> > aspect of the repository, that can be combined to help you address\n> > various different goals.\n> \n> Good points. From my perspective, the benefit of having a single command\n> here is to provide a simple means to generate a report of the general\n> repository shape. In this context, \"key stats\" reflect certain\n> charactistics about the repository that may be concerning performance\n> wise for typical repository operations or just of interest in general.\n> \n> One of the motivations here is to enable a user to easily generate such\n> a report and be able to share it with others that may not have access to\n> the underlying repository.\n> \n> I think this still could leave room for more fine-grained commands that\n> can surface more targeted information about a repository with other\n> goals in mind in the future though.\n\nThinking about this some more, a single \"stats\" command is indeed rather\nvauge. Furthermore, as Junio mentioned, there could be other aspects of\na repository that we want to display stats for in the future.\n\nSince the goal of this command is to surface info about a repositories\nstructure, may we should instead call this command `git repo structure`?\nOr something else along those lines that in more specific and related to\ngoal of the command?\n\n-Justin\n"},{"id":"528473","messageId":"aOithKtHf_U9S_m3@pks.im","threadId":"64187","inReplyTo":"gzjaa3nmv6s44dszregri7kcnfxg6optkycg2wggtib6bxvdgq@elearr6dishf","subject":"Re: [PATCH v4 4/7] builtin/repo: introduce stats subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-10T06:53:56Z","receivedAt":"2025-10-10T06:54:03Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Oct 09, 2025 at 07:42:40PM -0500, Justin Tobler wrote:\n> On 25/10/09 05:09PM, Justin Tobler wrote:\n> > On 25/09/27 09:32AM, Junio C Hamano wrote:\n> > > With Devil's advocate hat on, a single command that gives a set of\n> > > stats that are \"key\" to a goal of a single use case may not be as\n> > > useful as a collection of commands, each of which gives stats on one\n> > > aspect of the repository, that can be combined to help you address\n> > > various different goals.\n> > \n> > Good points. From my perspective, the benefit of having a single command\n> > here is to provide a simple means to generate a report of the general\n> > repository shape. In this context, \"key stats\" reflect certain\n> > charactistics about the repository that may be concerning performance\n> > wise for typical repository operations or just of interest in general.\n> > \n> > One of the motivations here is to enable a user to easily generate such\n> > a report and be able to share it with others that may not have access to\n> > the underlying repository.\n> > \n> > I think this still could leave room for more fine-grained commands that\n> > can surface more targeted information about a repository with other\n> > goals in mind in the future though.\n> \n> Thinking about this some more, a single \"stats\" command is indeed rather\n> vauge. Furthermore, as Junio mentioned, there could be other aspects of\n> a repository that we want to display stats for in the future.\n> \n> Since the goal of this command is to surface info about a repositories\n> structure, may we should instead call this command `git repo structure`?\n> Or something else along those lines that in more specific and related to\n> goal of the command?\n\nSome alternatives that come to my mind:\n\n  - inspect\n  - analyze\n  - scan\n  - survey\n  - measure\n\nI don't have any specific preference. What I like though is that those\nare verbs, which makes it a bit more natural to use them.\n\nFeel free to take any or ignore all of these.\n\nPatrick\n"},{"id":"528510","messageId":"lnk4zvu5zgokd3p5lxnl2nwbqu3fi3yer753tr2f2jaknrtb3h@fcbjvvu6irtc","threadId":"64187","inReplyTo":"aOithKtHf_U9S_m3@pks.im","subject":"Re: [PATCH v4 4/7] builtin/repo: introduce stats subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-10T14:34:54Z","receivedAt":"2025-10-10T14:34:56Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/10/10 08:53AM, Patrick Steinhardt wrote:\n> On Thu, Oct 09, 2025 at 07:42:40PM -0500, Justin Tobler wrote:\n> > Thinking about this some more, a single \"stats\" command is indeed rather\n> > vauge. Furthermore, as Junio mentioned, there could be other aspects of\n> > a repository that we want to display stats for in the future.\n> > \n> > Since the goal of this command is to surface info about a repositories\n> > structure, may we should instead call this command `git repo structure`?\n> > Or something else along those lines that in more specific and related to\n> > goal of the command?\n> \n> Some alternatives that come to my mind:\n> \n>   - inspect\n>   - analyze\n>   - scan\n>   - survey\n>   - measure\n> \n> I don't have any specific preference. What I like though is that those\n> are verbs, which makes it a bit more natural to use them.\n\nIf we want the theme of this command to be the repository's\nstructure/shape and have a name that matches this scope, I'm not sure\nany of the above examples would move us closer to that. Aligning the\ncommand name to its scope is beneficial if we forsee the potential to\nintroduce additional subcommands for git-repo(1) that target other\naspects of a repository.\n\nFrom my perspective, the main question is: should git-repo-stats be a\ngeneric command that can eventual provides all sorts of different stats?\nOr should it stick to repository structure/shape information? I think\nI'm currently leaning towards the latter.\n\n-Justin\n"},{"id":"528591","messageId":"aOyYWQ5mU4zc1oCH@pks.im","threadId":"64187","inReplyTo":"lnk4zvu5zgokd3p5lxnl2nwbqu3fi3yer753tr2f2jaknrtb3h@fcbjvvu6irtc","subject":"Re: [PATCH v4 4/7] builtin/repo: introduce stats subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-13T06:13:03Z","receivedAt":"2025-10-13T06:13:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Oct 10, 2025 at 09:34:54AM -0500, Justin Tobler wrote:\n> On 25/10/10 08:53AM, Patrick Steinhardt wrote:\n> > On Thu, Oct 09, 2025 at 07:42:40PM -0500, Justin Tobler wrote:\n> > > Thinking about this some more, a single \"stats\" command is indeed rather\n> > > vauge. Furthermore, as Junio mentioned, there could be other aspects of\n> > > a repository that we want to display stats for in the future.\n> > > \n> > > Since the goal of this command is to surface info about a repositories\n> > > structure, may we should instead call this command `git repo structure`?\n> > > Or something else along those lines that in more specific and related to\n> > > goal of the command?\n> > \n> > Some alternatives that come to my mind:\n> > \n> >   - inspect\n> >   - analyze\n> >   - scan\n> >   - survey\n> >   - measure\n> > \n> > I don't have any specific preference. What I like though is that those\n> > are verbs, which makes it a bit more natural to use them.\n> \n> If we want the theme of this command to be the repository's\n> structure/shape and have a name that matches this scope, I'm not sure\n> any of the above examples would move us closer to that. Aligning the\n> command name to its scope is beneficial if we forsee the potential to\n> introduce additional subcommands for git-repo(1) that target other\n> aspects of a repository.\n\nFair.\n\n> From my perspective, the main question is: should git-repo-stats be a\n> generic command that can eventual provides all sorts of different stats?\n> Or should it stick to repository structure/shape information? I think\n> I'm currently leaning towards the latter.\n\nYeah, I'm leaning towards the latter, as well.\n\nPatrick\n"},{"id":"528837","messageId":"20251015211213.361797-1-jltobler@gmail.com","threadId":"64187","inReplyTo":"20250927145049.723341-1-jltobler@gmail.com","subject":"[PATCH v5 0/6] builtin/repo: introduce structure subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-15T21:12:07Z","receivedAt":"2025-10-15T21:12:20Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Greetings,\n\nThe structure of a repository's history can have huge impacts on the\nperformance and health of the repository itself. Currently, Git lacks a\nmeans to surface repository metrics regarding its structure/shape via a\nsingle command. Acquiring this information requires users to be familiar\nwith the relevant data points and the various Git commands required to\nsurface them. To fill this gap, supplemental tools such as git-sizer(1)\nhave been developed.\n\nTo allow users to more readily identify repository structure related\ninformation, introduce the \"structure\" subcommand in git-repo(1). The\ngoal of this subcommand is to eventually provide similar functionality\nto git-sizer(1), but natively in Git.\n\nIn this initial version, the \"structure\" subcommand only surfaces counts\nof the various reference and object types in a repository. In a\nfollow-up series, I would like to introduce additional data points that\nare present in git-sizer(1) such as largest objects, combined object\nsizes by type, and other general repository shape information.\n\nSome other general features that would be nice to introduce eventually:\n\n- A \"level of concern\" meter for reported stats. This could indicate to\n  users which stats may be worth looking into further.\n- Links to OIDs of interesting objects that correspond to certain stats.\n- Options to limit which references to use when evaluating the\n  repository.\n\nChanges since V4:\n- The subcommand was renamed from \"stats\" to \"structure\". This was done\n  to define a more narrow scope for the types of stats that would be\n  outtputted. This also also for other types of stat-related subcommands\n  to be implemented in the future that may cover different aspects of\n  the repository.\n- Table column widths are now stored as ints to avoid the unneeded back\n  and forth conversions.\n- Dropped the clang-format patch as it has been upstreamed separately.\n- Updated commit messages accordingly.\n\nChanges since V3:\n- Changed from using strlen() to utf8_strlen() to take into\n  consideration that translatable strings may have characters that are\n  more than one byte.\n\nChanges since V2:\n- Added clang-format patch to address false postive triggered in this\n  series.\n- Use varargs for stats_table_add() family of functions.\n- Print to stdout directly instead of using strbuf.\n- Add parse_option() earlier in the series.\n- Use start_delayed_progress() instead of start_progress().\n- Add test to validate --[no-]progress options.\n- Some other small fixes.\n\nChanges since V1:\n- Translatable terms displayed in the table have formatting separated\n  out.\n- Squashed the `keyvalue` and `nul` output format patches into one.\n- Added a progress meter to provide users with more feedback.\n- Updated docs to outline to outline reported data in a bulleted list.\n- Combined similar tests together to reduce repetitive setup.\n- Added patch to improve ref-filter interface so we don't have to create\n  a dummy patterns array.\n- Many other renames and cleanups to improve patch clarity.\n\nThanks,\n-Justin\n\nJustin Tobler (6):\n  builtin/repo: rename repo_info() to cmd_repo_info()\n  ref-filter: allow NULL filter pattern\n  builtin/repo: introduce structure subcommand\n  builtin/repo: add object counts in structure output\n  builtin/repo: add keyvalue and nul format for structure stats\n  builtin/repo: add progress meter for structure stats\n\n Documentation/git-repo.adoc |  30 +++\n builtin/repo.c              | 370 +++++++++++++++++++++++++++++++++++-\n ref-filter.c                |   4 +-\n t/meson.build               |   1 +\n t/t1901-repo-structure.sh   | 129 +++++++++++++\n 5 files changed, 529 insertions(+), 5 deletions(-)\n create mode 100755 t/t1901-repo-structure.sh\n\nRange-diff against v4:\n1:  ed04168562 = 1:  ed04168562 builtin/repo: rename repo_info() to cmd_repo_info()\n2:  6aa76d1323 = 2:  6aa76d1323 ref-filter: allow NULL filter pattern\n3:  02a3fcc5fb < -:  ---------- clang-format: exclude control macros from SpaceBeforeParens\n4:  8ec9914886 ! 3:  eda1afbe3d builtin/repo: introduce stats subcommand\n    @@ Metadata\n     Author: Justin Tobler <jltobler@gmail.com>\n     \n      ## Commit message ##\n    -    builtin/repo: introduce stats subcommand\n    +    builtin/repo: introduce structure subcommand\n     \n    -    The shape of a repository's history can have huge impacts on the\n    +    The structure of a repository's history can have huge impacts on the\n         performance and health of the repository itself. Currently, Git lacks a\n    -    means to surface key stats/information regarding the shape of a\n    -    repository via a single command. Acquiring this information requires\n    -    users to be fairly knowledgeable about the structure of a Git repository\n    -    and how to identify the relevant data points. To fill this gap,\n    -    supplemental tools such as git-sizer(1) have been developed.\n    +    means to surface repository metrics regarding its structure/shape via a\n    +    single command. Acquiring this information requires users to be familiar\n    +    with the relevant data points and the various Git commands required to\n    +    surface them. To fill this gap, supplemental tools such as git-sizer(1)\n    +    have been developed.\n     \n    -    To allow users to more readily identify potential issues for a\n    -    repository, introduce the \"stats\" subcommand in git-repo(1) to output\n    -    stats for the repository that may be of interest to users. The goal of\n    -    this subcommand is to eventually provide similar functionality to\n    -    git-sizer(1), but natively in Git.\n    +    To allow users to more readily identify repository structure related\n    +    information, introduce the \"structure\" subcommand in git-repo(1). The\n    +    goal of this subcommand is to eventually provide similar functionality\n    +    to git-sizer(1), but natively in Git.\n     \n         The initial version of this command only iterates through all references\n         in the repository and tracks the count of branches, tags, remote refs,\n    @@ Commit message\n         to satisfy the requirements of the widest row contained.\n     \n         Subsequent commits will surface additional relevant data points to\n    -    output.\n    +    output and also provide other more machine-friendly output formats.\n     \n         Based-on-patch-by: Derrick Stolee <stolee@gmail.com>\n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n    @@ Documentation/git-repo.adoc: SYNOPSIS\n      --------\n      [synopsis]\n      git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n    -+git repo stats\n    ++git repo structure\n      \n      DESCRIPTION\n      -----------\n    @@ Documentation/git-repo.adoc: supported:\n      +\n      `-z` is an alias for `--format=nul`.\n      \n    -+`stats`::\n    -+\tRetrieve statistics about the current repository. The following kinds\n    -+\tof information are reported:\n    ++`structure`::\n    ++\tRetrieve statistics about the current repository structure. The\n    ++\tfollowing kinds of information are reported:\n     ++\n     +* Reference counts categorized by type\n     +\n    @@ builtin/repo.c\n      \n      static const char *const repo_usage[] = {\n      \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n    -+\t\"git repo stats\",\n    ++\t\"git repo structure\",\n      \tNULL\n      };\n      \n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +struct stats_table {\n     +\tstruct string_list rows;\n     +\n    -+\tsize_t name_col_width;\n    -+\tsize_t value_col_width;\n    ++\tint name_col_width;\n    ++\tint value_col_width;\n     +};\n     +\n     +/*\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\tstruct strbuf buf = STRBUF_INIT;\n     +\tstruct string_list_item *item;\n     +\tchar *formatted_name;\n    -+\tsize_t name_width;\n    ++\tint name_width;\n     +\n     +\tstrbuf_vaddf(&buf, format, ap);\n     +\tformatted_name = strbuf_detach(&buf, NULL);\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\tif (name_width > table->name_col_width)\n     +\t\ttable->name_col_width = name_width;\n     +\tif (entry) {\n    -+\t\tsize_t value_width = utf8_strwidth(entry->value);\n    ++\t\tint value_width = utf8_strwidth(entry->value);\n     +\t\tif (value_width > table->value_col_width)\n     +\t\t\ttable->value_col_width = value_width;\n     +\t}\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\tva_end(ap);\n     +}\n     +\n    -+static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n    ++static void stats_table_setup_structure(struct stats_table *table,\n    ++\t\t\t\t\tstruct ref_stats *refs)\n     +{\n     +\tsize_t ref_total;\n     +\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\tstats_table_count_addf(table, refs->others, \"    * %s\", _(\"Others\"));\n     +}\n     +\n    -+static inline size_t max_size_t(size_t a, size_t b)\n    ++static void stats_table_print_structure(const struct stats_table *table)\n     +{\n    -+\treturn (a > b) ? a : b;\n    -+}\n    -+\n    -+static void stats_table_print(const struct stats_table *table)\n    -+{\n    -+\tconst char *name_col_title = _(\"Repository stats\");\n    ++\tconst char *name_col_title = _(\"Repository structure\");\n     +\tconst char *value_col_title = _(\"Value\");\n    -+\tsize_t name_title_len = utf8_strwidth(name_col_title);\n    -+\tsize_t value_title_len = utf8_strwidth(value_col_title);\n    ++\tint name_col_width = utf8_strwidth(name_col_title);\n    ++\tint value_col_width = utf8_strwidth(value_col_title);\n     +\tstruct string_list_item *item;\n    -+\tint name_col_width;\n    -+\tint value_col_width;\n     +\n    -+\tname_col_width = cast_size_t_to_int(\n    -+\t\tmax_size_t(table->name_col_width, name_title_len));\n    -+\tvalue_col_width = cast_size_t_to_int(\n    -+\t\tmax_size_t(table->value_col_width, value_title_len));\n    ++\tif (table->name_col_width > name_col_width)\n    ++\t\tname_col_width = table->name_col_width;\n    ++\tif (table->value_col_width > value_col_width)\n    ++\t\tvalue_col_width = table->value_col_width;\n     +\n     +\tprintf(\"| %-*s | %-*s |\\n\", name_col_width, name_col_title,\n     +\t       value_col_width, value_col_title);\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\tstring_list_clear(&table->rows, 1);\n     +}\n     +\n    -+static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n    ++static void structure_count_references(struct ref_stats *stats,\n    ++\t\t\t\t       struct ref_array *refs)\n     +{\n     +\tfor (int i = 0; i < refs->nr; i++) {\n     +\t\tstruct ref_array_item *ref = refs->items[i];\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\t}\n     +}\n     +\n    -+static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n    -+\t\t\t  struct repository *repo UNUSED)\n    ++static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n    ++\t\t\t      struct repository *repo UNUSED)\n     +{\n     +\tstruct ref_filter filter = REF_FILTER_INIT;\n     +\tstruct stats_table table = {\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n     +\t\tdie(_(\"unable to filter refs\"));\n     +\n    -+\tstats_count_references(&stats, &refs);\n    ++\tstructure_count_references(&stats, &refs);\n     +\n    -+\tstats_table_setup(&table, &stats);\n    -+\tstats_table_print(&table);\n    ++\tstats_table_setup_structure(&table, &stats);\n    ++\tstats_table_print_structure(&table);\n     +\n     +\tstats_table_clear(&table);\n     +\tref_array_clear(&refs);\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n      \tparse_opt_subcommand_fn *fn = NULL;\n      \tstruct option options[] = {\n      \t\tOPT_SUBCOMMAND(\"info\", &fn, cmd_repo_info),\n    -+\t\tOPT_SUBCOMMAND(\"stats\", &fn, cmd_repo_stats),\n    ++\t\tOPT_SUBCOMMAND(\"structure\", &fn, cmd_repo_structure),\n      \t\tOPT_END()\n      \t};\n      \n    @@ t/meson.build: integration_tests = [\n        't1701-racy-split-index.sh',\n        't1800-hook.sh',\n        't1900-repo.sh',\n    -+  't1901-repo-stats.sh',\n    ++  't1901-repo-structure.sh',\n        't2000-conflict-when-checking-files-out.sh',\n        't2002-checkout-cache-u.sh',\n        't2003-checkout-cache-mkdir.sh',\n     \n    - ## t/t1901-repo-stats.sh (new) ##\n    + ## t/t1901-repo-structure.sh (new) ##\n     @@\n     +#!/bin/sh\n     +\n    -+test_description='test git repo stats'\n    ++test_description='test git repo structure'\n     +\n     +. ./test-lib.sh\n     +\n    @@ t/t1901-repo-stats.sh (new)\n     +\t(\n     +\t\tcd repo &&\n     +\t\tcat >expect <<-\\EOF &&\n    -+\t\t| Repository stats | Value |\n    -+\t\t| ---------------- | ----- |\n    -+\t\t| * References     |       |\n    -+\t\t|   * Count        |     0 |\n    -+\t\t|     * Branches   |     0 |\n    -+\t\t|     * Tags       |     0 |\n    -+\t\t|     * Remotes    |     0 |\n    -+\t\t|     * Others     |     0 |\n    ++\t\t| Repository structure | Value |\n    ++\t\t| -------------------- | ----- |\n    ++\t\t| * References         |       |\n    ++\t\t|   * Count            |     0 |\n    ++\t\t|     * Branches       |     0 |\n    ++\t\t|     * Tags           |     0 |\n    ++\t\t|     * Remotes        |     0 |\n    ++\t\t|     * Others         |     0 |\n     +\t\tEOF\n     +\n    -+\t\tgit repo stats >out 2>err &&\n    ++\t\tgit repo structure >out 2>err &&\n     +\n     +\t\ttest_cmp expect out &&\n     +\t\ttest_line_count = 0 err\n    @@ t/t1901-repo-stats.sh (new)\n     +\t\tgit notes add -m foo &&\n     +\n     +\t\tcat >expect <<-\\EOF &&\n    -+\t\t| Repository stats | Value |\n    -+\t\t| ---------------- | ----- |\n    -+\t\t| * References     |       |\n    -+\t\t|   * Count        |     4 |\n    -+\t\t|     * Branches   |     1 |\n    -+\t\t|     * Tags       |     1 |\n    -+\t\t|     * Remotes    |     1 |\n    -+\t\t|     * Others     |     1 |\n    ++\t\t| Repository structure | Value |\n    ++\t\t| -------------------- | ----- |\n    ++\t\t| * References         |       |\n    ++\t\t|   * Count            |     4 |\n    ++\t\t|     * Branches       |     1 |\n    ++\t\t|     * Tags           |     1 |\n    ++\t\t|     * Remotes        |     1 |\n    ++\t\t|     * Others         |     1 |\n     +\t\tEOF\n     +\n    -+\t\tgit repo stats >out 2>err &&\n    ++\t\tgit repo structure >out 2>err &&\n     +\n     +\t\ttest_cmp expect out &&\n     +\t\ttest_line_count = 0 err\n5:  584d35f2c7 ! 4:  503af885d3 builtin/repo: add object counts in stats output\n    @@ Metadata\n     Author: Justin Tobler <jltobler@gmail.com>\n     \n      ## Commit message ##\n    -    builtin/repo: add object counts in stats output\n    +    builtin/repo: add object counts in structure output\n     \n         The amount of objects in a repository can provide insight regarding its\n         shape. To surface this information, use the path-walk API to count the\n    @@ Commit message\n     \n      ## Documentation/git-repo.adoc ##\n     @@ Documentation/git-repo.adoc: supported:\n    - \tof information are reported:\n    + \tfollowing kinds of information are reported:\n      +\n      * Reference counts categorized by type\n     +* Reachable object counts categorized by type\n    @@ builtin/repo.c: struct ref_stats {\n     +\tsize_t blobs;\n     +};\n     +\n    -+struct repo_stats {\n    ++struct repo_structure {\n     +\tstruct ref_stats refs;\n     +\tstruct object_stats objects;\n     +};\n    @@ builtin/repo.c: static void stats_table_count_addf(struct stats_table *table, si\n      \tva_end(ap);\n      }\n      \n    --static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n     +static inline size_t get_total_object_count(struct object_stats *stats)\n    - {\n    ++{\n     +\treturn stats->tags + stats->commits + stats->trees + stats->blobs;\n     +}\n     +\n    -+static void stats_table_setup(struct stats_table *table, struct repo_stats *stats)\n    -+{\n    + static void stats_table_setup_structure(struct stats_table *table,\n    +-\t\t\t\t\tstruct ref_stats *refs)\n    ++\t\t\t\t\tstruct repo_structure *stats)\n    + {\n     +\tstruct object_stats *objects = &stats->objects;\n     +\tstruct ref_stats *refs = &stats->refs;\n     +\tsize_t object_total;\n      \tsize_t ref_total;\n      \n      \tref_total = refs->branches + refs->remotes + refs->tags + refs->others;\n    -@@ builtin/repo.c: static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)\n    +@@ builtin/repo.c: static void stats_table_setup_structure(struct stats_table *table,\n      \tstats_table_count_addf(table, refs->tags, \"    * %s\", _(\"Tags\"));\n      \tstats_table_count_addf(table, refs->remotes, \"    * %s\", _(\"Remotes\"));\n      \tstats_table_count_addf(table, refs->others, \"    * %s\", _(\"Others\"));\n    @@ builtin/repo.c: static void stats_table_setup(struct stats_table *table, struct\n     +\tstats_table_count_addf(table, objects->tags, \"    * %s\", _(\"Tags\"));\n      }\n      \n    - static inline size_t max_size_t(size_t a, size_t b)\n    -@@ builtin/repo.c: static void stats_count_references(struct ref_stats *stats, struct ref_array *re\n    + static void stats_table_print_structure(const struct stats_table *table)\n    +@@ builtin/repo.c: static void structure_count_references(struct ref_stats *stats,\n      \t}\n      }\n      \n    @@ builtin/repo.c: static void stats_count_references(struct ref_stats *stats, stru\n     +\treturn 0;\n     +}\n     +\n    -+static void stats_count_objects(struct object_stats *stats,\n    -+\t\t\t\tstruct ref_array *refs, struct rev_info *revs)\n    ++static void structure_count_objects(struct object_stats *stats,\n    ++\t\t\t\t    struct ref_array *refs,\n    ++\t\t\t\t    struct rev_info *revs)\n     +{\n     +\tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n     +\n    @@ builtin/repo.c: static void stats_count_references(struct ref_stats *stats, stru\n     +\tpath_walk_info_clear(&info);\n     +}\n     +\n    - static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n    --\t\t\t  struct repository *repo UNUSED)\n    -+\t\t\t  struct repository *repo)\n    + static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n    +-\t\t\t      struct repository *repo UNUSED)\n    ++\t\t\t      struct repository *repo)\n      {\n      \tstruct ref_filter filter = REF_FILTER_INIT;\n      \tstruct stats_table table = {\n      \t\t.rows = STRING_LIST_INIT_DUP,\n      \t};\n     -\tstruct ref_stats stats = { 0 };\n    -+\tstruct repo_stats stats = { 0 };\n    ++\tstruct repo_structure stats = { 0 };\n      \tstruct ref_array refs = { 0 };\n     +\tstruct rev_info revs;\n      \tstruct option options[] = { 0 };\n    @@ builtin/repo.c: static void stats_count_references(struct ref_stats *stats, stru\n      \tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n      \t\tdie(_(\"unable to filter refs\"));\n      \n    --\tstats_count_references(&stats, &refs);\n    -+\tstats_count_references(&stats.refs, &refs);\n    -+\tstats_count_objects(&stats.objects, &refs, &revs);\n    +-\tstructure_count_references(&stats, &refs);\n    ++\tstructure_count_references(&stats.refs, &refs);\n    ++\tstructure_count_objects(&stats.objects, &refs, &revs);\n      \n    - \tstats_table_setup(&table, &stats);\n    - \tstats_table_print(&table);\n    + \tstats_table_setup_structure(&table, &stats);\n    + \tstats_table_print_structure(&table);\n      \n      \tstats_table_clear(&table);\n     +\trelease_revisions(&revs);\n    @@ builtin/repo.c: static void stats_count_references(struct ref_stats *stats, stru\n      \n      \treturn 0;\n     \n    - ## t/t1901-repo-stats.sh ##\n    -@@ t/t1901-repo-stats.sh: test_expect_success 'empty repository' '\n    - \t(\n    - \t\tcd repo &&\n    - \t\tcat >expect <<-\\EOF &&\n    --\t\t| Repository stats | Value |\n    --\t\t| ---------------- | ----- |\n    --\t\t| * References     |       |\n    --\t\t|   * Count        |     0 |\n    --\t\t|     * Branches   |     0 |\n    --\t\t|     * Tags       |     0 |\n    --\t\t|     * Remotes    |     0 |\n    --\t\t|     * Others     |     0 |\n    -+\t\t| Repository stats    | Value |\n    -+\t\t| ------------------- | ----- |\n    -+\t\t| * References        |       |\n    -+\t\t|   * Count           |     0 |\n    -+\t\t|     * Branches      |     0 |\n    -+\t\t|     * Tags          |     0 |\n    -+\t\t|     * Remotes       |     0 |\n    -+\t\t|     * Others        |     0 |\n    -+\t\t|                     |       |\n    -+\t\t| * Reachable objects |       |\n    -+\t\t|   * Count           |     0 |\n    -+\t\t|     * Commits       |     0 |\n    -+\t\t|     * Trees         |     0 |\n    -+\t\t|     * Blobs         |     0 |\n    -+\t\t|     * Tags          |     0 |\n    + ## t/t1901-repo-structure.sh ##\n    +@@ t/t1901-repo-structure.sh: test_expect_success 'empty repository' '\n    + \t\t|     * Tags           |     0 |\n    + \t\t|     * Remotes        |     0 |\n    + \t\t|     * Others         |     0 |\n    ++\t\t|                      |       |\n    ++\t\t| * Reachable objects  |       |\n    ++\t\t|   * Count            |     0 |\n    ++\t\t|     * Commits        |     0 |\n    ++\t\t|     * Trees          |     0 |\n    ++\t\t|     * Blobs          |     0 |\n    ++\t\t|     * Tags           |     0 |\n      \t\tEOF\n      \n    - \t\tgit repo stats >out 2>err &&\n    -@@ t/t1901-repo-stats.sh: test_expect_success 'empty repository' '\n    + \t\tgit repo structure >out 2>err &&\n    +@@ t/t1901-repo-structure.sh: test_expect_success 'empty repository' '\n      \t)\n      '\n      \n    @@ t/t1901-repo-stats.sh: test_expect_success 'empty repository' '\n      \t\tgit notes add -m foo &&\n      \n      \t\tcat >expect <<-\\EOF &&\n    --\t\t| Repository stats | Value |\n    --\t\t| ---------------- | ----- |\n    --\t\t| * References     |       |\n    --\t\t|   * Count        |     4 |\n    --\t\t|     * Branches   |     1 |\n    --\t\t|     * Tags       |     1 |\n    --\t\t|     * Remotes    |     1 |\n    --\t\t|     * Others     |     1 |\n    -+\t\t| Repository stats    | Value |\n    -+\t\t| ------------------- | ----- |\n    -+\t\t| * References        |       |\n    -+\t\t|   * Count           |     4 |\n    -+\t\t|     * Branches      |     1 |\n    -+\t\t|     * Tags          |     1 |\n    -+\t\t|     * Remotes       |     1 |\n    -+\t\t|     * Others        |     1 |\n    -+\t\t|                     |       |\n    -+\t\t| * Reachable objects |       |\n    -+\t\t|   * Count           |   130 |\n    -+\t\t|     * Commits       |    43 |\n    -+\t\t|     * Trees         |    43 |\n    -+\t\t|     * Blobs         |    43 |\n    -+\t\t|     * Tags          |     1 |\n    +@@ t/t1901-repo-structure.sh: test_expect_success 'repository with references' '\n    + \t\t|     * Tags           |     1 |\n    + \t\t|     * Remotes        |     1 |\n    + \t\t|     * Others         |     1 |\n    ++\t\t|                      |       |\n    ++\t\t| * Reachable objects  |       |\n    ++\t\t|   * Count            |   130 |\n    ++\t\t|     * Commits        |    43 |\n    ++\t\t|     * Trees          |    43 |\n    ++\t\t|     * Blobs          |    43 |\n    ++\t\t|     * Tags           |     1 |\n      \t\tEOF\n      \n    - \t\tgit repo stats >out 2>err &&\n    + \t\tgit repo structure >out 2>err &&\n6:  76975b2eab ! 5:  b336578445 builtin/repo: add keyvalue and nul format for stats\n    @@ Metadata\n     Author: Justin Tobler <jltobler@gmail.com>\n     \n      ## Commit message ##\n    -    builtin/repo: add keyvalue and nul format for stats\n    +    builtin/repo: add keyvalue and nul format for structure stats\n     \n    -    All repository stats are outputted in a human-friendly table form. This\n    -    format is not suitable for machine parsing. Add a --format option that\n    -    supports three output modes: `table`, `keyvalue`, and `nul`. The `table`\n    -    mode is the default format and prints the same table output as before.\n    +    All repository structure stats are outputted in a human-friendly table\n    +    form. This format is not suitable for machine parsing. Add a --format\n    +    option that supports three output modes: `table`, `keyvalue`, and `nul`.\n    +    The `table` mode is the default format and prints the same table output\n    +    as before.\n     \n         With the `keyvalue` mode, each line of output contains a key-value pair\n         of a repository stat. The '=' character is used to delimit between keys\n    @@ Documentation/git-repo.adoc: SYNOPSIS\n      --------\n      [synopsis]\n      git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n    --git repo stats\n    -+git repo stats [--format=(table|keyvalue|nul)]\n    +-git repo structure\n    ++git repo structure [--format=(table|keyvalue|nul)]\n      \n      DESCRIPTION\n      -----------\n    @@ Documentation/git-repo.adoc: supported:\n      +\n      `-z` is an alias for `--format=nul`.\n      \n    --`stats`::\n    -+`stats [--format=(table|keyvalue|nul)]`::\n    - \tRetrieve statistics about the current repository. The following kinds\n    - \tof information are reported:\n    +-`structure`::\n    ++`structure [--format=(table|keyvalue|nul)]`::\n    + \tRetrieve statistics about the current repository structure. The\n    + \tfollowing kinds of information are reported:\n      +\n     @@ Documentation/git-repo.adoc: supported:\n      * Reachable object counts categorized by type\n    @@ builtin/repo.c\n      \n      static const char *const repo_usage[] = {\n      \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n    --\t\"git repo stats\",\n    -+\t\"git repo stats [--format=(table|keyvalue|nul)]\",\n    +-\t\"git repo structure\",\n    ++\t\"git repo structure [--format=(table|keyvalue|nul)]\",\n      \tNULL\n      };\n      \n    @@ builtin/repo.c: static void stats_table_clear(struct stats_table *table)\n      \tstring_list_clear(&table->rows, 1);\n      }\n      \n    -+static void stats_keyvalue_print(struct repo_stats *stats, char key_delim,\n    -+\t\t\t\t char value_delim)\n    ++static void structure_keyvalue_print(struct repo_structure *stats,\n    ++\t\t\t\t     char key_delim, char value_delim)\n     +{\n     +\tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n     +\t       (uintmax_t)stats->refs.branches, value_delim);\n    @@ builtin/repo.c: static void stats_table_clear(struct stats_table *table)\n     +\tfflush(stdout);\n     +}\n     +\n    - static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n    + static void structure_count_references(struct ref_stats *stats,\n    + \t\t\t\t       struct ref_array *refs)\n      {\n    - \tfor (int i = 0; i < refs->nr; i++) {\n    -@@ builtin/repo.c: static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n    +@@ builtin/repo.c: static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n      \tstruct stats_table table = {\n      \t\t.rows = STRING_LIST_INIT_DUP,\n      \t};\n     +\tenum output_format format = FORMAT_TABLE;\n    - \tstruct repo_stats stats = { 0 };\n    + \tstruct repo_structure stats = { 0 };\n      \tstruct ref_array refs = { 0 };\n      \tstruct rev_info revs;\n     -\tstruct option options[] = { 0 };\n    @@ builtin/repo.c: static int cmd_repo_stats(int argc, const char **argv, const cha\n      \n      \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n      \tif (argc)\n    -@@ builtin/repo.c: static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n    - \tstats_count_references(&stats.refs, &refs);\n    - \tstats_count_objects(&stats.objects, &refs, &revs);\n    +@@ builtin/repo.c: static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n    + \tstructure_count_references(&stats.refs, &refs);\n    + \tstructure_count_objects(&stats.objects, &refs, &revs);\n      \n    --\tstats_table_setup(&table, &stats);\n    --\tstats_table_print(&table);\n    +-\tstats_table_setup_structure(&table, &stats);\n    +-\tstats_table_print_structure(&table);\n     +\tswitch (format) {\n     +\tcase FORMAT_TABLE:\n    -+\t\tstats_table_setup(&table, &stats);\n    -+\t\tstats_table_print(&table);\n    ++\t\tstats_table_setup_structure(&table, &stats);\n    ++\t\tstats_table_print_structure(&table);\n     +\t\tbreak;\n     +\tcase FORMAT_KEYVALUE:\n    -+\t\tstats_keyvalue_print(&stats, '=', '\\n');\n    ++\t\tstructure_keyvalue_print(&stats, '=', '\\n');\n     +\t\tbreak;\n     +\tcase FORMAT_NUL_TERMINATED:\n    -+\t\tstats_keyvalue_print(&stats, '\\n', '\\0');\n    ++\t\tstructure_keyvalue_print(&stats, '\\n', '\\0');\n     +\t\tbreak;\n     +\tdefault:\n     +\t\tBUG(\"invalid output format\");\n    @@ builtin/repo.c: static int cmd_repo_stats(int argc, const char **argv, const cha\n      \tstats_table_clear(&table);\n      \trelease_revisions(&revs);\n     \n    - ## t/t1901-repo-stats.sh ##\n    -@@ t/t1901-repo-stats.sh: test_expect_success 'repository with references and objects' '\n    + ## t/t1901-repo-structure.sh ##\n    +@@ t/t1901-repo-structure.sh: test_expect_success 'repository with references and objects' '\n      \t)\n      '\n      \n    @@ t/t1901-repo-stats.sh: test_expect_success 'repository with references and objec\n     +\t\tobjects.tags.count=1\n     +\t\tEOF\n     +\n    -+\t\tgit repo stats --format=keyvalue >out 2>err &&\n    ++\t\tgit repo structure --format=keyvalue >out 2>err &&\n     +\n     +\t\ttest_cmp expect out &&\n     +\t\ttest_line_count = 0 err &&\n     +\n     +\t\t# Replace key and value delimiters for nul format.\n     +\t\ttr \"\\n=\" \"\\0\\n\" <expect >expect_nul &&\n    -+\t\tgit repo stats --format=nul >out 2>err &&\n    ++\t\tgit repo structure --format=nul >out 2>err &&\n     +\n     +\t\ttest_cmp expect_nul out &&\n     +\t\ttest_line_count = 0 err\n7:  1105346a3c ! 6:  70c0b7e200 builtin/repo: add progress meter for stats\n    @@ Metadata\n     Author: Justin Tobler <jltobler@gmail.com>\n     \n      ## Commit message ##\n    -    builtin/repo: add progress meter for stats\n    +    builtin/repo: add progress meter for structure stats\n     \n    -    When using the stats subcommand for git-repo(1), evaluating a repository\n    -    may take some time depending on its shape. Add a progress meter to\n    -    provide feedback to the user about what is happening. The progress meter\n    -    is enabled by default when the command is executed from a tty. It can\n    -    also be explicitly enabled/disabled via the --[no-]progress option.\n    +    When using the structure subcommand for git-repo(1), evaluating a\n    +    repository may take some time depending on its shape. Add a progress\n    +    meter to provide feedback to the user about what is happening. The\n    +    progress meter is enabled by default when the command is executed from a\n    +    tty. It can also be explicitly enabled/disabled via the --[no-]progress\n    +    option.\n     \n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n    @@ builtin/repo.c\n      #include \"quote.h\"\n      #include \"ref-filter.h\"\n      #include \"refs.h\"\n    -@@ builtin/repo.c: static void stats_keyvalue_print(struct repo_stats *stats, char key_delim,\n    - \tfflush(stdout);\n    +@@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stats,\n      }\n      \n    --static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)\n    -+static void stats_count_references(struct ref_stats *stats, struct ref_array *refs,\n    -+\t\t\t\t   struct repository *repo, int show_progress)\n    + static void structure_count_references(struct ref_stats *stats,\n    +-\t\t\t\t       struct ref_array *refs)\n    ++\t\t\t\t       struct ref_array *refs,\n    ++\t\t\t\t       struct repository *repo,\n    ++\t\t\t\t       int show_progress)\n      {\n     +\tstruct progress *progress = NULL;\n     +\n    @@ builtin/repo.c: static void stats_keyvalue_print(struct repo_stats *stats, char\n      \tfor (int i = 0; i < refs->nr; i++) {\n      \t\tstruct ref_array_item *ref = refs->items[i];\n      \n    -@@ builtin/repo.c: static void stats_count_references(struct ref_stats *stats, struct ref_array *re\n    +@@ builtin/repo.c: static void structure_count_references(struct ref_stats *stats,\n      \t\tdefault:\n      \t\t\tBUG(\"unexpected reference type\");\n      \t\t}\n    @@ builtin/repo.c: static int count_objects(const char *path UNUSED, struct oid_arr\n      \treturn 0;\n      }\n      \n    - static void stats_count_objects(struct object_stats *stats,\n    --\t\t\t\tstruct ref_array *refs, struct rev_info *revs)\n    -+\t\t\t\tstruct ref_array *refs, struct rev_info *revs,\n    -+\t\t\t\tstruct repository *repo, int show_progress)\n    + static void structure_count_objects(struct object_stats *stats,\n    +-\t\t\t\t    struct ref_array *refs,\n    +-\t\t\t\t    struct rev_info *revs)\n    ++\t\t\t\t    struct ref_array *refs, struct rev_info *revs,\n    ++\t\t\t\t    struct repository *repo, int show_progress)\n      {\n      \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n     +\tstruct count_objects_data data = {\n    @@ builtin/repo.c: static int count_objects(const char *path UNUSED, struct oid_arr\n      \n      \tfor (int i = 0; i < refs->nr; i++) {\n      \t\tstruct ref_array_item *ref = refs->items[i];\n    -@@ builtin/repo.c: static void stats_count_objects(struct object_stats *stats,\n    +@@ builtin/repo.c: static void structure_count_objects(struct object_stats *stats,\n      \t\t}\n      \t}\n      \n    @@ builtin/repo.c: static void stats_count_objects(struct object_stats *stats,\n     +\tstop_progress(&data.progress);\n      }\n      \n    - static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n    -@@ builtin/repo.c: static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n    - \tstruct repo_stats stats = { 0 };\n    + static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n    +@@ builtin/repo.c: static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n    + \tstruct repo_structure stats = { 0 };\n      \tstruct ref_array refs = { 0 };\n      \tstruct rev_info revs;\n     +\tint show_progress = -1;\n    @@ builtin/repo.c: static int cmd_repo_stats(int argc, const char **argv, const cha\n      \t\tOPT_END()\n      \t};\n      \n    -@@ builtin/repo.c: static int cmd_repo_stats(int argc, const char **argv, const char *prefix,\n    +@@ builtin/repo.c: static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n      \tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n      \t\tdie(_(\"unable to filter refs\"));\n      \n    --\tstats_count_references(&stats.refs, &refs);\n    --\tstats_count_objects(&stats.objects, &refs, &revs);\n    +-\tstructure_count_references(&stats.refs, &refs);\n    +-\tstructure_count_objects(&stats.objects, &refs, &revs);\n     +\tif (show_progress < 0)\n     +\t\tshow_progress = isatty(2);\n     +\n    -+\tstats_count_references(&stats.refs, &refs, repo, show_progress);\n    -+\tstats_count_objects(&stats.objects, &refs, &revs, repo, show_progress);\n    ++\tstructure_count_references(&stats.refs, &refs, repo, show_progress);\n    ++\tstructure_count_objects(&stats.objects, &refs, &revs, repo, show_progress);\n      \n      \tswitch (format) {\n      \tcase FORMAT_TABLE:\n     \n    - ## t/t1901-repo-stats.sh ##\n    -@@ t/t1901-repo-stats.sh: test_expect_success 'keyvalue and nul format' '\n    + ## t/t1901-repo-structure.sh ##\n    +@@ t/t1901-repo-structure.sh: test_expect_success 'keyvalue and nul format' '\n      \t)\n      '\n      \n    @@ t/t1901-repo-stats.sh: test_expect_success 'keyvalue and nul format' '\n     +\t\tcd repo &&\n     +\t\ttest_commit foo &&\n     +\n    -+\t\tGIT_PROGRESS_DELAY=0 git repo stats --progress >out 2>err &&\n    ++\t\tGIT_PROGRESS_DELAY=0 git repo structure --progress >out 2>err &&\n     +\n     +\t\ttest_file_not_empty out &&\n     +\t\ttest_grep \"Counting references: 100% (2/2), done.\" err &&\n     +\t\ttest_grep \"Counting objects: 3, done.\" err &&\n     +\n    -+\t\tGIT_PROGRESS_DELAY=0 git repo stats --no-progress >out 2>err &&\n    ++\t\tGIT_PROGRESS_DELAY=0 git repo structure --no-progress >out 2>err &&\n     +\n     +\t\ttest_file_not_empty out &&\n     +\t\ttest_line_count = 0 err\n\nbase-commit: ca2559c1d630eb4f04cdee2328aaf1c768907a9e\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"528834","messageId":"20251015211213.361797-2-jltobler@gmail.com","threadId":"64187","inReplyTo":"20251015211213.361797-1-jltobler@gmail.com","subject":"[PATCH v5 1/6] builtin/repo: rename repo_info() to cmd_repo_info()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-15T21:12:08Z","receivedAt":"2025-10-15T21:12:21Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Subcommand functions are often prefixed with `cmd_` to denote that they\nare an entrypoint. Rename repo_info() to cmd_repo_info() accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/repo.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex bbb0966f2d..eeeab8fbd2 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -136,8 +136,8 @@ static int parse_format_cb(const struct option *opt,\n \treturn 0;\n }\n \n-static int repo_info(int argc, const char **argv, const char *prefix,\n-\t\t     struct repository *repo)\n+static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n+\t\t\t struct repository *repo)\n {\n \tenum output_format format = FORMAT_KEYVALUE;\n \tstruct option options[] = {\n@@ -161,7 +161,7 @@ int cmd_repo(int argc, const char **argv, const char *prefix,\n {\n \tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n-\t\tOPT_SUBCOMMAND(\"info\", &fn, repo_info),\n+\t\tOPT_SUBCOMMAND(\"info\", &fn, cmd_repo_info),\n \t\tOPT_END()\n \t};\n \n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"528835","messageId":"20251015211213.361797-3-jltobler@gmail.com","threadId":"64187","inReplyTo":"20251015211213.361797-1-jltobler@gmail.com","subject":"[PATCH v5 2/6] ref-filter: allow NULL filter pattern","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-15T21:12:09Z","receivedAt":"2025-10-15T21:12:21Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"When setting up `struct ref_filter` for filter_refs(), the\n`name_patterns` field must point to an array of pattern strings even if\nno patterns are required. To improve this interface, treat a NULL\n`name_patterns` field the same as when it points to an empty array.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n ref-filter.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/ref-filter.c b/ref-filter.c\nindex 520d2539c9..2cb5a166d6 100644\n--- a/ref-filter.c\n+++ b/ref-filter.c\n@@ -2664,7 +2664,7 @@ static int match_name_as_path(const char **pattern, const char *refname,\n /* Return 1 if the refname matches one of the patterns, otherwise 0. */\n static int filter_pattern_match(struct ref_filter *filter, const char *refname)\n {\n-\tif (!*filter->name_patterns)\n+\tif (!filter->name_patterns || !*filter->name_patterns)\n \t\treturn 1; /* No pattern always matches */\n \tif (filter->match_as_path)\n \t\treturn match_name_as_path(filter->name_patterns, refname,\n@@ -2751,7 +2751,7 @@ static int for_each_fullref_in_pattern(struct ref_filter *filter,\n \t\treturn for_each_fullref_with_seek(filter, cb, cb_data, 0);\n \t}\n \n-\tif (!filter->name_patterns[0]) {\n+\tif (!filter->name_patterns || !filter->name_patterns[0]) {\n \t\t/* no patterns; we have to look at everything */\n \t\treturn for_each_fullref_with_seek(filter, cb, cb_data, 0);\n \t}\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"528836","messageId":"20251015211213.361797-4-jltobler@gmail.com","threadId":"64187","inReplyTo":"20251015211213.361797-1-jltobler@gmail.com","subject":"[PATCH v5 3/6] builtin/repo: introduce structure subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-15T21:12:10Z","receivedAt":"2025-10-15T21:12:22Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The structure of a repository's history can have huge impacts on the\nperformance and health of the repository itself. Currently, Git lacks a\nmeans to surface repository metrics regarding its structure/shape via a\nsingle command. Acquiring this information requires users to be familiar\nwith the relevant data points and the various Git commands required to\nsurface them. To fill this gap, supplemental tools such as git-sizer(1)\nhave been developed.\n\nTo allow users to more readily identify repository structure related\ninformation, introduce the \"structure\" subcommand in git-repo(1). The\ngoal of this subcommand is to eventually provide similar functionality\nto git-sizer(1), but natively in Git.\n\nThe initial version of this command only iterates through all references\nin the repository and tracks the count of branches, tags, remote refs,\nand other reference types. The corresponding information is displayed in\na human-friendly table formatted in a very similar manner to\ngit-sizer(1). The width of each table column is adjusted automatically\nto satisfy the requirements of the widest row contained.\n\nSubsequent commits will surface additional relevant data points to\noutput and also provide other more machine-friendly output formats.\n\nBased-on-patch-by: Derrick Stolee <stolee@gmail.com>\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc |  10 ++\n builtin/repo.c              | 193 ++++++++++++++++++++++++++++++++++++\n t/meson.build               |   1 +\n t/t1901-repo-structure.sh   |  61 ++++++++++++\n 4 files changed, 265 insertions(+)\n create mode 100755 t/t1901-repo-structure.sh\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 209afd1b61..8193298dd5 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -9,6 +9,7 @@ SYNOPSIS\n --------\n [synopsis]\n git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n+git repo structure\n \n DESCRIPTION\n -----------\n@@ -43,6 +44,15 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n+`structure`::\n+\tRetrieve statistics about the current repository structure. The\n+\tfollowing kinds of information are reported:\n++\n+* Reference counts categorized by type\n+\n++\n+The table output format may change and is not intended for machine parsing.\n+\n INFO KEYS\n ---------\n In order to obtain a set of values from `git repo info`, you should provide\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex eeeab8fbd2..4575cf9467 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -4,12 +4,16 @@\n #include \"environment.h\"\n #include \"parse-options.h\"\n #include \"quote.h\"\n+#include \"ref-filter.h\"\n #include \"refs.h\"\n #include \"strbuf.h\"\n+#include \"string-list.h\"\n #include \"shallow.h\"\n+#include \"utf8.h\"\n \n static const char *const repo_usage[] = {\n \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n+\t\"git repo structure\",\n \tNULL\n };\n \n@@ -156,12 +160,201 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \treturn print_fields(argc, argv, repo, format);\n }\n \n+struct ref_stats {\n+\tsize_t branches;\n+\tsize_t remotes;\n+\tsize_t tags;\n+\tsize_t others;\n+};\n+\n+struct stats_table {\n+\tstruct string_list rows;\n+\n+\tint name_col_width;\n+\tint value_col_width;\n+};\n+\n+/*\n+ * Holds column data that gets stored for each row.\n+ */\n+struct stats_table_entry {\n+\tchar *value;\n+};\n+\n+static void stats_table_vaddf(struct stats_table *table,\n+\t\t\t      struct stats_table_entry *entry,\n+\t\t\t      const char *format, va_list ap)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct string_list_item *item;\n+\tchar *formatted_name;\n+\tint name_width;\n+\n+\tstrbuf_vaddf(&buf, format, ap);\n+\tformatted_name = strbuf_detach(&buf, NULL);\n+\tname_width = utf8_strwidth(formatted_name);\n+\n+\titem = string_list_append_nodup(&table->rows, formatted_name);\n+\titem->util = entry;\n+\n+\tif (name_width > table->name_col_width)\n+\t\ttable->name_col_width = name_width;\n+\tif (entry) {\n+\t\tint value_width = utf8_strwidth(entry->value);\n+\t\tif (value_width > table->value_col_width)\n+\t\t\ttable->value_col_width = value_width;\n+\t}\n+}\n+\n+static void stats_table_addf(struct stats_table *table, const char *format, ...)\n+{\n+\tva_list ap;\n+\n+\tva_start(ap, format);\n+\tstats_table_vaddf(table, NULL, format, ap);\n+\tva_end(ap);\n+}\n+\n+static void stats_table_count_addf(struct stats_table *table, size_t value,\n+\t\t\t\t   const char *format, ...)\n+{\n+\tstruct stats_table_entry *entry;\n+\tva_list ap;\n+\n+\tCALLOC_ARRAY(entry, 1);\n+\tentry->value = xstrfmt(\"%\" PRIuMAX, (uintmax_t)value);\n+\n+\tva_start(ap, format);\n+\tstats_table_vaddf(table, entry, format, ap);\n+\tva_end(ap);\n+}\n+\n+static void stats_table_setup_structure(struct stats_table *table,\n+\t\t\t\t\tstruct ref_stats *refs)\n+{\n+\tsize_t ref_total;\n+\n+\tref_total = refs->branches + refs->remotes + refs->tags + refs->others;\n+\tstats_table_addf(table, \"* %s\", _(\"References\"));\n+\tstats_table_count_addf(table, ref_total, \"  * %s\", _(\"Count\"));\n+\tstats_table_count_addf(table, refs->branches, \"    * %s\", _(\"Branches\"));\n+\tstats_table_count_addf(table, refs->tags, \"    * %s\", _(\"Tags\"));\n+\tstats_table_count_addf(table, refs->remotes, \"    * %s\", _(\"Remotes\"));\n+\tstats_table_count_addf(table, refs->others, \"    * %s\", _(\"Others\"));\n+}\n+\n+static void stats_table_print_structure(const struct stats_table *table)\n+{\n+\tconst char *name_col_title = _(\"Repository structure\");\n+\tconst char *value_col_title = _(\"Value\");\n+\tint name_col_width = utf8_strwidth(name_col_title);\n+\tint value_col_width = utf8_strwidth(value_col_title);\n+\tstruct string_list_item *item;\n+\n+\tif (table->name_col_width > name_col_width)\n+\t\tname_col_width = table->name_col_width;\n+\tif (table->value_col_width > value_col_width)\n+\t\tvalue_col_width = table->value_col_width;\n+\n+\tprintf(\"| %-*s | %-*s |\\n\", name_col_width, name_col_title,\n+\t       value_col_width, value_col_title);\n+\tprintf(\"| \");\n+\tfor (int i = 0; i < name_col_width; i++)\n+\t\tputchar('-');\n+\tprintf(\" | \");\n+\tfor (int i = 0; i < value_col_width; i++)\n+\t\tputchar('-');\n+\tprintf(\" |\\n\");\n+\n+\tfor_each_string_list_item(item, &table->rows) {\n+\t\tstruct stats_table_entry *entry = item->util;\n+\t\tconst char *value = \"\";\n+\n+\t\tif (entry) {\n+\t\t\tstruct stats_table_entry *entry = item->util;\n+\t\t\tvalue = entry->value;\n+\t\t}\n+\n+\t\tprintf(\"| %-*s | %*s |\\n\", name_col_width, item->string,\n+\t\t       value_col_width, value);\n+\t}\n+}\n+\n+static void stats_table_clear(struct stats_table *table)\n+{\n+\tstruct stats_table_entry *entry;\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, &table->rows) {\n+\t\tentry = item->util;\n+\t\tif (entry)\n+\t\t\tfree(entry->value);\n+\t}\n+\n+\tstring_list_clear(&table->rows, 1);\n+}\n+\n+static void structure_count_references(struct ref_stats *stats,\n+\t\t\t\t       struct ref_array *refs)\n+{\n+\tfor (int i = 0; i < refs->nr; i++) {\n+\t\tstruct ref_array_item *ref = refs->items[i];\n+\n+\t\tswitch (ref->kind) {\n+\t\tcase FILTER_REFS_BRANCHES:\n+\t\t\tstats->branches++;\n+\t\t\tbreak;\n+\t\tcase FILTER_REFS_REMOTES:\n+\t\t\tstats->remotes++;\n+\t\t\tbreak;\n+\t\tcase FILTER_REFS_TAGS:\n+\t\t\tstats->tags++;\n+\t\t\tbreak;\n+\t\tcase FILTER_REFS_OTHERS:\n+\t\t\tstats->others++;\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tBUG(\"unexpected reference type\");\n+\t\t}\n+\t}\n+}\n+\n+static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n+\t\t\t      struct repository *repo UNUSED)\n+{\n+\tstruct ref_filter filter = REF_FILTER_INIT;\n+\tstruct stats_table table = {\n+\t\t.rows = STRING_LIST_INIT_DUP,\n+\t};\n+\tstruct ref_stats stats = { 0 };\n+\tstruct ref_array refs = { 0 };\n+\tstruct option options[] = { 0 };\n+\n+\targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n+\tif (argc)\n+\t\tusage(_(\"too many arguments\"));\n+\n+\tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n+\t\tdie(_(\"unable to filter refs\"));\n+\n+\tstructure_count_references(&stats, &refs);\n+\n+\tstats_table_setup_structure(&table, &stats);\n+\tstats_table_print_structure(&table);\n+\n+\tstats_table_clear(&table);\n+\tref_array_clear(&refs);\n+\n+\treturn 0;\n+}\n+\n int cmd_repo(int argc, const char **argv, const char *prefix,\n \t     struct repository *repo)\n {\n \tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n \t\tOPT_SUBCOMMAND(\"info\", &fn, cmd_repo_info),\n+\t\tOPT_SUBCOMMAND(\"structure\", &fn, cmd_repo_structure),\n \t\tOPT_END()\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 7974795fe4..9e426f8edc 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -236,6 +236,7 @@ integration_tests = [\n   't1701-racy-split-index.sh',\n   't1800-hook.sh',\n   't1900-repo.sh',\n+  't1901-repo-structure.sh',\n   't2000-conflict-when-checking-files-out.sh',\n   't2002-checkout-cache-u.sh',\n   't2003-checkout-cache-mkdir.sh',\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nnew file mode 100755\nindex 0000000000..e592eea0eb\n--- /dev/null\n+++ b/t/t1901-repo-structure.sh\n@@ -0,0 +1,61 @@\n+#!/bin/sh\n+\n+test_description='test git repo structure'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'empty repository' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\t| Repository structure | Value |\n+\t\t| -------------------- | ----- |\n+\t\t| * References         |       |\n+\t\t|   * Count            |     0 |\n+\t\t|     * Branches       |     0 |\n+\t\t|     * Tags           |     0 |\n+\t\t|     * Remotes        |     0 |\n+\t\t|     * Others         |     0 |\n+\t\tEOF\n+\n+\t\tgit repo structure >out 2>err &&\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n+test_expect_success 'repository with references' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit commit --allow-empty -m init &&\n+\t\tgit tag -a foo -m bar &&\n+\n+\t\toid=\"$(git rev-parse HEAD)\" &&\n+\t\tgit update-ref refs/remotes/origin/foo \"$oid\" &&\n+\n+\t\tgit notes add -m foo &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\t\t| Repository structure | Value |\n+\t\t| -------------------- | ----- |\n+\t\t| * References         |       |\n+\t\t|   * Count            |     4 |\n+\t\t|     * Branches       |     1 |\n+\t\t|     * Tags           |     1 |\n+\t\t|     * Remotes        |     1 |\n+\t\t|     * Others         |     1 |\n+\t\tEOF\n+\n+\t\tgit repo structure >out 2>err &&\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n+test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"528838","messageId":"20251015211213.361797-5-jltobler@gmail.com","threadId":"64187","inReplyTo":"20251015211213.361797-1-jltobler@gmail.com","subject":"[PATCH v5 4/6] builtin/repo: add object counts in structure output","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-15T21:12:11Z","receivedAt":"2025-10-15T21:12:23Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The amount of objects in a repository can provide insight regarding its\nshape. To surface this information, use the path-walk API to count the\nnumber of reachable objects in the repository by object type. All\nregular references are used to determine the reachable set of objects.\nThe object counts are appended to the same table containing the\nreference information.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc |  1 +\n builtin/repo.c              | 97 +++++++++++++++++++++++++++++++++++--\n t/t1901-repo-structure.sh   | 19 +++++++-\n 3 files changed, 111 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 8193298dd5..ae62d2415f 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -49,6 +49,7 @@ supported:\n \tfollowing kinds of information are reported:\n +\n * Reference counts categorized by type\n+* Reachable object counts categorized by type\n \n +\n The table output format may change and is not intended for machine parsing.\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 4575cf9467..0bc3c1e458 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -3,9 +3,11 @@\n #include \"builtin.h\"\n #include \"environment.h\"\n #include \"parse-options.h\"\n+#include \"path-walk.h\"\n #include \"quote.h\"\n #include \"ref-filter.h\"\n #include \"refs.h\"\n+#include \"revision.h\"\n #include \"strbuf.h\"\n #include \"string-list.h\"\n #include \"shallow.h\"\n@@ -167,6 +169,18 @@ struct ref_stats {\n \tsize_t others;\n };\n \n+struct object_stats {\n+\tsize_t tags;\n+\tsize_t commits;\n+\tsize_t trees;\n+\tsize_t blobs;\n+};\n+\n+struct repo_structure {\n+\tstruct ref_stats refs;\n+\tstruct object_stats objects;\n+};\n+\n struct stats_table {\n \tstruct string_list rows;\n \n@@ -229,9 +243,17 @@ static void stats_table_count_addf(struct stats_table *table, size_t value,\n \tva_end(ap);\n }\n \n+static inline size_t get_total_object_count(struct object_stats *stats)\n+{\n+\treturn stats->tags + stats->commits + stats->trees + stats->blobs;\n+}\n+\n static void stats_table_setup_structure(struct stats_table *table,\n-\t\t\t\t\tstruct ref_stats *refs)\n+\t\t\t\t\tstruct repo_structure *stats)\n {\n+\tstruct object_stats *objects = &stats->objects;\n+\tstruct ref_stats *refs = &stats->refs;\n+\tsize_t object_total;\n \tsize_t ref_total;\n \n \tref_total = refs->branches + refs->remotes + refs->tags + refs->others;\n@@ -241,6 +263,15 @@ static void stats_table_setup_structure(struct stats_table *table,\n \tstats_table_count_addf(table, refs->tags, \"    * %s\", _(\"Tags\"));\n \tstats_table_count_addf(table, refs->remotes, \"    * %s\", _(\"Remotes\"));\n \tstats_table_count_addf(table, refs->others, \"    * %s\", _(\"Others\"));\n+\n+\tobject_total = get_total_object_count(objects);\n+\tstats_table_addf(table, \"\");\n+\tstats_table_addf(table, \"* %s\", _(\"Reachable objects\"));\n+\tstats_table_count_addf(table, object_total, \"  * %s\", _(\"Count\"));\n+\tstats_table_count_addf(table, objects->commits, \"    * %s\", _(\"Commits\"));\n+\tstats_table_count_addf(table, objects->trees, \"    * %s\", _(\"Trees\"));\n+\tstats_table_count_addf(table, objects->blobs, \"    * %s\", _(\"Blobs\"));\n+\tstats_table_count_addf(table, objects->tags, \"    * %s\", _(\"Tags\"));\n }\n \n static void stats_table_print_structure(const struct stats_table *table)\n@@ -319,30 +350,88 @@ static void structure_count_references(struct ref_stats *stats,\n \t}\n }\n \n+static int count_objects(const char *path UNUSED, struct oid_array *oids,\n+\t\t\t enum object_type type, void *cb_data)\n+{\n+\tstruct object_stats *stats = cb_data;\n+\n+\tswitch (type) {\n+\tcase OBJ_TAG:\n+\t\tstats->tags += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_COMMIT:\n+\t\tstats->commits += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_TREE:\n+\t\tstats->trees += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_BLOB:\n+\t\tstats->blobs += oids->nr;\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"invalid object type\");\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void structure_count_objects(struct object_stats *stats,\n+\t\t\t\t    struct ref_array *refs,\n+\t\t\t\t    struct rev_info *revs)\n+{\n+\tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n+\n+\tinfo.revs = revs;\n+\tinfo.path_fn = count_objects;\n+\tinfo.path_fn_data = stats;\n+\n+\tfor (int i = 0; i < refs->nr; i++) {\n+\t\tstruct ref_array_item *ref = refs->items[i];\n+\n+\t\tswitch (ref->kind) {\n+\t\tcase FILTER_REFS_BRANCHES:\n+\t\tcase FILTER_REFS_TAGS:\n+\t\tcase FILTER_REFS_REMOTES:\n+\t\tcase FILTER_REFS_OTHERS:\n+\t\t\tadd_pending_oid(revs, NULL, &ref->objectname, 0);\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tBUG(\"unexpected reference type\");\n+\t\t}\n+\t}\n+\n+\twalk_objects_by_path(&info);\n+\tpath_walk_info_clear(&info);\n+}\n+\n static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n-\t\t\t      struct repository *repo UNUSED)\n+\t\t\t      struct repository *repo)\n {\n \tstruct ref_filter filter = REF_FILTER_INIT;\n \tstruct stats_table table = {\n \t\t.rows = STRING_LIST_INIT_DUP,\n \t};\n-\tstruct ref_stats stats = { 0 };\n+\tstruct repo_structure stats = { 0 };\n \tstruct ref_array refs = { 0 };\n+\tstruct rev_info revs;\n \tstruct option options[] = { 0 };\n \n \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n \tif (argc)\n \t\tusage(_(\"too many arguments\"));\n \n+\trepo_init_revisions(repo, &revs, prefix);\n \tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n \t\tdie(_(\"unable to filter refs\"));\n \n-\tstructure_count_references(&stats, &refs);\n+\tstructure_count_references(&stats.refs, &refs);\n+\tstructure_count_objects(&stats.objects, &refs, &revs);\n \n \tstats_table_setup_structure(&table, &stats);\n \tstats_table_print_structure(&table);\n \n \tstats_table_clear(&table);\n+\trelease_revisions(&revs);\n \tref_array_clear(&refs);\n \n \treturn 0;\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nindex e592eea0eb..c32cf4e239 100755\n--- a/t/t1901-repo-structure.sh\n+++ b/t/t1901-repo-structure.sh\n@@ -18,6 +18,13 @@ test_expect_success 'empty repository' '\n \t\t|     * Tags           |     0 |\n \t\t|     * Remotes        |     0 |\n \t\t|     * Others         |     0 |\n+\t\t|                      |       |\n+\t\t| * Reachable objects  |       |\n+\t\t|   * Count            |     0 |\n+\t\t|     * Commits        |     0 |\n+\t\t|     * Trees          |     0 |\n+\t\t|     * Blobs          |     0 |\n+\t\t|     * Tags           |     0 |\n \t\tEOF\n \n \t\tgit repo structure >out 2>err &&\n@@ -27,17 +34,18 @@ test_expect_success 'empty repository' '\n \t)\n '\n \n-test_expect_success 'repository with references' '\n+test_expect_success 'repository with references and objects' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n \t(\n \t\tcd repo &&\n-\t\tgit commit --allow-empty -m init &&\n+\t\ttest_commit_bulk 42 &&\n \t\tgit tag -a foo -m bar &&\n \n \t\toid=\"$(git rev-parse HEAD)\" &&\n \t\tgit update-ref refs/remotes/origin/foo \"$oid\" &&\n \n+\t\t# Also creates a commit, tree, and blob.\n \t\tgit notes add -m foo &&\n \n \t\tcat >expect <<-\\EOF &&\n@@ -49,6 +57,13 @@ test_expect_success 'repository with references' '\n \t\t|     * Tags           |     1 |\n \t\t|     * Remotes        |     1 |\n \t\t|     * Others         |     1 |\n+\t\t|                      |       |\n+\t\t| * Reachable objects  |       |\n+\t\t|   * Count            |   130 |\n+\t\t|     * Commits        |    43 |\n+\t\t|     * Trees          |    43 |\n+\t\t|     * Blobs          |    43 |\n+\t\t|     * Tags           |     1 |\n \t\tEOF\n \n \t\tgit repo structure >out 2>err &&\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"528839","messageId":"20251015211213.361797-6-jltobler@gmail.com","threadId":"64187","inReplyTo":"20251015211213.361797-1-jltobler@gmail.com","subject":"[PATCH v5 5/6] builtin/repo: add keyvalue and nul format for structure stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-15T21:12:12Z","receivedAt":"2025-10-15T21:12:24Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"All repository structure stats are outputted in a human-friendly table\nform. This format is not suitable for machine parsing. Add a --format\noption that supports three output modes: `table`, `keyvalue`, and `nul`.\nThe `table` mode is the default format and prints the same table output\nas before.\n\nWith the `keyvalue` mode, each line of output contains a key-value pair\nof a repository stat. The '=' character is used to delimit between keys\nand values. The `nul` mode is similar to `keyvalue`, but key-values are\ndelimited by a NUL character instead of a newline. Also, instead of a\n'=' character to delimit between keys and values, a newline character is\nused. This allows stat values to support special characters without\nhaving to cquote them. These two new modes provides output that is more\nmachine-friendly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc | 25 +++++++++++++++--\n builtin/repo.c              | 55 ++++++++++++++++++++++++++++++++++---\n t/t1901-repo-structure.sh   | 33 ++++++++++++++++++++++\n 3 files changed, 106 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex ae62d2415f..ce43cb19c8 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -9,7 +9,7 @@ SYNOPSIS\n --------\n [synopsis]\n git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n-git repo structure\n+git repo structure [--format=(table|keyvalue|nul)]\n \n DESCRIPTION\n -----------\n@@ -44,7 +44,7 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n-`structure`::\n+`structure [--format=(table|keyvalue|nul)]`::\n \tRetrieve statistics about the current repository structure. The\n \tfollowing kinds of information are reported:\n +\n@@ -52,7 +52,26 @@ supported:\n * Reachable object counts categorized by type\n \n +\n-The table output format may change and is not intended for machine parsing.\n+The output format can be chosen through the flag `--format`. Three formats are\n+supported:\n++\n+`table`:::\n+\tOutputs repository stats in a human-friendly table. This format may\n+\tchange and is not intended for machine parsing. This is the default\n+\tformat.\n+\n+`keyvalue`:::\n+\tEach line of output contains a key-value pair for a repository stat.\n+\tThe '=' character is used to delimit between the key and the value.\n+\tValues containing \"unusual\" characters are quoted as explained for the\n+\tconfiguration variable `core.quotePath` (see linkgit:git-config[1]).\n+\n+`nul`:::\n+\tSimilar to `keyvalue`, but uses a NUL character to delimit between\n+\tkey-value pairs instead of a newline. Also uses a newline character as\n+\tthe delimiter between the key and value instead of '='. Unlike the\n+\t`keyvalue` format, values containing \"unusual\" characters are never\n+\tquoted.\n \n INFO KEYS\n ---------\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 0bc3c1e458..6bf93b6da8 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -15,13 +15,14 @@\n \n static const char *const repo_usage[] = {\n \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n-\t\"git repo structure\",\n+\t\"git repo structure [--format=(table|keyvalue|nul)]\",\n \tNULL\n };\n \n typedef int get_value_fn(struct repository *repo, struct strbuf *buf);\n \n enum output_format {\n+\tFORMAT_TABLE,\n \tFORMAT_KEYVALUE,\n \tFORMAT_NUL_TERMINATED,\n };\n@@ -136,6 +137,8 @@ static int parse_format_cb(const struct option *opt,\n \t\t*format = FORMAT_NUL_TERMINATED;\n \telse if (!strcmp(arg, \"keyvalue\"))\n \t\t*format = FORMAT_KEYVALUE;\n+\telse if (!strcmp(arg, \"table\"))\n+\t\t*format = FORMAT_TABLE;\n \telse\n \t\tdie(_(\"invalid format '%s'\"), arg);\n \n@@ -158,6 +161,8 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n+\tif (format != FORMAT_KEYVALUE && format != FORMAT_NUL_TERMINATED)\n+\t\tdie(_(\"unsupported output format\"));\n \n \treturn print_fields(argc, argv, repo, format);\n }\n@@ -325,6 +330,30 @@ static void stats_table_clear(struct stats_table *table)\n \tstring_list_clear(&table->rows, 1);\n }\n \n+static void structure_keyvalue_print(struct repo_structure *stats,\n+\t\t\t\t     char key_delim, char value_delim)\n+{\n+\tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->refs.branches, value_delim);\n+\tprintf(\"references.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->refs.tags, value_delim);\n+\tprintf(\"references.remotes.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->refs.remotes, value_delim);\n+\tprintf(\"references.others.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->refs.others, value_delim);\n+\n+\tprintf(\"objects.commits.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.commits, value_delim);\n+\tprintf(\"objects.trees.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.trees, value_delim);\n+\tprintf(\"objects.blobs.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.blobs, value_delim);\n+\tprintf(\"objects.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.tags, value_delim);\n+\n+\tfflush(stdout);\n+}\n+\n static void structure_count_references(struct ref_stats *stats,\n \t\t\t\t       struct ref_array *refs)\n {\n@@ -411,10 +440,16 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n \tstruct stats_table table = {\n \t\t.rows = STRING_LIST_INIT_DUP,\n \t};\n+\tenum output_format format = FORMAT_TABLE;\n \tstruct repo_structure stats = { 0 };\n \tstruct ref_array refs = { 0 };\n \tstruct rev_info revs;\n-\tstruct option options[] = { 0 };\n+\tstruct option options[] = {\n+\t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n+\t\t\t       N_(\"output format\"),\n+\t\t\t       PARSE_OPT_NONEG, parse_format_cb),\n+\t\tOPT_END()\n+\t};\n \n \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n \tif (argc)\n@@ -427,8 +462,20 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n \tstructure_count_references(&stats.refs, &refs);\n \tstructure_count_objects(&stats.objects, &refs, &revs);\n \n-\tstats_table_setup_structure(&table, &stats);\n-\tstats_table_print_structure(&table);\n+\tswitch (format) {\n+\tcase FORMAT_TABLE:\n+\t\tstats_table_setup_structure(&table, &stats);\n+\t\tstats_table_print_structure(&table);\n+\t\tbreak;\n+\tcase FORMAT_KEYVALUE:\n+\t\tstructure_keyvalue_print(&stats, '=', '\\n');\n+\t\tbreak;\n+\tcase FORMAT_NUL_TERMINATED:\n+\t\tstructure_keyvalue_print(&stats, '\\n', '\\0');\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"invalid output format\");\n+\t}\n \n \tstats_table_clear(&table);\n \trelease_revisions(&revs);\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nindex c32cf4e239..14bd8aede5 100755\n--- a/t/t1901-repo-structure.sh\n+++ b/t/t1901-repo-structure.sh\n@@ -73,4 +73,37 @@ test_expect_success 'repository with references and objects' '\n \t)\n '\n \n+test_expect_success 'keyvalue and nul format' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit_bulk 42 &&\n+\t\tgit tag -a foo -m bar &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\t\treferences.branches.count=1\n+\t\treferences.tags.count=1\n+\t\treferences.remotes.count=0\n+\t\treferences.others.count=0\n+\t\tobjects.commits.count=42\n+\t\tobjects.trees.count=42\n+\t\tobjects.blobs.count=42\n+\t\tobjects.tags.count=1\n+\t\tEOF\n+\n+\t\tgit repo structure --format=keyvalue >out 2>err &&\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err &&\n+\n+\t\t# Replace key and value delimiters for nul format.\n+\t\ttr \"\\n=\" \"\\0\\n\" <expect >expect_nul &&\n+\t\tgit repo structure --format=nul >out 2>err &&\n+\n+\t\ttest_cmp expect_nul out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"528840","messageId":"20251015211213.361797-7-jltobler@gmail.com","threadId":"64187","inReplyTo":"20251015211213.361797-1-jltobler@gmail.com","subject":"[PATCH v5 6/6] builtin/repo: add progress meter for structure stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-15T21:12:13Z","receivedAt":"2025-10-15T21:12:25Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"When using the structure subcommand for git-repo(1), evaluating a\nrepository may take some time depending on its shape. Add a progress\nmeter to provide feedback to the user about what is happening. The\nprogress meter is enabled by default when the command is executed from a\ntty. It can also be explicitly enabled/disabled via the --[no-]progress\noption.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/repo.c            | 49 +++++++++++++++++++++++++++++++++------\n t/t1901-repo-structure.sh | 20 ++++++++++++++++\n 2 files changed, 62 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 6bf93b6da8..763da436ad 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -4,6 +4,7 @@\n #include \"environment.h\"\n #include \"parse-options.h\"\n #include \"path-walk.h\"\n+#include \"progress.h\"\n #include \"quote.h\"\n #include \"ref-filter.h\"\n #include \"refs.h\"\n@@ -355,8 +356,16 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n }\n \n static void structure_count_references(struct ref_stats *stats,\n-\t\t\t\t       struct ref_array *refs)\n+\t\t\t\t       struct ref_array *refs,\n+\t\t\t\t       struct repository *repo,\n+\t\t\t\t       int show_progress)\n {\n+\tstruct progress *progress = NULL;\n+\n+\tif (show_progress)\n+\t\tprogress = start_delayed_progress(repo, _(\"Counting references\"),\n+\t\t\t\t\t\t  refs->nr);\n+\n \tfor (int i = 0; i < refs->nr; i++) {\n \t\tstruct ref_array_item *ref = refs->items[i];\n \n@@ -376,13 +385,24 @@ static void structure_count_references(struct ref_stats *stats,\n \t\tdefault:\n \t\t\tBUG(\"unexpected reference type\");\n \t\t}\n+\n+\t\tdisplay_progress(progress, i + 1);\n \t}\n+\n+\tstop_progress(&progress);\n }\n \n+struct count_objects_data {\n+\tstruct object_stats *stats;\n+\tstruct progress *progress;\n+};\n+\n static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\t enum object_type type, void *cb_data)\n {\n-\tstruct object_stats *stats = cb_data;\n+\tstruct count_objects_data *data = cb_data;\n+\tstruct object_stats *stats = data->stats;\n+\tsize_t object_count;\n \n \tswitch (type) {\n \tcase OBJ_TAG:\n@@ -401,18 +421,24 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\tBUG(\"invalid object type\");\n \t}\n \n+\tobject_count = get_total_object_count(stats);\n+\tdisplay_progress(data->progress, object_count);\n+\n \treturn 0;\n }\n \n static void structure_count_objects(struct object_stats *stats,\n-\t\t\t\t    struct ref_array *refs,\n-\t\t\t\t    struct rev_info *revs)\n+\t\t\t\t    struct ref_array *refs, struct rev_info *revs,\n+\t\t\t\t    struct repository *repo, int show_progress)\n {\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n+\tstruct count_objects_data data = {\n+\t\t.stats = stats,\n+\t};\n \n \tinfo.revs = revs;\n \tinfo.path_fn = count_objects;\n-\tinfo.path_fn_data = stats;\n+\tinfo.path_fn_data = &data;\n \n \tfor (int i = 0; i < refs->nr; i++) {\n \t\tstruct ref_array_item *ref = refs->items[i];\n@@ -429,8 +455,12 @@ static void structure_count_objects(struct object_stats *stats,\n \t\t}\n \t}\n \n+\tif (show_progress)\n+\t\tdata.progress = start_delayed_progress(repo, _(\"Counting objects\"), 0);\n+\n \twalk_objects_by_path(&info);\n \tpath_walk_info_clear(&info);\n+\tstop_progress(&data.progress);\n }\n \n static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n@@ -444,10 +474,12 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n \tstruct repo_structure stats = { 0 };\n \tstruct ref_array refs = { 0 };\n \tstruct rev_info revs;\n+\tint show_progress = -1;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n \t\t\t       N_(\"output format\"),\n \t\t\t       PARSE_OPT_NONEG, parse_format_cb),\n+\t\tOPT_BOOL(0, \"progress\", &show_progress, N_(\"show progress\")),\n \t\tOPT_END()\n \t};\n \n@@ -459,8 +491,11 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n \tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n \t\tdie(_(\"unable to filter refs\"));\n \n-\tstructure_count_references(&stats.refs, &refs);\n-\tstructure_count_objects(&stats.objects, &refs, &revs);\n+\tif (show_progress < 0)\n+\t\tshow_progress = isatty(2);\n+\n+\tstructure_count_references(&stats.refs, &refs, repo, show_progress);\n+\tstructure_count_objects(&stats.objects, &refs, &revs, repo, show_progress);\n \n \tswitch (format) {\n \tcase FORMAT_TABLE:\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nindex 14bd8aede5..5f513feadb 100755\n--- a/t/t1901-repo-structure.sh\n+++ b/t/t1901-repo-structure.sh\n@@ -106,4 +106,24 @@ test_expect_success 'keyvalue and nul format' '\n \t)\n '\n \n+test_expect_success 'progress meter option' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit foo &&\n+\n+\t\tGIT_PROGRESS_DELAY=0 git repo structure --progress >out 2>err &&\n+\n+\t\ttest_file_not_empty out &&\n+\t\ttest_grep \"Counting references: 100% (2/2), done.\" err &&\n+\t\ttest_grep \"Counting objects: 3, done.\" err &&\n+\n+\t\tGIT_PROGRESS_DELAY=0 git repo structure --no-progress >out 2>err &&\n+\n+\t\ttest_file_not_empty out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"528948","messageId":"aPDPyEVOTSU6NuOH@pks.im","threadId":"64187","inReplyTo":"20251015211213.361797-4-jltobler@gmail.com","subject":"Re: [PATCH v5 3/6] builtin/repo: introduce structure subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-16T10:58:16Z","receivedAt":"2025-10-16T10:58:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Oct 15, 2025 at 04:12:10PM -0500, Justin Tobler wrote:\n> diff --git a/builtin/repo.c b/builtin/repo.c\n> index eeeab8fbd2..4575cf9467 100644\n> --- a/builtin/repo.c\n> +++ b/builtin/repo.c\n> @@ -156,12 +160,201 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n[snip]\n> +static void structure_count_references(struct ref_stats *stats,\n> +\t\t\t\t       struct ref_array *refs)\n> +{\n> +\tfor (int i = 0; i < refs->nr; i++) {\n> +\t\tstruct ref_array_item *ref = refs->items[i];\n> +\n> +\t\tswitch (ref->kind) {\n> +\t\tcase FILTER_REFS_BRANCHES:\n> +\t\t\tstats->branches++;\n> +\t\t\tbreak;\n> +\t\tcase FILTER_REFS_REMOTES:\n> +\t\t\tstats->remotes++;\n> +\t\t\tbreak;\n> +\t\tcase FILTER_REFS_TAGS:\n> +\t\t\tstats->tags++;\n> +\t\t\tbreak;\n> +\t\tcase FILTER_REFS_OTHERS:\n> +\t\t\tstats->others++;\n> +\t\t\tbreak;\n> +\t\tdefault:\n> +\t\t\tBUG(\"unexpected reference type\");\n> +\t\t}\n> +\t}\n> +}\n> +\n> +static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n> +\t\t\t      struct repository *repo UNUSED)\n> +{\n> +\tstruct ref_filter filter = REF_FILTER_INIT;\n> +\tstruct stats_table table = {\n> +\t\t.rows = STRING_LIST_INIT_DUP,\n> +\t};\n> +\tstruct ref_stats stats = { 0 };\n> +\tstruct ref_array refs = { 0 };\n> +\tstruct option options[] = { 0 };\n> +\n> +\targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n> +\tif (argc)\n> +\t\tusage(_(\"too many arguments\"));\n> +\n> +\tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n> +\t\tdie(_(\"unable to filter refs\"));\n> +\n> +\tstructure_count_references(&stats, &refs);\n\nI only noticed this when taking a look at the last patch that introduces\na progress meter, but I think we should change how we count references.\nThe way you do it here means that we have to temporarily store all refs\nin an array, which is completely unnecessary and thus a waste of memory.\nFurthermore, the resulting progress meter will be somewhat useless\nbecause it only starts counting _after_ we have enumerated all\nreferences already. The second phase where we basically just classify\nthe refs by type is going to be orders of magnitude faster and probably\nnot noticeable even with millions of refs.\n\nInstead, I think we should use e.g. `refs_for_each_ref()` and count them\nin the callback function. This means we don't have to store them\nanymore, and also the progress meter becomes more useful.\n\nThe only downside is that we cannot set up the progress meter with an\nupper limit. But I think that is an acceptable tradeoff.\n\nThe other patches all look good to me, thanks!\n\nPatrick\n"},{"id":"529301","messageId":"4csjjw5sin2luhsl2pq5g3ra56r463h6arc2vwfnitwqhhrww7@s6y7hhvgzwui","threadId":"64187","inReplyTo":"aPDPyEVOTSU6NuOH@pks.im","subject":"Re: [PATCH v5 3/6] builtin/repo: introduce structure subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-21T16:04:46Z","receivedAt":"2025-10-21T16:04:51Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/10/16 12:58PM, Patrick Steinhardt wrote:\n> I only noticed this when taking a look at the last patch that introduces\n> a progress meter, but I think we should change how we count references.\n> The way you do it here means that we have to temporarily store all refs\n> in an array, which is completely unnecessary and thus a waste of memory.\n> Furthermore, the resulting progress meter will be somewhat useless\n> because it only starts counting _after_ we have enumerated all\n> references already. The second phase where we basically just classify\n> the refs by type is going to be orders of magnitude faster and probably\n> not noticeable even with millions of refs.\n> \n> Instead, I think we should use e.g. `refs_for_each_ref()` and count them\n> in the callback function. This means we don't have to store them\n> anymore, and also the progress meter becomes more useful.\n\nOne nice thing about `filter_refs()` is that we can easily set it up to\nexclude certain sets of references. In the future, I do forsee options\nbeing introduced that restrict what references we use when performing\nthis operation. We really only need to iterate through the references\nonce though to count references and append OIDs in preparation for the\npath walk. So I agree that storing all the references in an array is\nquite wasteful.\n\nIn the next version, I'll update to use `refs_for_each_ref()` and set up\na callback that counts references and appends their OIDs in preparation\nfor the path walk in a single iteration. When we introduce the ability\nto restrict references, we could export `do_filter_refs()` via\n\"ref-filter.h\" and be able to reuse the existing filtering machinery.\n\n> The only downside is that we cannot set up the progress meter with an\n> upper limit. But I think that is an acceptable tradeoff.\n\nI agree that this is only a minor downside and sounds reasonable.\n\n-Justin\n"},{"id":"529311","messageId":"20251021182601.2687284-2-jltobler@gmail.com","threadId":"64187","inReplyTo":"20251021182601.2687284-1-jltobler@gmail.com","subject":"[PATCH v6 1/7] builtin/repo: rename repo_info() to cmd_repo_info()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-21T18:25:55Z","receivedAt":"2025-10-21T18:26:36Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Subcommand functions are often prefixed with `cmd_` to denote that they\nare an entrypoint. Rename repo_info() to cmd_repo_info() accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/repo.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex bbb0966f2d..eeeab8fbd2 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -136,8 +136,8 @@ static int parse_format_cb(const struct option *opt,\n \treturn 0;\n }\n \n-static int repo_info(int argc, const char **argv, const char *prefix,\n-\t\t     struct repository *repo)\n+static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n+\t\t\t struct repository *repo)\n {\n \tenum output_format format = FORMAT_KEYVALUE;\n \tstruct option options[] = {\n@@ -161,7 +161,7 @@ int cmd_repo(int argc, const char **argv, const char *prefix,\n {\n \tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n-\t\tOPT_SUBCOMMAND(\"info\", &fn, repo_info),\n+\t\tOPT_SUBCOMMAND(\"info\", &fn, cmd_repo_info),\n \t\tOPT_END()\n \t};\n \n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"529312","messageId":"20251021182601.2687284-1-jltobler@gmail.com","threadId":"64187","inReplyTo":"20251015211213.361797-1-jltobler@gmail.com","subject":"[PATCH v6 0/7] builtin/repo: introduce structure subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-21T18:25:54Z","receivedAt":"2025-10-21T18:26:36Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Greetings,\n\nThe structure of a repository's history can have huge impacts on the\nperformance and health of the repository itself. Currently, Git lacks a\nmeans to surface repository metrics regarding its structure/shape via a\nsingle command. Acquiring this information requires users to be familiar\nwith the relevant data points and the various Git commands required to\nsurface them. To fill this gap, supplemental tools such as git-sizer(1)\nhave been developed.\n\nTo allow users to more readily identify repository structure related\ninformation, introduce the \"structure\" subcommand in git-repo(1). The\ngoal of this subcommand is to eventually provide similar functionality\nto git-sizer(1), but natively in Git.\n\nIn this initial version, the \"structure\" subcommand only surfaces counts\nof the various reference and object types in a repository. In a\nfollow-up series, I would like to introduce additional data points that\nare present in git-sizer(1) such as largest objects, combined object\nsizes by type, and other general repository shape information.\n\nSome other general features that would be nice to introduce eventually:\n\n- A \"level of concern\" meter for reported stats. This could indicate to\n  users which stats may be worth looking into further.\n- Links to OIDs of interesting objects that correspond to certain stats.\n- Options to limit which references to use when evaluating the\n  repository.\n\nChanges since V5:\n- Instead of using `filter_refs()` to get an array of all references, we\n  now use `refs_for_each_ref()` to count references, and setup OIDs for\n  the path walk, in place. Doing this not only allows us to avoid\n  wasting memory storing all the reference info, but also to display\n  progress info to the user while iterating across the references\n  initially.\n- Add a prepatory patch to export `ref_kind_from_refname()` via\n  \"ref_filter.h\" so we can reuse logic to categorize references while\n  counting.\n\nChanges since V4:\n- The subcommand was renamed from \"stats\" to \"structure\". This was done\n  to define a more narrow scope for the types of stats that would be\n  outtputted. This also also for other types of stat-related subcommands\n  to be implemented in the future that may cover different aspects of\n  the repository.\n- Table column widths are now stored as ints to avoid the unneeded back\n  and forth conversions.\n- Dropped the clang-format patch as it has been upstreamed separately.\n- Updated commit messages accordingly.\n\nChanges since V3:\n- Changed from using strlen() to utf8_strlen() to take into\n  consideration that translatable strings may have characters that are\n  more than one byte.\n\nChanges since V2:\n- Added clang-format patch to address false postive triggered in this\n  series.\n- Use varargs for stats_table_add() family of functions.\n- Print to stdout directly instead of using strbuf.\n- Add parse_option() earlier in the series.\n- Use start_delayed_progress() instead of start_progress().\n- Add test to validate --[no-]progress options.\n- Some other small fixes.\n\nChanges since V1:\n- Translatable terms displayed in the table have formatting separated\n  out.\n- Squashed the `keyvalue` and `nul` output format patches into one.\n- Added a progress meter to provide users with more feedback.\n- Updated docs to outline to outline reported data in a bulleted list.\n- Combined similar tests together to reduce repetitive setup.\n- Added patch to improve ref-filter interface so we don't have to create\n  a dummy patterns array.\n- Many other renames and cleanups to improve patch clarity.\n\nThanks,\n-Justin\n\nJustin Tobler (7):\n  builtin/repo: rename repo_info() to cmd_repo_info()\n  ref-filter: allow NULL filter pattern\n  ref-filter: export ref_kind_from_refname()\n  builtin/repo: introduce structure subcommand\n  builtin/repo: add object counts in structure output\n  builtin/repo: add keyvalue and nul format for structure stats\n  builtin/repo: add progress meter for structure stats\n\n Documentation/git-repo.adoc |  30 +++\n builtin/repo.c              | 380 +++++++++++++++++++++++++++++++++++-\n ref-filter.c                |   6 +-\n ref-filter.h                |   2 +\n t/meson.build               |   1 +\n t/t1901-repo-structure.sh   | 129 ++++++++++++\n 6 files changed, 542 insertions(+), 6 deletions(-)\n create mode 100755 t/t1901-repo-structure.sh\n\nRange-diff against v5:\n1:  ed04168562 = 1:  ed04168562 builtin/repo: rename repo_info() to cmd_repo_info()\n2:  6aa76d1323 = 2:  6aa76d1323 ref-filter: allow NULL filter pattern\n-:  ---------- > 3:  aee696c69b ref-filter: export ref_kind_from_refname()\n3:  eda1afbe3d ! 4:  4ad599d0ec builtin/repo: introduce structure subcommand\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\tva_end(ap);\n     +}\n     +\n    ++static inline size_t get_total_reference_count(struct ref_stats *stats)\n    ++{\n    ++\treturn stats->branches + stats->remotes + stats->tags + stats->others;\n    ++}\n    ++\n     +static void stats_table_setup_structure(struct stats_table *table,\n     +\t\t\t\t\tstruct ref_stats *refs)\n     +{\n     +\tsize_t ref_total;\n     +\n    -+\tref_total = refs->branches + refs->remotes + refs->tags + refs->others;\n    ++\tref_total = get_total_reference_count(refs);\n     +\tstats_table_addf(table, \"* %s\", _(\"References\"));\n     +\tstats_table_count_addf(table, ref_total, \"  * %s\", _(\"Count\"));\n     +\tstats_table_count_addf(table, refs->branches, \"    * %s\", _(\"Branches\"));\n    @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n     +\tstring_list_clear(&table->rows, 1);\n     +}\n     +\n    -+static void structure_count_references(struct ref_stats *stats,\n    -+\t\t\t\t       struct ref_array *refs)\n    ++static int count_references(const char *refname,\n    ++\t\t\t    const char *referent UNUSED,\n    ++\t\t\t    const struct object_id *oid UNUSED,\n    ++\t\t\t    int flags UNUSED, void *cb_data)\n     +{\n    -+\tfor (int i = 0; i < refs->nr; i++) {\n    -+\t\tstruct ref_array_item *ref = refs->items[i];\n    -+\n    -+\t\tswitch (ref->kind) {\n    -+\t\tcase FILTER_REFS_BRANCHES:\n    -+\t\t\tstats->branches++;\n    -+\t\t\tbreak;\n    -+\t\tcase FILTER_REFS_REMOTES:\n    -+\t\t\tstats->remotes++;\n    -+\t\t\tbreak;\n    -+\t\tcase FILTER_REFS_TAGS:\n    -+\t\t\tstats->tags++;\n    -+\t\t\tbreak;\n    -+\t\tcase FILTER_REFS_OTHERS:\n    -+\t\t\tstats->others++;\n    -+\t\t\tbreak;\n    -+\t\tdefault:\n    -+\t\t\tBUG(\"unexpected reference type\");\n    -+\t\t}\n    ++\tstruct ref_stats *stats = cb_data;\n    ++\n    ++\tswitch (ref_kind_from_refname(refname)) {\n    ++\tcase FILTER_REFS_BRANCHES:\n    ++\t\tstats->branches++;\n    ++\t\tbreak;\n    ++\tcase FILTER_REFS_REMOTES:\n    ++\t\tstats->remotes++;\n    ++\t\tbreak;\n    ++\tcase FILTER_REFS_TAGS:\n    ++\t\tstats->tags++;\n    ++\t\tbreak;\n    ++\tcase FILTER_REFS_OTHERS:\n    ++\t\tstats->others++;\n    ++\t\tbreak;\n    ++\tdefault:\n    ++\t\tBUG(\"unexpected reference type\");\n     +\t}\n    ++\n    ++\treturn 0;\n    ++}\n    ++\n    ++static void structure_count_references(struct ref_stats *stats,\n    ++\t\t\t\t       struct repository *repo)\n    ++{\n    ++\trefs_for_each_ref(get_main_ref_store(repo), count_references, &stats);\n     +}\n     +\n     +static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n    -+\t\t\t      struct repository *repo UNUSED)\n    ++\t\t\t      struct repository *repo)\n     +{\n    -+\tstruct ref_filter filter = REF_FILTER_INIT;\n     +\tstruct stats_table table = {\n     +\t\t.rows = STRING_LIST_INIT_DUP,\n     +\t};\n     +\tstruct ref_stats stats = { 0 };\n    -+\tstruct ref_array refs = { 0 };\n     +\tstruct option options[] = { 0 };\n     +\n     +\targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n     +\tif (argc)\n     +\t\tusage(_(\"too many arguments\"));\n     +\n    -+\tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n    -+\t\tdie(_(\"unable to filter refs\"));\n    -+\n    -+\tstructure_count_references(&stats, &refs);\n    ++\tstructure_count_references(&stats, repo);\n     +\n     +\tstats_table_setup_structure(&table, &stats);\n     +\tstats_table_print_structure(&table);\n     +\n     +\tstats_table_clear(&table);\n    -+\tref_array_clear(&refs);\n     +\n     +\treturn 0;\n     +}\n4:  503af885d3 ! 5:  4d37f65331 builtin/repo: add object counts in structure output\n    @@ builtin/repo.c: struct ref_stats {\n      struct stats_table {\n      \tstruct string_list rows;\n      \n    -@@ builtin/repo.c: static void stats_table_count_addf(struct stats_table *table, size_t value,\n    - \tva_end(ap);\n    +@@ builtin/repo.c: static inline size_t get_total_reference_count(struct ref_stats *stats)\n    + \treturn stats->branches + stats->remotes + stats->tags + stats->others;\n      }\n      \n     +static inline size_t get_total_object_count(struct object_stats *stats)\n    @@ builtin/repo.c: static void stats_table_count_addf(struct stats_table *table, si\n     +\tsize_t object_total;\n      \tsize_t ref_total;\n      \n    - \tref_total = refs->branches + refs->remotes + refs->tags + refs->others;\n    + \tref_total = get_total_reference_count(refs);\n     @@ builtin/repo.c: static void stats_table_setup_structure(struct stats_table *table,\n      \tstats_table_count_addf(table, refs->tags, \"    * %s\", _(\"Tags\"));\n      \tstats_table_count_addf(table, refs->remotes, \"    * %s\", _(\"Remotes\"));\n    @@ builtin/repo.c: static void stats_table_setup_structure(struct stats_table *tabl\n      }\n      \n      static void stats_table_print_structure(const struct stats_table *table)\n    -@@ builtin/repo.c: static void structure_count_references(struct ref_stats *stats,\n    +@@ builtin/repo.c: static void stats_table_clear(struct stats_table *table)\n    + \tstring_list_clear(&table->rows, 1);\n    + }\n    + \n    ++struct count_references_data {\n    ++\tstruct ref_stats *stats;\n    ++\tstruct rev_info *revs;\n    ++};\n    ++\n    + static int count_references(const char *refname,\n    + \t\t\t    const char *referent UNUSED,\n    +-\t\t\t    const struct object_id *oid UNUSED,\n    ++\t\t\t    const struct object_id *oid,\n    + \t\t\t    int flags UNUSED, void *cb_data)\n    + {\n    +-\tstruct ref_stats *stats = cb_data;\n    ++\tstruct count_references_data *data = cb_data;\n    ++\tstruct ref_stats *stats = data->stats;\n    + \n    + \tswitch (ref_kind_from_refname(refname)) {\n    + \tcase FILTER_REFS_BRANCHES:\n    +@@ builtin/repo.c: static int count_references(const char *refname,\n    + \t\tBUG(\"unexpected reference type\");\n      \t}\n    + \n    ++\t/*\n    ++\t * While iterating through references for counting, also add OIDs in\n    ++\t * preparation for the path walk.\n    ++\t */\n    ++\tadd_pending_oid(data->revs, NULL, oid, 0);\n    ++\n    + \treturn 0;\n      }\n      \n    + static void structure_count_references(struct ref_stats *stats,\n    ++\t\t\t\t       struct rev_info *revs,\n    + \t\t\t\t       struct repository *repo)\n    + {\n    +-\trefs_for_each_ref(get_main_ref_store(repo), count_references, &stats);\n    ++\tstruct count_references_data data = {\n    ++\t\t.stats = stats,\n    ++\t\t.revs = revs,\n    ++\t};\n    ++\n    ++\trefs_for_each_ref(get_main_ref_store(repo), count_references, &data);\n    ++}\n    ++\n    ++\n     +static int count_objects(const char *path UNUSED, struct oid_array *oids,\n     +\t\t\t enum object_type type, void *cb_data)\n     +{\n    @@ builtin/repo.c: static void structure_count_references(struct ref_stats *stats,\n     +}\n     +\n     +static void structure_count_objects(struct object_stats *stats,\n    -+\t\t\t\t    struct ref_array *refs,\n     +\t\t\t\t    struct rev_info *revs)\n     +{\n     +\tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n    @@ builtin/repo.c: static void structure_count_references(struct ref_stats *stats,\n     +\tinfo.path_fn = count_objects;\n     +\tinfo.path_fn_data = stats;\n     +\n    -+\tfor (int i = 0; i < refs->nr; i++) {\n    -+\t\tstruct ref_array_item *ref = refs->items[i];\n    -+\n    -+\t\tswitch (ref->kind) {\n    -+\t\tcase FILTER_REFS_BRANCHES:\n    -+\t\tcase FILTER_REFS_TAGS:\n    -+\t\tcase FILTER_REFS_REMOTES:\n    -+\t\tcase FILTER_REFS_OTHERS:\n    -+\t\t\tadd_pending_oid(revs, NULL, &ref->objectname, 0);\n    -+\t\t\tbreak;\n    -+\t\tdefault:\n    -+\t\t\tBUG(\"unexpected reference type\");\n    -+\t\t}\n    -+\t}\n    -+\n     +\twalk_objects_by_path(&info);\n     +\tpath_walk_info_clear(&info);\n    -+}\n    -+\n    + }\n    + \n      static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n    --\t\t\t      struct repository *repo UNUSED)\n    -+\t\t\t      struct repository *repo)\n    - {\n    - \tstruct ref_filter filter = REF_FILTER_INIT;\n    +@@ builtin/repo.c: static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n      \tstruct stats_table table = {\n      \t\t.rows = STRING_LIST_INIT_DUP,\n      \t};\n     -\tstruct ref_stats stats = { 0 };\n     +\tstruct repo_structure stats = { 0 };\n    - \tstruct ref_array refs = { 0 };\n     +\tstruct rev_info revs;\n      \tstruct option options[] = { 0 };\n      \n    @@ builtin/repo.c: static void structure_count_references(struct ref_stats *stats,\n      \tif (argc)\n      \t\tusage(_(\"too many arguments\"));\n      \n    +-\tstructure_count_references(&stats, repo);\n     +\trepo_init_revisions(repo, &revs, prefix);\n    - \tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n    - \t\tdie(_(\"unable to filter refs\"));\n    - \n    --\tstructure_count_references(&stats, &refs);\n    -+\tstructure_count_references(&stats.refs, &refs);\n    -+\tstructure_count_objects(&stats.objects, &refs, &revs);\n    ++\n    ++\tstructure_count_references(&stats.refs, &revs, repo);\n    ++\tstructure_count_objects(&stats.objects, &revs);\n      \n      \tstats_table_setup_structure(&table, &stats);\n      \tstats_table_print_structure(&table);\n      \n      \tstats_table_clear(&table);\n     +\trelease_revisions(&revs);\n    - \tref_array_clear(&refs);\n      \n      \treturn 0;\n    + }\n     \n      ## t/t1901-repo-structure.sh ##\n     @@ t/t1901-repo-structure.sh: test_expect_success 'empty repository' '\n5:  b336578445 ! 6:  3d42929434 builtin/repo: add keyvalue and nul format for structure stats\n    @@ builtin/repo.c: static void stats_table_clear(struct stats_table *table)\n     +\tfflush(stdout);\n     +}\n     +\n    - static void structure_count_references(struct ref_stats *stats,\n    - \t\t\t\t       struct ref_array *refs)\n    - {\n    + struct count_references_data {\n    + \tstruct ref_stats *stats;\n    + \tstruct rev_info *revs;\n     @@ builtin/repo.c: static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n      \tstruct stats_table table = {\n      \t\t.rows = STRING_LIST_INIT_DUP,\n      \t};\n     +\tenum output_format format = FORMAT_TABLE;\n      \tstruct repo_structure stats = { 0 };\n    - \tstruct ref_array refs = { 0 };\n      \tstruct rev_info revs;\n     -\tstruct option options[] = { 0 };\n     +\tstruct option options[] = {\n    @@ builtin/repo.c: static int cmd_repo_structure(int argc, const char **argv, const\n      \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n      \tif (argc)\n     @@ builtin/repo.c: static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n    - \tstructure_count_references(&stats.refs, &refs);\n    - \tstructure_count_objects(&stats.objects, &refs, &revs);\n    + \tstructure_count_references(&stats.refs, &revs, repo);\n    + \tstructure_count_objects(&stats.objects, &revs);\n      \n     -\tstats_table_setup_structure(&table, &stats);\n     -\tstats_table_print_structure(&table);\n6:  70c0b7e200 ! 7:  67d7d8eb8c builtin/repo: add progress meter for structure stats\n    @@ builtin/repo.c\n      #include \"ref-filter.h\"\n      #include \"refs.h\"\n     @@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stats,\n    + struct count_references_data {\n    + \tstruct ref_stats *stats;\n    + \tstruct rev_info *revs;\n    ++\tstruct progress *progress;\n    + };\n    + \n    + static int count_references(const char *refname,\n    +@@ builtin/repo.c: static int count_references(const char *refname,\n    + {\n    + \tstruct count_references_data *data = cb_data;\n    + \tstruct ref_stats *stats = data->stats;\n    ++\tsize_t ref_count;\n    + \n    + \tswitch (ref_kind_from_refname(refname)) {\n    + \tcase FILTER_REFS_BRANCHES:\n    +@@ builtin/repo.c: static int count_references(const char *refname,\n    + \t */\n    + \tadd_pending_oid(data->revs, NULL, oid, 0);\n    + \n    ++\tref_count = get_total_reference_count(stats);\n    ++\tdisplay_progress(data->progress, ref_count);\n    ++\n    + \treturn 0;\n      }\n      \n      static void structure_count_references(struct ref_stats *stats,\n    --\t\t\t\t       struct ref_array *refs)\n    -+\t\t\t\t       struct ref_array *refs,\n    + \t\t\t\t       struct rev_info *revs,\n    +-\t\t\t\t       struct repository *repo)\n     +\t\t\t\t       struct repository *repo,\n     +\t\t\t\t       int show_progress)\n      {\n    -+\tstruct progress *progress = NULL;\n    -+\n    -+\tif (show_progress)\n    -+\t\tprogress = start_delayed_progress(repo, _(\"Counting references\"),\n    -+\t\t\t\t\t\t  refs->nr);\n    -+\n    - \tfor (int i = 0; i < refs->nr; i++) {\n    - \t\tstruct ref_array_item *ref = refs->items[i];\n    + \tstruct count_references_data data = {\n    + \t\t.stats = stats,\n    + \t\t.revs = revs,\n    + \t};\n      \n    -@@ builtin/repo.c: static void structure_count_references(struct ref_stats *stats,\n    - \t\tdefault:\n    - \t\t\tBUG(\"unexpected reference type\");\n    - \t\t}\n    -+\n    -+\t\tdisplay_progress(progress, i + 1);\n    - \t}\n    ++\tif (show_progress)\n    ++\t\tdata.progress = start_delayed_progress(repo,\n    ++\t\t\t\t\t\t       _(\"Counting references\"), 0);\n     +\n    -+\tstop_progress(&progress);\n    + \trefs_for_each_ref(get_main_ref_store(repo), count_references, &data);\n    ++\tstop_progress(&data.progress);\n      }\n      \n     +struct count_objects_data {\n     +\tstruct object_stats *stats;\n     +\tstruct progress *progress;\n     +};\n    -+\n    + \n      static int count_objects(const char *path UNUSED, struct oid_array *oids,\n      \t\t\t enum object_type type, void *cb_data)\n      {\n    @@ builtin/repo.c: static int count_objects(const char *path UNUSED, struct oid_arr\n      }\n      \n      static void structure_count_objects(struct object_stats *stats,\n    --\t\t\t\t    struct ref_array *refs,\n     -\t\t\t\t    struct rev_info *revs)\n    -+\t\t\t\t    struct ref_array *refs, struct rev_info *revs,\n    ++\t\t\t\t    struct rev_info *revs,\n     +\t\t\t\t    struct repository *repo, int show_progress)\n      {\n      \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n    @@ builtin/repo.c: static int count_objects(const char *path UNUSED, struct oid_arr\n      \tinfo.path_fn = count_objects;\n     -\tinfo.path_fn_data = stats;\n     +\tinfo.path_fn_data = &data;\n    - \n    - \tfor (int i = 0; i < refs->nr; i++) {\n    - \t\tstruct ref_array_item *ref = refs->items[i];\n    -@@ builtin/repo.c: static void structure_count_objects(struct object_stats *stats,\n    - \t\t}\n    - \t}\n    - \n    ++\n     +\tif (show_progress)\n     +\t\tdata.progress = start_delayed_progress(repo, _(\"Counting objects\"), 0);\n    -+\n    + \n      \twalk_objects_by_path(&info);\n      \tpath_walk_info_clear(&info);\n     +\tstop_progress(&data.progress);\n    @@ builtin/repo.c: static void structure_count_objects(struct object_stats *stats,\n      \n      static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n     @@ builtin/repo.c: static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n    + \tenum output_format format = FORMAT_TABLE;\n      \tstruct repo_structure stats = { 0 };\n    - \tstruct ref_array refs = { 0 };\n      \tstruct rev_info revs;\n     +\tint show_progress = -1;\n      \tstruct option options[] = {\n    @@ builtin/repo.c: static int cmd_repo_structure(int argc, const char **argv, const\n      \t};\n      \n     @@ builtin/repo.c: static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n    - \tif (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))\n    - \t\tdie(_(\"unable to filter refs\"));\n      \n    --\tstructure_count_references(&stats.refs, &refs);\n    --\tstructure_count_objects(&stats.objects, &refs, &revs);\n    + \trepo_init_revisions(repo, &revs, prefix);\n    + \n    +-\tstructure_count_references(&stats.refs, &revs, repo);\n    +-\tstructure_count_objects(&stats.objects, &revs);\n     +\tif (show_progress < 0)\n     +\t\tshow_progress = isatty(2);\n     +\n    -+\tstructure_count_references(&stats.refs, &refs, repo, show_progress);\n    -+\tstructure_count_objects(&stats.objects, &refs, &revs, repo, show_progress);\n    ++\tstructure_count_references(&stats.refs, &revs, repo, show_progress);\n    ++\tstructure_count_objects(&stats.objects, &revs, repo, show_progress);\n      \n      \tswitch (format) {\n      \tcase FORMAT_TABLE:\n    @@ t/t1901-repo-structure.sh: test_expect_success 'keyvalue and nul format' '\n     +\t\tGIT_PROGRESS_DELAY=0 git repo structure --progress >out 2>err &&\n     +\n     +\t\ttest_file_not_empty out &&\n    -+\t\ttest_grep \"Counting references: 100% (2/2), done.\" err &&\n    ++\t\ttest_grep \"Counting references: 2, done.\" err &&\n     +\t\ttest_grep \"Counting objects: 3, done.\" err &&\n     +\n     +\t\tGIT_PROGRESS_DELAY=0 git repo structure --no-progress >out 2>err &&\n\nbase-commit: ca2559c1d630eb4f04cdee2328aaf1c768907a9e\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"529313","messageId":"20251021182601.2687284-3-jltobler@gmail.com","threadId":"64187","inReplyTo":"20251021182601.2687284-1-jltobler@gmail.com","subject":"[PATCH v6 2/7] ref-filter: allow NULL filter pattern","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-21T18:25:56Z","receivedAt":"2025-10-21T18:26:37Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"When setting up `struct ref_filter` for filter_refs(), the\n`name_patterns` field must point to an array of pattern strings even if\nno patterns are required. To improve this interface, treat a NULL\n`name_patterns` field the same as when it points to an empty array.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n ref-filter.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/ref-filter.c b/ref-filter.c\nindex 520d2539c9..2cb5a166d6 100644\n--- a/ref-filter.c\n+++ b/ref-filter.c\n@@ -2664,7 +2664,7 @@ static int match_name_as_path(const char **pattern, const char *refname,\n /* Return 1 if the refname matches one of the patterns, otherwise 0. */\n static int filter_pattern_match(struct ref_filter *filter, const char *refname)\n {\n-\tif (!*filter->name_patterns)\n+\tif (!filter->name_patterns || !*filter->name_patterns)\n \t\treturn 1; /* No pattern always matches */\n \tif (filter->match_as_path)\n \t\treturn match_name_as_path(filter->name_patterns, refname,\n@@ -2751,7 +2751,7 @@ static int for_each_fullref_in_pattern(struct ref_filter *filter,\n \t\treturn for_each_fullref_with_seek(filter, cb, cb_data, 0);\n \t}\n \n-\tif (!filter->name_patterns[0]) {\n+\tif (!filter->name_patterns || !filter->name_patterns[0]) {\n \t\t/* no patterns; we have to look at everything */\n \t\treturn for_each_fullref_with_seek(filter, cb, cb_data, 0);\n \t}\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"529314","messageId":"20251021182601.2687284-4-jltobler@gmail.com","threadId":"64187","inReplyTo":"20251021182601.2687284-1-jltobler@gmail.com","subject":"[PATCH v6 3/7] ref-filter: export ref_kind_from_refname()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-21T18:25:57Z","receivedAt":"2025-10-21T18:26:38Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"When filtering refs, `ref_kind_from_refname()` is used to determine the\nref type. In a subsequent commit, this same logic is reused when\ncounting refs by type. Export the function to prepare for this change.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n ref-filter.c | 2 +-\n ref-filter.h | 2 ++\n 2 files changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/ref-filter.c b/ref-filter.c\nindex 2cb5a166d6..30cc488d8a 100644\n--- a/ref-filter.c\n+++ b/ref-filter.c\n@@ -2833,7 +2833,7 @@ struct ref_array_item *ref_array_push(struct ref_array *array,\n \treturn ref;\n }\n \n-static int ref_kind_from_refname(const char *refname)\n+int ref_kind_from_refname(const char *refname)\n {\n \tunsigned int i;\n \ndiff --git a/ref-filter.h b/ref-filter.h\nindex f22ca94b49..4ed1edf09a 100644\n--- a/ref-filter.h\n+++ b/ref-filter.h\n@@ -135,6 +135,8 @@ struct ref_format {\n \tOPT_STRVEC(0, \"exclude\", &(var)->exclude, \\\n \t\t   N_(\"pattern\"), N_(\"exclude refs which match pattern\"))\n \n+/* Get the reference kind from the provided reference name. */\n+int ref_kind_from_refname(const char *refname);\n /*\n  * API for filtering a set of refs. Based on the type of refs the user\n  * has requested, we iterate through those refs and apply filters\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"529315","messageId":"20251021182601.2687284-5-jltobler@gmail.com","threadId":"64187","inReplyTo":"20251021182601.2687284-1-jltobler@gmail.com","subject":"[PATCH v6 4/7] builtin/repo: introduce structure subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-21T18:25:58Z","receivedAt":"2025-10-21T18:26:39Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The structure of a repository's history can have huge impacts on the\nperformance and health of the repository itself. Currently, Git lacks a\nmeans to surface repository metrics regarding its structure/shape via a\nsingle command. Acquiring this information requires users to be familiar\nwith the relevant data points and the various Git commands required to\nsurface them. To fill this gap, supplemental tools such as git-sizer(1)\nhave been developed.\n\nTo allow users to more readily identify repository structure related\ninformation, introduce the \"structure\" subcommand in git-repo(1). The\ngoal of this subcommand is to eventually provide similar functionality\nto git-sizer(1), but natively in Git.\n\nThe initial version of this command only iterates through all references\nin the repository and tracks the count of branches, tags, remote refs,\nand other reference types. The corresponding information is displayed in\na human-friendly table formatted in a very similar manner to\ngit-sizer(1). The width of each table column is adjusted automatically\nto satisfy the requirements of the widest row contained.\n\nSubsequent commits will surface additional relevant data points to\noutput and also provide other more machine-friendly output formats.\n\nBased-on-patch-by: Derrick Stolee <stolee@gmail.com>\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc |  10 ++\n builtin/repo.c              | 200 ++++++++++++++++++++++++++++++++++++\n t/meson.build               |   1 +\n t/t1901-repo-structure.sh   |  61 +++++++++++\n 4 files changed, 272 insertions(+)\n create mode 100755 t/t1901-repo-structure.sh\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 209afd1b61..8193298dd5 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -9,6 +9,7 @@ SYNOPSIS\n --------\n [synopsis]\n git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n+git repo structure\n \n DESCRIPTION\n -----------\n@@ -43,6 +44,15 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n+`structure`::\n+\tRetrieve statistics about the current repository structure. The\n+\tfollowing kinds of information are reported:\n++\n+* Reference counts categorized by type\n+\n++\n+The table output format may change and is not intended for machine parsing.\n+\n INFO KEYS\n ---------\n In order to obtain a set of values from `git repo info`, you should provide\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex eeeab8fbd2..e77e8db563 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -4,12 +4,16 @@\n #include \"environment.h\"\n #include \"parse-options.h\"\n #include \"quote.h\"\n+#include \"ref-filter.h\"\n #include \"refs.h\"\n #include \"strbuf.h\"\n+#include \"string-list.h\"\n #include \"shallow.h\"\n+#include \"utf8.h\"\n \n static const char *const repo_usage[] = {\n \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n+\t\"git repo structure\",\n \tNULL\n };\n \n@@ -156,12 +160,208 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \treturn print_fields(argc, argv, repo, format);\n }\n \n+struct ref_stats {\n+\tsize_t branches;\n+\tsize_t remotes;\n+\tsize_t tags;\n+\tsize_t others;\n+};\n+\n+struct stats_table {\n+\tstruct string_list rows;\n+\n+\tint name_col_width;\n+\tint value_col_width;\n+};\n+\n+/*\n+ * Holds column data that gets stored for each row.\n+ */\n+struct stats_table_entry {\n+\tchar *value;\n+};\n+\n+static void stats_table_vaddf(struct stats_table *table,\n+\t\t\t      struct stats_table_entry *entry,\n+\t\t\t      const char *format, va_list ap)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct string_list_item *item;\n+\tchar *formatted_name;\n+\tint name_width;\n+\n+\tstrbuf_vaddf(&buf, format, ap);\n+\tformatted_name = strbuf_detach(&buf, NULL);\n+\tname_width = utf8_strwidth(formatted_name);\n+\n+\titem = string_list_append_nodup(&table->rows, formatted_name);\n+\titem->util = entry;\n+\n+\tif (name_width > table->name_col_width)\n+\t\ttable->name_col_width = name_width;\n+\tif (entry) {\n+\t\tint value_width = utf8_strwidth(entry->value);\n+\t\tif (value_width > table->value_col_width)\n+\t\t\ttable->value_col_width = value_width;\n+\t}\n+}\n+\n+static void stats_table_addf(struct stats_table *table, const char *format, ...)\n+{\n+\tva_list ap;\n+\n+\tva_start(ap, format);\n+\tstats_table_vaddf(table, NULL, format, ap);\n+\tva_end(ap);\n+}\n+\n+static void stats_table_count_addf(struct stats_table *table, size_t value,\n+\t\t\t\t   const char *format, ...)\n+{\n+\tstruct stats_table_entry *entry;\n+\tva_list ap;\n+\n+\tCALLOC_ARRAY(entry, 1);\n+\tentry->value = xstrfmt(\"%\" PRIuMAX, (uintmax_t)value);\n+\n+\tva_start(ap, format);\n+\tstats_table_vaddf(table, entry, format, ap);\n+\tva_end(ap);\n+}\n+\n+static inline size_t get_total_reference_count(struct ref_stats *stats)\n+{\n+\treturn stats->branches + stats->remotes + stats->tags + stats->others;\n+}\n+\n+static void stats_table_setup_structure(struct stats_table *table,\n+\t\t\t\t\tstruct ref_stats *refs)\n+{\n+\tsize_t ref_total;\n+\n+\tref_total = get_total_reference_count(refs);\n+\tstats_table_addf(table, \"* %s\", _(\"References\"));\n+\tstats_table_count_addf(table, ref_total, \"  * %s\", _(\"Count\"));\n+\tstats_table_count_addf(table, refs->branches, \"    * %s\", _(\"Branches\"));\n+\tstats_table_count_addf(table, refs->tags, \"    * %s\", _(\"Tags\"));\n+\tstats_table_count_addf(table, refs->remotes, \"    * %s\", _(\"Remotes\"));\n+\tstats_table_count_addf(table, refs->others, \"    * %s\", _(\"Others\"));\n+}\n+\n+static void stats_table_print_structure(const struct stats_table *table)\n+{\n+\tconst char *name_col_title = _(\"Repository structure\");\n+\tconst char *value_col_title = _(\"Value\");\n+\tint name_col_width = utf8_strwidth(name_col_title);\n+\tint value_col_width = utf8_strwidth(value_col_title);\n+\tstruct string_list_item *item;\n+\n+\tif (table->name_col_width > name_col_width)\n+\t\tname_col_width = table->name_col_width;\n+\tif (table->value_col_width > value_col_width)\n+\t\tvalue_col_width = table->value_col_width;\n+\n+\tprintf(\"| %-*s | %-*s |\\n\", name_col_width, name_col_title,\n+\t       value_col_width, value_col_title);\n+\tprintf(\"| \");\n+\tfor (int i = 0; i < name_col_width; i++)\n+\t\tputchar('-');\n+\tprintf(\" | \");\n+\tfor (int i = 0; i < value_col_width; i++)\n+\t\tputchar('-');\n+\tprintf(\" |\\n\");\n+\n+\tfor_each_string_list_item(item, &table->rows) {\n+\t\tstruct stats_table_entry *entry = item->util;\n+\t\tconst char *value = \"\";\n+\n+\t\tif (entry) {\n+\t\t\tstruct stats_table_entry *entry = item->util;\n+\t\t\tvalue = entry->value;\n+\t\t}\n+\n+\t\tprintf(\"| %-*s | %*s |\\n\", name_col_width, item->string,\n+\t\t       value_col_width, value);\n+\t}\n+}\n+\n+static void stats_table_clear(struct stats_table *table)\n+{\n+\tstruct stats_table_entry *entry;\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, &table->rows) {\n+\t\tentry = item->util;\n+\t\tif (entry)\n+\t\t\tfree(entry->value);\n+\t}\n+\n+\tstring_list_clear(&table->rows, 1);\n+}\n+\n+static int count_references(const char *refname,\n+\t\t\t    const char *referent UNUSED,\n+\t\t\t    const struct object_id *oid UNUSED,\n+\t\t\t    int flags UNUSED, void *cb_data)\n+{\n+\tstruct ref_stats *stats = cb_data;\n+\n+\tswitch (ref_kind_from_refname(refname)) {\n+\tcase FILTER_REFS_BRANCHES:\n+\t\tstats->branches++;\n+\t\tbreak;\n+\tcase FILTER_REFS_REMOTES:\n+\t\tstats->remotes++;\n+\t\tbreak;\n+\tcase FILTER_REFS_TAGS:\n+\t\tstats->tags++;\n+\t\tbreak;\n+\tcase FILTER_REFS_OTHERS:\n+\t\tstats->others++;\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"unexpected reference type\");\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void structure_count_references(struct ref_stats *stats,\n+\t\t\t\t       struct repository *repo)\n+{\n+\trefs_for_each_ref(get_main_ref_store(repo), count_references, &stats);\n+}\n+\n+static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n+\t\t\t      struct repository *repo)\n+{\n+\tstruct stats_table table = {\n+\t\t.rows = STRING_LIST_INIT_DUP,\n+\t};\n+\tstruct ref_stats stats = { 0 };\n+\tstruct option options[] = { 0 };\n+\n+\targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n+\tif (argc)\n+\t\tusage(_(\"too many arguments\"));\n+\n+\tstructure_count_references(&stats, repo);\n+\n+\tstats_table_setup_structure(&table, &stats);\n+\tstats_table_print_structure(&table);\n+\n+\tstats_table_clear(&table);\n+\n+\treturn 0;\n+}\n+\n int cmd_repo(int argc, const char **argv, const char *prefix,\n \t     struct repository *repo)\n {\n \tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n \t\tOPT_SUBCOMMAND(\"info\", &fn, cmd_repo_info),\n+\t\tOPT_SUBCOMMAND(\"structure\", &fn, cmd_repo_structure),\n \t\tOPT_END()\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 7974795fe4..9e426f8edc 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -236,6 +236,7 @@ integration_tests = [\n   't1701-racy-split-index.sh',\n   't1800-hook.sh',\n   't1900-repo.sh',\n+  't1901-repo-structure.sh',\n   't2000-conflict-when-checking-files-out.sh',\n   't2002-checkout-cache-u.sh',\n   't2003-checkout-cache-mkdir.sh',\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nnew file mode 100755\nindex 0000000000..e592eea0eb\n--- /dev/null\n+++ b/t/t1901-repo-structure.sh\n@@ -0,0 +1,61 @@\n+#!/bin/sh\n+\n+test_description='test git repo structure'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'empty repository' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\t| Repository structure | Value |\n+\t\t| -------------------- | ----- |\n+\t\t| * References         |       |\n+\t\t|   * Count            |     0 |\n+\t\t|     * Branches       |     0 |\n+\t\t|     * Tags           |     0 |\n+\t\t|     * Remotes        |     0 |\n+\t\t|     * Others         |     0 |\n+\t\tEOF\n+\n+\t\tgit repo structure >out 2>err &&\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n+test_expect_success 'repository with references' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit commit --allow-empty -m init &&\n+\t\tgit tag -a foo -m bar &&\n+\n+\t\toid=\"$(git rev-parse HEAD)\" &&\n+\t\tgit update-ref refs/remotes/origin/foo \"$oid\" &&\n+\n+\t\tgit notes add -m foo &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\t\t| Repository structure | Value |\n+\t\t| -------------------- | ----- |\n+\t\t| * References         |       |\n+\t\t|   * Count            |     4 |\n+\t\t|     * Branches       |     1 |\n+\t\t|     * Tags           |     1 |\n+\t\t|     * Remotes        |     1 |\n+\t\t|     * Others         |     1 |\n+\t\tEOF\n+\n+\t\tgit repo structure >out 2>err &&\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n+test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"529316","messageId":"20251021182601.2687284-6-jltobler@gmail.com","threadId":"64187","inReplyTo":"20251021182601.2687284-1-jltobler@gmail.com","subject":"[PATCH v6 5/7] builtin/repo: add object counts in structure output","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-21T18:25:59Z","receivedAt":"2025-10-21T18:26:40Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The amount of objects in a repository can provide insight regarding its\nshape. To surface this information, use the path-walk API to count the\nnumber of reachable objects in the repository by object type. All\nregular references are used to determine the reachable set of objects.\nThe object counts are appended to the same table containing the\nreference information.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc |   1 +\n builtin/repo.c              | 105 +++++++++++++++++++++++++++++++++---\n t/t1901-repo-structure.sh   |  19 ++++++-\n 3 files changed, 117 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 8193298dd5..ae62d2415f 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -49,6 +49,7 @@ supported:\n \tfollowing kinds of information are reported:\n +\n * Reference counts categorized by type\n+* Reachable object counts categorized by type\n \n +\n The table output format may change and is not intended for machine parsing.\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex e77e8db563..f39f06ee8c 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -3,9 +3,11 @@\n #include \"builtin.h\"\n #include \"environment.h\"\n #include \"parse-options.h\"\n+#include \"path-walk.h\"\n #include \"quote.h\"\n #include \"ref-filter.h\"\n #include \"refs.h\"\n+#include \"revision.h\"\n #include \"strbuf.h\"\n #include \"string-list.h\"\n #include \"shallow.h\"\n@@ -167,6 +169,18 @@ struct ref_stats {\n \tsize_t others;\n };\n \n+struct object_stats {\n+\tsize_t tags;\n+\tsize_t commits;\n+\tsize_t trees;\n+\tsize_t blobs;\n+};\n+\n+struct repo_structure {\n+\tstruct ref_stats refs;\n+\tstruct object_stats objects;\n+};\n+\n struct stats_table {\n \tstruct string_list rows;\n \n@@ -234,9 +248,17 @@ static inline size_t get_total_reference_count(struct ref_stats *stats)\n \treturn stats->branches + stats->remotes + stats->tags + stats->others;\n }\n \n+static inline size_t get_total_object_count(struct object_stats *stats)\n+{\n+\treturn stats->tags + stats->commits + stats->trees + stats->blobs;\n+}\n+\n static void stats_table_setup_structure(struct stats_table *table,\n-\t\t\t\t\tstruct ref_stats *refs)\n+\t\t\t\t\tstruct repo_structure *stats)\n {\n+\tstruct object_stats *objects = &stats->objects;\n+\tstruct ref_stats *refs = &stats->refs;\n+\tsize_t object_total;\n \tsize_t ref_total;\n \n \tref_total = get_total_reference_count(refs);\n@@ -246,6 +268,15 @@ static void stats_table_setup_structure(struct stats_table *table,\n \tstats_table_count_addf(table, refs->tags, \"    * %s\", _(\"Tags\"));\n \tstats_table_count_addf(table, refs->remotes, \"    * %s\", _(\"Remotes\"));\n \tstats_table_count_addf(table, refs->others, \"    * %s\", _(\"Others\"));\n+\n+\tobject_total = get_total_object_count(objects);\n+\tstats_table_addf(table, \"\");\n+\tstats_table_addf(table, \"* %s\", _(\"Reachable objects\"));\n+\tstats_table_count_addf(table, object_total, \"  * %s\", _(\"Count\"));\n+\tstats_table_count_addf(table, objects->commits, \"    * %s\", _(\"Commits\"));\n+\tstats_table_count_addf(table, objects->trees, \"    * %s\", _(\"Trees\"));\n+\tstats_table_count_addf(table, objects->blobs, \"    * %s\", _(\"Blobs\"));\n+\tstats_table_count_addf(table, objects->tags, \"    * %s\", _(\"Tags\"));\n }\n \n static void stats_table_print_structure(const struct stats_table *table)\n@@ -299,12 +330,18 @@ static void stats_table_clear(struct stats_table *table)\n \tstring_list_clear(&table->rows, 1);\n }\n \n+struct count_references_data {\n+\tstruct ref_stats *stats;\n+\tstruct rev_info *revs;\n+};\n+\n static int count_references(const char *refname,\n \t\t\t    const char *referent UNUSED,\n-\t\t\t    const struct object_id *oid UNUSED,\n+\t\t\t    const struct object_id *oid,\n \t\t\t    int flags UNUSED, void *cb_data)\n {\n-\tstruct ref_stats *stats = cb_data;\n+\tstruct count_references_data *data = cb_data;\n+\tstruct ref_stats *stats = data->stats;\n \n \tswitch (ref_kind_from_refname(refname)) {\n \tcase FILTER_REFS_BRANCHES:\n@@ -323,13 +360,64 @@ static int count_references(const char *refname,\n \t\tBUG(\"unexpected reference type\");\n \t}\n \n+\t/*\n+\t * While iterating through references for counting, also add OIDs in\n+\t * preparation for the path walk.\n+\t */\n+\tadd_pending_oid(data->revs, NULL, oid, 0);\n+\n \treturn 0;\n }\n \n static void structure_count_references(struct ref_stats *stats,\n+\t\t\t\t       struct rev_info *revs,\n \t\t\t\t       struct repository *repo)\n {\n-\trefs_for_each_ref(get_main_ref_store(repo), count_references, &stats);\n+\tstruct count_references_data data = {\n+\t\t.stats = stats,\n+\t\t.revs = revs,\n+\t};\n+\n+\trefs_for_each_ref(get_main_ref_store(repo), count_references, &data);\n+}\n+\n+\n+static int count_objects(const char *path UNUSED, struct oid_array *oids,\n+\t\t\t enum object_type type, void *cb_data)\n+{\n+\tstruct object_stats *stats = cb_data;\n+\n+\tswitch (type) {\n+\tcase OBJ_TAG:\n+\t\tstats->tags += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_COMMIT:\n+\t\tstats->commits += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_TREE:\n+\t\tstats->trees += oids->nr;\n+\t\tbreak;\n+\tcase OBJ_BLOB:\n+\t\tstats->blobs += oids->nr;\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"invalid object type\");\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void structure_count_objects(struct object_stats *stats,\n+\t\t\t\t    struct rev_info *revs)\n+{\n+\tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n+\n+\tinfo.revs = revs;\n+\tinfo.path_fn = count_objects;\n+\tinfo.path_fn_data = stats;\n+\n+\twalk_objects_by_path(&info);\n+\tpath_walk_info_clear(&info);\n }\n \n static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n@@ -338,19 +426,24 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n \tstruct stats_table table = {\n \t\t.rows = STRING_LIST_INIT_DUP,\n \t};\n-\tstruct ref_stats stats = { 0 };\n+\tstruct repo_structure stats = { 0 };\n+\tstruct rev_info revs;\n \tstruct option options[] = { 0 };\n \n \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n \tif (argc)\n \t\tusage(_(\"too many arguments\"));\n \n-\tstructure_count_references(&stats, repo);\n+\trepo_init_revisions(repo, &revs, prefix);\n+\n+\tstructure_count_references(&stats.refs, &revs, repo);\n+\tstructure_count_objects(&stats.objects, &revs);\n \n \tstats_table_setup_structure(&table, &stats);\n \tstats_table_print_structure(&table);\n \n \tstats_table_clear(&table);\n+\trelease_revisions(&revs);\n \n \treturn 0;\n }\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nindex e592eea0eb..c32cf4e239 100755\n--- a/t/t1901-repo-structure.sh\n+++ b/t/t1901-repo-structure.sh\n@@ -18,6 +18,13 @@ test_expect_success 'empty repository' '\n \t\t|     * Tags           |     0 |\n \t\t|     * Remotes        |     0 |\n \t\t|     * Others         |     0 |\n+\t\t|                      |       |\n+\t\t| * Reachable objects  |       |\n+\t\t|   * Count            |     0 |\n+\t\t|     * Commits        |     0 |\n+\t\t|     * Trees          |     0 |\n+\t\t|     * Blobs          |     0 |\n+\t\t|     * Tags           |     0 |\n \t\tEOF\n \n \t\tgit repo structure >out 2>err &&\n@@ -27,17 +34,18 @@ test_expect_success 'empty repository' '\n \t)\n '\n \n-test_expect_success 'repository with references' '\n+test_expect_success 'repository with references and objects' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n \t(\n \t\tcd repo &&\n-\t\tgit commit --allow-empty -m init &&\n+\t\ttest_commit_bulk 42 &&\n \t\tgit tag -a foo -m bar &&\n \n \t\toid=\"$(git rev-parse HEAD)\" &&\n \t\tgit update-ref refs/remotes/origin/foo \"$oid\" &&\n \n+\t\t# Also creates a commit, tree, and blob.\n \t\tgit notes add -m foo &&\n \n \t\tcat >expect <<-\\EOF &&\n@@ -49,6 +57,13 @@ test_expect_success 'repository with references' '\n \t\t|     * Tags           |     1 |\n \t\t|     * Remotes        |     1 |\n \t\t|     * Others         |     1 |\n+\t\t|                      |       |\n+\t\t| * Reachable objects  |       |\n+\t\t|   * Count            |   130 |\n+\t\t|     * Commits        |    43 |\n+\t\t|     * Trees          |    43 |\n+\t\t|     * Blobs          |    43 |\n+\t\t|     * Tags           |     1 |\n \t\tEOF\n \n \t\tgit repo structure >out 2>err &&\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"529317","messageId":"20251021182601.2687284-7-jltobler@gmail.com","threadId":"64187","inReplyTo":"20251021182601.2687284-1-jltobler@gmail.com","subject":"[PATCH v6 6/7] builtin/repo: add keyvalue and nul format for structure stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-21T18:26:00Z","receivedAt":"2025-10-21T18:26:41Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"All repository structure stats are outputted in a human-friendly table\nform. This format is not suitable for machine parsing. Add a --format\noption that supports three output modes: `table`, `keyvalue`, and `nul`.\nThe `table` mode is the default format and prints the same table output\nas before.\n\nWith the `keyvalue` mode, each line of output contains a key-value pair\nof a repository stat. The '=' character is used to delimit between keys\nand values. The `nul` mode is similar to `keyvalue`, but key-values are\ndelimited by a NUL character instead of a newline. Also, instead of a\n'=' character to delimit between keys and values, a newline character is\nused. This allows stat values to support special characters without\nhaving to cquote them. These two new modes provides output that is more\nmachine-friendly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-repo.adoc | 25 +++++++++++++++--\n builtin/repo.c              | 55 ++++++++++++++++++++++++++++++++++---\n t/t1901-repo-structure.sh   | 33 ++++++++++++++++++++++\n 3 files changed, 106 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex ae62d2415f..ce43cb19c8 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -9,7 +9,7 @@ SYNOPSIS\n --------\n [synopsis]\n git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\n-git repo structure\n+git repo structure [--format=(table|keyvalue|nul)]\n \n DESCRIPTION\n -----------\n@@ -44,7 +44,7 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n-`structure`::\n+`structure [--format=(table|keyvalue|nul)]`::\n \tRetrieve statistics about the current repository structure. The\n \tfollowing kinds of information are reported:\n +\n@@ -52,7 +52,26 @@ supported:\n * Reachable object counts categorized by type\n \n +\n-The table output format may change and is not intended for machine parsing.\n+The output format can be chosen through the flag `--format`. Three formats are\n+supported:\n++\n+`table`:::\n+\tOutputs repository stats in a human-friendly table. This format may\n+\tchange and is not intended for machine parsing. This is the default\n+\tformat.\n+\n+`keyvalue`:::\n+\tEach line of output contains a key-value pair for a repository stat.\n+\tThe '=' character is used to delimit between the key and the value.\n+\tValues containing \"unusual\" characters are quoted as explained for the\n+\tconfiguration variable `core.quotePath` (see linkgit:git-config[1]).\n+\n+`nul`:::\n+\tSimilar to `keyvalue`, but uses a NUL character to delimit between\n+\tkey-value pairs instead of a newline. Also uses a newline character as\n+\tthe delimiter between the key and value instead of '='. Unlike the\n+\t`keyvalue` format, values containing \"unusual\" characters are never\n+\tquoted.\n \n INFO KEYS\n ---------\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex f39f06ee8c..1754cc7e5d 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -15,13 +15,14 @@\n \n static const char *const repo_usage[] = {\n \t\"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n-\t\"git repo structure\",\n+\t\"git repo structure [--format=(table|keyvalue|nul)]\",\n \tNULL\n };\n \n typedef int get_value_fn(struct repository *repo, struct strbuf *buf);\n \n enum output_format {\n+\tFORMAT_TABLE,\n \tFORMAT_KEYVALUE,\n \tFORMAT_NUL_TERMINATED,\n };\n@@ -136,6 +137,8 @@ static int parse_format_cb(const struct option *opt,\n \t\t*format = FORMAT_NUL_TERMINATED;\n \telse if (!strcmp(arg, \"keyvalue\"))\n \t\t*format = FORMAT_KEYVALUE;\n+\telse if (!strcmp(arg, \"table\"))\n+\t\t*format = FORMAT_TABLE;\n \telse\n \t\tdie(_(\"invalid format '%s'\"), arg);\n \n@@ -158,6 +161,8 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n+\tif (format != FORMAT_KEYVALUE && format != FORMAT_NUL_TERMINATED)\n+\t\tdie(_(\"unsupported output format\"));\n \n \treturn print_fields(argc, argv, repo, format);\n }\n@@ -330,6 +335,30 @@ static void stats_table_clear(struct stats_table *table)\n \tstring_list_clear(&table->rows, 1);\n }\n \n+static void structure_keyvalue_print(struct repo_structure *stats,\n+\t\t\t\t     char key_delim, char value_delim)\n+{\n+\tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->refs.branches, value_delim);\n+\tprintf(\"references.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->refs.tags, value_delim);\n+\tprintf(\"references.remotes.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->refs.remotes, value_delim);\n+\tprintf(\"references.others.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->refs.others, value_delim);\n+\n+\tprintf(\"objects.commits.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.commits, value_delim);\n+\tprintf(\"objects.trees.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.trees, value_delim);\n+\tprintf(\"objects.blobs.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.blobs, value_delim);\n+\tprintf(\"objects.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.tags, value_delim);\n+\n+\tfflush(stdout);\n+}\n+\n struct count_references_data {\n \tstruct ref_stats *stats;\n \tstruct rev_info *revs;\n@@ -426,9 +455,15 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n \tstruct stats_table table = {\n \t\t.rows = STRING_LIST_INIT_DUP,\n \t};\n+\tenum output_format format = FORMAT_TABLE;\n \tstruct repo_structure stats = { 0 };\n \tstruct rev_info revs;\n-\tstruct option options[] = { 0 };\n+\tstruct option options[] = {\n+\t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n+\t\t\t       N_(\"output format\"),\n+\t\t\t       PARSE_OPT_NONEG, parse_format_cb),\n+\t\tOPT_END()\n+\t};\n \n \targc = parse_options(argc, argv, prefix, options, repo_usage, 0);\n \tif (argc)\n@@ -439,8 +474,20 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n \tstructure_count_references(&stats.refs, &revs, repo);\n \tstructure_count_objects(&stats.objects, &revs);\n \n-\tstats_table_setup_structure(&table, &stats);\n-\tstats_table_print_structure(&table);\n+\tswitch (format) {\n+\tcase FORMAT_TABLE:\n+\t\tstats_table_setup_structure(&table, &stats);\n+\t\tstats_table_print_structure(&table);\n+\t\tbreak;\n+\tcase FORMAT_KEYVALUE:\n+\t\tstructure_keyvalue_print(&stats, '=', '\\n');\n+\t\tbreak;\n+\tcase FORMAT_NUL_TERMINATED:\n+\t\tstructure_keyvalue_print(&stats, '\\n', '\\0');\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"invalid output format\");\n+\t}\n \n \tstats_table_clear(&table);\n \trelease_revisions(&revs);\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nindex c32cf4e239..14bd8aede5 100755\n--- a/t/t1901-repo-structure.sh\n+++ b/t/t1901-repo-structure.sh\n@@ -73,4 +73,37 @@ test_expect_success 'repository with references and objects' '\n \t)\n '\n \n+test_expect_success 'keyvalue and nul format' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit_bulk 42 &&\n+\t\tgit tag -a foo -m bar &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\t\treferences.branches.count=1\n+\t\treferences.tags.count=1\n+\t\treferences.remotes.count=0\n+\t\treferences.others.count=0\n+\t\tobjects.commits.count=42\n+\t\tobjects.trees.count=42\n+\t\tobjects.blobs.count=42\n+\t\tobjects.tags.count=1\n+\t\tEOF\n+\n+\t\tgit repo structure --format=keyvalue >out 2>err &&\n+\n+\t\ttest_cmp expect out &&\n+\t\ttest_line_count = 0 err &&\n+\n+\t\t# Replace key and value delimiters for nul format.\n+\t\ttr \"\\n=\" \"\\0\\n\" <expect >expect_nul &&\n+\t\tgit repo structure --format=nul >out 2>err &&\n+\n+\t\ttest_cmp expect_nul out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"529318","messageId":"20251021182601.2687284-8-jltobler@gmail.com","threadId":"64187","inReplyTo":"20251021182601.2687284-1-jltobler@gmail.com","subject":"[PATCH v6 7/7] builtin/repo: add progress meter for structure stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-21T18:26:01Z","receivedAt":"2025-10-21T18:26:42Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"When using the structure subcommand for git-repo(1), evaluating a\nrepository may take some time depending on its shape. Add a progress\nmeter to provide feedback to the user about what is happening. The\nprogress meter is enabled by default when the command is executed from a\ntty. It can also be explicitly enabled/disabled via the --[no-]progress\noption.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/repo.c            | 46 ++++++++++++++++++++++++++++++++++-----\n t/t1901-repo-structure.sh | 20 +++++++++++++++++\n 2 files changed, 60 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 1754cc7e5d..9d4749f79b 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -4,6 +4,7 @@\n #include \"environment.h\"\n #include \"parse-options.h\"\n #include \"path-walk.h\"\n+#include \"progress.h\"\n #include \"quote.h\"\n #include \"ref-filter.h\"\n #include \"refs.h\"\n@@ -362,6 +363,7 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n struct count_references_data {\n \tstruct ref_stats *stats;\n \tstruct rev_info *revs;\n+\tstruct progress *progress;\n };\n \n static int count_references(const char *refname,\n@@ -371,6 +373,7 @@ static int count_references(const char *refname,\n {\n \tstruct count_references_data *data = cb_data;\n \tstruct ref_stats *stats = data->stats;\n+\tsize_t ref_count;\n \n \tswitch (ref_kind_from_refname(refname)) {\n \tcase FILTER_REFS_BRANCHES:\n@@ -395,26 +398,41 @@ static int count_references(const char *refname,\n \t */\n \tadd_pending_oid(data->revs, NULL, oid, 0);\n \n+\tref_count = get_total_reference_count(stats);\n+\tdisplay_progress(data->progress, ref_count);\n+\n \treturn 0;\n }\n \n static void structure_count_references(struct ref_stats *stats,\n \t\t\t\t       struct rev_info *revs,\n-\t\t\t\t       struct repository *repo)\n+\t\t\t\t       struct repository *repo,\n+\t\t\t\t       int show_progress)\n {\n \tstruct count_references_data data = {\n \t\t.stats = stats,\n \t\t.revs = revs,\n \t};\n \n+\tif (show_progress)\n+\t\tdata.progress = start_delayed_progress(repo,\n+\t\t\t\t\t\t       _(\"Counting references\"), 0);\n+\n \trefs_for_each_ref(get_main_ref_store(repo), count_references, &data);\n+\tstop_progress(&data.progress);\n }\n \n+struct count_objects_data {\n+\tstruct object_stats *stats;\n+\tstruct progress *progress;\n+};\n \n static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\t enum object_type type, void *cb_data)\n {\n-\tstruct object_stats *stats = cb_data;\n+\tstruct count_objects_data *data = cb_data;\n+\tstruct object_stats *stats = data->stats;\n+\tsize_t object_count;\n \n \tswitch (type) {\n \tcase OBJ_TAG:\n@@ -433,20 +451,31 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\tBUG(\"invalid object type\");\n \t}\n \n+\tobject_count = get_total_object_count(stats);\n+\tdisplay_progress(data->progress, object_count);\n+\n \treturn 0;\n }\n \n static void structure_count_objects(struct object_stats *stats,\n-\t\t\t\t    struct rev_info *revs)\n+\t\t\t\t    struct rev_info *revs,\n+\t\t\t\t    struct repository *repo, int show_progress)\n {\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n+\tstruct count_objects_data data = {\n+\t\t.stats = stats,\n+\t};\n \n \tinfo.revs = revs;\n \tinfo.path_fn = count_objects;\n-\tinfo.path_fn_data = stats;\n+\tinfo.path_fn_data = &data;\n+\n+\tif (show_progress)\n+\t\tdata.progress = start_delayed_progress(repo, _(\"Counting objects\"), 0);\n \n \twalk_objects_by_path(&info);\n \tpath_walk_info_clear(&info);\n+\tstop_progress(&data.progress);\n }\n \n static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n@@ -458,10 +487,12 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n \tenum output_format format = FORMAT_TABLE;\n \tstruct repo_structure stats = { 0 };\n \tstruct rev_info revs;\n+\tint show_progress = -1;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n \t\t\t       N_(\"output format\"),\n \t\t\t       PARSE_OPT_NONEG, parse_format_cb),\n+\t\tOPT_BOOL(0, \"progress\", &show_progress, N_(\"show progress\")),\n \t\tOPT_END()\n \t};\n \n@@ -471,8 +502,11 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n \n \trepo_init_revisions(repo, &revs, prefix);\n \n-\tstructure_count_references(&stats.refs, &revs, repo);\n-\tstructure_count_objects(&stats.objects, &revs);\n+\tif (show_progress < 0)\n+\t\tshow_progress = isatty(2);\n+\n+\tstructure_count_references(&stats.refs, &revs, repo, show_progress);\n+\tstructure_count_objects(&stats.objects, &revs, repo, show_progress);\n \n \tswitch (format) {\n \tcase FORMAT_TABLE:\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nindex 14bd8aede5..36a71a144e 100755\n--- a/t/t1901-repo-structure.sh\n+++ b/t/t1901-repo-structure.sh\n@@ -106,4 +106,24 @@ test_expect_success 'keyvalue and nul format' '\n \t)\n '\n \n+test_expect_success 'progress meter option' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit foo &&\n+\n+\t\tGIT_PROGRESS_DELAY=0 git repo structure --progress >out 2>err &&\n+\n+\t\ttest_file_not_empty out &&\n+\t\ttest_grep \"Counting references: 2, done.\" err &&\n+\t\ttest_grep \"Counting objects: 3, done.\" err &&\n+\n+\t\tGIT_PROGRESS_DELAY=0 git repo structure --no-progress >out 2>err &&\n+\n+\t\ttest_file_not_empty out &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n test_done\n-- \n2.51.0.193.g4975ec3473b\n\n"},{"id":"529350","messageId":"aPhlLbsOFxeOv5KZ@pks.im","threadId":"64187","inReplyTo":"20251021182601.2687284-5-jltobler@gmail.com","subject":"Re: [PATCH v6 4/7] builtin/repo: introduce structure subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-22T05:01:33Z","receivedAt":"2025-10-22T05:01:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Oct 21, 2025 at 01:25:58PM -0500, Justin Tobler wrote:\n[snip]\n> +static int count_references(const char *refname,\n> +\t\t\t    const char *referent UNUSED,\n> +\t\t\t    const struct object_id *oid UNUSED,\n> +\t\t\t    int flags UNUSED, void *cb_data)\n\nTiniest nit, not worth a reroll: we tend to use these callbacks in\nsingular, as you end up doing the thing for one specific entity.\n\nOther than that this series looks good to me now and is ready to be\nmerged from my point of view. Thanks!\n\nPatrick\n"},{"id":"529416","messageId":"xnxnqvnz22n5tognru676vczqseuc2zpyjdkffz4svji7glkw3@loo2xq33wueh","threadId":"64187","inReplyTo":"aPhlLbsOFxeOv5KZ@pks.im","subject":"Re: [PATCH v6 4/7] builtin/repo: introduce structure subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-22T13:50:17Z","receivedAt":"2025-10-22T13:50:21Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/10/22 07:01AM, Patrick Steinhardt wrote:\n> On Tue, Oct 21, 2025 at 01:25:58PM -0500, Justin Tobler wrote:\n> > +static int count_references(const char *refname,\n> > +\t\t\t    const char *referent UNUSED,\n> > +\t\t\t    const struct object_id *oid UNUSED,\n> > +\t\t\t    int flags UNUSED, void *cb_data)\n> \n> Tiniest nit, not worth a reroll: we tend to use these callbacks in\n> singular, as you end up doing the thing for one specific entity.\n\nGood to know and makes sense. I'll hold off from sending another version\nfor now. I can also probably adjust as part of my planned followup\nseries.\n\n> Other than that this series looks good to me now and is ready to be\n> merged from my point of view. Thanks!\n\nThanks for the review.\n\n-Justin\n"},{"id":"529441","messageId":"64D5752D-AAEB-4559-A5FD-EA7BDEF16553@gmail.com","threadId":"64187","inReplyTo":"20251021182601.2687284-1-jltobler@gmail.com","subject":"Re: [PATCH v6 0/7] builtin/repo: introduce structure subcommand","fromName":"Lucas Seiki Oshiro","fromEmail":"lucasseikioshiro@gmail.com","sentAt":"2025-10-22T19:23:44Z","receivedAt":"2025-10-22T19:24:00Z","isPatch":true,"sender":{"key":"lucasseikioshiro@gmail.com","avatar":"https://avatars.githubusercontent.com/u/12701580?v=4"},"body":"\nHi, Justin!\n\nNice to see this happening. I'm really happy to see `git repo` getting\nnew features, and I think that these new features will be very useful\nespecially for people who research on free/open source software.\n\nSorry for only review this today, I've been busy finishing my master's\nand I didn't have enough time to see your work here. Can you please CC\nme in the next versions?\n"},{"id":"529444","messageId":"DBB5E6F1-D2C5-4A3C-9FA7-89147D64BCEC@gmail.com","threadId":"64187","inReplyTo":"20251021182601.2687284-5-jltobler@gmail.com","subject":"Re: [PATCH v6 4/7] builtin/repo: introduce structure subcommand","fromName":"Lucas Seiki Oshiro","fromEmail":"lucasseikioshiro@gmail.com","sentAt":"2025-10-22T20:15:48Z","receivedAt":"2025-10-22T20:16:03Z","isPatch":true,"sender":{"key":"lucasseikioshiro@gmail.com","avatar":"https://avatars.githubusercontent.com/u/12701580?v=4"},"body":"\n> +static void stats_table_print_structure(const struct stats_table *table)\n\nQuestion: isn't it possible to use the tables from column.c by\nallowing them to use a cell delimiter? This table formatter seems to\nbe useful for other places, like git-repo-info itself.\n"},{"id":"529446","messageId":"A076B823-F619-435F-9A7D-40C6D60254D1@gmail.com","threadId":"64187","inReplyTo":"20251021182601.2687284-7-jltobler@gmail.com","subject":"Re: [PATCH v6 6/7] builtin/repo: add keyvalue and nul format for structure stats","fromName":"Lucas Seiki Oshiro","fromEmail":"lucasseikioshiro@gmail.com","sentAt":"2025-10-22T20:34:43Z","receivedAt":"2025-10-22T20:34:57Z","isPatch":true,"sender":{"key":"lucasseikioshiro@gmail.com","avatar":"https://avatars.githubusercontent.com/u/12701580?v=4"},"body":"\n> All repository structure stats are outputted in a human-friendly table\n> form. This format is not suitable for machine parsing. Add a --format\n> option that supports three output modes: `table`, `keyvalue`, and `nul`.\n> The `table` mode is the default format and prints the same table output\n> as before.\n\nNow I'm thinking... What about making --format a flag for git-repo,\nworking for both git-repo-info and git-repo-structure? It doesn't seem\nto be hard to make git-repo-info compatible with your table format and\nit looks to me that it would make git-repo more consistent.\n\nI'm also wondering if git-repo-info use this table format by default.\ngit-repo-info and git-repo-structure are completely different under\nthe hood, but their interface are very similar and it seems to be that\nthey could be more closer to each other.\n\n\n> \"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n> - \"git repo structure\",\n> + \"git repo structure [--format=(table|keyvalue|nul)]\",\n\nDo you intend to add a key parameter like in git-repo-info? This way,\nwe could run:\n\n  git repo structure references.branches.count references.tags.count\n\nand it would return only the branch and tag count.\n"},{"id":"529458","messageId":"wmprwnikryda3zairkyyqyn5lapernapivgq63ckhwlt2ispqu@onebdrpp3axf","threadId":"64187","inReplyTo":"DBB5E6F1-D2C5-4A3C-9FA7-89147D64BCEC@gmail.com","subject":"Re: [PATCH v6 4/7] builtin/repo: introduce structure subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-22T23:42:02Z","receivedAt":"2025-10-22T23:42:07Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/10/22 05:15PM, Lucas Seiki Oshiro wrote:\n> \n> > +static void stats_table_print_structure(const struct stats_table *table)\n> \n> Question: isn't it possible to use the tables from column.c by\n> allowing them to use a cell delimiter? This table formatter seems to\n> be useful for other places, like git-repo-info itself.\n\nI opted to create a table formatter specific stats output so we could be\nmore flexible with how individual column data is displayed.\ngit-repo-structure having its own table format allows it to more easily\nconfigure things like cell value alignment column-by-column. Also, in a\nfollowup series I plan to introduce measurement units to values\ndisplayed in the table, but I still want to align values and units\nseparately while still being in the same cell. This sort of\nconfiguration wouldn't fit well in the existing \"column.c\" table format\nIMO.\n\nI did try to design this new stats table format in a way that its could\nbe reused by other git-repo(1) subcommands if ever needed. So we want to\nuse it for git-repo-info itself in the future, I think we could do so\nrelatively easily.\n\n-Justin\n"},{"id":"529459","messageId":"f3lthzionr4wktkgddb4lwu7njucrny2jmjhtbtiifbwsceuaf@oqx7q7y6alze","threadId":"64187","inReplyTo":"A076B823-F619-435F-9A7D-40C6D60254D1@gmail.com","subject":"Re: [PATCH v6 6/7] builtin/repo: add keyvalue and nul format for structure stats","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-23T00:03:53Z","receivedAt":"2025-10-23T00:03:58Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/10/22 05:34PM, Lucas Seiki Oshiro wrote:\n> \n> > All repository structure stats are outputted in a human-friendly table\n> > form. This format is not suitable for machine parsing. Add a --format\n> > option that supports three output modes: `table`, `keyvalue`, and `nul`.\n> > The `table` mode is the default format and prints the same table output\n> > as before.\n> \n> Now I'm thinking... What about making --format a flag for git-repo,\n> working for both git-repo-info and git-repo-structure? It doesn't seem\n> to be hard to make git-repo-info compatible with your table format and\n> it looks to me that it would make git-repo more consistent.\n\nSince currently these different subcommands don't support all the same\nformats anyways, I don't think it matters too much that they both define\ntheir own `--format` option. If git-repo-info were to add support for a\ntable format in the future though, I do think we could probably lift\nthis option up at that time if we wanted.\n\n> I'm also wondering if git-repo-info use this table format by default.\n> git-repo-info and git-repo-structure are completely different under\n> the hood, but their interface are very similar and it seems to be that\n> they could be more closer to each other.\n\nI think that could be nice, but is should probably be done in a separate\nseries. I do like the idea of aligning the interfaces of these\nsubcommands though. :)\n\n> > \"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]\",\n> > - \"git repo structure\",\n> > + \"git repo structure [--format=(table|keyvalue|nul)]\",\n> \n> Do you intend to add a key parameter like in git-repo-info? This way,\n> we could run:\n> \n>   git repo structure references.branches.count references.tags.count\n> \n> and it would return only the branch and tag count.\n\nCurrently I do not intend to add key parameters similar to\ngit-repo-info. For git-repo-structure, I don't think it would add much\nvalue to filter out certain values unless it would simplify overall\ncomputation. In the case of `references.branches.count` and\n`references.tags.count`, figuring these values out requires iterating\nacross all the references anyways. Even if we only wanted object count\ndata we would still have to iterate across all the references too.\n\nIf we just wanted reference data, we could have an option to skip the\npath walking, but that would really limit the data we display and\nprobably not be that useful overall.\n\n-Justin\n"},{"id":"529460","messageId":"eesy5qhqzi56qrzv7wlpenxag23l2zykakvpmr6p5rnak6cril@ap3rugp44bcz","threadId":"64187","inReplyTo":"64D5752D-AAEB-4559-A5FD-EA7BDEF16553@gmail.com","subject":"Re: [PATCH v6 0/7] builtin/repo: introduce structure subcommand","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-10-23T00:05:24Z","receivedAt":"2025-10-23T00:05:26Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/10/22 04:23PM, Lucas Seiki Oshiro wrote:\n> \n> Hi, Justin!\n> \n> Nice to see this happening. I'm really happy to see `git repo` getting\n> new features, and I think that these new features will be very useful\n> especially for people who research on free/open source software.\n> \n> Sorry for only review this today, I've been busy finishing my master's\n> and I didn't have enough time to see your work here. Can you please CC\n> me in the next versions?\n\nThanks Lucas for taking a look. If I do need to send out another\nversion, I'll make sure to CC you. :)\n\n-Justin\n"},{"id":"529528","messageId":"xmqqtszps5ja.fsf@gitster.g","threadId":"64187","inReplyTo":"20251021182601.2687284-1-jltobler@gmail.com","subject":"Re: [PATCH v6 0/7] builtin/repo: introduce structure subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-23T20:54:17Z","receivedAt":"2025-10-23T20:54:20Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> In this initial version, the \"structure\" subcommand only surfaces counts\n> of the various reference and object types in a repository. In a\n> follow-up series, I would like to introduce additional data points that\n> are present in git-sizer(1) such as largest objects, combined object\n> sizes by type, and other general repository shape information.\n>\n> Some other general features that would be nice to introduce eventually:\n>\n> - A \"level of concern\" meter for reported stats. This could indicate to\n>   users which stats may be worth looking into further.\n> - Links to OIDs of interesting objects that correspond to certain stats.\n> - Options to limit which references to use when evaluating the\n>   repository.\n>\n> Changes since V5:\n> - Instead of using `filter_refs()` to get an array of all references, we\n>   now use `refs_for_each_ref()` to count references, and setup OIDs for\n>   the path walk, in place. Doing this not only allows us to avoid\n>   wasting memory storing all the reference info, but also to display\n>   progress info to the user while iterating across the references\n>   initially.\n> - Add a prepatory patch to export `ref_kind_from_refname()` via\n>   \"ref_filter.h\" so we can reuse logic to categorize references while\n>   counting.\n\nThis round looked pretty well done to me.  Shall we declare victory\nand mark it for 'next' real soon now?\n\nThanks.\n"},{"id":"529553","messageId":"aPsLS6lbzG27W-C8@pks.im","threadId":"64187","inReplyTo":"xmqqtszps5ja.fsf@gitster.g","subject":"Re: [PATCH v6 0/7] builtin/repo: introduce structure subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-24T05:14:51Z","receivedAt":"2025-10-24T05:14:57Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Oct 23, 2025 at 01:54:17PM -0700, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > In this initial version, the \"structure\" subcommand only surfaces counts\n> > of the various reference and object types in a repository. In a\n> > follow-up series, I would like to introduce additional data points that\n> > are present in git-sizer(1) such as largest objects, combined object\n> > sizes by type, and other general repository shape information.\n> >\n> > Some other general features that would be nice to introduce eventually:\n> >\n> > - A \"level of concern\" meter for reported stats. This could indicate to\n> >   users which stats may be worth looking into further.\n> > - Links to OIDs of interesting objects that correspond to certain stats.\n> > - Options to limit which references to use when evaluating the\n> >   repository.\n> >\n> > Changes since V5:\n> > - Instead of using `filter_refs()` to get an array of all references, we\n> >   now use `refs_for_each_ref()` to count references, and setup OIDs for\n> >   the path walk, in place. Doing this not only allows us to avoid\n> >   wasting memory storing all the reference info, but also to display\n> >   progress info to the user while iterating across the references\n> >   initially.\n> > - Add a prepatory patch to export `ref_kind_from_refname()` via\n> >   \"ref_filter.h\" so we can reuse logic to categorize references while\n> >   counting.\n> \n> This round looked pretty well done to me.  Shall we declare victory\n> and mark it for 'next' real soon now?\n\nAgreed, I also think that this round looks good and is ready to be\nmerged down to next.\n\nThanks!\n\nPatrick\n"}]}