Volume XXII, number 279Tuesday, October 6, 2026Latest message 1 hour ago

The Git List

News and archive of git@vger.kernel.org, since April 2005

patchrepo: add filtering options to "repo structure"

7 messages between Sep 24, 2026 and Oct 6, 2026, from Mark C. Chu-Carroll, Patrick Steinhardt.

Plain Markdown or JSON for tools and agents. Diffs are folded; open one to read it.

Mark C. Chu-CarrollSep 24, 2026, 16:45 UTC on lore
    repo: add filtering options to "repo structure"
    "git repo structure" provides a collection of useful information
    about the information stored in a repo. In particular, it's
    valuable for diagnosing performance issues caused by large objects
    stored in a repo.
    The current implementation of "git repo stucture" provides summary
    information about everything in the repository - all of the
    branches, remotes, tags, stashes, and notes. But sometimes
    to properly diagnose a problem, it's useful to be able to exclude
    refs that are known to not be relevant to the issue at hand.
    Add a set of flags that allow a user to selective exclude
    reference types from the report generated by "git repo structure".
    When a ref type is excluded by the filter, it no longer appears
    in the report (ie, if "--no-tags" is passed, the report line
    for "Branches" will no longer appear under "* References").
    Following the pattern of flags that are only used to
    disable functionality (eg, "--no-verify" in "builtins/push.c"),
    only the "--no-<reftype>" syntax is listed in the updated
    documentation.
    Overview of the changes:
    - Add an enum to represent the structure flags.
    - Add structure flags to the options for the "repo structure" commands.
    - For each reference flag, add a conditional in "count_references"
      which decides whether or not to add a ref to the pending list.
      If an references is not added to the pending list, the things it
      transitively references will not be added to the stats.
    - Add a set of test cases to verify that reference counts
      in the repo structure report correctly omit the specified
      resource types.
    - Update the documentation for git-repo to include the new options.
Mark C. Chu-Carroll (1):
  repo: add filtering options to "repo structure"
 Documentation/git-repo.adoc |  35 ++++-
 builtin/repo.c              | 158 +++++++++++++++------
 t/t1901-repo-structure.sh   | 276 ++++++++++++++++++++++++++++++++++++
 3 files changed, 427 insertions(+), 42 deletions(-)
-- 
2.53.0
Mark C. Chu-CarrollSep 24, 2026, 16:45 UTC in reply to Mark C. Chu-Carroll on lore

[PATCH 1/1] repo: add filtering options to "repo structure"

"git repo structure" provides a collection of useful information about the information stored in a repo. In particular, it's valuable for diagnosing performance issues caused by large objects stored in a repo.

The current implementation of "git repo stucture" provides summary information about everything in the repository - all of the branches, remotes, tags, stashes, and notes. But sometimes to properly diagnose a problem, it's useful to be able to exclude refs that are known to not be relevant to the issue at hand.

Add a set of flags that allow a user to selective exclude reference types from the report generated by "git repo structure". When a ref type is excluded by the filter, it no longer appears in the report (ie, if "--no-tags" is passed, the report line for "Branches" will no longer appear under "* References"). Following the pattern of flags that are only used to disable functionality (eg, "--no-verify" in "builtins/push.c"), only the "--no-<reftype>" syntax is listed in the updated documentation.

Overview of the changes:
- Add an enum to represent the structure flags.
- Add structure flags to the options for the "repo structure" commands.
- For each reference flag, add a conditional in "count_references"
  which decides whether or not to add a ref to the pending list.
  If an references is not added to the pending list, the things it
  transitively references will not be added to the stats.
- Add a set of test cases to verify that reference counts
  in the repo structure report correctly omit the specified
  resource types.
- Update the documentation for git-repo to include the new options.
Signed-off-by: Mark C. Chu-Carroll <markchucarroll@fastmail.com>
---
 Documentation/git-repo.adoc |  35 ++++-
 builtin/repo.c              | 158 +++++++++++++++------
 t/t1901-repo-structure.sh   | 276 ++++++++++++++++++++++++++++++++++++
 3 files changed, 427 insertions(+), 42 deletions(-)
Show changes to 3 files +427 −42

Documentation/git-repo.adoc, builtin/repo.c, t/t1901-repo-structure.sh

diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index ed7d80c690..463260cd23 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -10,7 +10,7 @@ SYNOPSIS
 [synopsis]
 git repo info [--format=(lines|nul) | -z] [--all | <key>...]
 git repo info --keys [--format=(lines|nul) | -z]
