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

From: Justin Tobler <[email-masked]>
Date: Wed, 24 Sep 2025 16:24:24 -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 |  1 +
 builtin/repo.c              | 91 +++++++++++++++++++++++++++++++++++--
 t/t1901-repo-stats.sh       | 51 +++++++++++++--------
 3 files changed, 121 insertions(+), 22 deletions(-)

diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index a009bf8cf1..0b8d74ed3e 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -49,6 +49,7 @@ supported:
 	of information are reported:
 +
 * Reference counts categorized by type
+* Reachable object counts categorized by type
 
 +
 The table output format may change and is not intended for machine parsing.
diff --git a/builtin/repo.c b/builtin/repo.c
index 32ddf2350e..8f130bca66 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"
@@ -166,6 +168,18 @@ struct ref_stats {
 	size_t others;
 };
 
+struct object_stats {
+	size_t tags;
+	size_t commits;
+	size_t trees;
+	size_t blobs;
+};
+
+struct repo_stats {
+	struct ref_stats refs;
+	struct object_stats objects;
+};
+
 struct stats_table {
 	struct string_list rows;
 
@@ -213,8 +227,11 @@ static void stats_table_add_count(struct stats_table *table, const char *format,
 	stats_table_add(table, format, name, entry);
 }
 
-static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)
+static void stats_table_setup(struct stats_table *table, struct repo_stats *stats)
 {
+	struct object_stats *objects = &stats->objects;
+	struct ref_stats *refs = &stats->refs;
+	size_t object_total;
 	size_t ref_total;
 
 	ref_total = refs->branches + refs->remotes + refs->tags + refs->others;
@@ -224,6 +241,15 @@ static void stats_table_setup(struct stats_table *table, struct ref_stats *refs)
 	stats_table_add_count(table, "    * %s", _("Tags"), refs->tags);
 	stats_table_add_count(table, "    * %s", _("Remotes"), refs->remotes);
 	stats_table_add_count(table, "    * %s", _("Others"), refs->others);
+
+	object_total = objects->commits + objects->trees + objects->blobs + objects->tags;
+	stats_table_add(table, "%s", "", NULL);
+	stats_table_add(table, "* %s", _("Reachable objects"), NULL);
+	stats_table_add_count(table, "  * %s", _("Count"), object_total);
+	stats_table_add_count(table, "    * %s", _("Commits"), objects->commits);
+	stats_table_add_count(table, "    * %s", _("Trees"), objects->trees);
+	stats_table_add_count(table, "    * %s", _("Blobs"), objects->blobs);
+	stats_table_add_count(table, "    * %s", _("Tags"), objects->tags);
 }
 
 static inline size_t max_size_t(size_t a, size_t b)
@@ -310,25 +336,82 @@ static void stats_count_references(struct ref_stats *stats, struct ref_array *re
 	}
 }
 
+static int count_objects(const char *path UNUSED, struct oid_array *oids,
+			 enum object_type type, void *cb_data)
+{
+	struct object_stats *stats = cb_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:
+		BUG("invalid object type");
+	}
+
+	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;
+		default:
+			BUG("unexpected reference type");
+		}
+	}
+
+	walk_objects_by_path(&info);
+	path_walk_info_clear(&info);
+}
+
 static int cmd_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 stats_table table = {
 		.rows = STRING_LIST_INIT_DUP,
 	};
-	struct ref_stats stats = { 0 };
+	struct repo_stats stats = { 0 };
 	struct ref_array refs = { 0 };
+	struct rev_info revs;
 
+	repo_init_revisions(repo, &revs, prefix);
 	if (filter_refs(&refs, &filter, FILTER_REFS_REGULAR))
 		die(_("unable to filter refs"));
 
-	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);
 
 	stats_table_clear(&table);
+	release_revisions(&revs);
 	ref_array_clear(&refs);
 
 	return 0;
diff --git a/t/t1901-repo-stats.sh b/t/t1901-repo-stats.sh
index 535ac511dd..315b9e1767 100755
--- a/t/t1901-repo-stats.sh
+++ b/t/t1901-repo-stats.sh
@@ -10,14 +10,21 @@ test_expect_success 'empty repository' '
 	(
 		cd repo &&
 		cat >expect <<-\EOF &&
-		| Repository stats | Value |
-		| ---------------- | ----- |
-		| * References     |       |
-		|   * Count        |     0 |
-		|     * Branches   |     0 |
-		|     * Tags       |     0 |
-		|     * Remotes    |     0 |
-		|     * Others     |     0 |
+		| Repository stats    | Value |
+		| ------------------- | ----- |
+		| * References        |       |
+		|   * Count           |     0 |
+		|     * Branches      |     0 |
+		|     * Tags          |     0 |
+		|     * Remotes       |     0 |
+		|     * Others        |     0 |
+		|                     |       |
+		| * Reachable objects |       |
+		|   * Count           |     0 |
+		|     * Commits       |     0 |
+		|     * Trees         |     0 |
+		|     * Blobs         |     0 |
+		|     * Tags          |     0 |
 		EOF
 
 		git repo stats >out 2>err &&
@@ -27,28 +34,36 @@ test_expect_success 'empty repository' '
 	)
 '
 
-test_expect_success 'repository with references' '
+test_expect_success 'repository with references and objects' '
 	test_when_finished "rm -rf repo" &&
 	git init repo &&
 	(
 		cd repo &&
-		git commit --allow-empty -m init &&
+		test_commit_bulk 42 &&
 		git tag -a foo -m bar &&
 
 		oid="$(git rev-parse HEAD)" &&
 		git update-ref refs/remotes/origin/foo "$oid" &&
 
+		# Also creates a commit, tree, and blob.
 		git notes add -m foo &&
 
 		cat >expect <<-\EOF &&
-		| Repository stats | Value |
-		| ---------------- | ----- |
-		| * References     |       |
-		|   * Count        |     4 |
-		|     * Branches   |     1 |
-		|     * Tags       |     1 |
-		|     * Remotes    |     1 |
-		|     * Others     |     1 |
+		| Repository stats    | Value |
+		| ------------------- | ----- |
+		| * References        |       |
+		|   * Count           |     4 |
+		|     * Branches      |     1 |
+		|     * Tags          |     1 |
+		|     * Remotes       |     1 |
+		|     * Others        |     1 |
+		|                     |       |
+		| * Reachable objects |       |
+		|   * Count           |   130 |
+		|     * Commits       |    43 |
+		|     * Trees         |    43 |
+		|     * Blobs         |    43 |
+		|     * Tags          |     1 |
 		EOF
 
 		git repo stats >out 2>err &&
-- 
2.51.0.193.g4975ec3473b