{"thread":{"id":"66388","subject":"[PATCH 0/1] repo: add filtering options to \"repo structure\"","startedAt":"2026-09-24T16:45:15Z","lastAt":"2026-10-06T05:51:27Z","messageCount":7,"participants":["Mark C. Chu-Carroll","Patrick Steinhardt"],"isPatch":true,"patchVersion":1,"patchTotal":1},"messages":[{"id":"553207","messageId":"20260924164503.119506-1-markchucarroll@fastmail.com","threadId":"66388","inReplyTo":null,"subject":"[PATCH 0/1] repo: add filtering options to \"repo structure\"","fromName":"Mark C. Chu-Carroll","fromEmail":"markchucarroll@fastmail.com","sentAt":"2026-09-24T16:45:02Z","receivedAt":"2026-09-24T16:45:15Z","isPatch":true,"body":"    repo: add filtering options to \"repo structure\"\n\n    \"git repo structure\" provides a collection of useful information\n    about the information stored in a repo. In particular, it's\n    valuable for diagnosing performance issues caused by large objects\n    stored in a repo.\n\n    The current implementation of \"git repo stucture\" provides summary\n    information about everything in the repository - all of the\n    branches, remotes, tags, stashes, and notes. But sometimes\n    to properly diagnose a problem, it's useful to be able to exclude\n    refs that are known to not be relevant to the issue at hand.\n\n    Add a set of flags that allow a user to selective exclude\n    reference types from the report generated by \"git repo structure\".\n    When a ref type is excluded by the filter, it no longer appears\n    in the report (ie, if \"--no-tags\" is passed, the report line\n    for \"Branches\" will no longer appear under \"* References\").\n    Following the pattern of flags that are only used to\n    disable functionality (eg, \"--no-verify\" in \"builtins/push.c\"),\n    only the \"--no-<reftype>\" syntax is listed in the updated\n    documentation.\n\n    Overview of the changes:\n    - Add an enum to represent the structure flags.\n    - Add structure flags to the options for the \"repo structure\" commands.\n    - For each reference flag, add a conditional in \"count_references\"\n      which decides whether or not to add a ref to the pending list.\n      If an references is not added to the pending list, the things it\n      transitively references will not be added to the stats.\n    - Add a set of test cases to verify that reference counts\n      in the repo structure report correctly omit the specified\n      resource types.\n    - Update the documentation for git-repo to include the new options.\n\nMark C. Chu-Carroll (1):\n  repo: add filtering options to \"repo structure\"\n\n Documentation/git-repo.adoc |  35 ++++-\n builtin/repo.c              | 158 +++++++++++++++------\n t/t1901-repo-structure.sh   | 276 ++++++++++++++++++++++++++++++++++++\n 3 files changed, 427 insertions(+), 42 deletions(-)\n\n-- \n2.53.0\n\n"},{"id":"553208","messageId":"20260924164503.119506-2-markchucarroll@fastmail.com","threadId":"66388","inReplyTo":"20260924164503.119506-1-markchucarroll@fastmail.com","subject":"[PATCH 1/1] repo: add filtering options to \"repo structure\"","fromName":"Mark C. Chu-Carroll","fromEmail":"markchucarroll@fastmail.com","sentAt":"2026-09-24T16:45:03Z","receivedAt":"2026-09-24T16:45:17Z","isPatch":true,"body":"\"git repo structure\" provides a collection of useful information\nabout the information stored in a repo. In particular, it's\nvaluable for diagnosing performance issues caused by large objects\nstored in a repo.\n\nThe current implementation of \"git repo stucture\" provides summary\ninformation about everything in the repository - all of the\nbranches, remotes, tags, stashes, and notes. But sometimes\nto properly diagnose a problem, it's useful to be able to exclude\nrefs that are known to not be relevant to the issue at hand.\n\nAdd a set of flags that allow a user to selective exclude\nreference types from the report generated by \"git repo structure\".\nWhen a ref type is excluded by the filter, it no longer appears\nin the report (ie, if \"--no-tags\" is passed, the report line\nfor \"Branches\" will no longer appear under \"* References\").\nFollowing the pattern of flags that are only used to\ndisable functionality (eg, \"--no-verify\" in \"builtins/push.c\"),\nonly the \"--no-<reftype>\" syntax is listed in the updated\ndocumentation.\n\nOverview of the changes:\n- Add an enum to represent the structure flags.\n- Add structure flags to the options for the \"repo structure\" commands.\n- For each reference flag, add a conditional in \"count_references\"\n  which decides whether or not to add a ref to the pending list.\n  If an references is not added to the pending list, the things it\n  transitively references will not be added to the stats.\n- Add a set of test cases to verify that reference counts\n  in the repo structure report correctly omit the specified\n  resource types.\n- Update the documentation for git-repo to include the new options.\n\nSigned-off-by: Mark C. Chu-Carroll <markchucarroll@fastmail.com>\n---\n Documentation/git-repo.adoc |  35 ++++-\n builtin/repo.c              | 158 +++++++++++++++------\n t/t1901-repo-structure.sh   | 276 ++++++++++++++++++++++++++++++++++++\n 3 files changed, 427 insertions(+), 42 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex ed7d80c690..463260cd23 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -10,7 +10,7 @@ SYNOPSIS\n [synopsis]\n git repo info [--format=(lines|nul) | -z] [--all | <key>...]\n git repo info --keys [--format=(lines|nul) | -z]\n-git repo structure [--format=(table|lines|nul) | -z]\n+git repo structure [--format=(table|lines|nul) | -z] [--no-branches] [--no-tags] [--no-remotes] [--no-notes] [--no-stashes]`::\n \n DESCRIPTION\n -----------\n@@ -56,7 +56,7 @@ supported:\n `nul`:::\n \tSimilar to `lines`, but using a _NUL_ character after each value.\n \n-`structure [--format=(table|lines|nul) | -z]`::\n+`structure [--format=(table|lines|nul) | -z] [--no-branches] [--no-tags] [--no-remotes] [--no-notes] [--no-stashes]`::\n \tRetrieve statistics about the current repository structure. The\n \tfollowing kinds of information are reported:\n +\n@@ -66,6 +66,23 @@ supported:\n * Total disk size of reachable objects by type\n * Largest reachable objects in the repository by type\n +\n+The types of objects counted can be filtered using flags:\n++\n+`--no-branches`:::\n+\tOmit branch refs from the set of objects to count.\n+\n+`--no-tags`:::\n+\tOmit tags from the set of objects to count.\n+\n+`--no-remotes`:::\n+\tOmit remotes from the set of objects to count.\n+\n+`--no-notes`:::\n+\tOmit notes from the set of objects to count.\n+\n+`--no-stashes`:::\n+\tOmit stashes from the set of objects to count.\n++\n The output format can be chosen through the flag `--format`. Three formats are\n supported:\n +\n@@ -141,6 +158,20 @@ using the `nul` format:\n git repo info --format=nul layout.bare layout.shallow\n ------------\n \n+* Generates information about storage usage in the repository:\n++\n+------------\n+git repo structure\n+------------\n++\n+\n+* Generates information about storage usage in the repository omitting\n+remotes:\n++\n+------------\n+git repo structure --no-remotes\n+------------\n+\n SEE ALSO\n --------\n linkgit:git-rev-parse[1]\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 84e012f83f..c8f6e38011 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -53,6 +53,20 @@ enum output_format {\n \tFORMAT_NUL_TERMINATED,\n };\n \n+enum repo_structure_filter_flags {\n+\tREPO_STRUCTURE_FILTER_BRANCHES = (1 << 0),\n+\tREPO_STRUCTURE_FILTER_TAGS = (1 << 1),\n+\tREPO_STRUCTURE_FILTER_REMOTES = (1 << 2),\n+\tREPO_STRUCTURE_FILTER_NOTES  = (1 << 3),\n+\tREPO_STRUCTURE_FILTER_STASHES = (1 << 4),\n+};\n+\n+#define REPO_STRUCTURE_FILTER_FLAGS_ALL (REPO_STRUCTURE_FILTER_BRANCHES | \\\n+\t\t\t\t\t REPO_STRUCTURE_FILTER_TAGS |\t\\\n+\t\t\t\t\t REPO_STRUCTURE_FILTER_REMOTES | \\\n+\t\t\t\t\t REPO_STRUCTURE_FILTER_NOTES |\\\n+\t\t\t\t\t REPO_STRUCTURE_FILTER_STASHES)\n+\n struct repo_info_field {\n \tconst char *key;\n \tget_value_fn *get_value;\n@@ -490,7 +504,8 @@ static inline size_t get_total_object_values(struct object_values *values)\n }\n \n static void stats_table_setup_structure(struct stats_table *table,\n-\t\t\t\t\tstruct repo_structure *stats)\n+\t\t\t\t\tstruct repo_structure *stats,\n+\t\t\t\t\tenum repo_structure_filter_flags flags)\n {\n \tstruct object_stats *objects = &stats->objects;\n \tstruct ref_stats *refs = &stats->refs;\n@@ -502,9 +517,15 @@ static void stats_table_setup_structure(struct stats_table *table,\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+\tif (flags & REPO_STRUCTURE_FILTER_BRANCHES) {\n+\t\tstats_table_count_addf(table, refs->branches, \"    * %s\", _(\"Branches\"));\n+\t}\n+\tif (flags & REPO_STRUCTURE_FILTER_TAGS) {\n+\t\tstats_table_count_addf(table, refs->tags, \"    * %s\", _(\"Tags\"));\n+\t}\n+\tif (flags & REPO_STRUCTURE_FILTER_REMOTES) {\n+\t\tstats_table_count_addf(table, refs->remotes, \"    * %s\", _(\"Remotes\"));\n+\t}\n \tstats_table_count_addf(table, refs->others, \"    * %s\", _(\"Others\"));\n \n \tobject_count_total = get_total_object_values(&objects->type_counts);\n@@ -517,8 +538,10 @@ static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t       \"    * %s\", _(\"Trees\"));\n \tstats_table_count_addf(table, objects->type_counts.blobs,\n \t\t\t       \"    * %s\", _(\"Blobs\"));\n-\tstats_table_count_addf(table, objects->type_counts.tags,\n-\t\t\t       \"    * %s\", _(\"Tags\"));\n+\tif (flags & REPO_STRUCTURE_FILTER_TAGS) {\n+\t\tstats_table_count_addf(table, objects->type_counts.tags,\n+\t\t\t\t       \"    * %s\", _(\"Tags\"));\n+\t}\n \n \tinflated_object_total = get_total_object_values(&objects->inflated_sizes);\n \tstats_table_size_addf(table, inflated_object_total,\n@@ -529,8 +552,11 @@ static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t      \"    * %s\", _(\"Trees\"));\n \tstats_table_size_addf(table, objects->inflated_sizes.blobs,\n \t\t\t      \"    * %s\", _(\"Blobs\"));\n-\tstats_table_size_addf(table, objects->inflated_sizes.tags,\n-\t\t\t      \"    * %s\", _(\"Tags\"));\n+\n+\tif (flags & REPO_STRUCTURE_FILTER_TAGS) {\n+\t\tstats_table_size_addf(table, objects->inflated_sizes.tags,\n+\t\t\t\t      \"    * %s\", _(\"Tags\"));\n+\t}\n \n \tdisk_object_total = get_total_object_values(&objects->disk_sizes);\n \tstats_table_size_addf(table, disk_object_total,\n@@ -541,8 +567,10 @@ static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t      \"    * %s\", _(\"Trees\"));\n \tstats_table_size_addf(table, objects->disk_sizes.blobs,\n \t\t\t      \"    * %s\", _(\"Blobs\"));\n-\tstats_table_size_addf(table, objects->disk_sizes.tags,\n-\t\t\t      \"    * %s\", _(\"Tags\"));\n+\tif (flags & REPO_STRUCTURE_FILTER_TAGS) {\n+\t\tstats_table_size_addf(table, objects->disk_sizes.tags,\n+\t\t\t\t      \"    * %s\", _(\"Tags\"));\n+\t}\n \n \tstats_table_addf(table, \"\");\n \tstats_table_addf(table, \"* %s\", _(\"Largest objects\"));\n@@ -569,11 +597,13 @@ static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t\t     &objects->largest.blob_size.oid,\n \t\t\t\t     objects->largest.blob_size.value,\n \t\t\t\t     \"    * %s\", _(\"Maximum size\"));\n-\tstats_table_addf(table, \"  * %s\", _(\"Tags\"));\n-\tstats_table_object_size_addf(table,\n-\t\t\t\t     &objects->largest.tag_size.oid,\n-\t\t\t\t     objects->largest.tag_size.value,\n-\t\t\t\t     \"    * %s\", _(\"Maximum size\"));\n+\tif (flags & REPO_STRUCTURE_FILTER_TAGS) {\n+\t\tstats_table_addf(table, \"  * %s\", _(\"Tags\"));\n+\t\tstats_table_object_size_addf(table,\n+\t\t\t\t\t     &objects->largest.tag_size.oid,\n+\t\t\t\t\t     objects->largest.tag_size.value,\n+\t\t\t\t\t     \"    * %s\", _(\"Maximum size\"));\n+\t}\n }\n \n #define INDEX_WIDTH 4\n@@ -681,14 +711,21 @@ static void print_object_data(const char *key, char key_delim,\n }\n \n static void structure_keyvalue_print(struct repo_structure *stats,\n-\t\t\t\t     char key_delim, char value_delim)\n+\t\t\t\t     char key_delim, char value_delim,\n+\t\t\t\t     enum repo_structure_filter_flags flags)\n {\n-\tprint_keyvalue(\"references.branches.count\", key_delim,\n-\t\t       stats->refs.branches, value_delim);\n+\tif (flags & REPO_STRUCTURE_FILTER_BRANCHES) {\n+\t\tprint_keyvalue(\"references.branches.count\", key_delim,\n+\t\t\t       stats->refs.branches, value_delim);\n+\t}\n+\tif (flags & REPO_STRUCTURE_FILTER_TAGS) {\n \tprint_keyvalue(\"references.tags.count\", key_delim,\n \t\t       stats->refs.tags, value_delim);\n-\tprint_keyvalue(\"references.remotes.count\", key_delim,\n-\t\t       stats->refs.remotes, value_delim);\n+\t}\n+\tif (flags & REPO_STRUCTURE_FILTER_REMOTES) {\n+\t\tprint_keyvalue(\"references.remotes.count\", key_delim,\n+\t\t\t       stats->refs.remotes, value_delim);\n+\t}\n \tprint_keyvalue(\"references.others.count\", key_delim,\n \t\t       stats->refs.others, value_delim);\n \n@@ -698,8 +735,10 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \t\t       stats->objects.type_counts.trees, value_delim);\n \tprint_keyvalue(\"objects.blobs.count\", key_delim,\n \t\t       stats->objects.type_counts.blobs, value_delim);\n-\tprint_keyvalue(\"objects.tags.count\", key_delim,\n-\t\t       stats->objects.type_counts.tags, value_delim);\n+\tif (flags & REPO_STRUCTURE_FILTER_TAGS) {\n+\t\tprint_keyvalue(\"objects.tags.count\", key_delim,\n+\t\t\t       stats->objects.type_counts.tags, value_delim);\n+\t}\n \n \tprint_keyvalue(\"objects.commits.inflated_size\", key_delim,\n \t\t       stats->objects.inflated_sizes.commits, value_delim);\n@@ -707,8 +746,10 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \t\t       stats->objects.inflated_sizes.trees, value_delim);\n \tprint_keyvalue(\"objects.blobs.inflated_size\", key_delim,\n \t\t       stats->objects.inflated_sizes.blobs, value_delim);\n-\tprint_keyvalue(\"objects.tags.inflated_size\", key_delim,\n-\t\t       stats->objects.inflated_sizes.tags, value_delim);\n+\tif (flags & REPO_STRUCTURE_FILTER_TAGS) {\n+\t\tprint_keyvalue(\"objects.tags.inflated_size\", key_delim,\n+\t\t\t       stats->objects.inflated_sizes.tags, value_delim);\n+\t}\n \n \tprint_keyvalue(\"objects.commits.disk_size\", key_delim,\n \t\t       stats->objects.disk_sizes.commits, value_delim);\n@@ -716,8 +757,10 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \t\t       stats->objects.disk_sizes.trees, value_delim);\n \tprint_keyvalue(\"objects.blobs.disk_size\", key_delim,\n \t\t       stats->objects.disk_sizes.blobs, value_delim);\n-\tprint_keyvalue(\"objects.tags.disk_size\", key_delim,\n-\t\t       stats->objects.disk_sizes.tags, value_delim);\n+\tif (flags & REPO_STRUCTURE_FILTER_TAGS) {\n+\t\tprint_keyvalue(\"objects.tags.disk_size\", key_delim,\n+\t\t\t       stats->objects.disk_sizes.tags, value_delim);\n+\t}\n \n \tprint_object_data(\"objects.commits.max_size\", key_delim,\n \t\t\t  &stats->objects.largest.commit_size, value_delim);\n@@ -725,8 +768,10 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \t\t\t  &stats->objects.largest.tree_size, value_delim);\n \tprint_object_data(\"objects.blobs.max_size\", key_delim,\n \t\t\t  &stats->objects.largest.blob_size, value_delim);\n-\tprint_object_data(\"objects.tags.max_size\", key_delim,\n-\t\t\t  &stats->objects.largest.tag_size, value_delim);\n+\tif (flags & REPO_STRUCTURE_FILTER_TAGS) {\n+\t\tprint_object_data(\"objects.tags.max_size\", key_delim,\n+\t\t\t\t  &stats->objects.largest.tag_size, value_delim);\n+\t}\n \n \tprint_object_data(\"objects.commits.max_parents\", key_delim,\n \t\t\t  &stats->objects.largest.parent_count, value_delim);\n@@ -739,6 +784,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+\tenum repo_structure_filter_flags flags;\n \tstruct progress *progress;\n };\n \n@@ -747,19 +793,42 @@ static int count_references(const struct reference *ref, void *cb_data)\n \tstruct count_references_data *data = cb_data;\n \tstruct ref_stats *stats = data->stats;\n \tsize_t ref_count;\n+\tbool add_pending = false;\n \n \tswitch (ref_kind_from_refname(ref->name)) {\n \tcase FILTER_REFS_BRANCHES:\n-\t\tstats->branches++;\n+\t\tif (data->flags & REPO_STRUCTURE_FILTER_BRANCHES) {\n+\t\t\tstats->branches++;\n+\t\t\tadd_pending = true;\n+\t\t}\n \t\tbreak;\n \tcase FILTER_REFS_REMOTES:\n-\t\tstats->remotes++;\n+\t\tif (data->flags & REPO_STRUCTURE_FILTER_REMOTES) {\n+\t\t\tstats->remotes++;\n+\t\t\tadd_pending = true;\n+\t\t}\n \t\tbreak;\n \tcase FILTER_REFS_TAGS:\n-\t\tstats->tags++;\n+\t\tif (data->flags & REPO_STRUCTURE_FILTER_TAGS) {\n+\t\t\tstats->tags++;\n+\t\t\tadd_pending = true;\n+\t\t}\n \t\tbreak;\n \tcase FILTER_REFS_OTHERS:\n-\t\tstats->others++;\n+\t\tif (!strcmp(ref->name, \"refs/stash\")) {\n+\t\t\tif (data->flags & REPO_STRUCTURE_FILTER_STASHES) {\n+\t\t\t\tstats->others++;\n+\t\t\t\tadd_pending = true;\n+\t\t\t}\n+\t\t} else if (starts_with(ref->name, \"refs/notes\")) {\n+\t\t\tif (data->flags & REPO_STRUCTURE_FILTER_NOTES) {\n+\t\t\t\tstats->others++;\n+\t\t\t\tadd_pending = true;\n+\t\t\t}\n+\t\t} else {\n+\t\t\tstats->others++;\n+\t\t\tadd_pending = true;\n+\t\t}\n \t\tbreak;\n \tdefault:\n \t\tBUG(\"unexpected reference type\");\n@@ -769,10 +838,11 @@ static int count_references(const struct reference *ref, void *cb_data)\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, ref->oid, 0);\n-\n-\tref_count = get_total_reference_count(stats);\n-\tdisplay_progress(data->progress, ref_count);\n+\tif (add_pending) {\n+\t\tadd_pending_oid(data->revs, NULL, ref->oid, 0);\n+\t\tref_count = get_total_reference_count(stats);\n+\t\tdisplay_progress(data->progress, ref_count);\n+\t}\n \n \treturn 0;\n }\n@@ -780,11 +850,13 @@ static int count_references(const struct reference *ref, void *cb_data)\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       enum repo_structure_filter_flags flags,\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\t.flags = flags,\n \t};\n \n \tif (show_progress)\n@@ -935,6 +1007,7 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n \tstruct repo_structure stats = { 0 };\n \tstruct rev_info revs;\n \tint show_progress = -1;\n+\tint flags = REPO_STRUCTURE_FILTER_FLAGS_ALL;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n \t\t\t       N_(\"output format\"),\n@@ -944,6 +1017,11 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n \t\t\t       PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n \t\t\t       parse_format_cb),\n \t\tOPT_BOOL(0, \"progress\", &show_progress, N_(\"show progress\")),\n+\t\tOPT_BIT(0, \"branches\", &flags, N_(\"include branches in structure statistics\"), REPO_STRUCTURE_FILTER_BRANCHES),\n+\t\tOPT_BIT(0, \"tags\", &flags, N_(\"include tags in structure statistics\"), REPO_STRUCTURE_FILTER_TAGS),\n+\t\tOPT_BIT(0, \"remotes\", &flags, N_(\"include remotes in structure statistics\"), REPO_STRUCTURE_FILTER_REMOTES),\n+\t\tOPT_BIT(0, \"notes\", &flags, N_(\"include notes in structure statistics\"), REPO_STRUCTURE_FILTER_NOTES),\n+\t\tOPT_BIT(0, \"stashes\", &flags, N_(\"include stashes in structure statistics\"), REPO_STRUCTURE_FILTER_STASHES),\n \t\tOPT_END()\n \t};\n \n@@ -956,19 +1034,19 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\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_references(&stats.refs, &revs, repo, flags, show_progress);\n \tstructure_count_objects(&stats.objects, &revs, repo, show_progress);\n \n \tswitch (format) {\n \tcase FORMAT_TABLE:\n-\t\tstats_table_setup_structure(&table, &stats);\n+\t\tstats_table_setup_structure(&table, &stats, flags);\n \t\tstats_table_print_structure(&table);\n \t\tbreak;\n \tcase FORMAT_NEWLINE_TERMINATED:\n-\t\tstructure_keyvalue_print(&stats, '=', '\\n');\n+\t\tstructure_keyvalue_print(&stats, '=', '\\n', flags);\n \t\tbreak;\n \tcase FORMAT_NUL_TERMINATED:\n-\t\tstructure_keyvalue_print(&stats, '\\n', '\\0');\n+\t\tstructure_keyvalue_print(&stats, '\\n', '\\0', flags);\n \t\tbreak;\n \tdefault:\n \t\tBUG(\"invalid output format\");\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nindex 02cc2b594a..60a82493c6 100755\n--- a/t/t1901-repo-structure.sh\n+++ b/t/t1901-repo-structure.sh\n@@ -144,6 +144,170 @@ test_expect_success SHA1 'repository with references and objects' '\n \t)\n '\n \n+\n+test_expect_success SHA1 'repository with references and objects, filtered with --no-origins' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit_bulk 1005 &&\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 checkout -b other_branch &&\n+\t\ttest_commit_bulk 5 &&\n+\n+\t\tgit checkout master &&\n+\n+\t\t# Also creates a commit, tree, and blob.\n+\t\tgit notes add -m foo &&\n+\n+\t\t# The tags disk size is handled specially due to the\n+\t\t# git-rev-list(1) --disk-usage=human option printing the full\n+\t\t# \"byte/bytes\" unit string instead of just \"B\".\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            |      2     |\n+\t\t|     * Tags                |      1     |\n+\t\t|     * Others              |      1     |\n+\t\t|                           |            |\n+\t\t| * Reachable objects       |            |\n+\t\t|   * Count                 |   3.02 k   |\n+\t\t|     * Commits             |   1.01 k   |\n+\t\t|     * Trees               |   1.01 k   |\n+\t\t|     * Blobs               |   1.01 k   |\n+\t\t|     * Tags                |      1     |\n+\t\t|   * Inflated size         |  16.04 MiB |\n+\t\t|     * Commits             | 219.00 KiB |\n+\t\t|     * Trees               |  15.81 MiB |\n+\t\t|     * Blobs               |  11.68 KiB |\n+\t\t|     * Tags                |    132 B   |\n+\t\t|   * Disk size             | $(object_type_disk_usage all true) |\n+\t\t|     * Commits             | $(object_type_disk_usage commit true) |\n+\t\t|     * Trees               | $(object_type_disk_usage tree true) |\n+\t\t|     * Blobs               |  $(object_type_disk_usage blob true) |\n+\t\t|     * Tags                |    $(object_type_disk_usage tag) B   |\n+\t\t|                           |            |\n+\t\t| * Largest objects         |            |\n+\t\t|   * Commits               |            |\n+\t\t|     * Maximum size    [1] |    223 B   |\n+\t\t|     * Maximum parents [2] |      1     |\n+\t\t|   * Trees                 |            |\n+\t\t|     * Maximum size    [3] |  32.29 KiB |\n+\t\t|     * Maximum entries [4] |   1.01 k   |\n+\t\t|   * Blobs                 |            |\n+\t\t|     * Maximum size    [5] |     13 B   |\n+\t\t|   * Tags                  |            |\n+\t\t|     * Maximum size    [6] |    132 B   |\n+\n+\t\t[1] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a\n+\t\t[2] 0a8f6a47f34078bb7c4b2bb4377c2133157f6703\n+\t\t[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c\n+\t\t[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c\n+\t\t[5] 97d808e45116bf02103490294d3d46dad7a2ac62\n+\t\t[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2\n+\t\tEOF\n+\t\tgit repo structure --no-remotes >actual 2>err &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n+test_expect_success SHA1 'repository with references and objects, filtered with \"--no-branches\"' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit_bulk 1005 &&\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 checkout -b other_branch &&\n+\t\ttest_commit_bulk 5 &&\n+\n+\t\tgit checkout master &&\n+\n+\t\t# Also creates a commit, tree, and blob.\n+\t\tgit notes add -m foo &&\n+\n+\t\tgit repo structure --no-branches >actual 2>err &&\n+\t\ttest_line_count = 0 err &&\n+\t\ttest_grep \"|   \\* Count                 |      3     |\" actual &&\n+\t\ttest_grep \"|     \\* Others              |      1     |\" actual &&\n+\t\ttest_grep !  \"Branches\" actual\n+\t)\n+'\n+\n+test_expect_success SHA1 'repository with references and objects, filtered with \"--no-notes\"' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit_bulk 1005 &&\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 checkout -b other_branch &&\n+\t\ttest_commit_bulk 5 &&\n+\n+\t\tgit checkout master &&\n+\n+\t\t# Also creates a commit, tree, and blob.\n+\t\tgit notes add -m foo &&\n+\n+\t\tgit repo structure --no-notes >actual 2>err &&\n+\t\ttest_line_count = 0 err &&\n+\t\ttest_grep \"|   \\* Count                 |      4     |\" actual &&\n+\t\ttest_grep \"|     \\* Others              |      0     |\" actual &&\n+\t\ttest_grep \"|     \\* Commits             | 218.81 KiB |\" actual\n+\t)\n+'\n+\n+test_expect_success SHA1 'repository with references and objects, filtered with multiple flags' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit_bulk 1005 &&\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 checkout -b other_branch &&\n+\t\ttest_commit_bulk 5 &&\n+\n+\t\tgit checkout master &&\n+\n+\t\t# Also creates a commit, tree, and blob.\n+\t\tgit notes add -m foo &&\n+\n+\t\tgit repo structure --no-notes --no-branches >actual 2>err &&\n+\t\ttest_line_count = 0 err &&\n+\t\ttest_grep \"|   \\* Count                 |      2     |\" actual &&\n+\t\ttest_grep \"|     \\* Others              |      0     |\" actual &&\n+\t\ttest_grep \"|     \\* Commits             | 217.73 KiB |\" actual &&\n+\t\ttest_grep !  \"Branches\" actual &&\n+\n+\t\tgit repo structure --no-notes --no-branches --no-remotes >actual 2>err &&\n+\t\ttest_line_count = 0 err &&\n+\t\ttest_grep \"|   \\* Count                 |      1     |\" actual &&\n+\t\ttest_grep \"|     \\* Others              |      0     |\" actual &&\n+\t\ttest_grep \"|     \\* Commits             | 217.73 KiB |\" actual &&\n+\t\ttest_grep !  \"Branches\" actual &&\n+\t\ttest_grep ! \"Remotes\" actual\n+\t)\n+'\n+\n test_expect_success SHA1 'lines and nul format' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -203,6 +367,118 @@ test_expect_success SHA1 'lines and nul format' '\n \t\ttest_line_count = 0 err\n \t)\n '\n+test_expect_success SHA1 'lines and nul format with --no-tags filter' '\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.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.commits.inflated_size=9225\n+\t\tobjects.trees.inflated_size=28554\n+\t\tobjects.blobs.inflated_size=453\n+\t\tobjects.commits.disk_size=$(object_type_disk_usage commit)\n+\t\tobjects.trees.disk_size=$(object_type_disk_usage tree)\n+\t\tobjects.blobs.disk_size=$(object_type_disk_usage blob)\n+\t\tobjects.commits.max_size=221\n+\t\tobjects.commits.max_size_oid=de3508174b5c2ace6993da67cae9be9069e2df39\n+\t\tobjects.trees.max_size=1335\n+\t\tobjects.trees.max_size_oid=09931deea9d81ec21300d3e13c74412f32eacec5\n+\t\tobjects.blobs.max_size=11\n+\t\tobjects.blobs.max_size_oid=eaeeedced46482bd4281fda5a5f05ce24854151f\n+\t\tobjects.commits.max_parents=1\n+\t\tobjects.commits.max_parents_oid=de3508174b5c2ace6993da67cae9be9069e2df39\n+\t\tobjects.trees.max_entries=42\n+\t\tobjects.trees.max_entries_oid=09931deea9d81ec21300d3e13c74412f32eacec5\n+\t\tEOF\n+\n+\t\tgit repo structure --format=lines --no-tags >actual 2>err &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest_line_count = 0 err &&\n+\n+\t\tgit repo structure --format=nul --no-tags >out 2>err &&\n+\t\ttr \"\\012\\000\" \"=\\012\" <out >actual &&\n+\n+\t\ttest_cmp expect actual &&\n+\t\ttest_line_count = 0 err &&\n+\n+\t\t# \"-z\", as a synonym to \"--format=nul\", participates in the\n+\t\t# usual \"last one wins\" rule.\n+\t\tgit repo structure --format=table -z --no-tags >out 2>err &&\n+\t\ttr \"\\012\\000\" \"=\\012\" <out >actual &&\n+\n+\t\ttest_cmp expect actual &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n+\n+test_expect_success SHA1 'lines and nul format with --no-branches filter' '\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.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\tobjects.commits.inflated_size=9225\n+\t\tobjects.trees.inflated_size=28554\n+\t\tobjects.blobs.inflated_size=453\n+\t\tobjects.tags.inflated_size=132\n+\t\tobjects.commits.disk_size=$(object_type_disk_usage commit)\n+\t\tobjects.trees.disk_size=$(object_type_disk_usage tree)\n+\t\tobjects.blobs.disk_size=$(object_type_disk_usage blob)\n+\t\tobjects.tags.disk_size=$(object_type_disk_usage tag)\n+\t\tobjects.commits.max_size=221\n+\t\tobjects.commits.max_size_oid=de3508174b5c2ace6993da67cae9be9069e2df39\n+\t\tobjects.trees.max_size=1335\n+\t\tobjects.trees.max_size_oid=09931deea9d81ec21300d3e13c74412f32eacec5\n+\t\tobjects.blobs.max_size=11\n+\t\tobjects.blobs.max_size_oid=eaeeedced46482bd4281fda5a5f05ce24854151f\n+\t\tobjects.tags.max_size=132\n+\t\tobjects.tags.max_size_oid=1ee0f2b16ea37d895dbe9dbd76cd2ac70446176c\n+\t\tobjects.commits.max_parents=1\n+\t\tobjects.commits.max_parents_oid=de3508174b5c2ace6993da67cae9be9069e2df39\n+\t\tobjects.trees.max_entries=42\n+\t\tobjects.trees.max_entries_oid=09931deea9d81ec21300d3e13c74412f32eacec5\n+\t\tEOF\n+\n+\t\tgit repo structure --format=lines --no-branches >actual 2>err &&\n+\t\tcp expect /tmp/expect &&\n+\t\tcp actual /tmp/actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest_line_count = 0 err &&\n+\n+\t\tgit repo structure --format=nul --no-branches >out 2>err &&\n+\t\ttr \"\\012\\000\" \"=\\012\" <out >actual &&\n+\n+\t\ttest_cmp expect actual &&\n+\t\ttest_line_count = 0 err &&\n+\n+\t\t# \"-z\", as a synonym to \"--format=nul\", participates in the\n+\t\t# usual \"last one wins\" rule.\n+\t\tgit repo structure --format=table -z --no-branches >out 2>err &&\n+\t\ttr \"\\012\\000\" \"=\\012\" <out >actual &&\n+\n+\t\ttest_cmp expect actual &&\n+\t\ttest_line_count = 0 err\n+\t)\n+'\n \n test_expect_success 'progress meter option' '\n \ttest_when_finished \"rm -rf repo\" &&\n-- \n2.53.0\n\n"},{"id":"553727","messageId":"ar04uStCZ4pnEJ38@pks.im","threadId":"66388","inReplyTo":"20260924164503.119506-2-markchucarroll@fastmail.com","subject":"Re: [PATCH 1/1] repo: add filtering options to \"repo structure\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-09-30T16:28:41Z","receivedAt":"2026-09-30T16:28:46Z","isPatch":true,"body":"On Thu, Sep 24, 2026 at 12:45:03PM -0400, Mark C. Chu-Carroll wrote:\n> \"git repo structure\" provides a collection of useful information\n> about the information stored in a repo. In particular, it's\n> valuable for diagnosing performance issues caused by large objects\n> stored in a repo.\n> \n> The current implementation of \"git repo stucture\" provides summary\n> information about everything in the repository - all of the\n> branches, remotes, tags, stashes, and notes. But sometimes\n> to properly diagnose a problem, it's useful to be able to exclude\n> refs that are known to not be relevant to the issue at hand.\n\nYes, indeed. Sometimes you may for example want to figure out where\nexactly the storage size of a particular repository is going. Or in the\ncase of GitLab for example, we may have bookkeeping references that are\nnot controllable by customers. So we may only want to get the structure\nfor all the customer-controllable branches there.\n\n> Add a set of flags that allow a user to selective exclude\n> reference types from the report generated by \"git repo structure\".\n> When a ref type is excluded by the filter, it no longer appears\n> in the report (ie, if \"--no-tags\" is passed, the report line\n> for \"Branches\" will no longer appear under \"* References\").\n> Following the pattern of flags that are only used to\n> disable functionality (eg, \"--no-verify\" in \"builtins/push.c\"),\n> only the \"--no-<reftype>\" syntax is listed in the updated\n> documentation.\n\nHmm, okay. I would have expected that the user can essentially pass\narbitrary revisions as understood by git-log(1) et al. And if they pass\nany such revisions, we should not enumerate anything but what they have\npassed, so the flags shouldn't only be used to exclude.\n\nSo, for example:\n\n    $ git repo structure --branches\n    $ git repo structure master\n    $ git repo structure --all --not --branches\n\nI would hope that git-repo(1) can achieve that rather easily because I\nexpect that it uses `struct rev_info`, but let's read on.\n\n> Overview of the changes:\n> - Add an enum to represent the structure flags.\n> - Add structure flags to the options for the \"repo structure\" commands.\n> - For each reference flag, add a conditional in \"count_references\"\n>   which decides whether or not to add a ref to the pending list.\n>   If an references is not added to the pending list, the things it\n>   transitively references will not be added to the stats.\n> - Add a set of test cases to verify that reference counts\n>   in the repo structure report correctly omit the specified\n>   resource types.\n> - Update the documentation for git-repo to include the new options.\n\nNote that we typically don't have lists of what exactly has changed in\nthe commit. That kind of information is already visible from the diff\nitself. So what the commit message itself should focus on is whether any\nof these changes are non-obvious or whethere there's any dragons to be\nfound.\n\nSo in summary: everything that may surprise the reader should be part of\nthe commit message, everything that's just obvious plumbing doesn't\nreally have to be mentioned.\n\n> diff --git a/builtin/repo.c b/builtin/repo.c\n> index 84e012f83f..c8f6e38011 100644\n> --- a/builtin/repo.c\n> +++ b/builtin/repo.c\n> @@ -490,7 +504,8 @@ static inline size_t get_total_object_values(struct object_values *values)\n>  }\n>  \n>  static void stats_table_setup_structure(struct stats_table *table,\n> -\t\t\t\t\tstruct repo_structure *stats)\n> +\t\t\t\t\tstruct repo_structure *stats,\n> +\t\t\t\t\tenum repo_structure_filter_flags flags)\n>  {\n>  \tstruct object_stats *objects = &stats->objects;\n>  \tstruct ref_stats *refs = &stats->refs;\n> @@ -502,9 +517,15 @@ static void stats_table_setup_structure(struct stats_table *table,\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> +\tif (flags & REPO_STRUCTURE_FILTER_BRANCHES) {\n> +\t\tstats_table_count_addf(table, refs->branches, \"    * %s\", _(\"Branches\"));\n> +\t}\n> +\tif (flags & REPO_STRUCTURE_FILTER_TAGS) {\n> +\t\tstats_table_count_addf(table, refs->tags, \"    * %s\", _(\"Tags\"));\n> +\t}\n> +\tif (flags & REPO_STRUCTURE_FILTER_REMOTES) {\n> +\t\tstats_table_count_addf(table, refs->remotes, \"    * %s\", _(\"Remotes\"));\n> +\t}\n>  \tstats_table_count_addf(table, refs->others, \"    * %s\", _(\"Others\"));\n>  \n>  \tobject_count_total = get_total_object_values(&objects->type_counts);\n\nCoding style: we don't use curly braces around single-line statements.\n\nBut more importantly, I think this is where the mismatch in expectations\ncomes from that I was pointing out further up. My expectation was that\nwhat we want to achieve is to filter the reachable objects by revisions,\nwhich I think is a much more useful thing to do. But what the flags do\ninstead us to filter the output in the \"References\" count.\n\nI think that we should rather go into the direction of filtering objects\nand not the ref output, as the latter isn't all that useful. It _may_\nmake sense to maybe make some sections of the output optional, but\nexcluding individual ref types is arguably too fine-grained.\n\nIn any case, to go into the direction of filtering objects you'd want to\nadapt `parse_options()` so that it accepts unknown options (you can\nachieve that by passing `PARSE_OPT_KEEP_ARGV0 |\nPARSE_OPT_KEEP_UNKNOWN_OPT`) and then pass argv to `setup_revisions()`.\nAnd I think that _should_ already achieve proper filtering of objects by\nrevisions.\n\nThanks!\n\nPatrick\n"},{"id":"554205","messageId":"20261005174045.1900391-1-markchucarroll@fastmail.com","threadId":"66388","inReplyTo":"20260924164503.119506-2-markchucarroll@fastmail.com","subject":"[PATCH v2 0/1] repo: add filtering options to \"repo structure\"","fromName":"Mark C. Chu-Carroll","fromEmail":"markchucarroll@fastmail.com","sentAt":"2026-10-05T17:40:43Z","receivedAt":"2026-10-05T17:40:43Z","isPatch":true,"body":"\"git repo structure\" provides a collection of useful information\nabout the information stored in a repo. In particular, it's\nvaluable for diagnosing performance issues caused by large objects\nstored in a repo.\n\nThe current implementation of \"git repo stucture\" provides summary\ninformation about everything in the repository - all of the\nbranches, remotes, tags, stashes, and notes. But sometimes\nto properly diagnose a problem, it's useful to be able to\nget information about the specific part of the repo that's\nexhibiting a problem.\n\nAdd the option to specify a set of filters in the form\nof a list of include and exclude queries. Each of these\nspecifies a commit or ref or range. The set of objects processed\nwill consist of all objects reachable from any of the\nincludes which are _not_ reached exclusively by paths\nincluding any of the excludes.\n\nUpdates since v1: this change has been completely rewritten. The\noriginal version implemented filters the same way as\n\"git-sizer\", by selecting object types to include or exclude.\nThis version drops he type-based filters in favor of\nspecifying object traversal roots and exclusions.\n\nMark C. Chu-Carroll (1):\n  repo: add filtering options to \"repo structure\"\n\n Documentation/git-repo.adoc | 41 ++++++++++++++++--\n builtin/repo.c              | 16 +++++--\n t/t1901-repo-structure.sh   | 84 +++++++++++++++++++++++++++++++++++++\n 3 files changed, 133 insertions(+), 8 deletions(-)\n\n-- \n2.53.0\n\n\n"},{"id":"554206","messageId":"20261005174045.1900391-2-markchucarroll@fastmail.com","threadId":"66388","inReplyTo":"20261005174045.1900391-1-markchucarroll@fastmail.com","subject":"[PATCH v2 1/1] repo: add filtering options to \"repo structure\"","fromName":"Mark C. Chu-Carroll","fromEmail":"markchucarroll@fastmail.com","sentAt":"2026-10-05T17:40:44Z","receivedAt":"2026-10-05T17:40:44Z","isPatch":true,"body":"Implement filtering for repo structure, imitating the mechanism\nused in \"git log\".\n\nSigned-off-by: Mark C. Chu-Carroll <markchucarroll@fastmail.com>\n---\n Documentation/git-repo.adoc | 41 ++++++++++++++++--\n builtin/repo.c              | 16 +++++--\n t/t1901-repo-structure.sh   | 84 +++++++++++++++++++++++++++++++++++++\n 3 files changed, 133 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex ed7d80c690..5cbdf8e727 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -10,7 +10,7 @@ SYNOPSIS\n [synopsis]\n git repo info [--format=(lines|nul) | -z] [--all | <key>...]\n git repo info --keys [--format=(lines|nul) | -z]\n-git repo structure [--format=(table|lines|nul) | -z]\n+git repo structure [--format=(table|lines|nul) | -z] [<include|^exclude>...]\n \n DESCRIPTION\n -----------\n@@ -56,9 +56,10 @@ supported:\n `nul`:::\n \tSimilar to `lines`, but using a _NUL_ character after each value.\n \n-`structure [--format=(table|lines|nul) | -z]`::\n-\tRetrieve statistics about the current repository structure. The\n-\tfollowing kinds of information are reported:\n+`structure [--format=(table|lines|nul) | -z] [<include|^exclude>...]::\n++\n+Retrieve statistics about the current repository structure. The\n+following kinds of information are reported:\n +\n * Reference counts categorized by type\n * Reachable object counts categorized by type\n@@ -66,6 +67,16 @@ supported:\n * Total disk size of reachable objects by type\n * Largest reachable objects in the repository by type\n +\n+The set of objects counted can be filtered by specifying a\n+collection of query clauses to select which objects will be\n+counted. These parameters follow the same syntax as the parameters\n+to similar commands like `git log`. Semantically, these parameters\n+are treated as a collection of include and exclude specifiers. Th\n+set of objects counted will consist of all objects reachable from\n+an object included by one of the include specifiers via a path that\n+does not include an object in an exclude clause. If no includes\n+are specified, then the include set is all reachable objects. \n++\n The output format can be chosen through the flag `--format`. Three formats are\n supported:\n +\n@@ -141,6 +152,28 @@ using the `nul` format:\n git repo info --format=nul layout.bare layout.shallow\n ------------\n \n+* Generates information about storage usage in the repository:\n++\n+------------\n+git repo structure\n+------------\n++\n+\n+* Generates information about storage usage in the repository omitting\n+the branch \"foo\":\n++\n+------------\n+git repo structure ^foo \n+------------\n++\n+* Generates information about repository objects reachable from\n+the references \"x\" and \"y\", but omitting anything that can\n+only be reached on a path including \"xchild\":\n++\n+------------\n+git repo structure x y ^xchild\n+------------\n++\n SEE ALSO\n --------\n linkgit:git-rev-parse[1]\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 84e012f83f..b3aca71298 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -27,7 +27,7 @@\n \t\"git repo info --keys [--format=(lines|nul) | -z]\"\n \n #define REPO_STRUCTURE_USAGE \\\n-\t\"git repo structure [--format=(table|lines|nul) | -z]\"\n+\t\"git repo structure [--format=(table|lines|nul) | -z] [<include|^exclude>...]\"\n \n static const char *const repo_usage[] = {\n \tREPO_INFO_USAGE,\n@@ -946,12 +946,20 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n \t\tOPT_BOOL(0, \"progress\", &show_progress, N_(\"show progress\")),\n \t\tOPT_END()\n \t};\n+\tstruct setup_revision_opt s_r_opt;\n+\tmemset(&s_r_opt, 0, sizeof(s_r_opt));\n+\ts_r_opt.def = \"HEAD\";\n+\ts_r_opt.revarg_opt = REVARG_COMMITTISH;\n \n-\targc = parse_options(argc, argv, prefix, options, repo_structure_usage, 0);\n-\tif (argc)\n-\t\tusage(_(\"too many arguments\"));\n+\targc = parse_options(argc, argv, prefix, options, repo_structure_usage,\n+\t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \trepo_init_revisions(repo, &revs, prefix);\n+\tif (argc > 1) {\n+\t\targc = setup_revisions(argc, argv, &revs, &s_r_opt);\n+\t\tif (argc > 1)\n+\t\t\tusage(_(\"too many arguments\"));\n+\t}\n \n \tif (show_progress < 0)\n \t\tshow_progress = isatty(2);\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nindex 02cc2b594a..eb2c595955 100755\n--- a/t/t1901-repo-structure.sh\n+++ b/t/t1901-repo-structure.sh\n@@ -144,6 +144,90 @@ test_expect_success SHA1 'repository with references and objects' '\n \t)\n '\n \n+test_expect_success SHA1 'repository with references and objects, filtered' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit_bulk 1005 &&\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+\t\tgit checkout -b grobble &&\n+\t\ttest_commit_bulk --ref=refs/heads/grobble 20 &&\n+\t\tgit checkout master &&\n+ \t\ttest_commit_bulk 20 &&\n+\t\t# Also creates a commit, tree, and blob.\n+\t\tgit notes add -m foo &&\n+\n+\t\t# git-rev-list(1) --disk-usage=human option printing the full\n+\t\t# \"byte/bytes\" unit string instead of just \"B\".\n+\t\tcat >expect <<-EOF &&\n+\t\t| Repository structure      | 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\t|                           |            |\n+\t\t| * Reachable objects       |            |\n+\t\t|   * Count                 |   3.06 k   |\n+\t\t|     * Commits             |   1.05 k   |\n+\t\t|     * Trees               |   1.01 k   |\n+\t\t|     * Blobs               |   1.01 k   |\n+\t\t|     * Tags                |      1     |\n+\t\t|   * Inflated size         |  16.04 MiB |\n+\t\t|     * Commits             | 226.54 KiB |\n+\t\t|     * Trees               |  15.81 MiB |\n+\t\t|     * Blobs               |  11.68 KiB |\n+\t\t|     * Tags                |    132 B   |\n+\t\t|   * Disk size             | $(object_type_disk_usage all true) |\n+\t\t|     * Commits             | $(object_type_disk_usage commit true) |\n+\t\t|     * Trees               | $(object_type_disk_usage tree true) |\n+\t\t|     * Blobs               |  $(object_type_disk_usage blob true) |\n+\t\t|     * Tags                |    $(object_type_disk_usage tag) B   |\n+\t\t|                           |            |\n+\t\t| * Largest objects         |            |\n+\t\t|   * Commits               |            |\n+\t\t|     * Maximum size    [1] |    223 B   |\n+\t\t|     * Maximum parents [2] |      1     |\n+\t\t|   * Trees                 |            |\n+\t\t|     * Maximum size    [3] |  32.29 KiB |\n+\t\t|     * Maximum entries [4] |   1.01 k   |\n+\t\t|   * Blobs                 |            |\n+\t\t|     * Maximum size    [5] |     13 B   |\n+\t\t|   * Tags                  |            |\n+\t\t|     * Maximum size    [6] |    132 B   |\n+\n+\t\t[1] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a\n+\t\t[2] df6400c01440c329f1011669c4c26cc0c7852887\n+\t\t[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c\n+\t\t[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c\n+\t\t[5] 97d808e45116bf02103490294d3d46dad7a2ac62\n+\t\t[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2\n+\t\tEOF\n+\n+\t\tgit repo structure  >actual 2>actual-err &&\n+\t\tcp actual /tmp/actual &&\n+\t\tcp expect /tmp/expect &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest_line_count = 0 actual-err &&\n+\n+\t\tgit repo structure grobble ^master >actual 2>actual-err &&\n+\t\tcp actual /tmp &&\n+\t\tcp actual-err /tmp &&\n+\t\ttest_grep \"|     \\* Commits             |    21     |\" actual &&\n+\t\ttest_grep \"|     \\* Trees               |     2     |\" actual &&\n+\t\ttest_grep \"|     \\* Commits             |  4.50 KiB |\" actual &&\n+\t\ttest_grep \"|     \\* Trees               | 32.35 KiB |\" actual &&\n+\t\ttest_grep \"|     \\* Blobs               | 11.68 KiB |\" actual &&\n+\t\ttest_line_count = 0 actual-err\n+\t)\n+'\n+\n test_expect_success SHA1 'lines and nul format' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n-- \n2.53.0\n\n\n"},{"id":"554212","messageId":"DLX4QVXQJTQ4.24JR7ES0P3H@fastmail.com","threadId":"66388","inReplyTo":"ar04uStCZ4pnEJ38@pks.im","subject":"Re: [PATCH 1/1] repo: add filtering options to \"repo structure\"","fromName":"Mark C. Chu-Carroll","fromEmail":"markchucarroll@fastmail.com","sentAt":"2026-10-05T18:52:27Z","receivedAt":"2026-10-05T18:52:27Z","isPatch":true,"body":"On Wed Sep 30, 2026 at 12:28 PM EDT, Patrick Steinhardt wrote:\n> On Thu, Sep 24, 2026 at 12:45:03PM -0400, Mark C. Chu-Carroll wrote:\n>> \"git repo structure\" provides a collection of useful information\n>> about the information stored in a repo. In particular, it's\n>> valuable for diagnosing performance issues caused by large objects\n>> stored in a repo.\n>> \n>> The current implementation of \"git repo stucture\" provides summary\n>> information about everything in the repository - all of the\n>> branches, remotes, tags, stashes, and notes. But sometimes\n>> to properly diagnose a problem, it's useful to be able to exclude\n>> refs that are known to not be relevant to the issue at hand.\n>\n> Yes, indeed. Sometimes you may for example want to figure out where\n> exactly the storage size of a particular repository is going. Or in the\n> case of GitLab for example, we may have bookkeeping references that are\n> not controllable by customers. So we may only want to get the structure\n> for all the customer-controllable branches there.\n>\n>> Add a set of flags that allow a user to selective exclude\n>> reference types from the report generated by \"git repo structure\".\n>> When a ref type is excluded by the filter, it no longer appears\n>> in the report (ie, if \"--no-tags\" is passed, the report line\n>> for \"Branches\" will no longer appear under \"* References\").\n>> Following the pattern of flags that are only used to\n>> disable functionality (eg, \"--no-verify\" in \"builtins/push.c\"),\n>> only the \"--no-<reftype>\" syntax is listed in the updated\n>> documentation.\n>\n> Hmm, okay. I would have expected that the user can essentially pass\n> arbitrary revisions as understood by git-log(1) et al. And if they pass\n> any such revisions, we should not enumerate anything but what they have\n> passed, so the flags shouldn't only be used to exclude.\n>\n> So, for example:\n>\n>     $ git repo structure --branches\n>     $ git repo structure master\n>     $ git repo structure --all --not --branches\n>\n> I would hope that git-repo(1) can achieve that rather easily because I\n> expect that it uses `struct rev_info`, but let's read on.\n\nThat makes sense. My initial understanding was that most of what \n\"git repo structure\" does is internalize the functionality of git-sizer\ninto the core of git. The only filters offered by git-sizer are \ntype-based. But I agree that a commit list based filter is a lot \nmore useful, so I've updated the patch set to implement it.\n\n   -Mark\n\n\n\n-- \nMark Craig Chu-Carroll (@MarkChuCarroll at gitlab)\n*** Software Tools/Math Geek - Software Engineer at Gitlab\n*** Work Email: mcarroll@gitlab.com / markchucarroll@fastmail.com\n*** Personal Blog: http://goodmath.org/blog / Personal email: markcc@gmail.com\n\n\n"},{"id":"554235","messageId":"asSMX-K2qLsaXc3v@pks.im","threadId":"66388","inReplyTo":"20261005174045.1900391-2-markchucarroll@fastmail.com","subject":"Re: [PATCH v2 1/1] repo: add filtering options to \"repo structure\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-06T05:51:27Z","receivedAt":"2026-10-06T05:51:27Z","isPatch":true,"body":"On Mon, Oct 05, 2026 at 01:40:44PM -0400, Mark C. Chu-Carroll wrote:\n> Implement filtering for repo structure, imitating the mechanism\n> used in \"git log\".\n\nThe message should give an explanation of what this change does, and\nwhat the motivation behind it is.\n\n> diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\n> index ed7d80c690..5cbdf8e727 100644\n> --- a/Documentation/git-repo.adoc\n> +++ b/Documentation/git-repo.adoc\n> @@ -10,7 +10,7 @@ SYNOPSIS\n>  [synopsis]\n>  git repo info [--format=(lines|nul) | -z] [--all | <key>...]\n>  git repo info --keys [--format=(lines|nul) | -z]\n> -git repo structure [--format=(table|lines|nul) | -z]\n> +git repo structure [--format=(table|lines|nul) | -z] [<include|^exclude>...]\n\nI think we should probably have this be `[<revs>...]`.\n\n> @@ -56,9 +56,10 @@ supported:\n>  `nul`:::\n>  \tSimilar to `lines`, but using a _NUL_ character after each value.\n>  \n> -`structure [--format=(table|lines|nul) | -z]`::\n> -\tRetrieve statistics about the current repository structure. The\n> -\tfollowing kinds of information are reported:\n> +`structure [--format=(table|lines|nul) | -z] [<include|^exclude>...]::\n\nSame here.\n\n> @@ -66,6 +67,16 @@ supported:\n>  * Total disk size of reachable objects by type\n>  * Largest reachable objects in the repository by type\n>  +\n> +The set of objects counted can be filtered by specifying a\n> +collection of query clauses to select which objects will be\n\ns/query clauses/revisions/, which is a well-defined term. So with this\nchange I think we can drop most of the remaining paragraph, except for\nthe last sentence.\n\n> +counted. These parameters follow the same syntax as the parameters\n> +to similar commands like `git log`. Semantically, these parameters\n> +are treated as a collection of include and exclude specifiers. Th\n> +set of objects counted will consist of all objects reachable from\n> +an object included by one of the include specifiers via a path that\n> +does not include an object in an exclude clause. If no includes\n> +are specified, then the include set is all reachable objects. \n> ++\n>  The output format can be chosen through the flag `--format`. Three formats are\n>  supported:\n>  +\n> diff --git a/builtin/repo.c b/builtin/repo.c\n> index 84e012f83f..b3aca71298 100644\n> --- a/builtin/repo.c\n> +++ b/builtin/repo.c\n> @@ -946,12 +946,20 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,\n>  \t\tOPT_BOOL(0, \"progress\", &show_progress, N_(\"show progress\")),\n>  \t\tOPT_END()\n>  \t};\n> +\tstruct setup_revision_opt s_r_opt;\n> +\tmemset(&s_r_opt, 0, sizeof(s_r_opt));\n> +\ts_r_opt.def = \"HEAD\";\n> +\ts_r_opt.revarg_opt = REVARG_COMMITTISH;\n\nThis can be:\n\n    struct setup_revision_opt s_r_opt = {\n        .def = \"HEAD\",\n        .revarg_opt = REVARG_COMMITTISH,\n    };\n\nBut I wonder whether we want to pass it at all:\n\n  - `.def` specifies the default, but do we even want to have one when\n    the user has passed arguments?\n\n  - `.revarg_opt` makes us treat it like a committish by default, but a\n    user may for example want to figure out the size of all objects\n    reachable from a specific tree, only.\n\nSo maybe we shouldn't be setting this at all and just pass `NULL` to\n`setup_revisions()`?\n\n> -\targc = parse_options(argc, argv, prefix, options, repo_structure_usage, 0);\n> -\tif (argc)\n> -\t\tusage(_(\"too many arguments\"));\n> +\targc = parse_options(argc, argv, prefix, options, repo_structure_usage,\n> +\t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n\nMakes sense. Here we keep argv0 because of `setup_revisions()`' weird\ncalling convention. And we also ignore any unknown options so that we\ncan pass them along, too.\n\n>  \trepo_init_revisions(repo, &revs, prefix);\n> +\tif (argc > 1) {\n> +\t\targc = setup_revisions(argc, argv, &revs, &s_r_opt);\n> +\t\tif (argc > 1)\n> +\t\t\tusage(_(\"too many arguments\"));\n> +\t}\n>  \n>  \tif (show_progress < 0)\n>  \t\tshow_progress = isatty(2);\n\nAnd then, if we have any additional parameters then we pass it on to\n`setup_revisions()`.\n\n> diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\n> index 02cc2b594a..eb2c595955 100755\n> --- a/t/t1901-repo-structure.sh\n> +++ b/t/t1901-repo-structure.sh\n> @@ -144,6 +144,90 @@ test_expect_success SHA1 'repository with references and objects' '\n>  \t)\n>  '\n>  \n> +test_expect_success SHA1 'repository with references and objects, filtered' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_commit_bulk 1005 &&\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> +\t\tgit checkout -b grobble &&\n> +\t\ttest_commit_bulk --ref=refs/heads/grobble 20 &&\n> +\t\tgit checkout master &&\n> + \t\ttest_commit_bulk 20 &&\n> +\t\t# Also creates a commit, tree, and blob.\n> +\t\tgit notes add -m foo &&\n> +\n> +\t\t# git-rev-list(1) --disk-usage=human option printing the full\n> +\t\t# \"byte/bytes\" unit string instead of just \"B\".\n> +\t\tcat >expect <<-EOF &&\n> +\t\t| Repository structure      | 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\t|                           |            |\n> +\t\t| * Reachable objects       |            |\n> +\t\t|   * Count                 |   3.06 k   |\n> +\t\t|     * Commits             |   1.05 k   |\n> +\t\t|     * Trees               |   1.01 k   |\n> +\t\t|     * Blobs               |   1.01 k   |\n> +\t\t|     * Tags                |      1     |\n> +\t\t|   * Inflated size         |  16.04 MiB |\n> +\t\t|     * Commits             | 226.54 KiB |\n> +\t\t|     * Trees               |  15.81 MiB |\n> +\t\t|     * Blobs               |  11.68 KiB |\n> +\t\t|     * Tags                |    132 B   |\n> +\t\t|   * Disk size             | $(object_type_disk_usage all true) |\n> +\t\t|     * Commits             | $(object_type_disk_usage commit true) |\n> +\t\t|     * Trees               | $(object_type_disk_usage tree true) |\n> +\t\t|     * Blobs               |  $(object_type_disk_usage blob true) |\n> +\t\t|     * Tags                |    $(object_type_disk_usage tag) B   |\n> +\t\t|                           |            |\n> +\t\t| * Largest objects         |            |\n> +\t\t|   * Commits               |            |\n> +\t\t|     * Maximum size    [1] |    223 B   |\n> +\t\t|     * Maximum parents [2] |      1     |\n> +\t\t|   * Trees                 |            |\n> +\t\t|     * Maximum size    [3] |  32.29 KiB |\n> +\t\t|     * Maximum entries [4] |   1.01 k   |\n> +\t\t|   * Blobs                 |            |\n> +\t\t|     * Maximum size    [5] |     13 B   |\n> +\t\t|   * Tags                  |            |\n> +\t\t|     * Maximum size    [6] |    132 B   |\n> +\n> +\t\t[1] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a\n> +\t\t[2] df6400c01440c329f1011669c4c26cc0c7852887\n> +\t\t[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c\n> +\t\t[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c\n> +\t\t[5] 97d808e45116bf02103490294d3d46dad7a2ac62\n> +\t\t[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2\n> +\t\tEOF\n> +\n> +\t\tgit repo structure  >actual 2>actual-err &&\n> +\t\tcp actual /tmp/actual &&\n> +\t\tcp expect /tmp/expect &&\n> +\t\ttest_cmp expect actual &&\n> +\t\ttest_line_count = 0 actual-err &&\n> +\n> +\t\tgit repo structure grobble ^master >actual 2>actual-err &&\n> +\t\tcp actual /tmp &&\n> +\t\tcp actual-err /tmp &&\n> +\t\ttest_grep \"|     \\* Commits             |    21     |\" actual &&\n> +\t\ttest_grep \"|     \\* Trees               |     2     |\" actual &&\n> +\t\ttest_grep \"|     \\* Commits             |  4.50 KiB |\" actual &&\n> +\t\ttest_grep \"|     \\* Trees               | 32.35 KiB |\" actual &&\n> +\t\ttest_grep \"|     \\* Blobs               | 11.68 KiB |\" actual &&\n> +\t\ttest_line_count = 0 actual-err\n> +\t)\n> +'\n\nI wonder whether we maybe want to have some additional tests that assert\nthat you can also pass e.g.:\n\n  - A tree or blob.\n\n  - Revision options, like for example `--all --filter=object:type=blob`.\n\nTo make the test a bit less repetitive we might also want to use\n`--format=lines` and then only check for\n\"objects.*.{inflated,disk}_size\" to exercise only the parts that matter\nto this test.\n\nThanks!\n\nPatrick\n\n"}]}