-git repo structure [--format=(table|lines|nul) | -z]
+git repo structure [--format=(table|lines|nul) | -z] [--no-branches] [--no-tags] [--no-remotes] [--no-notes] [--no-stashes]`::
 
 DESCRIPTION
 -----------
@@ -56,7 +56,7 @@ supported:
 `nul`:::
 	Similar to `lines`, but using a _NUL_ character after each value.
 
-`structure [--format=(table|lines|nul) | -z]`::
+`structure [--format=(table|lines|nul) | -z] [--no-branches] [--no-tags] [--no-remotes] [--no-notes] [--no-stashes]`::
 	Retrieve statistics about the current repository structure. The
 	following kinds of information are reported:
 +
@@ -66,6 +66,23 @@ supported:
 * Total disk size of reachable objects by type
 * Largest reachable objects in the repository by type
 +
+The types of objects counted can be filtered using flags:
++
+`--no-branches`:::
+	Omit branch refs from the set of objects to count.
+
+`--no-tags`:::
+	Omit tags from the set of objects to count.
+
+`--no-remotes`:::
+	Omit remotes from the set of objects to count.
+
+`--no-notes`:::
+	Omit notes from the set of objects to count.
+
+`--no-stashes`:::
+	Omit stashes from the set of objects to count.
++
 The output format can be chosen through the flag `--format`. Three formats are
 supported:
 +
@@ -141,6 +158,20 @@ using the `nul` format:
 git repo info --format=nul layout.bare layout.shallow
 ------------
 
+* Generates information about storage usage in the repository:
++
+------------
+git repo structure
+------------
++
+
+* Generates information about storage usage in the repository omitting
+remotes:
++
+------------
+git repo structure --no-remotes
+------------
+
 SEE ALSO
 --------
 linkgit:git-rev-parse[1]
diff --git a/builtin/repo.c b/builtin/repo.c
index 84e012f83f..c8f6e38011 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -53,6 +53,20 @@ enum output_format {
 	FORMAT_NUL_TERMINATED,
 };
 
+enum repo_structure_filter_flags {
+	REPO_STRUCTURE_FILTER_BRANCHES = (1 << 0),
+	REPO_STRUCTURE_FILTER_TAGS = (1 << 1),
+	REPO_STRUCTURE_FILTER_REMOTES = (1 << 2),
+	REPO_STRUCTURE_FILTER_NOTES  = (1 << 3),
+	REPO_STRUCTURE_FILTER_STASHES = (1 << 4),
+};
+
+#define REPO_STRUCTURE_FILTER_FLAGS_ALL (REPO_STRUCTURE_FILTER_BRANCHES | \
+					 REPO_STRUCTURE_FILTER_TAGS |	\
+					 REPO_STRUCTURE_FILTER_REMOTES | \
+					 REPO_STRUCTURE_FILTER_NOTES |\
+					 REPO_STRUCTURE_FILTER_STASHES)
+
 struct repo_info_field {
 	const char *key;
 	get_value_fn *get_value;
@@ -490,7 +504,8 @@ static inline size_t get_total_object_values(struct object_values *values)
 }
 
 static void stats_table_setup_structure(struct stats_table *table,
-					struct repo_structure *stats)
+					struct repo_structure *stats,
+					enum repo_structure_filter_flags flags)
 {
 	struct object_stats *objects = &stats->objects;
 	struct ref_stats *refs = &stats->refs;
@@ -502,9 +517,15 @@ static void stats_table_setup_structure(struct stats_table *table,
 	ref_total = get_total_reference_count(refs);
 	stats_table_addf(table, "* %s", _("References"));
 	stats_table_count_addf(table, ref_total, "  * %s", _("Count"));
-	stats_table_count_addf(table, refs->branches, "    * %s", _("Branches"));
-	stats_table_count_addf(table, refs->tags, "    * %s", _("Tags"));
-	stats_table_count_addf(table, refs->remotes, "    * %s", _("Remotes"));
+	if (flags & REPO_STRUCTURE_FILTER_BRANCHES) {
+		stats_table_count_addf(table, refs->branches, "    * %s", _("Branches"));
+	}
+	if (flags & REPO_STRUCTURE_FILTER_TAGS) {
+		stats_table_count_addf(table, refs->tags, "    * %s", _("Tags"));
+	}
+	if (flags & REPO_STRUCTURE_FILTER_REMOTES) {
+		stats_table_count_addf(table, refs->remotes, "    * %s", _("Remotes"));
+	}
 	stats_table_count_addf(table, refs->others, "    * %s", _("Others"));
 
 	object_count_total = get_total_object_values(&objects->type_counts);
@@ -517,8 +538,10 @@ static void stats_table_setup_structure(struct stats_table *table,
 			       "    * %s", _("Trees"));
 	stats_table_count_addf(table, objects->type_counts.blobs,
 			       "    * %s", _("Blobs"));
-	stats_table_count_addf(table, objects->type_counts.tags,
-			       "    * %s", _("Tags"));
+	if (flags & REPO_STRUCTURE_FILTER_TAGS) {
+		stats_table_count_addf(table, objects->type_counts.tags,
+				       "    * %s", _("Tags"));
+	}
 
 	inflated_object_total = get_total_object_values(&objects->inflated_sizes);
 	stats_table_size_addf(table, inflated_object_total,
@@ -529,8 +552,11 @@ static void stats_table_setup_structure(struct stats_table *table,
 			      "    * %s", _("Trees"));
 	stats_table_size_addf(table, objects->inflated_sizes.blobs,
 			      "    * %s", _("Blobs"));
-	stats_table_size_addf(table, objects->inflated_sizes.tags,
-			      "    * %s", _("Tags"));
+
+	if (flags & REPO_STRUCTURE_FILTER_TAGS) {
+		stats_table_size_addf(table, objects->inflated_sizes.tags,
+				      "    * %s", _("Tags"));
+	}
 
 	disk_object_total = get_total_object_values(&objects->disk_sizes);
 	stats_table_size_addf(table, disk_object_total,
@@ -541,8 +567,10 @@ static void stats_table_setup_structure(struct stats_table *table,
 			      "    * %s", _("Trees"));
 	stats_table_size_addf(table, objects->disk_sizes.blobs,
 			      "    * %s", _("Blobs"));
-	stats_table_size_addf(table, objects->disk_sizes.tags,
-			      "    * %s", _("Tags"));
+	if (flags & REPO_STRUCTURE_FILTER_TAGS) {
+		stats_table_size_addf(table, objects->disk_sizes.tags,
+				      "    * %s", _("Tags"));
+	}
 
 	stats_table_addf(table, "");
 	stats_table_addf(table, "* %s", _("Largest objects"));
@@ -569,11 +597,13 @@ static void stats_table_setup_structure(struct stats_table *table,
 				     &objects->largest.blob_size.oid,
 				     objects->largest.blob_size.value,
 				     "    * %s", _("Maximum size"));
-	stats_table_addf(table, "  * %s", _("Tags"));
-	stats_table_object_size_addf(table,
-				     &objects->largest.tag_size.oid,
-				     objects->largest.tag_size.value,
-				     "    * %s", _("Maximum size"));
+	if (flags & REPO_STRUCTURE_FILTER_TAGS) {
+		stats_table_addf(table, "  * %s", _("Tags"));
+		stats_table_object_size_addf(table,
+					     &objects->largest.tag_size.oid,
+					     objects->largest.tag_size.value,
+					     "    * %s", _("Maximum size"));
+	}
 }
 
 #define INDEX_WIDTH 4
@@ -681,14 +711,21 @@ static void print_object_data(const char *key, char key_delim,
 }
 
 static void structure_keyvalue_print(struct repo_structure *stats,
-				     char key_delim, char value_delim)
+				     char key_delim, char value_delim,
+				     enum repo_structure_filter_flags flags)
 {
-	print_keyvalue("references.branches.count", key_delim,
-		       stats->refs.branches, value_delim);
+	if (flags & REPO_STRUCTURE_FILTER_BRANCHES) {
+		print_keyvalue("references.branches.count", key_delim,
+			       stats->refs.branches, value_delim);
+	}
+	if (flags & REPO_STRUCTURE_FILTER_TAGS) {
 	print_keyvalue("references.tags.count", key_delim,
 		       stats->refs.tags, value_delim);
-	print_keyvalue("references.remotes.count", key_delim,
-		       stats->refs.remotes, value_delim);
+	}
+	if (flags & REPO_STRUCTURE_FILTER_REMOTES) {
+		print_keyvalue("references.remotes.count", key_delim,
+			       stats->refs.remotes, value_delim);
+	}
 	print_keyvalue("references.others.count", key_delim,
 		       stats->refs.others, value_delim);
 
@@ -698,8 +735,10 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 		       stats->objects.type_counts.trees, value_delim);
 	print_keyvalue("objects.blobs.count", key_delim,
 		       stats->objects.type_counts.blobs, value_delim);
-	print_keyvalue("objects.tags.count", key_delim,
-		       stats->objects.type_counts.tags, value_delim);
+	if (flags & REPO_STRUCTURE_FILTER_TAGS) {
+		print_keyvalue("objects.tags.count", key_delim,
+			       stats->objects.type_counts.tags, value_delim);
+	}
 
 	print_keyvalue("objects.commits.inflated_size", key_delim,
 		       stats->objects.inflated_sizes.commits, value_delim);
@@ -707,8 +746,10 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 		       stats->objects.inflated_sizes.trees, value_delim);
 	print_keyvalue("objects.blobs.inflated_size", key_delim,
 		       stats->objects.inflated_sizes.blobs, value_delim);
-	print_keyvalue("objects.tags.inflated_size", key_delim,
-		       stats->objects.inflated_sizes.tags, value_delim);
+	if (flags & REPO_STRUCTURE_FILTER_TAGS) {
+		print_keyvalue("objects.tags.inflated_size", key_delim,
+			       stats->objects.inflated_sizes.tags, value_delim);
+	}
 
 	print_keyvalue("objects.commits.disk_size", key_delim,
 		       stats->objects.disk_sizes.commits, value_delim);
@@ -716,8 +757,10 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 		       stats->objects.disk_sizes.trees, value_delim);
 	print_keyvalue("objects.blobs.disk_size", key_delim,
 		       stats->objects.disk_sizes.blobs, value_delim);
-	print_keyvalue("objects.tags.disk_size", key_delim,
-		       stats->objects.disk_sizes.tags, value_delim);
+	if (flags & REPO_STRUCTURE_FILTER_TAGS) {
+		print_keyvalue("objects.tags.disk_size", key_delim,
+			       stats->objects.disk_sizes.tags, value_delim);
+	}
 
 	print_object_data("objects.commits.max_size", key_delim,
 			  &stats->objects.largest.commit_size, value_delim);
@@ -725,8 +768,10 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 			  &stats->objects.largest.tree_size, value_delim);
 	print_object_data("objects.blobs.max_size", key_delim,
 			  &stats->objects.largest.blob_size, value_delim);
-	print_object_data("objects.tags.max_size", key_delim,
-			  &stats->objects.largest.tag_size, value_delim);
+	if (flags & REPO_STRUCTURE_FILTER_TAGS) {
+		print_object_data("objects.tags.max_size", key_delim,
+				  &stats->objects.largest.tag_size, value_delim);
+	}
 
 	print_object_data("objects.commits.max_parents", key_delim,
 			  &stats->objects.largest.parent_count, value_delim);
@@ -739,6 +784,7 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 struct count_references_data {
 	struct ref_stats *stats;
 	struct rev_info *revs;
+	enum repo_structure_filter_flags flags;
 	struct progress *progress;
 };
 
@@ -747,19 +793,42 @@ static int count_references(const struct reference *ref, void *cb_data)
 	struct count_references_data *data = cb_data;
 	struct ref_stats *stats = data->stats;
 	size_t ref_count;
+	bool add_pending = false;
 
 	switch (ref_kind_from_refname(ref->name)) {
 	case FILTER_REFS_BRANCHES:
-		stats->branches++;
+		if (data->flags & REPO_STRUCTURE_FILTER_BRANCHES) {
+			stats->branches++;
+			add_pending = true;
+		}
 		break;
 	case FILTER_REFS_REMOTES:
-		stats->remotes++;
+		if (data->flags & REPO_STRUCTURE_FILTER_REMOTES) {
+			stats->remotes++;
+			add_pending = true;
+		}
 		break;
 	case FILTER_REFS_TAGS:
-		stats->tags++;
+		if (data->flags & REPO_STRUCTURE_FILTER_TAGS) {
+			stats->tags++;
+			add_pending = true;
+		}
 		break;
 	case FILTER_REFS_OTHERS:
-		stats->others++;
+		if (!strcmp(ref->name, "refs/stash")) {
+			if (data->flags & REPO_STRUCTURE_FILTER_STASHES) {
+				stats->others++;
+				add_pending = true;
+			}
+		} else if (starts_with(ref->name, "refs/notes")) {
+			if (data->flags & REPO_STRUCTURE_FILTER_NOTES) {
+				stats->others++;
+				add_pending = true;
+			}
+		} else {
+			stats->others++;
+			add_pending = true;
+		}
 		break;
 	default:
 		BUG("unexpected reference type");
@@ -769,10 +838,11 @@ static int count_references(const struct reference *ref, void *cb_data)
 	 * While iterating through references for counting, also add OIDs in
 	 * preparation for the path walk.
 	 */
-	add_pending_oid(data->revs, NULL, ref->oid, 0);
-
-	ref_count = get_total_reference_count(stats);
-	display_progress(data->progress, ref_count);
+	if (add_pending) {
+		add_pending_oid(data->revs, NULL, ref->oid, 0);
+		ref_count = get_total_reference_count(stats);
+		display_progress(data->progress, ref_count);
+	}
 
 	return 0;
 }
@@ -780,11 +850,13 @@ static int count_references(const struct reference *ref, void *cb_data)
 static void structure_count_references(struct ref_stats *stats,
 				       struct rev_info *revs,
 				       struct repository *repo,
+				       enum repo_structure_filter_flags flags,
 				       int show_progress)
 {
 	struct count_references_data data = {
 		.stats = stats,
 		.revs = revs,
+		.flags = flags,
 	};
 
 	if (show_progress)
@@ -935,6 +1007,7 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
 	struct repo_structure stats = { 0 };
 	struct rev_info revs;
 	int show_progress = -1;
+	int flags = REPO_STRUCTURE_FILTER_FLAGS_ALL;
 	struct option options[] = {
 		OPT_CALLBACK_F(0, "format", &format, N_("format"),
 			       N_("output format"),
@@ -944,6 +1017,11 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
 			       PARSE_OPT_NONEG | PARSE_OPT_NOARG,
 			       parse_format_cb),
 		OPT_BOOL(0, "progress", &show_progress, N_("show progress")),
+		OPT_BIT(0, "branches", &flags, N_("include branches in structure statistics"), REPO_STRUCTURE_FILTER_BRANCHES),
+		OPT_BIT(0, "tags", &flags, N_("include tags in structure statistics"), REPO_STRUCTURE_FILTER_TAGS),
+		OPT_BIT(0, "remotes", &flags, N_("include remotes in structure statistics"), REPO_STRUCTURE_FILTER_REMOTES),
+		OPT_BIT(0, "notes", &flags, N_("include notes in structure statistics"), REPO_STRUCTURE_FILTER_NOTES),
+		OPT_BIT(0, "stashes", &flags, N_("include stashes in structure statistics"), REPO_STRUCTURE_FILTER_STASHES),
 		OPT_END()
 	};
 
@@ -956,19 +1034,19 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
 	if (show_progress < 0)
 		show_progress = isatty(2);
 
-	structure_count_references(&stats.refs, &revs, repo, show_progress);
+	structure_count_references(&stats.refs, &revs, repo, flags, show_progress);
 	structure_count_objects(&stats.objects, &revs, repo, show_progress);
 
 	switch (format) {
 	case FORMAT_TABLE:
-		stats_table_setup_structure(&table, &stats);
+		stats_table_setup_structure(&table, &stats, flags);
 		stats_table_print_structure(&table);
 		break;
 	case FORMAT_NEWLINE_TERMINATED:
-		structure_keyvalue_print(&stats, '=', '\n');
+		structure_keyvalue_print(&stats, '=', '\n', flags);
 		break;
 	case FORMAT_NUL_TERMINATED:
-		structure_keyvalue_print(&stats, '\n', '\0');
+		structure_keyvalue_print(&stats, '\n', '\0', flags);
 		break;
 	default:
 		BUG("invalid output format");
diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index 02cc2b594a..60a82493c6 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -144,6 +144,170 @@ test_expect_success SHA1 'repository with references and objects' '
 	)
 '
 
+
+test_expect_success SHA1 'repository with references and objects, filtered with --no-origins' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit_bulk 1005 &&
+		git tag -a foo -m bar &&
+
+		oid="$(git rev-parse HEAD)" &&
+		git update-ref refs/remotes/origin/foo "$oid" &&
+
+		git checkout -b other_branch &&
+		test_commit_bulk 5 &&
+
+		git checkout master &&
+
+		# Also creates a commit, tree, and blob.
+		git notes add -m foo &&
+
+		# The tags disk size is handled specially due to the
+		# git-rev-list(1) --disk-usage=human option printing the full
+		# "byte/bytes" unit string instead of just "B".
+		cat >expect <<-EOF &&
+		| Repository structure      | Value      |
+		| ------------------------- | ---------- |
+		| * References              |            |
+		|   * Count                 |      4     |
+		|     * Branches            |      2     |
+		|     * Tags                |      1     |
+		|     * Others              |      1     |
+		|                           |            |
+		| * Reachable objects       |            |
+		|   * Count                 |   3.02 k   |
+		|     * Commits             |   1.01 k   |
+		|     * Trees               |   1.01 k   |
+		|     * Blobs               |   1.01 k   |
+		|     * Tags                |      1     |
+		|   * Inflated size         |  16.04 MiB |
+		|     * Commits             | 219.00 KiB |
+		|     * Trees               |  15.81 MiB |
+		|     * Blobs               |  11.68 KiB |
+		|     * Tags                |    132 B   |
+		|   * Disk size             | $(object_type_disk_usage all true) |
+		|     * Commits             | $(object_type_disk_usage commit true) |
+		|     * Trees               | $(object_type_disk_usage tree true) |
+		|     * Blobs               |  $(object_type_disk_usage blob true) |
+		|     * Tags                |    $(object_type_disk_usage tag) B   |
+		|                           |            |
+		| * Largest objects         |            |
+		|   * Commits               |            |
+		|     * Maximum size    [1] |    223 B   |
+		|     * Maximum parents [2] |      1     |
+		|   * Trees                 |            |
+		|     * Maximum size    [3] |  32.29 KiB |
+		|     * Maximum entries [4] |   1.01 k   |
+		|   * Blobs                 |            |
+		|     * Maximum size    [5] |     13 B   |
+		|   * Tags                  |            |
+		|     * Maximum size    [6] |    132 B   |
+
+		[1] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
+		[2] 0a8f6a47f34078bb7c4b2bb4377c2133157f6703
+		[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[5] 97d808e45116bf02103490294d3d46dad7a2ac62
+		[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
+		EOF
+		git repo structure --no-remotes >actual 2>err &&
+		test_cmp expect actual &&
+		test_line_count = 0 err
+	)
+'
+
+test_expect_success SHA1 'repository with references and objects, filtered with "--no-branches"' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit_bulk 1005 &&
+		git tag -a foo -m bar &&
+
+		oid="$(git rev-parse HEAD)" &&
+		git update-ref refs/remotes/origin/foo "$oid" &&
+
+		git checkout -b other_branch &&
+		test_commit_bulk 5 &&
+
+		git checkout master &&
+
+		# Also creates a commit, tree, and blob.
+		git notes add -m foo &&
+
+		git repo structure --no-branches >actual 2>err &&
+		test_line_count = 0 err &&
+		test_grep "|   \* Count                 |      3     |" actual &&
+		test_grep "|     \* Others              |      1     |" actual &&
+		test_grep !  "Branches" actual
+	)
+'
+
+test_expect_success SHA1 'repository with references and objects, filtered with "--no-notes"' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit_bulk 1005 &&
+		git tag -a foo -m bar &&
+
+		oid="$(git rev-parse HEAD)" &&
+		git update-ref refs/remotes/origin/foo "$oid" &&
+
+		git checkout -b other_branch &&
+		test_commit_bulk 5 &&
+
+		git checkout master &&
+
+		# Also creates a commit, tree, and blob.
+		git notes add -m foo &&
+
+		git repo structure --no-notes >actual 2>err &&
+		test_line_count = 0 err &&
+		test_grep "|   \* Count                 |      4     |" actual &&
+		test_grep "|     \* Others              |      0     |" actual &&
+		test_grep "|     \* Commits             | 218.81 KiB |" actual
+	)
+'
+
+test_expect_success SHA1 'repository with references and objects, filtered with multiple flags' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit_bulk 1005 &&
+		git tag -a foo -m bar &&
+
+		oid="$(git rev-parse HEAD)" &&
+		git update-ref refs/remotes/origin/foo "$oid" &&
+
+		git checkout -b other_branch &&
+		test_commit_bulk 5 &&
+
+		git checkout master &&
+
+		# Also creates a commit, tree, and blob.
+		git notes add -m foo &&
+
+		git repo structure --no-notes --no-branches >actual 2>err &&
+		test_line_count = 0 err &&
+		test_grep "|   \* Count                 |      2     |" actual &&
+		test_grep "|     \* Others              |      0     |" actual &&
+		test_grep "|     \* Commits             | 217.73 KiB |" actual &&
+		test_grep !  "Branches" actual &&
+
+		git repo structure --no-notes --no-branches --no-remotes >actual 2>err &&
+		test_line_count = 0 err &&
+		test_grep "|   \* Count                 |      1     |" actual &&
+		test_grep "|     \* Others              |      0     |" actual &&
+		test_grep "|     \* Commits             | 217.73 KiB |" actual &&
+		test_grep !  "Branches" actual &&
+		test_grep ! "Remotes" actual
+	)
+'
+
 test_expect_success SHA1 'lines and nul format' '
 	test_when_finished "rm -rf repo" &&
 	git init repo &&
@@ -203,6 +367,118 @@ test_expect_success SHA1 'lines and nul format' '
 		test_line_count = 0 err
 	)
 '
+test_expect_success SHA1 'lines and nul format with --no-tags filter' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit_bulk 42 &&
+		git tag -a foo -m bar &&
+
+		cat >expect <<-EOF &&
+		references.branches.count=1
+		references.remotes.count=0
+		references.others.count=0
+		objects.commits.count=42
+		objects.trees.count=42
+		objects.blobs.count=42
+		objects.commits.inflated_size=9225
+		objects.trees.inflated_size=28554
+		objects.blobs.inflated_size=453
+		objects.commits.disk_size=$(object_type_disk_usage commit)
+		objects.trees.disk_size=$(object_type_disk_usage tree)
+		objects.blobs.disk_size=$(object_type_disk_usage blob)
+		objects.commits.max_size=221
+		objects.commits.max_size_oid=de3508174b5c2ace6993da67cae9be9069e2df39
+		objects.trees.max_size=1335
+		objects.trees.max_size_oid=09931deea9d81ec21300d3e13c74412f32eacec5
+		objects.blobs.max_size=11
+		objects.blobs.max_size_oid=eaeeedced46482bd4281fda5a5f05ce24854151f
+		objects.commits.max_parents=1
+		objects.commits.max_parents_oid=de3508174b5c2ace6993da67cae9be9069e2df39
+		objects.trees.max_entries=42
+		objects.trees.max_entries_oid=09931deea9d81ec21300d3e13c74412f32eacec5
+		EOF
+
+		git repo structure --format=lines --no-tags >actual 2>err &&
+		test_cmp expect actual &&
+		test_line_count = 0 err &&
+
+		git repo structure --format=nul --no-tags >out 2>err &&
+		tr "\012\000" "=\012" <out >actual &&
+
+		test_cmp expect actual &&
+		test_line_count = 0 err &&
+
+		# "-z", as a synonym to "--format=nul", participates in the
+		# usual "last one wins" rule.
+		git repo structure --format=table -z --no-tags >out 2>err &&
+		tr "\012\000" "=\012" <out >actual &&
+
+		test_cmp expect actual &&
+		test_line_count = 0 err
+	)
+'
+
+test_expect_success SHA1 'lines and nul format with --no-branches filter' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit_bulk 42 &&
+		git tag -a foo -m bar &&
+
+		cat >expect <<-EOF &&
+		references.tags.count=1
+		references.remotes.count=0
+		references.others.count=0
+		objects.commits.count=42
+		objects.trees.count=42
+		objects.blobs.count=42
+		objects.tags.count=1
+		objects.commits.inflated_size=9225
+		objects.trees.inflated_size=28554
+		objects.blobs.inflated_size=453
+		objects.tags.inflated_size=132
+		objects.commits.disk_size=$(object_type_disk_usage commit)
+		objects.trees.disk_size=$(object_type_disk_usage tree)
+		objects.blobs.disk_size=$(object_type_disk_usage blob)
+		objects.tags.disk_size=$(object_type_disk_usage tag)
+		objects.commits.max_size=221
+		objects.commits.max_size_oid=de3508174b5c2ace6993da67cae9be9069e2df39
+		objects.trees.max_size=1335
+		objects.trees.max_size_oid=09931deea9d81ec21300d3e13c74412f32eacec5
+		objects.blobs.max_size=11
+		objects.blobs.max_size_oid=eaeeedced46482bd4281fda5a5f05ce24854151f
+		objects.tags.max_size=132
+		objects.tags.max_size_oid=1ee0f2b16ea37d895dbe9dbd76cd2ac70446176c
+		objects.commits.max_parents=1
+		objects.commits.max_parents_oid=de3508174b5c2ace6993da67cae9be9069e2df39
+		objects.trees.max_entries=42
+		objects.trees.max_entries_oid=09931deea9d81ec21300d3e13c74412f32eacec5
+		EOF
+
+		git repo structure --format=lines --no-branches >actual 2>err &&
+		cp expect /tmp/expect &&
+		cp actual /tmp/actual &&
+		test_cmp expect actual &&
+		test_line_count = 0 err &&
+
+		git repo structure --format=nul --no-branches >out 2>err &&
+		tr "\012\000" "=\012" <out >actual &&
+
+		test_cmp expect actual &&
+		test_line_count = 0 err &&
+
+		# "-z", as a synonym to "--format=nul", participates in the
+		# usual "last one wins" rule.
+		git repo structure --format=table -z --no-branches >out 2>err &&
+		tr "\012\000" "=\012" <out >actual &&
+
+		test_cmp expect actual &&
+		test_line_count = 0 err
+	)
+'
 
 test_expect_success 'progress meter option' '
 	test_when_finished "rm -rf repo" &&
-- 
2.53.0
Patrick SteinhardtSep 30, 2026, 16:28 UTC in reply to Mark C. Chu-Carroll on lore

Re: [PATCH 1/1] repo: add filtering options to "repo structure"

On Thu, Sep 24, 2026 at 12:45:03PM -0400, Mark C. Chu-Carroll wrote:
Show 10 quoted lines
> "git repo structure" provides a collection of useful information
> about the information stored in a repo. In particular, it's
> valuable for diagnosing performance issues caused by large objects
> stored in a repo.
> 
> The current implementation of "git repo stucture" provides summary
> information about everything in the repository - all of the
> branches, remotes, tags, stashes, and notes. But sometimes
> to properly diagnose a problem, it's useful to be able to exclude
> refs that are known to not be relevant to the issue at hand.

Yes, indeed. Sometimes you may for example want to figure out where exactly the storage size of a particular repository is going. Or in the case of GitLab for example, we may have bookkeeping references that are not controllable by customers. So we may only want to get the structure for all the customer-controllable branches there.

Show 9 quoted lines
> Add a set of flags that allow a user to selective exclude
> reference types from the report generated by "git repo structure".
> When a ref type is excluded by the filter, it no longer appears
> in the report (ie, if "--no-tags" is passed, the report line
> for "Branches" will no longer appear under "* References").
> Following the pattern of flags that are only used to
> disable functionality (eg, "--no-verify" in "builtins/push.c"),
> only the "--no-<reftype>" syntax is listed in the updated
> documentation.

Hmm, okay. I would have expected that the user can essentially pass arbitrary revisions as understood by git-log(1) et al. And if they pass any such revisions, we should not enumerate anything but what they have passed, so the flags shouldn't only be used to exclude.

So, for example:
    $ git repo structure --branches
    $ git repo structure master
    $ git repo structure --all --not --branches

I would hope that git-repo(1) can achieve that rather easily because I expect that it uses `struct rev_info`, but let's read on.

Show 11 quoted lines
> Overview of the changes:
> - Add an enum to represent the structure flags.
> - Add structure flags to the options for the "repo structure" commands.
> - For each reference flag, add a conditional in "count_references"
>   which decides whether or not to add a ref to the pending list.
>   If an references is not added to the pending list, the things it
>   transitively references will not be added to the stats.
> - Add a set of test cases to verify that reference counts
>   in the repo structure report correctly omit the specified
>   resource types.
> - Update the documentation for git-repo to include the new options.

Note that we typically don't have lists of what exactly has changed in the commit. That kind of information is already visible from the diff itself. So what the commit message itself should focus on is whether any of these changes are non-obvious or whethere there's any dragons to be found.

So in summary: everything that may surprise the reader should be part of the commit message, everything that's just obvious plumbing doesn't really have to be mentioned.

Show 33 quoted lines
> diff --git a/builtin/repo.c b/builtin/repo.c
> index 84e012f83f..c8f6e38011 100644
> --- a/builtin/repo.c
> +++ b/builtin/repo.c
> @@ -490,7 +504,8 @@ static inline size_t get_total_object_values(struct object_values *values)
>  }
>  
>  static void stats_table_setup_structure(struct stats_table *table,
> -					struct repo_structure *stats)
> +					struct repo_structure *stats,
> +					enum repo_structure_filter_flags flags)
>  {
>  	struct object_stats *objects = &stats->objects;
>  	struct ref_stats *refs = &stats->refs;
> @@ -502,9 +517,15 @@ static void stats_table_setup_structure(struct stats_table *table,
>  	ref_total = get_total_reference_count(refs);
>  	stats_table_addf(table, "* %s", _("References"));
>  	stats_table_count_addf(table, ref_total, "  * %s", _("Count"));
> -	stats_table_count_addf(table, refs->branches, "    * %s", _("Branches"));
> -	stats_table_count_addf(table, refs->tags, "    * %s", _("Tags"));
> -	stats_table_count_addf(table, refs->remotes, "    * %s", _("Remotes"));
> +	if (flags & REPO_STRUCTURE_FILTER_BRANCHES) {
> +		stats_table_count_addf(table, refs->branches, "    * %s", _("Branches"));
> +	}
> +	if (flags & REPO_STRUCTURE_FILTER_TAGS) {
> +		stats_table_count_addf(table, refs->tags, "    * %s", _("Tags"));
> +	}
> +	if (flags & REPO_STRUCTURE_FILTER_REMOTES) {
> +		stats_table_count_addf(table, refs->remotes, "    * %s", _("Remotes"));
> +	}
>  	stats_table_count_addf(table, refs->others, "    * %s", _("Others"));
>  
>  	object_count_total = get_total_object_values(&objects->type_counts);
Coding style: we don't use curly braces around single-line statements.

But more importantly, I think this is where the mismatch in expectations comes from that I was pointing out further up. My expectation was that what we want to achieve is to filter the reachable objects by revisions, which I think is a much more useful thing to do. But what the flags do instead us to filter the output in the "References" count.

I think that we should rather go into the direction of filtering objects and not the ref output, as the latter isn't all that useful. It _may_ make sense to maybe make some sections of the output optional, but excluding individual ref types is arguably too fine-grained.

In any case, to go into the direction of filtering objects you'd want to adapt `parse_options()` so that it accepts unknown options (you can achieve that by passing `PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT`) and then pass argv to `setup_revisions()`. And I think that _should_ already achieve proper filtering of objects by revisions.

Thanks!
Patrick
Mark C. Chu-CarrollOct 5, 2026, 17:40 UTC in reply to Mark C. Chu-Carroll on lore

[PATCH v2 0/1] repo: add filtering options to "repo structure"

"git repo structure" provides a collection of useful information about the information stored in a repo. In particular, it's valuable for diagnosing performance issues caused by large objects stored in a repo.

The current implementation of "git repo stucture" provides summary information about everything in the repository - all of the branches, remotes, tags, stashes, and notes. But sometimes to properly diagnose a problem, it's useful to be able to get information about the specific part of the repo that's exhibiting a problem.

Add the option to specify a set of filters in the form of a list of include and exclude queries. Each of these specifies a commit or ref or range. The set of objects processed will consist of all objects reachable from any of the includes which are _not_ reached exclusively by paths including any of the excludes.

Updates since v1: this change has been completely rewritten. The original version implemented filters the same way as "git-sizer", by selecting object types to include or exclude. This version drops he type-based filters in favor of specifying object traversal roots and exclusions.

Mark C. Chu-Carroll (1):
  repo: add filtering options to "repo structure"
 Documentation/git-repo.adoc | 41 ++++++++++++++++--
 builtin/repo.c              | 16 +++++--
 t/t1901-repo-structure.sh   | 84 +++++++++++++++++++++++++++++++++++++
 3 files changed, 133 insertions(+), 8 deletions(-)
-- 
2.53.0
Mark C. Chu-CarrollOct 5, 2026, 17:40 UTC in reply to Mark C. Chu-Carroll on lore

[PATCH v2 1/1] repo: add filtering options to "repo structure"

Implement filtering for repo structure, imitating the mechanism used in "git log".

Signed-off-by: Mark C. Chu-Carroll <markchucarroll@fastmail.com>
---
 Documentation/git-repo.adoc | 41 ++++++++++++++++--
 builtin/repo.c              | 16 +++++--
 t/t1901-repo-structure.sh   | 84 +++++++++++++++++++++++++++++++++++++
 3 files changed, 133 insertions(+), 8 deletions(-)
Show changes to 3 files +133 −8

Documentation/git-repo.adoc, builtin/repo.c, t/t1901-repo-structure.sh

diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index ed7d80c690..5cbdf8e727 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -10,7 +10,7 @@ SYNOPSIS
 [synopsis]
 git repo info [--format=(lines|nul) | -z] [--all | <key>...]
 git repo info --keys [--format=(lines|nul) | -z]
-git repo structure [--format=(table|lines|nul) | -z]
+git repo structure [--format=(table|lines|nul) | -z] [<include|^exclude>...]
 
 DESCRIPTION
 -----------
@@ -56,9 +56,10 @@ supported:
 `nul`:::
 	Similar to `lines`, but using a _NUL_ character after each value.
 
-`structure [--format=(table|lines|nul) | -z]`::
-	Retrieve statistics about the current repository structure. The
-	following kinds of information are reported:
+`structure [--format=(table|lines|nul) | -z] [<include|^exclude>...]::
++
+Retrieve statistics about the current repository structure. The
+following kinds of information are reported:
 +
 * Reference counts categorized by type
 * Reachable object counts categorized by type
