Subject: [PATCH 2/4] builtin/repo: add object counts in stats output

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

The amount of objects in a repository can provide insight regarding its
shape. To surface this information, use the path-walk API to count the
number of reachable objects in the repository by object type. All
regular references are used to determine the reachable set of objects.
The object counts are appended to the same table containing the
reference information.

Signed-off-by: Justin Tobler <[email-masked]>
---
 Documentation/git-repo.adoc |  5 +-
 builtin/repo.c              | 99 +++++++++++++++++++++++++++++++++----
 t/t1901-repo-stats.sh       | 46 +++++++++++++++++
 3 files changed, 139 insertions(+), 11 deletions(-)

diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 7762329551..2a67abfca8 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -45,8 +45,9 @@ supported:
 `-z` is an alias for `--format=nul`.
 
 stats::
-	Retrieve stats about the current repository. All references in the
-	repository are categorized and counted accordingly.
+	Retrieve stats about the current repository. All references and
+	reachable objects in the repository are categorized and counted
+	accordingly.
 +
 The table output format may change and is not intended for machine parsing.
 
diff --git a/builtin/repo.c b/builtin/repo.c
index 15899dd74c..a24ea0e66b 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -3,9 +3,11 @@
 #include "builtin.h"
 #include "environment.h"
 #include "parse-options.h"
+#include "path-walk.h"
 #include "quote.h"
 #include "ref-filter.h"
 #include "refs.h"
+#include "revision.h"
 #include "strbuf.h"
 #include "string-list.h"
 #include "shallow.h"
@@ -159,13 +161,25 @@ static int repo_info(int argc, const char **argv, const char *prefix,
 	return print_fields(argc, argv, repo, format);
 }
 