@@ -66,6 +67,16 @@ supported:
 * Total disk size of reachable objects by type
 * Largest reachable objects in the repository by type
 +
+The set of objects counted can be filtered by specifying a
+collection of query clauses to select which objects will be
+counted. These parameters follow the same syntax as the parameters
+to similar commands like `git log`. Semantically, these parameters
+are treated as a collection of include and exclude specifiers. Th
+set of objects counted will consist of all objects reachable from
+an object included by one of the include specifiers via a path that
+does not include an object in an exclude clause. If no includes
+are specified, then the include set is all reachable objects. 
++
 The output format can be chosen through the flag `--format`. Three formats are
 supported:
 +
@@ -141,6 +152,28 @@ using the `nul` format:
 git repo info --format=nul layout.bare layout.shallow
 ------------
 
+* Generates information about storage usage in the repository:
++
+------------
+git repo structure
+------------
++
+
+* Generates information about storage usage in the repository omitting
+the branch "foo":
++
+------------
+git repo structure ^foo 
+------------
++
+* Generates information about repository objects reachable from
+the references "x" and "y", but omitting anything that can
+only be reached on a path including "xchild":
++
+------------
+git repo structure x y ^xchild
+------------
++
 SEE ALSO
 --------
 linkgit:git-rev-parse[1]
diff --git a/builtin/repo.c b/builtin/repo.c
index 84e012f83f..b3aca71298 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -27,7 +27,7 @@
 	"git repo info --keys [--format=(lines|nul) | -z]"
 
 #define REPO_STRUCTURE_USAGE \
-	"git repo structure [--format=(table|lines|nul) | -z]"
+	"git repo structure [--format=(table|lines|nul) | -z] [<include|^exclude>...]"
 
 static const char *const repo_usage[] = {
 	REPO_INFO_USAGE,
@@ -946,12 +946,20 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
 		OPT_BOOL(0, "progress", &show_progress, N_("show progress")),
 		OPT_END()
 	};
+	struct setup_revision_opt s_r_opt;
+	memset(&s_r_opt, 0, sizeof(s_r_opt));
+	s_r_opt.def = "HEAD";
+	s_r_opt.revarg_opt = REVARG_COMMITTISH;
 
-	argc = parse_options(argc, argv, prefix, options, repo_structure_usage, 0);
-	if (argc)
-		usage(_("too many arguments"));
+	argc = parse_options(argc, argv, prefix, options, repo_structure_usage,
+			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	repo_init_revisions(repo, &revs, prefix);
+	if (argc > 1) {
+		argc = setup_revisions(argc, argv, &revs, &s_r_opt);
+		if (argc > 1)
+			usage(_("too many arguments"));
+	}
 
 	if (show_progress < 0)
 		show_progress = isatty(2);
diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index 02cc2b594a..eb2c595955 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -144,6 +144,90 @@ test_expect_success SHA1 'repository with references and objects' '
 	)
 '
 