-struct stats {
+struct ref_stats {
 	size_t branches;
 	size_t remotes;
 	size_t tags;
 	size_t others;
 };
 
+struct object_stats {
+	size_t tags;
+	size_t commits;
+	size_t trees;
+	size_t blobs;
+};
+
+struct stats {
+	struct ref_stats refs;
+	struct object_stats objects;
+};
+
 struct stats_table {
 	struct string_list rows;
 
@@ -207,15 +221,27 @@ static void stats_table_add_count(struct stats_table *table, const char *name,
 
 static void stats_table_setup(struct stats_table *table, struct stats *stats)
 {
+	struct object_stats objects = stats->objects;
+	struct ref_stats refs = stats->refs;
+	size_t object_total;
 	size_t ref_total;
 
-	ref_total = stats->branches + stats->remotes + stats->tags + stats->others;
+	ref_total = refs.branches + refs.remotes + refs.tags + refs.others;
 	stats_table_add(table, _("* References"), NULL);
 	stats_table_add_count(table, _("  * Count"), ref_total);
-	stats_table_add_count(table, _("    * Branches"), stats->branches);
-	stats_table_add_count(table, _("    * Tags"), stats->tags);
-	stats_table_add_count(table, _("    * Remotes"), stats->remotes);
-	stats_table_add_count(table, _("    * Others"), stats->others);
+	stats_table_add_count(table, _("    * Branches"), refs.branches);
+	stats_table_add_count(table, _("    * Tags"), refs.tags);
+	stats_table_add_count(table, _("    * Remotes"), refs.remotes);
+	stats_table_add_count(table, _("    * Others"), refs.others);
+
+	object_total = objects.commits + objects.trees + objects.blobs + objects.tags;
+	stats_table_add(table, "", NULL);
+	stats_table_add(table, _("* Objects"), NULL);
+	stats_table_add_count(table, _("  * Count"), object_total);
+	stats_table_add_count(table, _("    * Commits"), objects.commits);
+	stats_table_add_count(table, _("    * Trees"), objects.trees);
+	stats_table_add_count(table, _("    * Blobs"), objects.blobs);
+	stats_table_add_count(table, _("    * Tags"), objects.tags);
 }
 
 static void stats_table_print(struct stats_table *table)
@@ -260,7 +286,7 @@ static void stats_table_print(struct stats_table *table)
 	strbuf_release(&buf);
 }
 
-static void stats_count_references(struct stats *stats, struct ref_array *refs)
+static void stats_count_references(struct ref_stats *stats, struct ref_array *refs)
 {
 	for (int i = 0; i < refs->nr; i++) {
 		struct ref_array_item *ref = refs->items[i];
@@ -282,25 +308,80 @@ static void stats_count_references(struct stats *stats, struct ref_array *refs)
 	}
 }
 
+static int count_objects(const char *path UNUSED, struct oid_array *oids,
+			 enum object_type type, void *data)
+{
+	struct object_stats *stats = data;
+
+	switch (type) {
+	case OBJ_TAG:
+		stats->tags += oids->nr;
+		break;
+	case OBJ_COMMIT:
+		stats->commits += oids->nr;
+		break;
+	case OBJ_TREE:
+		stats->trees += oids->nr;
+		break;
+	case OBJ_BLOB:
+		stats->blobs += oids->nr;
+		break;
+	default:
+		break;
+	}
+
+	return 0;
+}
+
+static void stats_count_objects(struct object_stats *stats,
+				struct ref_array *refs, struct rev_info *revs)
+{
+	struct path_walk_info info = PATH_WALK_INFO_INIT;
+
+	info.revs = revs;
+	info.path_fn = count_objects;
+	info.path_fn_data = stats;
+
+	for (int i = 0; i < refs->nr; i++) {
+		struct ref_array_item *ref = refs->items[i];
+
+		switch (ref->kind) {
+		case FILTER_REFS_BRANCHES:
+		case FILTER_REFS_TAGS:
+		case FILTER_REFS_REMOTES:
+		case FILTER_REFS_OTHERS:
+			add_pending_oid(revs, NULL, &ref->objectname, 0);
+			break;
+		}
+	}
+
+	walk_objects_by_path(&info);
+	path_walk_info_clear(&info);
+}
+
 static int repo_stats(int argc UNUSED, const char **argv UNUSED,
-		      const char *prefix UNUSED, struct repository *repo UNUSED)
+		      const char *prefix, struct repository *repo)
 {
 	struct ref_filter filter = REF_FILTER_INIT;
 	struct strvec ref_patterns = STRVEC_INIT;
 	struct stats_table table = { 0 };
 	struct ref_array refs = { 0 };
 	struct stats stats = { 0 };
+	struct rev_info revs;
 
+	repo_init_revisions(repo, &revs, prefix);
 	filter.name_patterns = ref_patterns.v;
 	filter_refs(&refs, &filter, FILTER_REFS_REGULAR);
 
-	stats_count_references(&stats, &refs);
+	stats_count_references(&stats.refs, &refs);
+	stats_count_objects(&stats.objects, &refs, &revs);
 
 	stats_table_setup(&table, &stats);
 	stats_table_print(&table);
 
 	string_list_clear(&table.rows, 1);
 	strvec_clear(&ref_patterns);
+	release_revisions(&revs);
 	ref_array_clear(&refs);
 
 	return 0;
diff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh
index 27c32ec45f..c6a7f08be5 100755
--- a/t/t1901-repo-stats.sh
+++ b/t/t1901-repo-stats.sh
@@ -20,6 +20,13 @@ test_expect_success 'empty repository stats' '
 		|     * Tags       |     0 |
 		|     * Remotes    |     0 |
 		|     * Others     |     0 |
+		|                  |       |
+		| * Objects        |       |
+		|   * Count        |     0 |
+		|     * Commits    |     0 |
+		|     * Trees      |     0 |
+		|     * Blobs      |     0 |
+		|     * Tags       |     0 |
 		EOF
 
 		test_cmp expect out &&
@@ -49,6 +56,45 @@ test_expect_success 'repository stats with references' '
 		|     * Tags       |     1 |
 		|     * Remotes    |     1 |
 		|     * Others     |     1 |
+		|                  |       |
+		| * Objects        |       |
+		|   * Count        |     5 |
+		|     * Commits    |     2 |
+		|     * Trees      |     2 |
+		|     * Blobs      |     1 |
+		|     * Tags       |     0 |
+		EOF
+
+		test_cmp expect out &&
+		test_line_count = 0 err
+	)
+'
+
+test_expect_success 'repository stats with objects' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit_bulk 42 &&
+		git tag -a foo -m bar &&
+		git repo stats >out 2>err &&
+
+		cat >expect <<-EOF &&
+		| Repository stats | Value |
+		| ---------------- | ----- |
+		| * References     |       |
+		|   * Count        |     2 |
+		|     * Branches   |     1 |
+		|     * Tags       |     1 |
+		|     * Remotes    |     0 |
+		|     * Others     |     0 |
+		|                  |       |
+		| * Objects        |       |
+		|   * Count        |   127 |
+		|     * Commits    |    42 |
+		|     * Trees      |    42 |
+		|     * Blobs      |    42 |
+		|     * Tags       |     1 |
 		EOF
 
 		test_cmp expect out &&
-- 
2.51.0.193.g4975ec3473b