+test_expect_success SHA1 'repository with references and objects, filtered' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit_bulk 1005 &&
+		git tag -a foo -m bar &&
+
+		oid="$(git rev-parse HEAD)" &&
+		git update-ref refs/remotes/origin/foo "$oid" &&
+		git checkout -b grobble &&
+		test_commit_bulk --ref=refs/heads/grobble 20 &&
+		git checkout master &&
+ 		test_commit_bulk 20 &&
+		# Also creates a commit, tree, and blob.
+		git notes add -m foo &&
+
+		# git-rev-list(1) --disk-usage=human option printing the full
+		# "byte/bytes" unit string instead of just "B".
+		cat >expect <<-EOF &&
+		| Repository structure      | Value      |
+		| ------------------------- | ---------- |
+		| * References              |            |
+		|   * Count                 |      5     |
+		|     * Branches            |      2     |
+		|     * Tags                |      1     |
+		|     * Remotes             |      1     |
+		|     * Others              |      1     |
+		|                           |            |
+		| * Reachable objects       |            |
+		|   * Count                 |   3.06 k   |
+		|     * Commits             |   1.05 k   |
+		|     * Trees               |   1.01 k   |
+		|     * Blobs               |   1.01 k   |
+		|     * Tags                |      1     |
+		|   * Inflated size         |  16.04 MiB |
+		|     * Commits             | 226.54 KiB |
+		|     * Trees               |  15.81 MiB |
+		|     * Blobs               |  11.68 KiB |
+		|     * Tags                |    132 B   |
+		|   * Disk size             | $(object_type_disk_usage all true) |
+		|     * Commits             | $(object_type_disk_usage commit true) |
+		|     * Trees               | $(object_type_disk_usage tree true) |
+		|     * Blobs               |  $(object_type_disk_usage blob true) |
+		|     * Tags                |    $(object_type_disk_usage tag) B   |
+		|                           |            |
+		| * Largest objects         |            |
+		|   * Commits               |            |
+		|     * Maximum size    [1] |    223 B   |
+		|     * Maximum parents [2] |      1     |
+		|   * Trees                 |            |
+		|     * Maximum size    [3] |  32.29 KiB |
+		|     * Maximum entries [4] |   1.01 k   |
+		|   * Blobs                 |            |
+		|     * Maximum size    [5] |     13 B   |
+		|   * Tags                  |            |
+		|     * Maximum size    [6] |    132 B   |
+
+		[1] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
+		[2] df6400c01440c329f1011669c4c26cc0c7852887
+		[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[5] 97d808e45116bf02103490294d3d46dad7a2ac62
+		[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
+		EOF
+
+		git repo structure  >actual 2>actual-err &&
+		cp actual /tmp/actual &&
+		cp expect /tmp/expect &&
+		test_cmp expect actual &&
+		test_line_count = 0 actual-err &&
+
+		git repo structure grobble ^master >actual 2>actual-err &&
+		cp actual /tmp &&
+		cp actual-err /tmp &&
+		test_grep "|     \* Commits             |    21     |" actual &&
+		test_grep "|     \* Trees               |     2     |" actual &&
+		test_grep "|     \* Commits             |  4.50 KiB |" actual &&
+		test_grep "|     \* Trees               | 32.35 KiB |" actual &&
+		test_grep "|     \* Blobs               | 11.68 KiB |" actual &&
+		test_line_count = 0 actual-err
+	)
+'
+
 test_expect_success SHA1 'lines and nul format' '
 	test_when_finished "rm -rf repo" &&
 	git init repo &&
-- 
2.53.0
Mark C. Chu-CarrollOct 5, 2026, 18:52 UTC in reply to Patrick Steinhardt on lore

Re: [PATCH 1/1] repo: add filtering options to "repo structure"

On Wed Sep 30, 2026 at 12:28 PM EDT, Patrick Steinhardt wrote:
Show 41 quoted lines
> On Thu, Sep 24, 2026 at 12:45:03PM -0400, Mark C. Chu-Carroll wrote:
>> "git repo structure" provides a collection of useful information
>> about the information stored in a repo. In particular, it's
>> valuable for diagnosing performance issues caused by large objects
>> stored in a repo.
>> 
>> The current implementation of "git repo stucture" provides summary
>> information about everything in the repository - all of the
>> branches, remotes, tags, stashes, and notes. But sometimes
>> to properly diagnose a problem, it's useful to be able to exclude
>> refs that are known to not be relevant to the issue at hand.
>
> Yes, indeed. Sometimes you may for example want to figure out where
> exactly the storage size of a particular repository is going. Or in the
> case of GitLab for example, we may have bookkeeping references that are
> not controllable by customers. So we may only want to get the structure
> for all the customer-controllable branches there.
>
>> Add a set of flags that allow a user to selective exclude
>> reference types from the report generated by "git repo structure".
>> When a ref type is excluded by the filter, it no longer appears
>> in the report (ie, if "--no-tags" is passed, the report line
>> for "Branches" will no longer appear under "* References").
>> Following the pattern of flags that are only used to
>> disable functionality (eg, "--no-verify" in "builtins/push.c"),
>> only the "--no-<reftype>" syntax is listed in the updated
>> documentation.
>
> Hmm, okay. I would have expected that the user can essentially pass
> arbitrary revisions as understood by git-log(1) et al. And if they pass
> any such revisions, we should not enumerate anything but what they have
> passed, so the flags shouldn't only be used to exclude.
>
> So, for example:
>
>     $ git repo structure --branches
>     $ git repo structure master
>     $ git repo structure --all --not --branches
>
> I would hope that git-repo(1) can achieve that rather easily because I
> expect that it uses `struct rev_info`, but let's read on.

That makes sense. My initial understanding was that most of what "git repo structure" does is internalize the functionality of git-sizer into the core of git. The only filters offered by git-sizer are type-based. But I agree that a commit list based filter is a lot more useful, so I've updated the patch set to implement it.

   -Mark
-- 
Mark Craig Chu-Carroll (@MarkChuCarroll at gitlab)
*** Software Tools/Math Geek - Software Engineer at Gitlab
*** Work Email: mcarroll@gitlab.com / markchucarroll@fastmail.com
*** Personal Blog: http://goodmath.org/blog / Personal email: markcc@gmail.com
Patrick SteinhardtOct 6, 2026, 05:51 UTC in reply to Mark C. Chu-Carroll on lore

Re: [PATCH v2 1/1] repo: add filtering options to "repo structure"

On Mon, Oct 05, 2026 at 01:40:44PM -0400, Mark C. Chu-Carroll wrote:
> Implement filtering for repo structure, imitating the mechanism
> used in "git log".

The message should give an explanation of what this change does, and what the motivation behind it is.

Show 10 quoted lines
> diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
> index ed7d80c690..5cbdf8e727 100644
> --- a/Documentation/git-repo.adoc
> +++ b/Documentation/git-repo.adoc
> @@ -10,7 +10,7 @@ SYNOPSIS
>  [synopsis]
>  git repo info [--format=(lines|nul) | -z] [--all | <key>...]
>  git repo info --keys [--format=(lines|nul) | -z]
> -git repo structure [--format=(table|lines|nul) | -z]
> +git repo structure [--format=(table|lines|nul) | -z] [<include|^exclude>...]
I think we should probably have this be `[<revs>...]`.
Show 8 quoted lines
> @@ -56,9 +56,10 @@ supported:
>  `nul`:::
>  	Similar to `lines`, but using a _NUL_ character after each value.
>  
> -`structure [--format=(table|lines|nul) | -z]`::
> -	Retrieve statistics about the current repository structure. The
> -	following kinds of information are reported:
> +`structure [--format=(table|lines|nul) | -z] [<include|^exclude>...]::
Same here.
Show 6 quoted lines
> @@ -66,6 +67,16 @@ supported:
>  * Total disk size of reachable objects by type
>  * Largest reachable objects in the repository by type
>  +
> +The set of objects counted can be filtered by specifying a
> +collection of query clauses to select which objects will be

s/query clauses/revisions/, which is a well-defined term. So with this change I think we can drop most of the remaining paragraph, except for the last sentence.

Show 23 quoted lines
> +counted. These parameters follow the same syntax as the parameters
> +to similar commands like `git log`. Semantically, these parameters
> +are treated as a collection of include and exclude specifiers. Th
> +set of objects counted will consist of all objects reachable from
> +an object included by one of the include specifiers via a path that
> +does not include an object in an exclude clause. If no includes
> +are specified, then the include set is all reachable objects. 
> ++
>  The output format can be chosen through the flag `--format`. Three formats are
>  supported:
>  +
> diff --git a/builtin/repo.c b/builtin/repo.c
> index 84e012f83f..b3aca71298 100644
> --- a/builtin/repo.c
> +++ b/builtin/repo.c
> @@ -946,12 +946,20 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
>  		OPT_BOOL(0, "progress", &show_progress, N_("show progress")),
>  		OPT_END()
>  	};
> +	struct setup_revision_opt s_r_opt;
> +	memset(&s_r_opt, 0, sizeof(s_r_opt));
> +	s_r_opt.def = "HEAD";
> +	s_r_opt.revarg_opt = REVARG_COMMITTISH;
This can be:
    struct setup_revision_opt s_r_opt = {
        .def = "HEAD",
        .revarg_opt = REVARG_COMMITTISH,
    };
But I wonder whether we want to pass it at all:
  - `.def` specifies the default, but do we even want to have one when
    the user has passed arguments?
  - `.revarg_opt` makes us treat it like a committish by default, but a
    user may for example want to figure out the size of all objects
    reachable from a specific tree, only.

So maybe we shouldn't be setting this at all and just pass `NULL` to `setup_revisions()`?

Show 5 quoted lines
> -	argc = parse_options(argc, argv, prefix, options, repo_structure_usage, 0);
> -	if (argc)
> -		usage(_("too many arguments"));
> +	argc = parse_options(argc, argv, prefix, options, repo_structure_usage,
> +			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);

Makes sense. Here we keep argv0 because of `setup_revisions()`' weird calling convention. And we also ignore any unknown options so that we can pass them along, too.

Show 9 quoted lines
>  	repo_init_revisions(repo, &revs, prefix);
> +	if (argc > 1) {
> +		argc = setup_revisions(argc, argv, &revs, &s_r_opt);
> +		if (argc > 1)
> +			usage(_("too many arguments"));
> +	}
>  
>  	if (show_progress < 0)
>  		show_progress = isatty(2);

And then, if we have any additional parameters then we pass it on to `setup_revisions()`.

Show 91 quoted lines
> diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
> index 02cc2b594a..eb2c595955 100755
> --- a/t/t1901-repo-structure.sh
> +++ b/t/t1901-repo-structure.sh
> @@ -144,6 +144,90 @@ test_expect_success SHA1 'repository with references and objects' '
>  	)
>  '
>  
> +test_expect_success SHA1 'repository with references and objects, filtered' '
> +	test_when_finished "rm -rf repo" &&
> +	git init repo &&
> +	(
> +		cd repo &&
> +		test_commit_bulk 1005 &&
> +		git tag -a foo -m bar &&
> +
> +		oid="$(git rev-parse HEAD)" &&
> +		git update-ref refs/remotes/origin/foo "$oid" &&
> +		git checkout -b grobble &&
> +		test_commit_bulk --ref=refs/heads/grobble 20 &&
> +		git checkout master &&
> + 		test_commit_bulk 20 &&
> +		# Also creates a commit, tree, and blob.
> +		git notes add -m foo &&
> +
> +		# git-rev-list(1) --disk-usage=human option printing the full
> +		# "byte/bytes" unit string instead of just "B".
> +		cat >expect <<-EOF &&
> +		| Repository structure      | Value      |
> +		| ------------------------- | ---------- |
> +		| * References              |            |
> +		|   * Count                 |      5     |
> +		|     * Branches            |      2     |
> +		|     * Tags                |      1     |
> +		|     * Remotes             |      1     |
> +		|     * Others              |      1     |
> +		|                           |            |
> +		| * Reachable objects       |            |
> +		|   * Count                 |   3.06 k   |
> +		|     * Commits             |   1.05 k   |
> +		|     * Trees               |   1.01 k   |
> +		|     * Blobs               |   1.01 k   |
> +		|     * Tags                |      1     |
> +		|   * Inflated size         |  16.04 MiB |
> +		|     * Commits             | 226.54 KiB |
> +		|     * Trees               |  15.81 MiB |
> +		|     * Blobs               |  11.68 KiB |
> +		|     * Tags                |    132 B   |
> +		|   * Disk size             | $(object_type_disk_usage all true) |
> +		|     * Commits             | $(object_type_disk_usage commit true) |
> +		|     * Trees               | $(object_type_disk_usage tree true) |
> +		|     * Blobs               |  $(object_type_disk_usage blob true) |
> +		|     * Tags                |    $(object_type_disk_usage tag) B   |
> +		|                           |            |
> +		| * Largest objects         |            |
> +		|   * Commits               |            |
> +		|     * Maximum size    [1] |    223 B   |
> +		|     * Maximum parents [2] |      1     |
> +		|   * Trees                 |            |
> +		|     * Maximum size    [3] |  32.29 KiB |
> +		|     * Maximum entries [4] |   1.01 k   |
> +		|   * Blobs                 |            |
> +		|     * Maximum size    [5] |     13 B   |
> +		|   * Tags                  |            |
> +		|     * Maximum size    [6] |    132 B   |
> +
> +		[1] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
> +		[2] df6400c01440c329f1011669c4c26cc0c7852887
> +		[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
> +		[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
> +		[5] 97d808e45116bf02103490294d3d46dad7a2ac62
> +		[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
> +		EOF
> +
> +		git repo structure  >actual 2>actual-err &&
> +		cp actual /tmp/actual &&
> +		cp expect /tmp/expect &&
> +		test_cmp expect actual &&
> +		test_line_count = 0 actual-err &&
> +
> +		git repo structure grobble ^master >actual 2>actual-err &&
> +		cp actual /tmp &&
> +		cp actual-err /tmp &&
> +		test_grep "|     \* Commits             |    21     |" actual &&
> +		test_grep "|     \* Trees               |     2     |" actual &&
> +		test_grep "|     \* Commits             |  4.50 KiB |" actual &&
> +		test_grep "|     \* Trees               | 32.35 KiB |" actual &&
> +		test_grep "|     \* Blobs               | 11.68 KiB |" actual &&
> +		test_line_count = 0 actual-err
> +	)
> +'

I wonder whether we maybe want to have some additional tests that assert that you can also pass e.g.:

  - A tree or blob.
  - Revision options, like for example `--all --filter=object:type=blob`.

To make the test a bit less repetitive we might also want to use `--format=lines` and then only check for "objects.*.{inflated,disk}_size" to exercise only the parts that matter to this test.

Thanks!
Patrick

Back to recent threads