Git development
 help / color / mirror / Atom feed
* [PATCH 0/3] repo: add degree-of-concern reporting to structure
@ 2026-10-21 13:55 Mark C. Chu-Carroll
  2026-10-21 13:55 ` [PATCH 1/3] repo: add level of concern reporting Mark C. Chu-Carroll
                   ` (5 more replies)
  0 siblings, 6 replies; 20+ messages in thread
From: Mark C. Chu-Carroll @ 2026-10-21 13:55 UTC (permalink / raw)
  To: git; +Cc: Mark C. Chu-Carroll

"git repo structure" computes a set of statistics that are useful for
diagnosing performance issues in a git repository. But for a typical
user who hasn't encountered any issues yet, it can be difficult to
interpret the data - when should a value be considered concerning?

This patchset introduces a feature to "git repo structure" that reports
on how concerning certain measures of storage structure  are for a
repository.  In table mode, concern is displayed as a column containing
a number of stars indicating the degree of concern; in line or nul mode,
concerns are displayed as additional lines with a numeric value.

To select defaults for the thresholds where a value becomes concern,
a (hopefully) representative selection of repositories were analyzed.
Based on empirical exploration of the data, the measurement that seemed
closest to accurately assessing when a repository's structure becomes
concerning was based on the base 2 logarithm of the value. Across all of
the sample repositories, for all of the measurements, the point at which
things become problematic was when a base 2 logarithm of a value was
more than 1 standard deviation from the mean of the log_2 values in
the sample set.  Based on this, the concern threshold was set for each
of the measures.

While this seems like a reasonable approach for setting default
thresholds, it's obviously still very ad-hoc. It's likely that over
time, the default values will evolve; and in the short term, projects
may want to set their own thresholds. So this patchset includes support
for configuring the thresholds on a per-reposiry basis.

Overview of the change:

* The first commit adds the command-line option "--concern", and creates the basic implementation of concern reporting.
* The next commit adds configurability for the concern thresholds.
* The final commit update the documentation for the command and
  configuration options.

Signed-off-by: Mark C. Chu-Carroll <markchucarroll@fastmail.com>
---
Mark C. Chu-Carroll (3):
      repo: add level of concern reporting
      repo: make concern thresholds configurable
      repo: add documentation for level-of-concern.

 Documentation/config/repo.adoc |  60 ++++++++
 Documentation/git-repo.adoc    |  27 +++-
 builtin/repo.c                 | 335 ++++++++++++++++++++++++++++++++++++++---
 t/t1901-repo-structure.sh      | 134 +++++++++++++++++
 4 files changed, 535 insertions(+), 21 deletions(-)


---
base-commit: 3bc0341126508f78f5869cbfc0005e987efdf0c7
change-id: 20261019-b4-repo-structure-concern-f1d6f6326095


^ permalink raw reply	[flat|nested] 20+ messages in thread

* [PATCH 1/3] repo: add level of concern reporting
  2026-10-21 13:55 [PATCH 0/3] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
@ 2026-10-21 13:55 ` Mark C. Chu-Carroll
  2026-10-21 13:55 ` [PATCH 2/3] repo: make concern thresholds configurable Mark C. Chu-Carroll
                   ` (4 subsequent siblings)
  5 siblings, 0 replies; 20+ messages in thread
From: Mark C. Chu-Carroll @ 2026-10-21 13:55 UTC (permalink / raw)
  To: git; +Cc: Mark C. Chu-Carroll

"git repo structure" computes a set of statistics
that are useful for diagnosing performance issues in
a git repository. But for a typical user who hasn't
encountered any issues yet, it can be difficult to
interpret the data - when should a value be considered
concerning?

Add an option to "git repo structure" that adds a "degree
of concern" column to the structure report, which indicates
when a stat values has reached a point where it might be
concerning, and indicate how severe the concern should be.
---
 builtin/repo.c            | 329 +++++++++++++++++++++++++++++++++++++++++++---
 sideband.c                |   2 +-
 t/t1901-repo-structure.sh | 125 ++++++++----------
 3 files changed, 366 insertions(+), 90 deletions(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index 84e012f83f..a450497d4e 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] [--[no-]concern]"
 
 static const char *const repo_usage[] = {
 	REPO_INFO_USAGE,
@@ -348,8 +348,118 @@ struct stats_table {
 	int name_col_width;
 	int value_col_width;
 	int unit_col_width;
+	int concern_col_width;
 };
 
+/*
+ * For computing level of concern about a stat, we need to have some
+ * baseline values. For each of these, we'define a minimum level
+ * of concern, which will appear as 1 star. Then we scale exponentially:
+ * each time that value doubles, we add another star.
+ *
+ * The threshold values for concern are expressed as log_2 values of the size
+ * at which the statistic becomes concerning. The default values for the
+ * threshold were selected based on observed values from a collection of
+ * representative-ish repositories of varying size, by finding the mean log_2
+ * value and standard deviation (sigma) for each statistic over the collection.
+ * The threshold for each metric is then set at mean(log_2) + sigma.
+ */
+struct concern_stat_threshold {
+	const char *name;
+	size_t threshold;
+};
+
+enum concern_stat {
+	CONCERN_STAT_COMMIT_COUNT,
+	CONCERN_STAT_TREE_COUNT,
+	CONCERN_STAT_BLOB_COUNT,
+	CONCERN_STAT_TAG_COUNT,
+	CONCERN_STAT_COMMIT_SIZE,
+	CONCERN_STAT_TREE_SIZE,
+	CONCERN_STAT_BLOB_SIZE,
+	CONCERN_STAT_TAG_SIZE,
+	CONCERN_STAT_COMMIT_LARGEST,
+	CONCERN_STAT_TREE_LARGEST,
+	CONCERN_STAT_BLOB_LARGEST,
+	CONCERN_STAT_TAG_LARGEST,
+	CONCERN_STAT_COMMIT_INFLATED_SIZE,
+	CONCERN_STAT_TREE_INFLATED_SIZE,
+	CONCERN_STAT_BLOB_INFLATED_SIZE,
+	CONCERN_STAT_TAG_INFLATED_SIZE,
+};
+
+struct concern_stat_threshold repo_structure_concern_thresholds[] = {
+	{ .name = "commit_count", .threshold = 19 },
+	{.name = "tree_count", .threshold = 21 },
+	{ .name = "blob_count", .threshold = 22 },
+	{ .name = "tag_count", .threshold = 15 },
+	{ .name = "commit_size", .threshold = 28 },
+	{ .name = "tree_size", .threshold = 27 },
+	{ .name = "blob_size", .threshold = 36 },
+	{ .name = "tags_size", .threshold = 18 },
+	{ .name = "commit_largest", .threshold = 18  },
+	{ .name = "tree_largest", .threshold = 18 },
+	{ .name = "blob_largest", .threshold = 26 },
+	{ .name = "tag_largest", .threshold =  15 },
+	{ .name = "commit_inflated_size", .threshold = 28 },
+	{ .name = "tree_inflated_size", .threshold = 32 },
+	{ .name = "blob_inflated_size", .threshold = 34 },
+	{ .name = "tag_inflated_size", .threshold = 25}
+};
+
+/*
+ * Concern thresholds aren't universal. We try to set reasonable default
+ * values, but projects may want to set these thresholds differently. To
+ * support that, they can set the thresholds for their project in the
+ * project's git config, in the section "repo.structure.concern".
+  */
+static int config_concern_thresholds(const char *prefixed_var,
+				     const char *str_val,
+				     const struct config_context *ctx,
+				     void *cb UNUSED) {
+	const char *config_prefix = "repo.structure.concern.";
+	if (!strncmp(config_prefix, prefixed_var, strlen(config_prefix))) {
+		const char *concern_name = &prefixed_var[strlen(config_prefix)];
+		for (int i = 0; concern_stat_thresholds[i].name != 0; i++) {
+			if (!strcmp(concern_stat_thresholds[i].name, concern_name)) {
+				int value = git_config_int(prefixed_var, str_val, ctx->kvi);
+				if (value < 0)
+					return -1;
+				concern_stat_thresholds[i].threshold = value;
+				return 0;
+			}
+		}
+		warning(_("Invalid concern threshold name: '%s'"), concern_name);
+		return 0;
+	}
+	return 0;
+}
+
+static size_t approx_log2(long value) {
+	int position = 0;
+	if (value <= 0) {
+		return 0;
+	}
+	while (value > 1) {
+		position++;
+		value >>= 1;
+	}
+	return position;
+}
+
+static size_t repo_structure_concern_level(enum concern_stat stat_id,
+					   size_t value) {
+	size_t log_value;
+	if (stat_id >= ARRAY_SIZE(concern_stat_thresholds))
+		BUG("unexpected repository stat type");
+	log_value = approx_log2(value);
+	if (log_value > concern_stat_thresholds[stat_id].threshold)
+		return log_value -
+			concern_stat_thresholds[stat_id].threshold;
+	else
+		return 0;
+}
+
 /*
  * Holds column data that gets stored for each row.
  */
@@ -358,6 +468,7 @@ struct stats_table_entry {
 	const char *unit;
 	size_t index;
 	struct object_id *oid;
+	size_t concern;
 };
 
 static void stats_table_vaddf(struct stats_table *table,
@@ -391,6 +502,8 @@ static void stats_table_vaddf(struct stats_table *table,
 		if (value_width > table->value_col_width)
 			table->value_col_width = value_width;
 	}
+	if ((int)entry->concern > table->concern_col_width)
+		table->concern_col_width = entry->concern;
 	if (entry->unit) {
 		int unit_width = utf8_strwidth(entry->unit);
 		if (unit_width > table->unit_col_width)
@@ -410,13 +523,14 @@ static void stats_table_addf(struct stats_table *table, const char *format, ...)
 }
 
 static void stats_table_count_addf(struct stats_table *table, size_t value,
-				   const char *format, ...)
+				   size_t concern, const char *format, ...)
 {
 	struct stats_table_entry *entry;
 	va_list ap;
 
 	CALLOC_ARRAY(entry, 1);
 	humanise_count(value, &entry->value, &entry->unit);
+	entry->concern = concern;
 
 	va_start(ap, format);
 	stats_table_vaddf(table, entry, format, ap);
@@ -425,13 +539,14 @@ static void stats_table_count_addf(struct stats_table *table, size_t value,
 
 static void stats_table_object_count_addf(struct stats_table *table,
 					  struct object_id *oid, size_t value,
-					  const char *format, ...)
+					  size_t concern, const char *format, ...)
 {
 	struct stats_table_entry *entry;
 	va_list ap;
 
 	CALLOC_ARRAY(entry, 1);
 	humanise_count(value, &entry->value, &entry->unit);
+	entry->concern = concern;
 
 	/*
 	 * A NULL OID should not have a table annotation.
@@ -445,6 +560,7 @@ static void stats_table_object_count_addf(struct stats_table *table,
 }
 
 static void stats_table_size_addf(struct stats_table *table, size_t value,
+				  size_t concern,
 				  const char *format, ...)
 {
 	struct stats_table_entry *entry;
@@ -452,6 +568,7 @@ static void stats_table_size_addf(struct stats_table *table, size_t value,
 
 	CALLOC_ARRAY(entry, 1);
 	humanise_bytes(value, &entry->value, &entry->unit, HUMANISE_COMPACT);
+	entry->concern = concern;
 
 	va_start(ap, format);
 	stats_table_vaddf(table, entry, format, ap);
@@ -460,6 +577,7 @@ static void stats_table_size_addf(struct stats_table *table, size_t value,
 
 static void stats_table_object_size_addf(struct stats_table *table,
 					 struct object_id *oid, size_t value,
+					 size_t concern,
 					 const char *format, ...)
 {
 	struct stats_table_entry *entry;
@@ -467,6 +585,7 @@ static void stats_table_object_size_addf(struct stats_table *table,
 
 	CALLOC_ARRAY(entry, 1);
 	humanise_bytes(value, &entry->value, &entry->unit, HUMANISE_COMPACT);
+	entry->concern = concern;
 
 	/*
 	 * A NULL OID should not have a table annotation.
@@ -501,47 +620,92 @@ 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"));
-	stats_table_count_addf(table, refs->others, "    * %s", _("Others"));
+	stats_table_count_addf(table, ref_total, 0,
+			       "  * %s", _("Count"));
+	stats_table_count_addf(table, refs->branches, 0,
+			       "    * %s", _("Branches"));
+	stats_table_count_addf(table, refs->tags,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TAG_COUNT,
+				       refs->tags),
+			       "    * %s", _("Tags"));
+	stats_table_count_addf(table, refs->remotes, 0,
+			       "    * %s", _("Remotes"));
+	stats_table_count_addf(table, refs->others, 0,
+			       "    * %s", _("Others"));
 
 	object_count_total = get_total_object_values(&objects->type_counts);
 	stats_table_addf(table, "");
 	stats_table_addf(table, "* %s", _("Reachable objects"));
-	stats_table_count_addf(table, object_count_total, "  * %s", _("Count"));
+	stats_table_count_addf(table, object_count_total, 0,
+			       "  * %s", _("Count"));
 	stats_table_count_addf(table, objects->type_counts.commits,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_COMMIT_COUNT,
+				       objects->type_counts.commits),
 			       "    * %s", _("Commits"));
 	stats_table_count_addf(table, objects->type_counts.trees,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TREE_COUNT,
+				       objects->type_counts.trees),
 			       "    * %s", _("Trees"));
 	stats_table_count_addf(table, objects->type_counts.blobs,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_BLOB_COUNT,
+				       objects->type_counts.blobs),
 			       "    * %s", _("Blobs"));
 	stats_table_count_addf(table, objects->type_counts.tags,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TAG_COUNT,
+				       objects->type_counts.tags),
 			       "    * %s", _("Tags"));
 
 	inflated_object_total = get_total_object_values(&objects->inflated_sizes);
-	stats_table_size_addf(table, inflated_object_total,
+	stats_table_size_addf(table, inflated_object_total, 0,
 			      "  * %s", _("Inflated size"));
 	stats_table_size_addf(table, objects->inflated_sizes.commits,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_COMMIT_INFLATED_SIZE,
+				      objects->inflated_sizes.commits),
 			      "    * %s", _("Commits"));
 	stats_table_size_addf(table, objects->inflated_sizes.trees,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_TREE_INFLATED_SIZE,
+				      objects->inflated_sizes.trees),
 			      "    * %s", _("Trees"));
 	stats_table_size_addf(table, objects->inflated_sizes.blobs,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_BLOB_INFLATED_SIZE,
+				      objects->inflated_sizes.blobs),
 			      "    * %s", _("Blobs"));
 	stats_table_size_addf(table, objects->inflated_sizes.tags,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_TAG_INFLATED_SIZE,
+				      objects->inflated_sizes.tags),
 			      "    * %s", _("Tags"));
 
 	disk_object_total = get_total_object_values(&objects->disk_sizes);
-	stats_table_size_addf(table, disk_object_total,
+	stats_table_size_addf(table, disk_object_total, 0,
 			      "  * %s", _("Disk size"));
 	stats_table_size_addf(table, objects->disk_sizes.commits,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_COMMIT_SIZE,
+				      objects->disk_sizes.commits),
 			      "    * %s", _("Commits"));
 	stats_table_size_addf(table, objects->disk_sizes.trees,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_TREE_SIZE,
+				      objects->disk_sizes.trees),
 			      "    * %s", _("Trees"));
 	stats_table_size_addf(table, objects->disk_sizes.blobs,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_BLOB_SIZE,
+				      objects->disk_sizes.blobs),
 			      "    * %s", _("Blobs"));
 	stats_table_size_addf(table, objects->disk_sizes.tags,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_TAG_SIZE,
+				      objects->disk_sizes.tags),
 			      "    * %s", _("Tags"));
 
 	stats_table_addf(table, "");
@@ -550,51 +714,71 @@ static void stats_table_setup_structure(struct stats_table *table,
 	stats_table_object_size_addf(table,
 				     &objects->largest.commit_size.oid,
 				     objects->largest.commit_size.value,
+				     repo_structure_concern_level(
+					     CONCERN_STAT_COMMIT_LARGEST,
+					     objects->largest.commit_size.value),
 				     "    * %s", _("Maximum size"));
 	stats_table_object_count_addf(table,
 				      &objects->largest.parent_count.oid,
 				      objects->largest.parent_count.value,
+				      0,
 				      "    * %s", _("Maximum parents"));
 	stats_table_addf(table, "  * %s", _("Trees"));
 	stats_table_object_size_addf(table,
 				     &objects->largest.tree_size.oid,
 				     objects->largest.tree_size.value,
+				      repo_structure_concern_level(
+					      CONCERN_STAT_TREE_LARGEST,
+					      objects->largest.tree_size.value),
 				     "    * %s", _("Maximum size"));
 	stats_table_object_count_addf(table,
 				      &objects->largest.tree_entries.oid,
 				      objects->largest.tree_entries.value,
+				      0,
 				      "    * %s", _("Maximum entries"));
 	stats_table_addf(table, "  * %s", _("Blobs"));
 	stats_table_object_size_addf(table,
 				     &objects->largest.blob_size.oid,
 				     objects->largest.blob_size.value,
+				     repo_structure_concern_level(
+					     CONCERN_STAT_BLOB_LARGEST,
+					     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,
+				     repo_structure_concern_level(
+					     CONCERN_STAT_TAG_LARGEST,
+					     objects->largest.tag_size.value),
 				     "    * %s", _("Maximum size"));
 }
 
 #define INDEX_WIDTH 4
 
-static void stats_table_print_structure(const struct stats_table *table)
+static void stats_table_print_structure(const struct stats_table *table,
+					bool show_concern)
 {
 	const char *name_col_title = _("Repository structure");
 	const char *value_col_title = _("Value");
+	const char *concern_col_title = _("Concern");
 	int title_name_width = utf8_strwidth(name_col_title);
 	int title_value_width = utf8_strwidth(value_col_title);
+	int title_concern_width = utf8_strwidth(concern_col_title);
 	int name_col_width = table->name_col_width;
 	int value_col_width = table->value_col_width;
 	int unit_col_width = table->unit_col_width;
+	int concern_col_width = table->concern_col_width;
 	struct string_list_item *item;
 	struct strbuf buf = STRBUF_INIT;
+	struct strbuf concern_str = STRBUF_INIT;
 
 	if (title_name_width > name_col_width)
 		name_col_width = title_name_width;
 	if (title_value_width > value_col_width + unit_col_width + 1)
 		value_col_width = title_value_width - unit_col_width;
-
+	if (show_concern && title_concern_width > concern_col_width)
+		concern_col_width = title_concern_width;
 	strbuf_addstr(&buf, "| ");
 	strbuf_utf8_align(&buf, ALIGN_LEFT, name_col_width + INDEX_WIDTH,
 			  name_col_title);
@@ -602,6 +786,12 @@ static void stats_table_print_structure(const struct stats_table *table)
 	strbuf_utf8_align(&buf, ALIGN_LEFT,
 			  value_col_width + unit_col_width + 1, value_col_title);
 	strbuf_addstr(&buf, " |");
+	if (show_concern) {
+		strbuf_addch(&buf, ' ');
+		strbuf_utf8_align(&buf, ALIGN_LEFT, concern_col_width,
+			  concern_col_title);
+		strbuf_addstr(&buf, " |");
+	}
 	printf("%s\n", buf.buf);
 
 	printf("| ");
@@ -610,17 +800,28 @@ static void stats_table_print_structure(const struct stats_table *table)
 	printf(" | ");
 	for (int i = 0; i < value_col_width + unit_col_width + 1; i++)
 		putchar('-');
-	printf(" |\n");
+	printf(" |");
+	if (show_concern) {
+		putchar(' ');
+		for (int i = 0; i < concern_col_width; i++) {
+			putchar('-');
+		}
+		printf(" |");
+	}
+	putchar('\n');
 
 	for_each_string_list_item(item, &table->rows) {
 		struct stats_table_entry *entry = item->util;
 		const char *value = "";
 		const char *unit = "";
+		strbuf_reset(&concern_str);
 
 		if (entry) {
 			value = entry->value;
 			if (entry->unit)
 				unit = entry->unit;
+			if (entry->concern > 0)
+				strbuf_addchars(&concern_str, '*', entry->concern);
 		}
 
 		strbuf_reset(&buf);
@@ -638,6 +839,11 @@ static void stats_table_print_structure(const struct stats_table *table)
 		strbuf_addch(&buf, ' ');
 		strbuf_utf8_align(&buf, ALIGN_LEFT, unit_col_width, unit);
 		strbuf_addstr(&buf, " |");
+		if (show_concern) {
+			strbuf_addch(&buf, ' ');
+			strbuf_utf8_align(&buf, ALIGN_LEFT, concern_col_width, concern_str.buf);
+			strbuf_addstr(&buf, " |");
+		}
 		printf("%s\n", buf.buf);
 	}
 
@@ -681,12 +887,15 @@ 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,
+				     bool show_concern)
 {
 	print_keyvalue("references.branches.count", key_delim,
 		       stats->refs.branches, value_delim);
+
 	print_keyvalue("references.tags.count", key_delim,
 		       stats->refs.tags, value_delim);
+
 	print_keyvalue("references.remotes.count", key_delim,
 		       stats->refs.remotes, value_delim);
 	print_keyvalue("references.others.count", key_delim,
@@ -694,39 +903,120 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 
 	print_keyvalue("objects.commits.count", key_delim,
 		       stats->objects.type_counts.commits, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.commits.count.concern_level", key_delim,
+			      repo_structure_concern_level(CONCERN_STAT_COMMIT_COUNT,
+							   stats->objects.type_counts.commits),
+			      value_delim);
 	print_keyvalue("objects.trees.count", key_delim,
 		       stats->objects.type_counts.trees, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.trees.count.concern_level", key_delim,
+			      repo_structure_concern_level(CONCERN_STAT_TREE_COUNT,
+							   stats->objects.type_counts.trees),
+			      value_delim);
 	print_keyvalue("objects.blobs.count", key_delim,
 		       stats->objects.type_counts.blobs, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.blobs.count.concern_level", key_delim,
+			      repo_structure_concern_level(CONCERN_STAT_BLOB_COUNT,
+							   stats->objects.type_counts.blobs),
+			      value_delim);
 	print_keyvalue("objects.tags.count", key_delim,
 		       stats->objects.type_counts.tags, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.tags.count.concern_level", key_delim,
+			      repo_structure_concern_level(CONCERN_STAT_TAG_COUNT,
+							   stats->objects.type_counts.tags),
+			      value_delim);
 
 	print_keyvalue("objects.commits.inflated_size", key_delim,
 		       stats->objects.inflated_sizes.commits, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.commits.inflated_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_COMMIT_INFLATED_SIZE,
+							    stats->objects.inflated_sizes.commits),
+			      value_delim);
 	print_keyvalue("objects.trees.inflated_size", key_delim,
 		       stats->objects.inflated_sizes.trees, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.trees.inflated_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TREE_INFLATED_SIZE,
+							    stats->objects.inflated_sizes.trees),
+			      value_delim);
+
 	print_keyvalue("objects.blobs.inflated_size", key_delim,
 		       stats->objects.inflated_sizes.blobs, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.blobs.inflated_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_BLOB_INFLATED_SIZE,
+							    stats->objects.inflated_sizes.blobs),
+			      value_delim);
 	print_keyvalue("objects.tags.inflated_size", key_delim,
 		       stats->objects.inflated_sizes.tags, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.tags.inflated_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TAG_INFLATED_SIZE,
+							    stats->objects.inflated_sizes.tags),
+			      value_delim);
 
 	print_keyvalue("objects.commits.disk_size", key_delim,
 		       stats->objects.disk_sizes.commits, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.commits.disk_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_COMMIT_SIZE,
+							    stats->objects.disk_sizes.commits),
+			      value_delim);
 	print_keyvalue("objects.trees.disk_size", key_delim,
 		       stats->objects.disk_sizes.trees, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.trees.disk_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TREE_SIZE,
+							    stats->objects.disk_sizes.trees),
+			      value_delim);
 	print_keyvalue("objects.blobs.disk_size", key_delim,
 		       stats->objects.disk_sizes.blobs, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.blobs.blob_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_BLOB_SIZE,
+							    stats->objects.disk_sizes.blobs),
+			      value_delim);
 	print_keyvalue("objects.tags.disk_size", key_delim,
 		       stats->objects.disk_sizes.tags, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.tags.disk_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TAG_SIZE,
+							    stats->objects.disk_sizes.tags),
+			      value_delim);
 
 	print_object_data("objects.commits.max_size", key_delim,
 			  &stats->objects.largest.commit_size, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.commits.max_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_COMMIT_LARGEST,
+							    stats->objects.largest.commit_size.value),
+			      value_delim);
 	print_object_data("objects.trees.max_size", key_delim,
 			  &stats->objects.largest.tree_size, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.trees.max_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TREE_LARGEST,
+							    stats->objects.largest.tree_size.value),
+			      value_delim);
 	print_object_data("objects.blobs.max_size", key_delim,
 			  &stats->objects.largest.blob_size, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.blobs.max_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_BLOB_LARGEST,
+							    stats->objects.largest.blob_size.value),
+			      value_delim);
 	print_object_data("objects.tags.max_size", key_delim,
 			  &stats->objects.largest.tag_size, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.tags.max_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TAG_LARGEST,
+							    stats->objects.largest.tag_size.value),
+			      value_delim);
 
 	print_object_data("objects.commits.max_parents", key_delim,
 			  &stats->objects.largest.parent_count, value_delim);
@@ -935,6 +1225,8 @@ 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 show_concern = 0;
+
 	struct option options[] = {
 		OPT_CALLBACK_F(0, "format", &format, N_("format"),
 			       N_("output format"),
@@ -944,6 +1236,7 @@ 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_BOOL(0, "concern", &show_concern, N_("show level of concern")),
 		OPT_END()
 	};
 
@@ -962,13 +1255,13 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
 	switch (format) {
 	case FORMAT_TABLE:
 		stats_table_setup_structure(&table, &stats);
-		stats_table_print_structure(&table);
+		stats_table_print_structure(&table, show_concern);
 		break;
 	case FORMAT_NEWLINE_TERMINATED:
-		structure_keyvalue_print(&stats, '=', '\n');
+		structure_keyvalue_print(&stats, '=', '\n', show_concern);
 		break;
 	case FORMAT_NUL_TERMINATED:
-		structure_keyvalue_print(&stats, '\n', '\0');
+		structure_keyvalue_print(&stats, '\n', '\0', show_concern);
 		break;
 	default:
 		BUG("invalid output format");
diff --git a/sideband.c b/sideband.c
index 374187402e..0e75ab2317 100644
--- a/sideband.c
+++ b/sideband.c
@@ -1,4 +1,4 @@
-#define USE_THE_REPOSITORY_VARIABLE
+define USE_THE_REPOSITORY_VARIABLE
 #define DISABLE_SIGN_COMPARE_WARNINGS
 
 #include "git-compat-util.h"
diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index 02cc2b594a..f1e3fd2a75 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -67,77 +67,7 @@ test_expect_success 'empty repository' '
 		EOF
 
 		git repo structure >out 2>err &&
-
-		test_cmp expect out &&
-		test_line_count = 0 err
-	)
-'
-
-test_expect_success SHA1 'repository with references and objects' '
-	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" &&
-
-		# 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            |      1     |
-		|     * Tags                |      1     |
-		|     * Remotes             |      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.03 MiB |
-		|     * Commits             | 217.92 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] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
-		[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
-		[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
-		[5] 97d808e45116bf02103490294d3d46dad7a2ac62
-		[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
-		EOF
-
-		git repo structure >out 2>err &&
+		cp out /tmp/test1-out &&
 
 		test_cmp expect out &&
 		test_line_count = 0 err
@@ -204,6 +134,59 @@ test_expect_success SHA1 'lines and nul format' '
 	)
 '
 
+test_expect_success SHA1 'lines format with concerns' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		# configure thresholds to get concern indications
+		git config repo.structure.concern.commit-count 7 &&
+		git config repo.structure.concern.commit-size 14 &&
+		git config repo.structure.concern.tree-size 10 &&
+		git config repo.structure.concern.blob-size 8 &&
+		git config repo.structure.concern.blob-inflated-size 10 &&
+		git config repo.structure.concern.tag-size 2 &&
+		git config repo.structure.concern.commit-largest 4 &&
+		git config repo.structure.concern.blob-largest 2 &&
+		touch configured &&
+		test_commit_bulk 1005 &&
+		git tag -a foo -m bar &&
+		oid="$(git rev-parse HEAD)" &&
+		git update-ref refs/remotes/origin/foo "$oid" &&
+
+		# Also creates a commit, tree, and blob.
+		git notes add -m foo &&
+
+
+		# 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 &&
+		objects.commits.count.concern_level=2
+		objects.trees.count.concern_level=0
+		objects.blobs.count.concern_level=0
+		objects.tags.count.concern_level=0
+		objects.commits.inflated_size.concern_level=0
+		objects.trees.inflated_size.concern_level=0
+		objects.blobs.inflated_size.concern_level=3
+		objects.tags.inflated_size.concern_level=0
+		objects.commits.disk_size.concern_level=3
+		objects.trees.disk_size.concern_level=8
+		objects.blobs.blob_size.concern_level=6
+		objects.tags.disk_size.concern_level=4
+		objects.commits.max_size.concern_level=3
+		objects.trees.max_size.concern_level=0
+		objects.blobs.max_size.concern_level=1
+		objects.tags.max_size.concern_level=0
+		EOF
+
+		git repo structure --concern --format=lines >actual 2>err &&
+		test_grep "concern_level" actual >actual-concerns &&
+		test_cmp expect actual-concerns &&
+		test_line_count = 0 err
+	)
+'
+
 test_expect_success 'progress meter option' '
 	test_when_finished "rm -rf repo" &&
 	git init repo &&

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH 2/3] repo: make concern thresholds configurable
  2026-10-21 13:55 [PATCH 0/3] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
  2026-10-21 13:55 ` [PATCH 1/3] repo: add level of concern reporting Mark C. Chu-Carroll
@ 2026-10-21 13:55 ` Mark C. Chu-Carroll
  2026-10-21 13:55 ` [PATCH 3/3] repo: add documentation for level-of-concern Mark C. Chu-Carroll
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 20+ messages in thread
From: Mark C. Chu-Carroll @ 2026-10-21 13:55 UTC (permalink / raw)
  To: git; +Cc: Mark C. Chu-Carroll

The concern reporting mechanism so far provides a
measure of concern based on heuristic values estimated
by analyzing a collection of (hopefully) representative
repositories. But for different projects/usecases, the
thresholds at which a value becomes concerning may vary.

Provide a mechanism using git config to allow users to
configure per-repo concern levels for "git repo structure"
concern reporting.
---
 builtin/repo.c            |  44 +++++++------
 sideband.c                |   2 +-
 t/t1901-repo-structure.sh | 155 +++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 178 insertions(+), 23 deletions(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index a450497d4e..aa6efdfd25 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -2,6 +2,7 @@
 
 #include "builtin.h"
 #include "commit.h"
+#include "config.h"
 #include "environment.h"
 #include "hash.h"
 #include "hex.h"
@@ -386,25 +387,27 @@ enum concern_stat {
 	CONCERN_STAT_TREE_INFLATED_SIZE,
 	CONCERN_STAT_BLOB_INFLATED_SIZE,
 	CONCERN_STAT_TAG_INFLATED_SIZE,
+	CONCERN_STAT_MAXIMUM
 };
 
-struct concern_stat_threshold repo_structure_concern_thresholds[] = {
-	{ .name = "commit_count", .threshold = 19 },
-	{.name = "tree_count", .threshold = 21 },
-	{ .name = "blob_count", .threshold = 22 },
-	{ .name = "tag_count", .threshold = 15 },
-	{ .name = "commit_size", .threshold = 28 },
-	{ .name = "tree_size", .threshold = 27 },
-	{ .name = "blob_size", .threshold = 36 },
-	{ .name = "tags_size", .threshold = 18 },
-	{ .name = "commit_largest", .threshold = 18  },
-	{ .name = "tree_largest", .threshold = 18 },
-	{ .name = "blob_largest", .threshold = 26 },
-	{ .name = "tag_largest", .threshold =  15 },
-	{ .name = "commit_inflated_size", .threshold = 28 },
-	{ .name = "tree_inflated_size", .threshold = 32 },
-	{ .name = "blob_inflated_size", .threshold = 34 },
-	{ .name = "tag_inflated_size", .threshold = 25}
+struct concern_stat_threshold concern_stat_thresholds[] = {
+	{ .name = "commit-count", .threshold = 19 },
+	{.name = "tree-count", .threshold = 21 },
+	{ .name = "blob-count", .threshold = 22 },
+	{ .name = "tag-count", .threshold = 15 },
+	{ .name = "commit-size", .threshold = 28 },
+	{ .name = "tree-size", .threshold = 27 },
+	{ .name = "blob-size", .threshold = 36 },
+	{ .name = "tag-size", .threshold = 18 },
+	{ .name = "commit-largest", .threshold = 18  },
+	{ .name = "tree-largest", .threshold = 18 },
+	{ .name = "blob-largest", .threshold = 26 },
+	{ .name = "tag-largest", .threshold =  15 },
+	{ .name = "commit-inflated-size", .threshold = 28 },
+	{ .name = "tree-inflated-size", .threshold = 32 },
+	{ .name = "blob-inflated-size", .threshold = 34 },
+	{ .name = "tag-inflated-size", .threshold = 25},
+	{ 0 }
 };
 
 /*
@@ -450,7 +453,7 @@ static size_t approx_log2(long value) {
 static size_t repo_structure_concern_level(enum concern_stat stat_id,
 					   size_t value) {
 	size_t log_value;
-	if (stat_id >= ARRAY_SIZE(concern_stat_thresholds))
+	if (stat_id >= CONCERN_STAT_MAXIMUM)
 		BUG("unexpected repository stat type");
 	log_value = approx_log2(value);
 	if (log_value > concern_stat_thresholds[stat_id].threshold)
@@ -977,7 +980,7 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	print_keyvalue("objects.blobs.disk_size", key_delim,
 		       stats->objects.disk_sizes.blobs, value_delim);
 	if (show_concern)
-		print_keyvalue("objects.blobs.blob_size.concern_level", key_delim,
+		print_keyvalue("objects.blobs.disk_size.concern_level", key_delim,
 			       repo_structure_concern_level(CONCERN_STAT_BLOB_SIZE,
 							    stats->objects.disk_sizes.blobs),
 			      value_delim);
@@ -1243,7 +1246,8 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
 	argc = parse_options(argc, argv, prefix, options, repo_structure_usage, 0);
 	if (argc)
 		usage(_("too many arguments"));
-
+	if (show_concern)
+		repo_config(repo, &config_concern_thresholds, NULL);
 	repo_init_revisions(repo, &revs, prefix);
 
 	if (show_progress < 0)
diff --git a/sideband.c b/sideband.c
index 0e75ab2317..374187402e 100644
--- a/sideband.c
+++ b/sideband.c
@@ -1,4 +1,4 @@
-define USE_THE_REPOSITORY_VARIABLE
+#define USE_THE_REPOSITORY_VARIABLE
 #define DISABLE_SIGN_COMPARE_WARNINGS
 
 #include "git-compat-util.h"
diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index f1e3fd2a75..49ecc081ad 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -67,13 +67,164 @@ test_expect_success 'empty repository' '
 		EOF
 
 		git repo structure >out 2>err &&
-		cp out /tmp/test1-out &&
 
 		test_cmp expect out &&
 		test_line_count = 0 err
 	)
 '
 
+test_expect_success SHA1 'repository with references and objects' '
+	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" &&
+
+		# 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            |      1     |
+		|     * Tags                |      1     |
+		|     * Remotes             |      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.03 MiB |
+		|     * Commits             | 217.92 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] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
+		[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[5] 97d808e45116bf02103490294d3d46dad7a2ac62
+		[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
+		EOF
+
+		git repo structure >out 2>err &&
+
+		test_cmp expect out &&
+		test_line_count = 0 err
+	)
+'
+
+test_expect_success SHA1 'concern flagging' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		# configure thresholds to get concern indications
+		git config repo.structure.concern.commit-count 7 &&
+		git config repo.structure.concern.commit-size 14 &&
+		git config repo.structure.concern.tree-size 10 &&
+		git config repo.structure.concern.blob-size 8 &&
+		git config repo.structure.concern.blob-inflated-size 10 &&
+		git config repo.structure.concern.tag-size 2 &&
+		git config repo.structure.concern.commit-largest 4 &&
+		git config repo.structure.concern.blob-largest 2 &&
+		touch configured &&
+		test_commit_bulk 1005 &&
+		git tag -a foo -m bar &&
+		oid="$(git rev-parse HEAD)" &&
+		git update-ref refs/remotes/origin/foo "$oid" &&
+
+		# Also creates a commit, tree, and blob.
+		git notes add -m foo &&
+
+
+		# 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      | Concern  |
+		| ------------------------- | ---------- | -------- |
+		| * References              |            |          |
+		|   * Count                 |      4     |          |
+		|     * Branches            |      1     |          |
+		|     * Tags                |      1     |          |
+		|     * Remotes             |      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.03 MiB |          |
+		|     * Commits             | 217.92 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] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
+		[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[5] 97d808e45116bf02103490294d3d46dad7a2ac62
+		[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
+		EOF
+
+		git repo structure --concern >actual 2>err &&
+
+		test_cmp expect actual &&
+		test_line_count = 0 err
+	)
+'
+
 test_expect_success SHA1 'lines and nul format' '
 	test_when_finished "rm -rf repo" &&
 	git init repo &&
@@ -172,7 +323,7 @@ test_expect_success SHA1 'lines format with concerns' '
 		objects.tags.inflated_size.concern_level=0
 		objects.commits.disk_size.concern_level=3
 		objects.trees.disk_size.concern_level=8
-		objects.blobs.blob_size.concern_level=6
+		objects.blobs.disk_size.concern_level=6
 		objects.tags.disk_size.concern_level=4
 		objects.commits.max_size.concern_level=3
 		objects.trees.max_size.concern_level=0

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH 3/3] repo: add documentation for level-of-concern.
  2026-10-21 13:55 [PATCH 0/3] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
  2026-10-21 13:55 ` [PATCH 1/3] repo: add level of concern reporting Mark C. Chu-Carroll
  2026-10-21 13:55 ` [PATCH 2/3] repo: make concern thresholds configurable Mark C. Chu-Carroll
@ 2026-10-21 13:55 ` Mark C. Chu-Carroll
  2026-10-26 17:43 ` [PATCH v2 0/3] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 20+ messages in thread
From: Mark C. Chu-Carroll @ 2026-10-21 13:55 UTC (permalink / raw)
  To: git; +Cc: Mark C. Chu-Carroll

---
 Documentation/config/repo.adoc | 60 ++++++++++++++++++++++++++++++++++++++++++
 Documentation/git-repo.adoc    | 27 +++++++++++++++++--
 2 files changed, 85 insertions(+), 2 deletions(-)

diff --git a/Documentation/config/repo.adoc b/Documentation/config/repo.adoc
new file mode 100644
index 0000000000..ad16bdcc67
--- /dev/null
+++ b/Documentation/config/repo.adoc
@@ -0,0 +1,60 @@
+`repo.structure.concern.commit-count`::
+	Sets the value of the concern threshold for commit count.
+
+`repo.structure.concern.tree-count`::
+	Sets the value of the concern threshold for tree count.
+
+`repo.structure.concern.blob-count`::
+	Sets the value of the concern threshold for blob count.
+
+`repo.structure.concern.tag-count`::
+	Sets the value of the concern threshold for tag count.
+
+`repo.structure.concern.commit-size`::
+	Sets the value of the concern threshold for total commit size.
+
+`repo.structure.concern.tree-size`::
+	Sets the value of the concern threshold for total tree size.
+
+`repo.structure.concern.blob-size`::
+	Sets the value of the concern threshold for total blob size.
+
+`repo.structure.concern.tag-size`::
+	Sets the value of the concern threshold for total tag size.
+
+`repo.structure.concern.commit-inflated-size`::
+	Sets the value of the concern threshold for total size
+	of all inflated commits.
+
+`repo.structure.concern.tree-inflated-size`::
+	Sets the value of the concern threshold for total size of
+	all inflated trees.
+
+`repo.structure.concern.blob-inflated-size`::
+	Sets the value of the concern threshold for total size of
+	all inflated blobs.
+
+`repo.structure.concern.tag-inflated-size`::
+	Sets the value of the concern threshold for the total size
+	of all inflated tags.
+
+`repo.structure.concern.commit-largest`::
+	Sets the value of the concern threshold the size of the largest
+	commit.
+
+`repo.structure.concern.tree-largest`::
+	Sets the value of the concern threshold for the size of the
+	largest tree.
+
+`repo.structure.concern.blob-largest`::
+	Sets the value of the concern threshold for the size of the
+	largest blob.
+
+`repo.structure.concern.tag-largest`::
+	Sets the value of the concern threshold for the size of the
+	largest tag.
++
+All of the concern thresholds are integer values for the 
+floor of the base-2 log of the threshold value. The default values
+of each threshold are set to heuristically determined values.
+
diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index ed7d80c690..3c1045eb40 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-]concern]
 
 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-]concern]`::
 	Retrieve statistics about the current repository structure. The
 	following kinds of information are reported:
 +
@@ -88,6 +88,20 @@ supported:
 	quoted.
 +
 `-z` is an alias for `--format=nul`.
++
+To make it easier to know when one of the values is something that
+users should be concerned about, adding '--concern' will provide a
+level of concern to be added to the output. In 'table' mode, this will
+be a table column indicating the level of concern as a number of stars;
+in 'lines` or `nul' mode, a `concern_level` line will be added for each
+statistic with a numeric value.
++
+The level of corcern for each statistic is computed using the base_2 log of the
+statistic value. The value is considered concerning if it exceeds a heuristically
+chosen threshold value; the degree of concern is the difference between the
+base_2 log of the value, and the base_2 log of the corresponding threshold.
++
+The threshold values are configurable; see the configuration section below.
 
 INFO KEYS
 ---------
@@ -141,6 +155,15 @@ using the `nul` format:
 git repo info --format=nul layout.bare layout.shallow
 ------------
 
+CONFIGURATION
+-------------
+
+include::includes/cmd-config-section-all.adoc[]
+
+:git-repo: 1
+include::config/repo.adoc[]
+
+
 SEE ALSO
 --------
 linkgit:git-rev-parse[1]

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v2 0/3] repo: add degree-of-concern reporting to structure
  2026-10-21 13:55 [PATCH 0/3] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
                   ` (2 preceding siblings ...)
  2026-10-21 13:55 ` [PATCH 3/3] repo: add documentation for level-of-concern Mark C. Chu-Carroll
@ 2026-10-26 17:43 ` Mark C. Chu-Carroll
  2026-10-26 17:43   ` [PATCH v2 1/3] repo: add level of concern reporting Mark C. Chu-Carroll
                     ` (2 more replies)
  2026-10-27 15:39 ` [PATCH v3 0/2] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
  2026-10-29 17:52 ` [PATCH v4 0/2] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
  5 siblings, 3 replies; 20+ messages in thread
From: Mark C. Chu-Carroll @ 2026-10-26 17:43 UTC (permalink / raw)
  To: git; +Cc: Mark C. Chu-Carroll

"git repo structure" computes a set of statistics that are useful for
diagnosing performance issues in a git repository. But for a typical
user who hasn't encountered any issues yet, it can be difficult to
interpret the data - when should a value be considered concerning?

This patchset introduces a feature to "git repo structure" that reports
on how concerning certain measures of storage structure  are for a
repository.  In table mode, concern is displayed as a column containing
a number of stars indicating the degree of concern; in line or nul mode,
concerns are displayed as additionaL lines with a numeric value.

To select defaults for the thresholds where a value becomes concern,
a (hopefully) representative selection of repositories were analyzed.
Based on empirical exploration of the data, the measurement that seemed
closest to accurately assessing when a repository's structure becomes
concerning was based on the base 2 logarithm of the value. Across all of
the sample repositories, for all of the measurements, the point at which
things become problematic was when a base 2 logarithm of a value was
more than 1 standard deviation from the mean of the log_2 values in
the sample set.  Based on this, the concern threshold was set for each
of the measures.

While this seems like a reasonable approach for setting default
thresholds, it's obviously still very ad-hoc. It's likely that over
time, the default values will evolve; and in the short term, projects
may want to set their own thresholds. So this patchset includes support
for configuring the thresholds on a per-reposiry basis.

Overview of the change:

* The first commit adds the command-line option "--concern", and creates the basic implementation of concern reporting.
* The next commit adds configurability for the concern thresholds.
* The final commit update the documentation for the command and
  configuration options.

Signed-off-by: Mark C. Chu-Carroll <markchucarroll@fastmail.com>
---
Changes in v2:
- Fix memory leak in concern string generation.
- Fix CI/CD test issue involving zlib versions.
- Updating the naming of the config value prefix.
- Link to v1: https://patch.msgid.link/20261021-b4-repo-structure-concern-v1-0-e3010552292c@fastmail.com

---
Mark C. Chu-Carroll (3):
      repo: add level of concern reporting
      repo: make concern thresholds configurable
      repo: add documentation for level-of-concern.

 Documentation/config/repo.adoc |  59 +++++++
 Documentation/git-repo.adoc    |  27 +++-
 builtin/repo.c                 | 343 ++++++++++++++++++++++++++++++++++++++---
 t/t1901-repo-structure.sh      | 137 ++++++++++++++++
 4 files changed, 543 insertions(+), 23 deletions(-)

Range-diff versus v1:

1:  c2b2a79967 = 1:  e8971a4050 repo: add level of concern reporting
2:  67d77cfcd9 ! 2:  7a78eee517 repo: make concern thresholds configurable
    @@ builtin/repo.c
      #include "environment.h"
      #include "hash.h"
      #include "hex.h"
    +@@
    + #define REPO_STRUCTURE_USAGE \
    + 	"git repo structure [--format=(table|lines|nul) | -z] [--[no-]concern]"
    + 
    ++#define REPO_STRUCTURE_CONFIG_PREFIX "repo.concern."
    ++
    + static const char *const repo_usage[] = {
    + 	REPO_INFO_USAGE,
    + 	REPO_STRUCTURE_USAGE,
     @@ builtin/repo.c: enum concern_stat {
      	CONCERN_STAT_TREE_INFLATED_SIZE,
      	CONCERN_STAT_BLOB_INFLATED_SIZE,
    @@ builtin/repo.c: enum concern_stat {
     -	{ .name = "tag_inflated_size", .threshold = 25}
     +struct concern_stat_threshold concern_stat_thresholds[] = {
     +	{ .name = "commit-count", .threshold = 19 },
    -+	{.name = "tree-count", .threshold = 21 },
    ++	{ .name = "tree-count", .threshold = 21 },
     +	{ .name = "blob-count", .threshold = 22 },
     +	{ .name = "tag-count", .threshold = 15 },
     +	{ .name = "commit-size", .threshold = 28 },
    @@ builtin/repo.c: enum concern_stat {
      };
      
      /*
    +  * Concern thresholds aren't universal. We try to set reasonable default
    +  * values, but projects may want to set these thresholds differently. To
    +  * support that, they can set the thresholds for their project in the
    +- * project's git config, in the section "repo.structure.concern".
    ++ * project's git config, in the section "repo.concern".
    +   */
    + static int config_concern_thresholds(const char *prefixed_var,
    + 				     const char *str_val,
    + 				     const struct config_context *ctx,
    + 				     void *cb UNUSED) {
    +-	const char *config_prefix = "repo.structure.concern.";
    +-	if (!strncmp(config_prefix, prefixed_var, strlen(config_prefix))) {
    +-		const char *concern_name = &prefixed_var[strlen(config_prefix)];
    ++	if (!strncmp(REPO_STRUCTURE_CONFIG_PREFIX, prefixed_var,
    ++	      strlen(REPO_STRUCTURE_CONFIG_PREFIX))) {
    ++		const char *concern_name =
    ++			&prefixed_var[strlen(REPO_STRUCTURE_CONFIG_PREFIX)];
    + 		for (int i = 0; concern_stat_thresholds[i].name != 0; i++) {
    + 			if (!strcmp(concern_stat_thresholds[i].name, concern_name)) {
    + 				int value = git_config_int(prefixed_var, str_val, ctx->kvi);
    +@@ builtin/repo.c: static int config_concern_thresholds(const char *prefixed_var,
    + 	return 0;
    + }
    + 
    +-static size_t approx_log2(long value) {
    ++static size_t approx_log2(size_t value) {
    + 	int position = 0;
    + 	if (value <= 0) {
    + 		return 0;
     @@ builtin/repo.c: static size_t approx_log2(long value) {
      static size_t repo_structure_concern_level(enum concern_stat stat_id,
      					   size_t value) {
    @@ builtin/repo.c: static size_t approx_log2(long value) {
      		BUG("unexpected repository stat type");
      	log_value = approx_log2(value);
      	if (log_value > concern_stat_thresholds[stat_id].threshold)
    +@@ builtin/repo.c: static void stats_table_print_structure(const struct stats_table *table,
    + 			strbuf_utf8_align(&buf, ALIGN_LEFT, concern_col_width, concern_str.buf);
    + 			strbuf_addstr(&buf, " |");
    + 		}
    ++		strbuf_release(&concern_str);
    + 		printf("%s\n", buf.buf);
    + 	}
    + 
     @@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stats,
      	print_keyvalue("objects.blobs.disk_size", key_delim,
      		       stats->objects.disk_sizes.blobs, value_delim);
    @@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stat
      			       repo_structure_concern_level(CONCERN_STAT_BLOB_SIZE,
      							    stats->objects.disk_sizes.blobs),
      			      value_delim);
    +@@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stats,
    + 							    stats->objects.largest.commit_size.value),
    + 			      value_delim);
    + 	print_object_data("objects.trees.max_size", key_delim,
    +-			  &stats->objects.largest.tree_size, value_delim);
    ++			 &stats->objects.largest.tree_size, value_delim);
    + 	if (show_concern)
    + 		print_keyvalue("objects.trees.max_size.concern_level", key_delim,
    + 			       repo_structure_concern_level(CONCERN_STAT_TREE_LARGEST,
    +@@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stats,
    + 							    stats->objects.largest.blob_size.value),
    + 			      value_delim);
    + 	print_object_data("objects.tags.max_size", key_delim,
    +-			  &stats->objects.largest.tag_size, value_delim);
    ++			   &stats->objects.largest.tag_size, value_delim);
    + 	if (show_concern)
    + 		print_keyvalue("objects.tags.max_size.concern_level", key_delim,
    + 			       repo_structure_concern_level(CONCERN_STAT_TAG_LARGEST,
     @@ builtin/repo.c: static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
      	argc = parse_options(argc, argv, prefix, options, repo_structure_usage, 0);
      	if (argc)
    @@ t/t1901-repo-structure.sh: test_expect_success 'empty repository' '
     +	git init repo &&
     +	(
     +		cd repo &&
    ++		# differences between zlib and zlib-ng can cause this
    ++		# test to fail; so we just turn it off.
    ++		git config core.compression 0 &&
     +		# configure thresholds to get concern indications
    -+		git config repo.structure.concern.commit-count 7 &&
    -+		git config repo.structure.concern.commit-size 14 &&
    -+		git config repo.structure.concern.tree-size 10 &&
    -+		git config repo.structure.concern.blob-size 8 &&
    -+		git config repo.structure.concern.blob-inflated-size 10 &&
    -+		git config repo.structure.concern.tag-size 2 &&
    -+		git config repo.structure.concern.commit-largest 4 &&
    -+		git config repo.structure.concern.blob-largest 2 &&
    -+		touch configured &&
    ++		git config repo.concern.commit-count 7 &&
    ++		git config repo.concern.commit-size 14 &&
    ++		git config repo.concern.tree-size 10 &&
    ++		git config repo.concern.blob-size 8 &&
    ++		git config repo.concern.blob-inflated-size 10 &&
    ++		git config repo.concern.tag-size 2 &&
    ++		git config repo.concern.commit-largest 4 &&
    ++		git config repo.concern.blob-largest 2 &&
     +		test_commit_bulk 1005 &&
     +		git tag -a foo -m bar &&
     +		oid="$(git rev-parse HEAD)" &&
    @@ t/t1901-repo-structure.sh: test_expect_success 'empty repository' '
     +		|     * 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   | ****     |
    ++		|     * Tags                |    $(object_type_disk_usage tag) B   | *****    |
     +		|                           |            |          |
     +		| * Largest objects         |            |          |
     +		|   * Commits               |            |          |
    @@ t/t1901-repo-structure.sh: test_expect_success 'empty repository' '
      test_expect_success SHA1 'lines and nul format' '
      	test_when_finished "rm -rf repo" &&
      	git init repo &&
    +@@ t/t1901-repo-structure.sh: test_expect_success SHA1 'lines format with concerns' '
    + 	git init repo &&
    + 	(
    + 		cd repo &&
    ++		# differences between zlib and zlib-ng can cause this
    ++		# test to fail; so we just turn it off.
    ++		git config core.compression 0 &&
    + 		# configure thresholds to get concern indications
    +-		git config repo.structure.concern.commit-count 7 &&
    +-		git config repo.structure.concern.commit-size 14 &&
    +-		git config repo.structure.concern.tree-size 10 &&
    +-		git config repo.structure.concern.blob-size 8 &&
    +-		git config repo.structure.concern.blob-inflated-size 10 &&
    +-		git config repo.structure.concern.tag-size 2 &&
    +-		git config repo.structure.concern.commit-largest 4 &&
    +-		git config repo.structure.concern.blob-largest 2 &&
    +-		touch configured &&
    ++		git config repo.concern.commit-count 7 &&
    ++		git config repo.concern.commit-size 14 &&
    ++		git config repo.concern.tree-size 10 &&
    ++		git config repo.concern.blob-size 8 &&
    ++		git config repo.concern.blob-inflated-size 10 &&
    ++		git config repo.concern.tag-size 2 &&
    ++		git config repo.concern.commit-largest 4 &&
    ++		git config repo.concern.blob-largest 2 &&
    + 		test_commit_bulk 1005 &&
    + 		git tag -a foo -m bar &&
    + 		oid="$(git rev-parse HEAD)" &&
    +@@ t/t1901-repo-structure.sh: test_expect_success SHA1 'lines format with concerns' '
    + 		# 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".
     @@ t/t1901-repo-structure.sh: test_expect_success SHA1 'lines format with concerns' '
      		objects.tags.inflated_size.concern_level=0
      		objects.commits.disk_size.concern_level=3
      		objects.trees.disk_size.concern_level=8
     -		objects.blobs.blob_size.concern_level=6
    +-		objects.tags.disk_size.concern_level=4
     +		objects.blobs.disk_size.concern_level=6
    - 		objects.tags.disk_size.concern_level=4
    ++		objects.tags.disk_size.concern_level=5
      		objects.commits.max_size.concern_level=3
      		objects.trees.max_size.concern_level=0
    + 		objects.blobs.max_size.concern_level=1
3:  ac0adb7c9f ! 3:  cc78eaeca0 repo: add documentation for level-of-concern.
    @@ Commit message
     
      ## Documentation/config/repo.adoc (new) ##
     @@
    -+`repo.structure.concern.commit-count`::
    ++`repo.concern.commit-count`::
     +	Sets the value of the concern threshold for commit count.
     +
    -+`repo.structure.concern.tree-count`::
    ++`repo.concern.tree-count`::
     +	Sets the value of the concern threshold for tree count.
     +
    -+`repo.structure.concern.blob-count`::
    ++`repo.concern.blob-count`::
     +	Sets the value of the concern threshold for blob count.
     +
    -+`repo.structure.concern.tag-count`::
    ++`repo.concern.tag-count`::
     +	Sets the value of the concern threshold for tag count.
     +
    -+`repo.structure.concern.commit-size`::
    ++`repo.concern.commit-size`::
     +	Sets the value of the concern threshold for total commit size.
     +
    -+`repo.structure.concern.tree-size`::
    ++`repo.concern.tree-size`::
     +	Sets the value of the concern threshold for total tree size.
     +
    -+`repo.structure.concern.blob-size`::
    ++`repo.concern.blob-size`::
     +	Sets the value of the concern threshold for total blob size.
     +
    -+`repo.structure.concern.tag-size`::
    ++`repo.concern.tag-size`::
     +	Sets the value of the concern threshold for total tag size.
     +
    -+`repo.structure.concern.commit-inflated-size`::
    ++`repo.concern.commit-inflated-size`::
     +	Sets the value of the concern threshold for total size
     +	of all inflated commits.
     +
    -+`repo.structure.concern.tree-inflated-size`::
    ++`repo.concern.tree-inflated-size`::
     +	Sets the value of the concern threshold for total size of
     +	all inflated trees.
     +
    -+`repo.structure.concern.blob-inflated-size`::
    ++`repo.concern.blob-inflated-size`::
     +	Sets the value of the concern threshold for total size of
     +	all inflated blobs.
     +
    -+`repo.structure.concern.tag-inflated-size`::
    ++`repo.concern.tag-inflated-size`::
     +	Sets the value of the concern threshold for the total size
     +	of all inflated tags.
     +
    -+`repo.structure.concern.commit-largest`::
    ++`repo.concern.commit-largest`::
     +	Sets the value of the concern threshold the size of the largest
     +	commit.
     +
    -+`repo.structure.concern.tree-largest`::
    ++`repo.concern.tree-largest`::
     +	Sets the value of the concern threshold for the size of the
     +	largest tree.
     +
    -+`repo.structure.concern.blob-largest`::
    ++`repo.concern.blob-largest`::
     +	Sets the value of the concern threshold for the size of the
     +	largest blob.
     +
    -+`repo.structure.concern.tag-largest`::
    ++`repo.concern.tag-largest`::
     +	Sets the value of the concern threshold for the size of the
     +	largest tag.
    -++
    -+All of the concern thresholds are integer values for the 
    ++
    ++All of the concern thresholds are integer values for the
     +floor of the base-2 log of the threshold value. The default values
     +of each threshold are set to heuristically determined values.
    -+
     
      ## Documentation/git-repo.adoc ##
     @@ Documentation/git-repo.adoc: SYNOPSIS
    @@ Documentation/git-repo.adoc: supported:
     +users should be concerned about, adding '--concern' will provide a
     +level of concern to be added to the output. In 'table' mode, this will
     +be a table column indicating the level of concern as a number of stars;
    -+in 'lines` or `nul' mode, a `concern_level` line will be added for each
    ++in `lines` or `nul` mode, a `concern_level` line will be added for each
     +statistic with a numeric value.
     ++
    -+The level of corcern for each statistic is computed using the base_2 log of the
    ++The level of concern for each statistic is computed using the log_2 of the
     +statistic value. The value is considered concerning if it exceeds a heuristically
     +chosen threshold value; the degree of concern is the difference between the
    -+base_2 log of the value, and the base_2 log of the corresponding threshold.
    ++log_2 of the value, and the log_2 of the corresponding threshold.
     ++
     +The threshold values are configurable; see the configuration section below.
      

---
base-commit: 3bc0341126508f78f5869cbfc0005e987efdf0c7
change-id: 20261019-b4-repo-structure-concern-f1d6f6326095


^ permalink raw reply	[flat|nested] 20+ messages in thread

* [PATCH v2 1/3] repo: add level of concern reporting
  2026-10-26 17:43 ` [PATCH v2 0/3] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
@ 2026-10-26 17:43   ` Mark C. Chu-Carroll
  2026-10-27 14:55     ` Patrick Steinhardt
  2026-10-26 17:43   ` [PATCH v2 2/3] repo: make concern thresholds configurable Mark C. Chu-Carroll
  2026-10-26 17:43   ` [PATCH v2 3/3] repo: add documentation for level-of-concern Mark C. Chu-Carroll
  2 siblings, 1 reply; 20+ messages in thread
From: Mark C. Chu-Carroll @ 2026-10-26 17:43 UTC (permalink / raw)
  To: git; +Cc: Mark C. Chu-Carroll

"git repo structure" computes a set of statistics
that are useful for diagnosing performance issues in
a git repository. But for a typical user who hasn't
encountered any issues yet, it can be difficult to
interpret the data - when should a value be considered
concerning?

Add an option to "git repo structure" that adds a "degree
of concern" column to the structure report, which indicates
when a stat values has reached a point where it might be
concerning, and indicate how severe the concern should be.
---
 builtin/repo.c            | 329 +++++++++++++++++++++++++++++++++++++++++++---
 sideband.c                |   2 +-
 t/t1901-repo-structure.sh | 125 ++++++++----------
 3 files changed, 366 insertions(+), 90 deletions(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index 84e012f83f..a450497d4e 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] [--[no-]concern]"
 
 static const char *const repo_usage[] = {
 	REPO_INFO_USAGE,
@@ -348,8 +348,118 @@ struct stats_table {
 	int name_col_width;
 	int value_col_width;
 	int unit_col_width;
+	int concern_col_width;
 };
 
+/*
+ * For computing level of concern about a stat, we need to have some
+ * baseline values. For each of these, we'define a minimum level
+ * of concern, which will appear as 1 star. Then we scale exponentially:
+ * each time that value doubles, we add another star.
+ *
+ * The threshold values for concern are expressed as log_2 values of the size
+ * at which the statistic becomes concerning. The default values for the
+ * threshold were selected based on observed values from a collection of
+ * representative-ish repositories of varying size, by finding the mean log_2
+ * value and standard deviation (sigma) for each statistic over the collection.
+ * The threshold for each metric is then set at mean(log_2) + sigma.
+ */
+struct concern_stat_threshold {
+	const char *name;
+	size_t threshold;
+};
+
+enum concern_stat {
+	CONCERN_STAT_COMMIT_COUNT,
+	CONCERN_STAT_TREE_COUNT,
+	CONCERN_STAT_BLOB_COUNT,
+	CONCERN_STAT_TAG_COUNT,
+	CONCERN_STAT_COMMIT_SIZE,
+	CONCERN_STAT_TREE_SIZE,
+	CONCERN_STAT_BLOB_SIZE,
+	CONCERN_STAT_TAG_SIZE,
+	CONCERN_STAT_COMMIT_LARGEST,
+	CONCERN_STAT_TREE_LARGEST,
+	CONCERN_STAT_BLOB_LARGEST,
+	CONCERN_STAT_TAG_LARGEST,
+	CONCERN_STAT_COMMIT_INFLATED_SIZE,
+	CONCERN_STAT_TREE_INFLATED_SIZE,
+	CONCERN_STAT_BLOB_INFLATED_SIZE,
+	CONCERN_STAT_TAG_INFLATED_SIZE,
+};
+
+struct concern_stat_threshold repo_structure_concern_thresholds[] = {
+	{ .name = "commit_count", .threshold = 19 },
+	{.name = "tree_count", .threshold = 21 },
+	{ .name = "blob_count", .threshold = 22 },
+	{ .name = "tag_count", .threshold = 15 },
+	{ .name = "commit_size", .threshold = 28 },
+	{ .name = "tree_size", .threshold = 27 },
+	{ .name = "blob_size", .threshold = 36 },
+	{ .name = "tags_size", .threshold = 18 },
+	{ .name = "commit_largest", .threshold = 18  },
+	{ .name = "tree_largest", .threshold = 18 },
+	{ .name = "blob_largest", .threshold = 26 },
+	{ .name = "tag_largest", .threshold =  15 },
+	{ .name = "commit_inflated_size", .threshold = 28 },
+	{ .name = "tree_inflated_size", .threshold = 32 },
+	{ .name = "blob_inflated_size", .threshold = 34 },
+	{ .name = "tag_inflated_size", .threshold = 25}
+};
+
+/*
+ * Concern thresholds aren't universal. We try to set reasonable default
+ * values, but projects may want to set these thresholds differently. To
+ * support that, they can set the thresholds for their project in the
+ * project's git config, in the section "repo.structure.concern".
+  */
+static int config_concern_thresholds(const char *prefixed_var,
+				     const char *str_val,
+				     const struct config_context *ctx,
+				     void *cb UNUSED) {
+	const char *config_prefix = "repo.structure.concern.";
+	if (!strncmp(config_prefix, prefixed_var, strlen(config_prefix))) {
+		const char *concern_name = &prefixed_var[strlen(config_prefix)];
+		for (int i = 0; concern_stat_thresholds[i].name != 0; i++) {
+			if (!strcmp(concern_stat_thresholds[i].name, concern_name)) {
+				int value = git_config_int(prefixed_var, str_val, ctx->kvi);
+				if (value < 0)
+					return -1;
+				concern_stat_thresholds[i].threshold = value;
+				return 0;
+			}
+		}
+		warning(_("Invalid concern threshold name: '%s'"), concern_name);
+		return 0;
+	}
+	return 0;
+}
+
+static size_t approx_log2(long value) {
+	int position = 0;
+	if (value <= 0) {
+		return 0;
+	}
+	while (value > 1) {
+		position++;
+		value >>= 1;
+	}
+	return position;
+}
+
+static size_t repo_structure_concern_level(enum concern_stat stat_id,
+					   size_t value) {
+	size_t log_value;
+	if (stat_id >= ARRAY_SIZE(concern_stat_thresholds))
+		BUG("unexpected repository stat type");
+	log_value = approx_log2(value);
+	if (log_value > concern_stat_thresholds[stat_id].threshold)
+		return log_value -
+			concern_stat_thresholds[stat_id].threshold;
+	else
+		return 0;
+}
+
 /*
  * Holds column data that gets stored for each row.
  */
@@ -358,6 +468,7 @@ struct stats_table_entry {
 	const char *unit;
 	size_t index;
 	struct object_id *oid;
+	size_t concern;
 };
 
 static void stats_table_vaddf(struct stats_table *table,
@@ -391,6 +502,8 @@ static void stats_table_vaddf(struct stats_table *table,
 		if (value_width > table->value_col_width)
 			table->value_col_width = value_width;
 	}
+	if ((int)entry->concern > table->concern_col_width)
+		table->concern_col_width = entry->concern;
 	if (entry->unit) {
 		int unit_width = utf8_strwidth(entry->unit);
 		if (unit_width > table->unit_col_width)
@@ -410,13 +523,14 @@ static void stats_table_addf(struct stats_table *table, const char *format, ...)
 }
 
 static void stats_table_count_addf(struct stats_table *table, size_t value,
-				   const char *format, ...)
+				   size_t concern, const char *format, ...)
 {
 	struct stats_table_entry *entry;
 	va_list ap;
 
 	CALLOC_ARRAY(entry, 1);
 	humanise_count(value, &entry->value, &entry->unit);
+	entry->concern = concern;
 
 	va_start(ap, format);
 	stats_table_vaddf(table, entry, format, ap);
@@ -425,13 +539,14 @@ static void stats_table_count_addf(struct stats_table *table, size_t value,
 
 static void stats_table_object_count_addf(struct stats_table *table,
 					  struct object_id *oid, size_t value,
-					  const char *format, ...)
+					  size_t concern, const char *format, ...)
 {
 	struct stats_table_entry *entry;
 	va_list ap;
 
 	CALLOC_ARRAY(entry, 1);
 	humanise_count(value, &entry->value, &entry->unit);
+	entry->concern = concern;
 
 	/*
 	 * A NULL OID should not have a table annotation.
@@ -445,6 +560,7 @@ static void stats_table_object_count_addf(struct stats_table *table,
 }
 
 static void stats_table_size_addf(struct stats_table *table, size_t value,
+				  size_t concern,
 				  const char *format, ...)
 {
 	struct stats_table_entry *entry;
@@ -452,6 +568,7 @@ static void stats_table_size_addf(struct stats_table *table, size_t value,
 
 	CALLOC_ARRAY(entry, 1);
 	humanise_bytes(value, &entry->value, &entry->unit, HUMANISE_COMPACT);
+	entry->concern = concern;
 
 	va_start(ap, format);
 	stats_table_vaddf(table, entry, format, ap);
@@ -460,6 +577,7 @@ static void stats_table_size_addf(struct stats_table *table, size_t value,
 
 static void stats_table_object_size_addf(struct stats_table *table,
 					 struct object_id *oid, size_t value,
+					 size_t concern,
 					 const char *format, ...)
 {
 	struct stats_table_entry *entry;
@@ -467,6 +585,7 @@ static void stats_table_object_size_addf(struct stats_table *table,
 
 	CALLOC_ARRAY(entry, 1);
 	humanise_bytes(value, &entry->value, &entry->unit, HUMANISE_COMPACT);
+	entry->concern = concern;
 
 	/*
 	 * A NULL OID should not have a table annotation.
@@ -501,47 +620,92 @@ 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"));
-	stats_table_count_addf(table, refs->others, "    * %s", _("Others"));
+	stats_table_count_addf(table, ref_total, 0,
+			       "  * %s", _("Count"));
+	stats_table_count_addf(table, refs->branches, 0,
+			       "    * %s", _("Branches"));
+	stats_table_count_addf(table, refs->tags,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TAG_COUNT,
+				       refs->tags),
+			       "    * %s", _("Tags"));
+	stats_table_count_addf(table, refs->remotes, 0,
+			       "    * %s", _("Remotes"));
+	stats_table_count_addf(table, refs->others, 0,
+			       "    * %s", _("Others"));
 
 	object_count_total = get_total_object_values(&objects->type_counts);
 	stats_table_addf(table, "");
 	stats_table_addf(table, "* %s", _("Reachable objects"));
-	stats_table_count_addf(table, object_count_total, "  * %s", _("Count"));
+	stats_table_count_addf(table, object_count_total, 0,
+			       "  * %s", _("Count"));
 	stats_table_count_addf(table, objects->type_counts.commits,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_COMMIT_COUNT,
+				       objects->type_counts.commits),
 			       "    * %s", _("Commits"));
 	stats_table_count_addf(table, objects->type_counts.trees,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TREE_COUNT,
+				       objects->type_counts.trees),
 			       "    * %s", _("Trees"));
 	stats_table_count_addf(table, objects->type_counts.blobs,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_BLOB_COUNT,
+				       objects->type_counts.blobs),
 			       "    * %s", _("Blobs"));
 	stats_table_count_addf(table, objects->type_counts.tags,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TAG_COUNT,
+				       objects->type_counts.tags),
 			       "    * %s", _("Tags"));
 
 	inflated_object_total = get_total_object_values(&objects->inflated_sizes);
-	stats_table_size_addf(table, inflated_object_total,
+	stats_table_size_addf(table, inflated_object_total, 0,
 			      "  * %s", _("Inflated size"));
 	stats_table_size_addf(table, objects->inflated_sizes.commits,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_COMMIT_INFLATED_SIZE,
+				      objects->inflated_sizes.commits),
 			      "    * %s", _("Commits"));
 	stats_table_size_addf(table, objects->inflated_sizes.trees,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_TREE_INFLATED_SIZE,
+				      objects->inflated_sizes.trees),
 			      "    * %s", _("Trees"));
 	stats_table_size_addf(table, objects->inflated_sizes.blobs,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_BLOB_INFLATED_SIZE,
+				      objects->inflated_sizes.blobs),
 			      "    * %s", _("Blobs"));
 	stats_table_size_addf(table, objects->inflated_sizes.tags,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_TAG_INFLATED_SIZE,
+				      objects->inflated_sizes.tags),
 			      "    * %s", _("Tags"));
 
 	disk_object_total = get_total_object_values(&objects->disk_sizes);
-	stats_table_size_addf(table, disk_object_total,
+	stats_table_size_addf(table, disk_object_total, 0,
 			      "  * %s", _("Disk size"));
 	stats_table_size_addf(table, objects->disk_sizes.commits,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_COMMIT_SIZE,
+				      objects->disk_sizes.commits),
 			      "    * %s", _("Commits"));
 	stats_table_size_addf(table, objects->disk_sizes.trees,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_TREE_SIZE,
+				      objects->disk_sizes.trees),
 			      "    * %s", _("Trees"));
 	stats_table_size_addf(table, objects->disk_sizes.blobs,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_BLOB_SIZE,
+				      objects->disk_sizes.blobs),
 			      "    * %s", _("Blobs"));
 	stats_table_size_addf(table, objects->disk_sizes.tags,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_TAG_SIZE,
+				      objects->disk_sizes.tags),
 			      "    * %s", _("Tags"));
 
 	stats_table_addf(table, "");
@@ -550,51 +714,71 @@ static void stats_table_setup_structure(struct stats_table *table,
 	stats_table_object_size_addf(table,
 				     &objects->largest.commit_size.oid,
 				     objects->largest.commit_size.value,
+				     repo_structure_concern_level(
+					     CONCERN_STAT_COMMIT_LARGEST,
+					     objects->largest.commit_size.value),
 				     "    * %s", _("Maximum size"));
 	stats_table_object_count_addf(table,
 				      &objects->largest.parent_count.oid,
 				      objects->largest.parent_count.value,
+				      0,
 				      "    * %s", _("Maximum parents"));
 	stats_table_addf(table, "  * %s", _("Trees"));
 	stats_table_object_size_addf(table,
 				     &objects->largest.tree_size.oid,
 				     objects->largest.tree_size.value,
+				      repo_structure_concern_level(
+					      CONCERN_STAT_TREE_LARGEST,
+					      objects->largest.tree_size.value),
 				     "    * %s", _("Maximum size"));
 	stats_table_object_count_addf(table,
 				      &objects->largest.tree_entries.oid,
 				      objects->largest.tree_entries.value,
+				      0,
 				      "    * %s", _("Maximum entries"));
 	stats_table_addf(table, "  * %s", _("Blobs"));
 	stats_table_object_size_addf(table,
 				     &objects->largest.blob_size.oid,
 				     objects->largest.blob_size.value,
+				     repo_structure_concern_level(
+					     CONCERN_STAT_BLOB_LARGEST,
+					     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,
+				     repo_structure_concern_level(
+					     CONCERN_STAT_TAG_LARGEST,
+					     objects->largest.tag_size.value),
 				     "    * %s", _("Maximum size"));
 }
 
 #define INDEX_WIDTH 4
 
-static void stats_table_print_structure(const struct stats_table *table)
+static void stats_table_print_structure(const struct stats_table *table,
+					bool show_concern)
 {
 	const char *name_col_title = _("Repository structure");
 	const char *value_col_title = _("Value");
+	const char *concern_col_title = _("Concern");
 	int title_name_width = utf8_strwidth(name_col_title);
 	int title_value_width = utf8_strwidth(value_col_title);
+	int title_concern_width = utf8_strwidth(concern_col_title);
 	int name_col_width = table->name_col_width;
 	int value_col_width = table->value_col_width;
 	int unit_col_width = table->unit_col_width;
+	int concern_col_width = table->concern_col_width;
 	struct string_list_item *item;
 	struct strbuf buf = STRBUF_INIT;
+	struct strbuf concern_str = STRBUF_INIT;
 
 	if (title_name_width > name_col_width)
 		name_col_width = title_name_width;
 	if (title_value_width > value_col_width + unit_col_width + 1)
 		value_col_width = title_value_width - unit_col_width;
-
+	if (show_concern && title_concern_width > concern_col_width)
+		concern_col_width = title_concern_width;
 	strbuf_addstr(&buf, "| ");
 	strbuf_utf8_align(&buf, ALIGN_LEFT, name_col_width + INDEX_WIDTH,
 			  name_col_title);
@@ -602,6 +786,12 @@ static void stats_table_print_structure(const struct stats_table *table)
 	strbuf_utf8_align(&buf, ALIGN_LEFT,
 			  value_col_width + unit_col_width + 1, value_col_title);
 	strbuf_addstr(&buf, " |");
+	if (show_concern) {
+		strbuf_addch(&buf, ' ');
+		strbuf_utf8_align(&buf, ALIGN_LEFT, concern_col_width,
+			  concern_col_title);
+		strbuf_addstr(&buf, " |");
+	}
 	printf("%s\n", buf.buf);
 
 	printf("| ");
@@ -610,17 +800,28 @@ static void stats_table_print_structure(const struct stats_table *table)
 	printf(" | ");
 	for (int i = 0; i < value_col_width + unit_col_width + 1; i++)
 		putchar('-');
-	printf(" |\n");
+	printf(" |");
+	if (show_concern) {
+		putchar(' ');
+		for (int i = 0; i < concern_col_width; i++) {
+			putchar('-');
+		}
+		printf(" |");
+	}
+	putchar('\n');
 
 	for_each_string_list_item(item, &table->rows) {
 		struct stats_table_entry *entry = item->util;
 		const char *value = "";
 		const char *unit = "";
+		strbuf_reset(&concern_str);
 
 		if (entry) {
 			value = entry->value;
 			if (entry->unit)
 				unit = entry->unit;
+			if (entry->concern > 0)
+				strbuf_addchars(&concern_str, '*', entry->concern);
 		}
 
 		strbuf_reset(&buf);
@@ -638,6 +839,11 @@ static void stats_table_print_structure(const struct stats_table *table)
 		strbuf_addch(&buf, ' ');
 		strbuf_utf8_align(&buf, ALIGN_LEFT, unit_col_width, unit);
 		strbuf_addstr(&buf, " |");
+		if (show_concern) {
+			strbuf_addch(&buf, ' ');
+			strbuf_utf8_align(&buf, ALIGN_LEFT, concern_col_width, concern_str.buf);
+			strbuf_addstr(&buf, " |");
+		}
 		printf("%s\n", buf.buf);
 	}
 
@@ -681,12 +887,15 @@ 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,
+				     bool show_concern)
 {
 	print_keyvalue("references.branches.count", key_delim,
 		       stats->refs.branches, value_delim);
+
 	print_keyvalue("references.tags.count", key_delim,
 		       stats->refs.tags, value_delim);
+
 	print_keyvalue("references.remotes.count", key_delim,
 		       stats->refs.remotes, value_delim);
 	print_keyvalue("references.others.count", key_delim,
@@ -694,39 +903,120 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 
 	print_keyvalue("objects.commits.count", key_delim,
 		       stats->objects.type_counts.commits, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.commits.count.concern_level", key_delim,
+			      repo_structure_concern_level(CONCERN_STAT_COMMIT_COUNT,
+							   stats->objects.type_counts.commits),
+			      value_delim);
 	print_keyvalue("objects.trees.count", key_delim,
 		       stats->objects.type_counts.trees, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.trees.count.concern_level", key_delim,
+			      repo_structure_concern_level(CONCERN_STAT_TREE_COUNT,
+							   stats->objects.type_counts.trees),
+			      value_delim);
 	print_keyvalue("objects.blobs.count", key_delim,
 		       stats->objects.type_counts.blobs, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.blobs.count.concern_level", key_delim,
+			      repo_structure_concern_level(CONCERN_STAT_BLOB_COUNT,
+							   stats->objects.type_counts.blobs),
+			      value_delim);
 	print_keyvalue("objects.tags.count", key_delim,
 		       stats->objects.type_counts.tags, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.tags.count.concern_level", key_delim,
+			      repo_structure_concern_level(CONCERN_STAT_TAG_COUNT,
+							   stats->objects.type_counts.tags),
+			      value_delim);
 
 	print_keyvalue("objects.commits.inflated_size", key_delim,
 		       stats->objects.inflated_sizes.commits, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.commits.inflated_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_COMMIT_INFLATED_SIZE,
+							    stats->objects.inflated_sizes.commits),
+			      value_delim);
 	print_keyvalue("objects.trees.inflated_size", key_delim,
 		       stats->objects.inflated_sizes.trees, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.trees.inflated_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TREE_INFLATED_SIZE,
+							    stats->objects.inflated_sizes.trees),
+			      value_delim);
+
 	print_keyvalue("objects.blobs.inflated_size", key_delim,
 		       stats->objects.inflated_sizes.blobs, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.blobs.inflated_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_BLOB_INFLATED_SIZE,
+							    stats->objects.inflated_sizes.blobs),
+			      value_delim);
 	print_keyvalue("objects.tags.inflated_size", key_delim,
 		       stats->objects.inflated_sizes.tags, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.tags.inflated_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TAG_INFLATED_SIZE,
+							    stats->objects.inflated_sizes.tags),
+			      value_delim);
 
 	print_keyvalue("objects.commits.disk_size", key_delim,
 		       stats->objects.disk_sizes.commits, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.commits.disk_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_COMMIT_SIZE,
+							    stats->objects.disk_sizes.commits),
+			      value_delim);
 	print_keyvalue("objects.trees.disk_size", key_delim,
 		       stats->objects.disk_sizes.trees, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.trees.disk_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TREE_SIZE,
+							    stats->objects.disk_sizes.trees),
+			      value_delim);
 	print_keyvalue("objects.blobs.disk_size", key_delim,
 		       stats->objects.disk_sizes.blobs, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.blobs.blob_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_BLOB_SIZE,
+							    stats->objects.disk_sizes.blobs),
+			      value_delim);
 	print_keyvalue("objects.tags.disk_size", key_delim,
 		       stats->objects.disk_sizes.tags, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.tags.disk_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TAG_SIZE,
+							    stats->objects.disk_sizes.tags),
+			      value_delim);
 
 	print_object_data("objects.commits.max_size", key_delim,
 			  &stats->objects.largest.commit_size, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.commits.max_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_COMMIT_LARGEST,
+							    stats->objects.largest.commit_size.value),
+			      value_delim);
 	print_object_data("objects.trees.max_size", key_delim,
 			  &stats->objects.largest.tree_size, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.trees.max_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TREE_LARGEST,
+							    stats->objects.largest.tree_size.value),
+			      value_delim);
 	print_object_data("objects.blobs.max_size", key_delim,
 			  &stats->objects.largest.blob_size, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.blobs.max_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_BLOB_LARGEST,
+							    stats->objects.largest.blob_size.value),
+			      value_delim);
 	print_object_data("objects.tags.max_size", key_delim,
 			  &stats->objects.largest.tag_size, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.tags.max_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TAG_LARGEST,
+							    stats->objects.largest.tag_size.value),
+			      value_delim);
 
 	print_object_data("objects.commits.max_parents", key_delim,
 			  &stats->objects.largest.parent_count, value_delim);
@@ -935,6 +1225,8 @@ 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 show_concern = 0;
+
 	struct option options[] = {
 		OPT_CALLBACK_F(0, "format", &format, N_("format"),
 			       N_("output format"),
@@ -944,6 +1236,7 @@ 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_BOOL(0, "concern", &show_concern, N_("show level of concern")),
 		OPT_END()
 	};
 
@@ -962,13 +1255,13 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
 	switch (format) {
 	case FORMAT_TABLE:
 		stats_table_setup_structure(&table, &stats);
-		stats_table_print_structure(&table);
+		stats_table_print_structure(&table, show_concern);
 		break;
 	case FORMAT_NEWLINE_TERMINATED:
-		structure_keyvalue_print(&stats, '=', '\n');
+		structure_keyvalue_print(&stats, '=', '\n', show_concern);
 		break;
 	case FORMAT_NUL_TERMINATED:
-		structure_keyvalue_print(&stats, '\n', '\0');
+		structure_keyvalue_print(&stats, '\n', '\0', show_concern);
 		break;
 	default:
 		BUG("invalid output format");
diff --git a/sideband.c b/sideband.c
index 374187402e..0e75ab2317 100644
--- a/sideband.c
+++ b/sideband.c
@@ -1,4 +1,4 @@
-#define USE_THE_REPOSITORY_VARIABLE
+define USE_THE_REPOSITORY_VARIABLE
 #define DISABLE_SIGN_COMPARE_WARNINGS
 
 #include "git-compat-util.h"
diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index 02cc2b594a..f1e3fd2a75 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -67,77 +67,7 @@ test_expect_success 'empty repository' '
 		EOF
 
 		git repo structure >out 2>err &&
-
-		test_cmp expect out &&
-		test_line_count = 0 err
-	)
-'
-
-test_expect_success SHA1 'repository with references and objects' '
-	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" &&
-
-		# 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            |      1     |
-		|     * Tags                |      1     |
-		|     * Remotes             |      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.03 MiB |
-		|     * Commits             | 217.92 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] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
-		[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
-		[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
-		[5] 97d808e45116bf02103490294d3d46dad7a2ac62
-		[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
-		EOF
-
-		git repo structure >out 2>err &&
+		cp out /tmp/test1-out &&
 
 		test_cmp expect out &&
 		test_line_count = 0 err
@@ -204,6 +134,59 @@ test_expect_success SHA1 'lines and nul format' '
 	)
 '
 
+test_expect_success SHA1 'lines format with concerns' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		# configure thresholds to get concern indications
+		git config repo.structure.concern.commit-count 7 &&
+		git config repo.structure.concern.commit-size 14 &&
+		git config repo.structure.concern.tree-size 10 &&
+		git config repo.structure.concern.blob-size 8 &&
+		git config repo.structure.concern.blob-inflated-size 10 &&
+		git config repo.structure.concern.tag-size 2 &&
+		git config repo.structure.concern.commit-largest 4 &&
+		git config repo.structure.concern.blob-largest 2 &&
+		touch configured &&
+		test_commit_bulk 1005 &&
+		git tag -a foo -m bar &&
+		oid="$(git rev-parse HEAD)" &&
+		git update-ref refs/remotes/origin/foo "$oid" &&
+
+		# Also creates a commit, tree, and blob.
+		git notes add -m foo &&
+
+
+		# 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 &&
+		objects.commits.count.concern_level=2
+		objects.trees.count.concern_level=0
+		objects.blobs.count.concern_level=0
+		objects.tags.count.concern_level=0
+		objects.commits.inflated_size.concern_level=0
+		objects.trees.inflated_size.concern_level=0
+		objects.blobs.inflated_size.concern_level=3
+		objects.tags.inflated_size.concern_level=0
+		objects.commits.disk_size.concern_level=3
+		objects.trees.disk_size.concern_level=8
+		objects.blobs.blob_size.concern_level=6
+		objects.tags.disk_size.concern_level=4
+		objects.commits.max_size.concern_level=3
+		objects.trees.max_size.concern_level=0
+		objects.blobs.max_size.concern_level=1
+		objects.tags.max_size.concern_level=0
+		EOF
+
+		git repo structure --concern --format=lines >actual 2>err &&
+		test_grep "concern_level" actual >actual-concerns &&
+		test_cmp expect actual-concerns &&
+		test_line_count = 0 err
+	)
+'
+
 test_expect_success 'progress meter option' '
 	test_when_finished "rm -rf repo" &&
 	git init repo &&

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v2 2/3] repo: make concern thresholds configurable
  2026-10-26 17:43 ` [PATCH v2 0/3] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
  2026-10-26 17:43   ` [PATCH v2 1/3] repo: add level of concern reporting Mark C. Chu-Carroll
@ 2026-10-26 17:43   ` Mark C. Chu-Carroll
  2026-10-27 14:55     ` Patrick Steinhardt
  2026-10-26 17:43   ` [PATCH v2 3/3] repo: add documentation for level-of-concern Mark C. Chu-Carroll
  2 siblings, 1 reply; 20+ messages in thread
From: Mark C. Chu-Carroll @ 2026-10-26 17:43 UTC (permalink / raw)
  To: git; +Cc: Mark C. Chu-Carroll

The concern reporting mechanism so far provides a
measure of concern based on heuristic values estimated
by analyzing a collection of (hopefully) representative
repositories. But for different projects/usecases, the
thresholds at which a value becomes concerning may vary.

Provide a mechanism using git config to allow users to
configure per-repo concern levels for "git repo structure"
concern reporting.
---
 builtin/repo.c            |  62 +++++++++-------
 sideband.c                |   2 +-
 t/t1901-repo-structure.sh | 180 ++++++++++++++++++++++++++++++++++++++++++----
 3 files changed, 203 insertions(+), 41 deletions(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index a450497d4e..ed579e6685 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -2,6 +2,7 @@
 
 #include "builtin.h"
 #include "commit.h"
+#include "config.h"
 #include "environment.h"
 #include "hash.h"
 #include "hex.h"
@@ -29,6 +30,8 @@
 #define REPO_STRUCTURE_USAGE \
 	"git repo structure [--format=(table|lines|nul) | -z] [--[no-]concern]"
 
+#define REPO_STRUCTURE_CONFIG_PREFIX "repo.concern."
+
 static const char *const repo_usage[] = {
 	REPO_INFO_USAGE,
 	REPO_STRUCTURE_USAGE,
@@ -386,40 +389,43 @@ enum concern_stat {
 	CONCERN_STAT_TREE_INFLATED_SIZE,
 	CONCERN_STAT_BLOB_INFLATED_SIZE,
 	CONCERN_STAT_TAG_INFLATED_SIZE,
+	CONCERN_STAT_MAXIMUM
 };
 
-struct concern_stat_threshold repo_structure_concern_thresholds[] = {
-	{ .name = "commit_count", .threshold = 19 },
-	{.name = "tree_count", .threshold = 21 },
-	{ .name = "blob_count", .threshold = 22 },
-	{ .name = "tag_count", .threshold = 15 },
-	{ .name = "commit_size", .threshold = 28 },
-	{ .name = "tree_size", .threshold = 27 },
-	{ .name = "blob_size", .threshold = 36 },
-	{ .name = "tags_size", .threshold = 18 },
-	{ .name = "commit_largest", .threshold = 18  },
-	{ .name = "tree_largest", .threshold = 18 },
-	{ .name = "blob_largest", .threshold = 26 },
-	{ .name = "tag_largest", .threshold =  15 },
-	{ .name = "commit_inflated_size", .threshold = 28 },
-	{ .name = "tree_inflated_size", .threshold = 32 },
-	{ .name = "blob_inflated_size", .threshold = 34 },
-	{ .name = "tag_inflated_size", .threshold = 25}
+struct concern_stat_threshold concern_stat_thresholds[] = {
+	{ .name = "commit-count", .threshold = 19 },
+	{ .name = "tree-count", .threshold = 21 },
+	{ .name = "blob-count", .threshold = 22 },
+	{ .name = "tag-count", .threshold = 15 },
+	{ .name = "commit-size", .threshold = 28 },
+	{ .name = "tree-size", .threshold = 27 },
+	{ .name = "blob-size", .threshold = 36 },
+	{ .name = "tag-size", .threshold = 18 },
+	{ .name = "commit-largest", .threshold = 18  },
+	{ .name = "tree-largest", .threshold = 18 },
+	{ .name = "blob-largest", .threshold = 26 },
+	{ .name = "tag-largest", .threshold =  15 },
+	{ .name = "commit-inflated-size", .threshold = 28 },
+	{ .name = "tree-inflated-size", .threshold = 32 },
+	{ .name = "blob-inflated-size", .threshold = 34 },
+	{ .name = "tag-inflated-size", .threshold = 25},
+	{ 0 }
 };
 
 /*
  * Concern thresholds aren't universal. We try to set reasonable default
  * values, but projects may want to set these thresholds differently. To
  * support that, they can set the thresholds for their project in the
- * project's git config, in the section "repo.structure.concern".
+ * project's git config, in the section "repo.concern".
   */
 static int config_concern_thresholds(const char *prefixed_var,
 				     const char *str_val,
 				     const struct config_context *ctx,
 				     void *cb UNUSED) {
-	const char *config_prefix = "repo.structure.concern.";
-	if (!strncmp(config_prefix, prefixed_var, strlen(config_prefix))) {
-		const char *concern_name = &prefixed_var[strlen(config_prefix)];
+	if (!strncmp(REPO_STRUCTURE_CONFIG_PREFIX, prefixed_var,
+	      strlen(REPO_STRUCTURE_CONFIG_PREFIX))) {
+		const char *concern_name =
+			&prefixed_var[strlen(REPO_STRUCTURE_CONFIG_PREFIX)];
 		for (int i = 0; concern_stat_thresholds[i].name != 0; i++) {
 			if (!strcmp(concern_stat_thresholds[i].name, concern_name)) {
 				int value = git_config_int(prefixed_var, str_val, ctx->kvi);
@@ -435,7 +441,7 @@ static int config_concern_thresholds(const char *prefixed_var,
 	return 0;
 }
 
-static size_t approx_log2(long value) {
+static size_t approx_log2(size_t value) {
 	int position = 0;
 	if (value <= 0) {
 		return 0;
@@ -450,7 +456,7 @@ static size_t approx_log2(long value) {
 static size_t repo_structure_concern_level(enum concern_stat stat_id,
 					   size_t value) {
 	size_t log_value;
-	if (stat_id >= ARRAY_SIZE(concern_stat_thresholds))
+	if (stat_id >= CONCERN_STAT_MAXIMUM)
 		BUG("unexpected repository stat type");
 	log_value = approx_log2(value);
 	if (log_value > concern_stat_thresholds[stat_id].threshold)
@@ -844,6 +850,7 @@ static void stats_table_print_structure(const struct stats_table *table,
 			strbuf_utf8_align(&buf, ALIGN_LEFT, concern_col_width, concern_str.buf);
 			strbuf_addstr(&buf, " |");
 		}
+		strbuf_release(&concern_str);
 		printf("%s\n", buf.buf);
 	}
 
@@ -977,7 +984,7 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	print_keyvalue("objects.blobs.disk_size", key_delim,
 		       stats->objects.disk_sizes.blobs, value_delim);
 	if (show_concern)
-		print_keyvalue("objects.blobs.blob_size.concern_level", key_delim,
+		print_keyvalue("objects.blobs.disk_size.concern_level", key_delim,
 			       repo_structure_concern_level(CONCERN_STAT_BLOB_SIZE,
 							    stats->objects.disk_sizes.blobs),
 			      value_delim);
@@ -997,7 +1004,7 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 							    stats->objects.largest.commit_size.value),
 			      value_delim);
 	print_object_data("objects.trees.max_size", key_delim,
-			  &stats->objects.largest.tree_size, value_delim);
+			 &stats->objects.largest.tree_size, value_delim);
 	if (show_concern)
 		print_keyvalue("objects.trees.max_size.concern_level", key_delim,
 			       repo_structure_concern_level(CONCERN_STAT_TREE_LARGEST,
@@ -1011,7 +1018,7 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 							    stats->objects.largest.blob_size.value),
 			      value_delim);
 	print_object_data("objects.tags.max_size", key_delim,
-			  &stats->objects.largest.tag_size, value_delim);
+			   &stats->objects.largest.tag_size, value_delim);
 	if (show_concern)
 		print_keyvalue("objects.tags.max_size.concern_level", key_delim,
 			       repo_structure_concern_level(CONCERN_STAT_TAG_LARGEST,
@@ -1243,7 +1250,8 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
 	argc = parse_options(argc, argv, prefix, options, repo_structure_usage, 0);
 	if (argc)
 		usage(_("too many arguments"));
-
+	if (show_concern)
+		repo_config(repo, &config_concern_thresholds, NULL);
 	repo_init_revisions(repo, &revs, prefix);
 
 	if (show_progress < 0)
diff --git a/sideband.c b/sideband.c
index 0e75ab2317..374187402e 100644
--- a/sideband.c
+++ b/sideband.c
@@ -1,4 +1,4 @@
-define USE_THE_REPOSITORY_VARIABLE
+#define USE_THE_REPOSITORY_VARIABLE
 #define DISABLE_SIGN_COMPARE_WARNINGS
 
 #include "git-compat-util.h"
diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index f1e3fd2a75..e8f1269ad4 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -67,13 +67,166 @@ test_expect_success 'empty repository' '
 		EOF
 
 		git repo structure >out 2>err &&
-		cp out /tmp/test1-out &&
 
 		test_cmp expect out &&
 		test_line_count = 0 err
 	)
 '
 
+test_expect_success SHA1 'repository with references and objects' '
+	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" &&
+
+		# 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            |      1     |
+		|     * Tags                |      1     |
+		|     * Remotes             |      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.03 MiB |
+		|     * Commits             | 217.92 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] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
+		[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[5] 97d808e45116bf02103490294d3d46dad7a2ac62
+		[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
+		EOF
+
+		git repo structure >out 2>err &&
+
+		test_cmp expect out &&
+		test_line_count = 0 err
+	)
+'
+
+test_expect_success SHA1 'concern flagging' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		# differences between zlib and zlib-ng can cause this
+		# test to fail; so we just turn it off.
+		git config core.compression 0 &&
+		# configure thresholds to get concern indications
+		git config repo.concern.commit-count 7 &&
+		git config repo.concern.commit-size 14 &&
+		git config repo.concern.tree-size 10 &&
+		git config repo.concern.blob-size 8 &&
+		git config repo.concern.blob-inflated-size 10 &&
+		git config repo.concern.tag-size 2 &&
+		git config repo.concern.commit-largest 4 &&
+		git config repo.concern.blob-largest 2 &&
+		test_commit_bulk 1005 &&
+		git tag -a foo -m bar &&
+		oid="$(git rev-parse HEAD)" &&
+		git update-ref refs/remotes/origin/foo "$oid" &&
+
+		# Also creates a commit, tree, and blob.
+		git notes add -m foo &&
+
+
+		# 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      | Concern  |
+		| ------------------------- | ---------- | -------- |
+		| * References              |            |          |
+		|   * Count                 |      4     |          |
+		|     * Branches            |      1     |          |
+		|     * Tags                |      1     |          |
+		|     * Remotes             |      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.03 MiB |          |
+		|     * Commits             | 217.92 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] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
+		[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[5] 97d808e45116bf02103490294d3d46dad7a2ac62
+		[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
+		EOF
+
+		git repo structure --concern >actual 2>err &&
+
+		test_cmp expect actual &&
+		test_line_count = 0 err
+	)
+'
+
 test_expect_success SHA1 'lines and nul format' '
 	test_when_finished "rm -rf repo" &&
 	git init repo &&
@@ -139,16 +292,18 @@ test_expect_success SHA1 'lines format with concerns' '
 	git init repo &&
 	(
 		cd repo &&
+		# differences between zlib and zlib-ng can cause this
+		# test to fail; so we just turn it off.
+		git config core.compression 0 &&
 		# configure thresholds to get concern indications
-		git config repo.structure.concern.commit-count 7 &&
-		git config repo.structure.concern.commit-size 14 &&
-		git config repo.structure.concern.tree-size 10 &&
-		git config repo.structure.concern.blob-size 8 &&
-		git config repo.structure.concern.blob-inflated-size 10 &&
-		git config repo.structure.concern.tag-size 2 &&
-		git config repo.structure.concern.commit-largest 4 &&
-		git config repo.structure.concern.blob-largest 2 &&
-		touch configured &&
+		git config repo.concern.commit-count 7 &&
+		git config repo.concern.commit-size 14 &&
+		git config repo.concern.tree-size 10 &&
+		git config repo.concern.blob-size 8 &&
+		git config repo.concern.blob-inflated-size 10 &&
+		git config repo.concern.tag-size 2 &&
+		git config repo.concern.commit-largest 4 &&
+		git config repo.concern.blob-largest 2 &&
 		test_commit_bulk 1005 &&
 		git tag -a foo -m bar &&
 		oid="$(git rev-parse HEAD)" &&
@@ -157,7 +312,6 @@ test_expect_success SHA1 'lines format with concerns' '
 		# 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".
@@ -172,8 +326,8 @@ test_expect_success SHA1 'lines format with concerns' '
 		objects.tags.inflated_size.concern_level=0
 		objects.commits.disk_size.concern_level=3
 		objects.trees.disk_size.concern_level=8
-		objects.blobs.blob_size.concern_level=6
-		objects.tags.disk_size.concern_level=4
+		objects.blobs.disk_size.concern_level=6
+		objects.tags.disk_size.concern_level=5
 		objects.commits.max_size.concern_level=3
 		objects.trees.max_size.concern_level=0
 		objects.blobs.max_size.concern_level=1

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v2 3/3] repo: add documentation for level-of-concern.
  2026-10-26 17:43 ` [PATCH v2 0/3] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
  2026-10-26 17:43   ` [PATCH v2 1/3] repo: add level of concern reporting Mark C. Chu-Carroll
  2026-10-26 17:43   ` [PATCH v2 2/3] repo: make concern thresholds configurable Mark C. Chu-Carroll
@ 2026-10-26 17:43   ` Mark C. Chu-Carroll
  2026-10-27 14:55     ` Patrick Steinhardt
  2 siblings, 1 reply; 20+ messages in thread
From: Mark C. Chu-Carroll @ 2026-10-26 17:43 UTC (permalink / raw)
  To: git; +Cc: Mark C. Chu-Carroll

---
 Documentation/config/repo.adoc | 59 ++++++++++++++++++++++++++++++++++++++++++
 Documentation/git-repo.adoc    | 27 +++++++++++++++++--
 2 files changed, 84 insertions(+), 2 deletions(-)

diff --git a/Documentation/config/repo.adoc b/Documentation/config/repo.adoc
new file mode 100644
index 0000000000..811b099d72
--- /dev/null
+++ b/Documentation/config/repo.adoc
@@ -0,0 +1,59 @@
+`repo.concern.commit-count`::
+	Sets the value of the concern threshold for commit count.
+
+`repo.concern.tree-count`::
+	Sets the value of the concern threshold for tree count.
+
+`repo.concern.blob-count`::
+	Sets the value of the concern threshold for blob count.
+
+`repo.concern.tag-count`::
+	Sets the value of the concern threshold for tag count.
+
+`repo.concern.commit-size`::
+	Sets the value of the concern threshold for total commit size.
+
+`repo.concern.tree-size`::
+	Sets the value of the concern threshold for total tree size.
+
+`repo.concern.blob-size`::
+	Sets the value of the concern threshold for total blob size.
+
+`repo.concern.tag-size`::
+	Sets the value of the concern threshold for total tag size.
+
+`repo.concern.commit-inflated-size`::
+	Sets the value of the concern threshold for total size
+	of all inflated commits.
+
+`repo.concern.tree-inflated-size`::
+	Sets the value of the concern threshold for total size of
+	all inflated trees.
+
+`repo.concern.blob-inflated-size`::
+	Sets the value of the concern threshold for total size of
+	all inflated blobs.
+
+`repo.concern.tag-inflated-size`::
+	Sets the value of the concern threshold for the total size
+	of all inflated tags.
+
+`repo.concern.commit-largest`::
+	Sets the value of the concern threshold the size of the largest
+	commit.
+
+`repo.concern.tree-largest`::
+	Sets the value of the concern threshold for the size of the
+	largest tree.
+
+`repo.concern.blob-largest`::
+	Sets the value of the concern threshold for the size of the
+	largest blob.
+
+`repo.concern.tag-largest`::
+	Sets the value of the concern threshold for the size of the
+	largest tag.
+
+All of the concern thresholds are integer values for the
+floor of the base-2 log of the threshold value. The default values
+of each threshold are set to heuristically determined values.
diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index ed7d80c690..bc866c88a8 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-]concern]
 
 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-]concern]`::
 	Retrieve statistics about the current repository structure. The
 	following kinds of information are reported:
 +
@@ -88,6 +88,20 @@ supported:
 	quoted.
 +
 `-z` is an alias for `--format=nul`.
++
+To make it easier to know when one of the values is something that
+users should be concerned about, adding '--concern' will provide a
+level of concern to be added to the output. In 'table' mode, this will
+be a table column indicating the level of concern as a number of stars;
+in `lines` or `nul` mode, a `concern_level` line will be added for each
+statistic with a numeric value.
++
+The level of concern for each statistic is computed using the log_2 of the
+statistic value. The value is considered concerning if it exceeds a heuristically
+chosen threshold value; the degree of concern is the difference between the
+log_2 of the value, and the log_2 of the corresponding threshold.
++
+The threshold values are configurable; see the configuration section below.
 
 INFO KEYS
 ---------
@@ -141,6 +155,15 @@ using the `nul` format:
 git repo info --format=nul layout.bare layout.shallow
 ------------
 
+CONFIGURATION
+-------------
+
+include::includes/cmd-config-section-all.adoc[]
+
+:git-repo: 1
+include::config/repo.adoc[]
+
+
 SEE ALSO
 --------
 linkgit:git-rev-parse[1]

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* Re: [PATCH v2 1/3] repo: add level of concern reporting
  2026-10-26 17:43   ` [PATCH v2 1/3] repo: add level of concern reporting Mark C. Chu-Carroll
@ 2026-10-27 14:55     ` Patrick Steinhardt
  0 siblings, 0 replies; 20+ messages in thread
From: Patrick Steinhardt @ 2026-10-27 14:55 UTC (permalink / raw)
  To: Mark C. Chu-Carroll; +Cc: git

On Mon, Oct 26, 2026 at 01:43:54PM -0400, Mark C. Chu-Carroll wrote:
> "git repo structure" computes a set of statistics
> that are useful for diagnosing performance issues in
> a git repository. But for a typical user who hasn't
> encountered any issues yet, it can be difficult to
> interpret the data - when should a value be considered
> concerning?
> 
> Add an option to "git repo structure" that adds a "degree
> of concern" column to the structure report, which indicates
> when a stat values has reached a point where it might be
> concerning, and indicate how severe the concern should be.

Missing your Signed-off-by.

I think the commit message should also mention git-sizer(1) here, which
is the direct inspiration that we have for this additional data. To the
best of my knowledge, the level of concern is essentially the last item
that is missing for feature compatibility between these two tools.

Another interesting thing to document here is how you picked the level
of concern in the first place.

> diff --git a/builtin/repo.c b/builtin/repo.c
> index 84e012f83f..a450497d4e 100644
> --- a/builtin/repo.c
> +++ b/builtin/repo.c
> @@ -348,8 +348,118 @@ struct stats_table {
>  	int name_col_width;
>  	int value_col_width;
>  	int unit_col_width;
> +	int concern_col_width;
>  };
>  
> +/*
> + * For computing level of concern about a stat, we need to have some
> + * baseline values. For each of these, we'define a minimum level
> + * of concern, which will appear as 1 star. Then we scale exponentially:
> + * each time that value doubles, we add another star.
> + *
> + * The threshold values for concern are expressed as log_2 values of the size
> + * at which the statistic becomes concerning. The default values for the
> + * threshold were selected based on observed values from a collection of
> + * representative-ish repositories of varying size, by finding the mean log_2
> + * value and standard deviation (sigma) for each statistic over the collection.
> + * The threshold for each metric is then set at mean(log_2) + sigma.
> + */
> +struct concern_stat_threshold {
> +	const char *name;
> +	size_t threshold;
> +};
> +
> +enum concern_stat {
> +	CONCERN_STAT_COMMIT_COUNT,
> +	CONCERN_STAT_TREE_COUNT,
> +	CONCERN_STAT_BLOB_COUNT,
> +	CONCERN_STAT_TAG_COUNT,
> +	CONCERN_STAT_COMMIT_SIZE,
> +	CONCERN_STAT_TREE_SIZE,
> +	CONCERN_STAT_BLOB_SIZE,
> +	CONCERN_STAT_TAG_SIZE,
> +	CONCERN_STAT_COMMIT_LARGEST,
> +	CONCERN_STAT_TREE_LARGEST,
> +	CONCERN_STAT_BLOB_LARGEST,
> +	CONCERN_STAT_TAG_LARGEST,
> +	CONCERN_STAT_COMMIT_INFLATED_SIZE,
> +	CONCERN_STAT_TREE_INFLATED_SIZE,
> +	CONCERN_STAT_BLOB_INFLATED_SIZE,
> +	CONCERN_STAT_TAG_INFLATED_SIZE,
> +};
> +
> +struct concern_stat_threshold repo_structure_concern_thresholds[] = {
> +	{ .name = "commit_count", .threshold = 19 },
> +	{.name = "tree_count", .threshold = 21 },

Nit: missing space.

> +	{ .name = "blob_count", .threshold = 22 },
> +	{ .name = "tag_count", .threshold = 15 },
> +	{ .name = "commit_size", .threshold = 28 },
> +	{ .name = "tree_size", .threshold = 27 },
> +	{ .name = "blob_size", .threshold = 36 },
> +	{ .name = "tags_size", .threshold = 18 },
> +	{ .name = "commit_largest", .threshold = 18  },
> +	{ .name = "tree_largest", .threshold = 18 },
> +	{ .name = "blob_largest", .threshold = 26 },
> +	{ .name = "tag_largest", .threshold =  15 },
> +	{ .name = "commit_inflated_size", .threshold = 28 },
> +	{ .name = "tree_inflated_size", .threshold = 32 },
> +	{ .name = "blob_inflated_size", .threshold = 34 },
> +	{ .name = "tag_inflated_size", .threshold = 25}
> +};

I think this is a tiny bit unwieldy, as you now have to remember to
update multiple sites whenever you add another metric to this command.
Ideally, we'd have a single structure that knows to track all
information that we're about to print so that we can define this in a
single, central location.

For example, we could update `struct object_data` to have a configurable
threshold, and then do the same for other such structures. But that
might also get unwieldy by itself.

I think what bugs me a bit is that we have not only two, but three
locations because we have the enum, the thresholds and then also the
data structures themselves.

I'm not quite sure how to fix that myself though.

> +/*
> + * Concern thresholds aren't universal. We try to set reasonable default
> + * values, but projects may want to set these thresholds differently. To
> + * support that, they can set the thresholds for their project in the
> + * project's git config, in the section "repo.structure.concern".
> +  */
> +static int config_concern_thresholds(const char *prefixed_var,
> +				     const char *str_val,
> +				     const struct config_context *ctx,
> +				     void *cb UNUSED) {

We should probably use `cb` to pass around the structure that you
populate so that we can avoid global variables.

> +	const char *config_prefix = "repo.structure.concern.";
> +	if (!strncmp(config_prefix, prefixed_var, strlen(config_prefix))) {

We have `starts_with()`. But also note that if this is a config key then
it should match case-insensitively, so it should rather be
`istarts_with()`.

The config keys are also a bit off as our format only has nested keys up
to three levels deep, not four.

> +		const char *concern_name = &prefixed_var[strlen(config_prefix)];
> +		for (int i = 0; concern_stat_thresholds[i].name != 0; i++) {

We typically don't compare with a zero value directly, and indices
should be of type `size_t` when iterating, so this should rather be:

        for (size_t i = 0; concern_stat_thresholds[i].name; i++)

But the loop condition is a bit weird, because you don't have a final
entry there that has a guaranteed NULL name. So shouldn't we rather
iterate through `ARRAY_SIZE()`?

Also, I may be blind, but where is that struct getting defined? I can
only see `repo_structure_concern_thresholds`.

> +			if (!strcmp(concern_stat_thresholds[i].name, concern_name)) {
> +				int value = git_config_int(prefixed_var, str_val, ctx->kvi);
> +				if (value < 0)
> +					return -1;
> +				concern_stat_thresholds[i].threshold = value;
> +				return 0;
> +			}
> +		}
> +		warning(_("Invalid concern threshold name: '%s'"), concern_name);
> +		return 0;
> +	}
> +	return 0;
> +}
> +
> +static size_t approx_log2(long value) {

We have `log2u()` in "wrapper.h". Can we use that instead?

> @@ -681,12 +887,15 @@ 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,
> +				     bool show_concern)
>  {
>  	print_keyvalue("references.branches.count", key_delim,
>  		       stats->refs.branches, value_delim);
> +
>  	print_keyvalue("references.tags.count", key_delim,
>  		       stats->refs.tags, value_delim);
> +

Nit: stray newlines.

> diff --git a/sideband.c b/sideband.c
> index 374187402e..0e75ab2317 100644
> --- a/sideband.c
> +++ b/sideband.c
> @@ -1,4 +1,4 @@
> -#define USE_THE_REPOSITORY_VARIABLE
> +define USE_THE_REPOSITORY_VARIABLE
>  #define DISABLE_SIGN_COMPARE_WARNINGS
>  
>  #include "git-compat-util.h"

This change probably shouldn't be here.

Patrick

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v2 2/3] repo: make concern thresholds configurable
  2026-10-26 17:43   ` [PATCH v2 2/3] repo: make concern thresholds configurable Mark C. Chu-Carroll
@ 2026-10-27 14:55     ` Patrick Steinhardt
  0 siblings, 0 replies; 20+ messages in thread
From: Patrick Steinhardt @ 2026-10-27 14:55 UTC (permalink / raw)
  To: Mark C. Chu-Carroll; +Cc: git

On Mon, Oct 26, 2026 at 01:43:55PM -0400, Mark C. Chu-Carroll wrote:
> diff --git a/builtin/repo.c b/builtin/repo.c
> index a450497d4e..ed579e6685 100644
> --- a/builtin/repo.c
> +++ b/builtin/repo.c
> @@ -386,40 +389,43 @@ enum concern_stat {
>  	CONCERN_STAT_TREE_INFLATED_SIZE,
>  	CONCERN_STAT_BLOB_INFLATED_SIZE,
>  	CONCERN_STAT_TAG_INFLATED_SIZE,
> +	CONCERN_STAT_MAXIMUM
>  };
>  
> -struct concern_stat_threshold repo_structure_concern_thresholds[] = {
> -	{ .name = "commit_count", .threshold = 19 },
> -	{.name = "tree_count", .threshold = 21 },
> -	{ .name = "blob_count", .threshold = 22 },
> -	{ .name = "tag_count", .threshold = 15 },
> -	{ .name = "commit_size", .threshold = 28 },
> -	{ .name = "tree_size", .threshold = 27 },
> -	{ .name = "blob_size", .threshold = 36 },
> -	{ .name = "tags_size", .threshold = 18 },
> -	{ .name = "commit_largest", .threshold = 18  },
> -	{ .name = "tree_largest", .threshold = 18 },
> -	{ .name = "blob_largest", .threshold = 26 },
> -	{ .name = "tag_largest", .threshold =  15 },
> -	{ .name = "commit_inflated_size", .threshold = 28 },
> -	{ .name = "tree_inflated_size", .threshold = 32 },
> -	{ .name = "blob_inflated_size", .threshold = 34 },
> -	{ .name = "tag_inflated_size", .threshold = 25}
> +struct concern_stat_threshold concern_stat_thresholds[] = {
> +	{ .name = "commit-count", .threshold = 19 },
> +	{ .name = "tree-count", .threshold = 21 },
> +	{ .name = "blob-count", .threshold = 22 },
> +	{ .name = "tag-count", .threshold = 15 },
> +	{ .name = "commit-size", .threshold = 28 },
> +	{ .name = "tree-size", .threshold = 27 },
> +	{ .name = "blob-size", .threshold = 36 },
> +	{ .name = "tag-size", .threshold = 18 },
> +	{ .name = "commit-largest", .threshold = 18  },
> +	{ .name = "tree-largest", .threshold = 18 },
> +	{ .name = "blob-largest", .threshold = 26 },
> +	{ .name = "tag-largest", .threshold =  15 },
> +	{ .name = "commit-inflated-size", .threshold = 28 },
> +	{ .name = "tree-inflated-size", .threshold = 32 },
> +	{ .name = "blob-inflated-size", .threshold = 34 },
> +	{ .name = "tag-inflated-size", .threshold = 25},
> +	{ 0 }
>  };

This commit seems to fix up a couple of issues introduced in the
preceding commit. We generally require that every patch of a patch
series should compile and pass all tests so that we can bisect our
history.

Patrick

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v2 3/3] repo: add documentation for level-of-concern.
  2026-10-26 17:43   ` [PATCH v2 3/3] repo: add documentation for level-of-concern Mark C. Chu-Carroll
@ 2026-10-27 14:55     ` Patrick Steinhardt
  2026-10-28 13:23       ` Mark C. Chu-Carroll
  0 siblings, 1 reply; 20+ messages in thread
From: Patrick Steinhardt @ 2026-10-27 14:55 UTC (permalink / raw)
  To: Mark C. Chu-Carroll; +Cc: git

On Mon, Oct 26, 2026 at 01:43:56PM -0400, Mark C. Chu-Carroll wrote:
> ---
>  Documentation/config/repo.adoc | 59 ++++++++++++++++++++++++++++++++++++++++++
>  Documentation/git-repo.adoc    | 27 +++++++++++++++++--
>  2 files changed, 84 insertions(+), 2 deletions(-)
> 
> diff --git a/Documentation/config/repo.adoc b/Documentation/config/repo.adoc
> new file mode 100644
> index 0000000000..811b099d72
> --- /dev/null
> +++ b/Documentation/config/repo.adoc
> @@ -0,0 +1,59 @@
> +`repo.concern.commit-count`::
> +	Sets the value of the concern threshold for commit count.
> +
> +`repo.concern.tree-count`::
> +	Sets the value of the concern threshold for tree count.

I think this commit can be split and squashed into the other two
commits:

  - The change to synopsis and documentation of "--concern" should be
    squashed into the first commit.

  - These config values here should be squashed into the second commit.

Thanks!

Patrick

^ permalink raw reply	[flat|nested] 20+ messages in thread

* [PATCH v3 0/2] repo: add degree-of-concern reporting to structure
  2026-10-21 13:55 [PATCH 0/3] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
                   ` (3 preceding siblings ...)
  2026-10-26 17:43 ` [PATCH v2 0/3] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
@ 2026-10-27 15:39 ` Mark C. Chu-Carroll
  2026-10-27 15:39   ` [PATCH v3 1/2] repo: add level of concern reporting Mark C. Chu-Carroll
  2026-10-27 15:39   ` [PATCH v3 2/2] repo: make concern thresholds configurable Mark C. Chu-Carroll
  2026-10-29 17:52 ` [PATCH v4 0/2] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
  5 siblings, 2 replies; 20+ messages in thread
From: Mark C. Chu-Carroll @ 2026-10-27 15:39 UTC (permalink / raw)
  To: git; +Cc: Mark C. Chu-Carroll

"git repo structure" computes a set of statistics that are useful for
diagnosing performance issues in a git repository. But for a typical
user who hasn't encountered any issues yet, it can be difficult to
interpret the data - when should a value be considered concerning?

This patchset introduces a feature to "git repo structure" that reports
on how concerning certain measures of storage structure  are for a
repository.  In table mode, concern is displayed as a column containing
a number of stars indicating the degree of concern; in line or nul mode,
concerns are displayed as additionaL lines with a numeric value.

To select defaults for the thresholds where a value becomes concern,
a (hopefully) representative selection of repositories were analyzed.
Based on empirical exploration of the data, the measurement that seemed
closest to accurately assessing when a repository's structure becomes
concerning was based on the base 2 logarithm of the value. Across all of
the sample repositories, for all of the measurements, the point at which
things become problematic was when a base 2 logarithm of a value was
more than 1 standard deviation from the mean of the log_2 values in
the sample set.  Based on this, the concern threshold was set for each
of the measures.

While this seems like a reasonable approach for setting default
thresholds, it's obviously still very ad-hoc. It's likely that over
time, the default values will evolve; and in the short term, projects
may want to set their own thresholds. So this patchset includes support
for configuring the thresholds on a per-reposiry basis.

Overview of the change:

* The first commit adds the command-line option "--concern", and creates the basic implementation of concern reporting.
* The next commit adds configurability for the concern thresholds.

Signed-off-by: Mark C. Chu-Carroll <markchucarroll@fastmail.com>
---
Changes in v3:
- Merge the documentation commit backwards into the other two commits.
- Link to v2: https://patch.msgid.link/20261026-b4-repo-structure-concern-v2-0-e0df34228dc8@fastmail.com

Changes in v2:
- Fix memory leak in concern string generation.
- Fix CI/CD test issue involving zlib versions.
- Updating the naming of the config value prefix.
- Link to v1: https://patch.msgid.link/20261021-b4-repo-structure-concern-v1-0-e3010552292c@fastmail.com

---
Mark C. Chu-Carroll (2):
      repo: add level of concern reporting
      repo: make concern thresholds configurable

 Documentation/config/repo.adoc |  59 +++++++
 Documentation/git-repo.adoc    |  27 +++-
 builtin/repo.c                 | 343 ++++++++++++++++++++++++++++++++++++++---
 t/t1901-repo-structure.sh      | 137 ++++++++++++++++
 4 files changed, 543 insertions(+), 23 deletions(-)

Range-diff versus v2:

1:  b1ef56dd47 ! 1:  11c60f6d6d repo: add level of concern reporting
    @@ Commit message
         when a stat values has reached a point where it might be
         concerning, and indicate how severe the concern should be.
     
    + ## Documentation/git-repo.adoc ##
    +@@ Documentation/git-repo.adoc: 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-]concern]
    + 
    + DESCRIPTION
    + -----------
    +@@ Documentation/git-repo.adoc: 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-]concern]`::
    + 	Retrieve statistics about the current repository structure. The
    + 	following kinds of information are reported:
    + +
    +@@ Documentation/git-repo.adoc: supported:
    + 	quoted.
    + +
    + `-z` is an alias for `--format=nul`.
    +++
    ++To make it easier to know when one of the values is something that
    ++users should be concerned about, adding '--concern' will provide a
    ++level of concern to be added to the output. In 'table' mode, this will
    ++be a table column indicating the level of concern as a number of stars;
    ++in `lines` or `nul` mode, a `concern_level` line will be added for each
    ++statistic with a numeric value.
    +++
    ++The level of concern for each statistic is computed using the log_2 of the
    ++statistic value. The value is considered concerning if it exceeds a heuristically
    ++chosen threshold value; the degree of concern is the difference between the
    ++log_2 of the value, and the log_2 of the corresponding threshold.
    + 
    + INFO KEYS
    + ---------
    +
      ## builtin/repo.c ##
     @@
      	"git repo info --keys [--format=(lines|nul) | -z]"
2:  446391b362 ! 2:  4eba5072b0 repo: make concern thresholds configurable
    @@ Commit message
         configure per-repo concern levels for "git repo structure"
         concern reporting.
     
    + ## Documentation/config/repo.adoc (new) ##
    +@@
    ++`repo.concern.commit-count`::
    ++	Sets the value of the concern threshold for commit count.
    ++
    ++`repo.concern.tree-count`::
    ++	Sets the value of the concern threshold for tree count.
    ++
    ++`repo.concern.blob-count`::
    ++	Sets the value of the concern threshold for blob count.
    ++
    ++`repo.concern.tag-count`::
    ++	Sets the value of the concern threshold for tag count.
    ++
    ++`repo.concern.commit-size`::
    ++	Sets the value of the concern threshold for total commit size.
    ++
    ++`repo.concern.tree-size`::
    ++	Sets the value of the concern threshold for total tree size.
    ++
    ++`repo.concern.blob-size`::
    ++	Sets the value of the concern threshold for total blob size.
    ++
    ++`repo.concern.tag-size`::
    ++	Sets the value of the concern threshold for total tag size.
    ++
    ++`repo.concern.commit-inflated-size`::
    ++	Sets the value of the concern threshold for total size
    ++	of all inflated commits.
    ++
    ++`repo.concern.tree-inflated-size`::
    ++	Sets the value of the concern threshold for total size of
    ++	all inflated trees.
    ++
    ++`repo.concern.blob-inflated-size`::
    ++	Sets the value of the concern threshold for total size of
    ++	all inflated blobs.
    ++
    ++`repo.concern.tag-inflated-size`::
    ++	Sets the value of the concern threshold for the total size
    ++	of all inflated tags.
    ++
    ++`repo.concern.commit-largest`::
    ++	Sets the value of the concern threshold the size of the largest
    ++	commit.
    ++
    ++`repo.concern.tree-largest`::
    ++	Sets the value of the concern threshold for the size of the
    ++	largest tree.
    ++
    ++`repo.concern.blob-largest`::
    ++	Sets the value of the concern threshold for the size of the
    ++	largest blob.
    ++
    ++`repo.concern.tag-largest`::
    ++	Sets the value of the concern threshold for the size of the
    ++	largest tag.
    ++
    ++All of the concern thresholds are integer values for the
    ++floor of the base-2 log of the threshold value. The default values
    ++of each threshold are set to heuristically determined values.
    +
    + ## Documentation/git-repo.adoc ##
    +@@ Documentation/git-repo.adoc: statistic value. The value is considered concerning if it exceeds a heuristicall
    + chosen threshold value; the degree of concern is the difference between the
    + log_2 of the value, and the log_2 of the corresponding threshold.
    + 
    ++The threshold values are configurable; see the configuration section below.
    ++
    + INFO KEYS
    + ---------
    + In order to obtain a set of values from `git repo info`, you should provide
    +@@ Documentation/git-repo.adoc: using the `nul` format:
    + git repo info --format=nul layout.bare layout.shallow
    + ------------
    + 
    ++CONFIGURATION
    ++-------------
    ++
    ++include::includes/cmd-config-section-all.adoc[]
    ++
    ++:git-repo: 1
    ++include::config/repo.adoc[]
    ++
    ++
    + SEE ALSO
    + --------
    + linkgit:git-rev-parse[1]
    +
      ## builtin/repo.c ##
     @@
      
3:  92f1fafbcd < -:  ---------- repo: add documentation for level-of-concern.

---
base-commit: 3bc0341126508f78f5869cbfc0005e987efdf0c7
change-id: 20261019-b4-repo-structure-concern-f1d6f6326095


^ permalink raw reply	[flat|nested] 20+ messages in thread

* [PATCH v3 1/2] repo: add level of concern reporting
  2026-10-27 15:39 ` [PATCH v3 0/2] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
@ 2026-10-27 15:39   ` Mark C. Chu-Carroll
  2026-10-27 15:39   ` [PATCH v3 2/2] repo: make concern thresholds configurable Mark C. Chu-Carroll
  1 sibling, 0 replies; 20+ messages in thread
From: Mark C. Chu-Carroll @ 2026-10-27 15:39 UTC (permalink / raw)
  To: git; +Cc: Mark C. Chu-Carroll

"git repo structure" computes a set of statistics
that are useful for diagnosing performance issues in
a git repository. But for a typical user who hasn't
encountered any issues yet, it can be difficult to
interpret the data - when should a value be considered
concerning?

Add an option to "git repo structure" that adds a "degree
of concern" column to the structure report, which indicates
when a stat values has reached a point where it might be
concerning, and indicate how severe the concern should be.
---
 Documentation/git-repo.adoc |  16 ++-
 builtin/repo.c              | 329 +++++++++++++++++++++++++++++++++++++++++---
 sideband.c                  |   2 +-
 t/t1901-repo-structure.sh   | 125 ++++++++---------
 4 files changed, 380 insertions(+), 92 deletions(-)

diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index ed7d80c690..f186b27d87 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-]concern]
 
 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-]concern]`::
 	Retrieve statistics about the current repository structure. The
 	following kinds of information are reported:
 +
@@ -88,6 +88,18 @@ supported:
 	quoted.
 +
 `-z` is an alias for `--format=nul`.
++
+To make it easier to know when one of the values is something that
+users should be concerned about, adding '--concern' will provide a
+level of concern to be added to the output. In 'table' mode, this will
+be a table column indicating the level of concern as a number of stars;
+in `lines` or `nul` mode, a `concern_level` line will be added for each
+statistic with a numeric value.
++
+The level of concern for each statistic is computed using the log_2 of the
+statistic value. The value is considered concerning if it exceeds a heuristically
+chosen threshold value; the degree of concern is the difference between the
+log_2 of the value, and the log_2 of the corresponding threshold.
 
 INFO KEYS
 ---------
diff --git a/builtin/repo.c b/builtin/repo.c
index 84e012f83f..a450497d4e 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] [--[no-]concern]"
 
 static const char *const repo_usage[] = {
 	REPO_INFO_USAGE,
@@ -348,8 +348,118 @@ struct stats_table {
 	int name_col_width;
 	int value_col_width;
 	int unit_col_width;
+	int concern_col_width;
 };
 
+/*
+ * For computing level of concern about a stat, we need to have some
+ * baseline values. For each of these, we'define a minimum level
+ * of concern, which will appear as 1 star. Then we scale exponentially:
+ * each time that value doubles, we add another star.
+ *
+ * The threshold values for concern are expressed as log_2 values of the size
+ * at which the statistic becomes concerning. The default values for the
+ * threshold were selected based on observed values from a collection of
+ * representative-ish repositories of varying size, by finding the mean log_2
+ * value and standard deviation (sigma) for each statistic over the collection.
+ * The threshold for each metric is then set at mean(log_2) + sigma.
+ */
+struct concern_stat_threshold {
+	const char *name;
+	size_t threshold;
+};
+
+enum concern_stat {
+	CONCERN_STAT_COMMIT_COUNT,
+	CONCERN_STAT_TREE_COUNT,
+	CONCERN_STAT_BLOB_COUNT,
+	CONCERN_STAT_TAG_COUNT,
+	CONCERN_STAT_COMMIT_SIZE,
+	CONCERN_STAT_TREE_SIZE,
+	CONCERN_STAT_BLOB_SIZE,
+	CONCERN_STAT_TAG_SIZE,
+	CONCERN_STAT_COMMIT_LARGEST,
+	CONCERN_STAT_TREE_LARGEST,
+	CONCERN_STAT_BLOB_LARGEST,
+	CONCERN_STAT_TAG_LARGEST,
+	CONCERN_STAT_COMMIT_INFLATED_SIZE,
+	CONCERN_STAT_TREE_INFLATED_SIZE,
+	CONCERN_STAT_BLOB_INFLATED_SIZE,
+	CONCERN_STAT_TAG_INFLATED_SIZE,
+};
+
+struct concern_stat_threshold repo_structure_concern_thresholds[] = {
+	{ .name = "commit_count", .threshold = 19 },
+	{.name = "tree_count", .threshold = 21 },
+	{ .name = "blob_count", .threshold = 22 },
+	{ .name = "tag_count", .threshold = 15 },
+	{ .name = "commit_size", .threshold = 28 },
+	{ .name = "tree_size", .threshold = 27 },
+	{ .name = "blob_size", .threshold = 36 },
+	{ .name = "tags_size", .threshold = 18 },
+	{ .name = "commit_largest", .threshold = 18  },
+	{ .name = "tree_largest", .threshold = 18 },
+	{ .name = "blob_largest", .threshold = 26 },
+	{ .name = "tag_largest", .threshold =  15 },
+	{ .name = "commit_inflated_size", .threshold = 28 },
+	{ .name = "tree_inflated_size", .threshold = 32 },
+	{ .name = "blob_inflated_size", .threshold = 34 },
+	{ .name = "tag_inflated_size", .threshold = 25}
+};
+
+/*
+ * Concern thresholds aren't universal. We try to set reasonable default
+ * values, but projects may want to set these thresholds differently. To
+ * support that, they can set the thresholds for their project in the
+ * project's git config, in the section "repo.structure.concern".
+  */
+static int config_concern_thresholds(const char *prefixed_var,
+				     const char *str_val,
+				     const struct config_context *ctx,
+				     void *cb UNUSED) {
+	const char *config_prefix = "repo.structure.concern.";
+	if (!strncmp(config_prefix, prefixed_var, strlen(config_prefix))) {
+		const char *concern_name = &prefixed_var[strlen(config_prefix)];
+		for (int i = 0; concern_stat_thresholds[i].name != 0; i++) {
+			if (!strcmp(concern_stat_thresholds[i].name, concern_name)) {
+				int value = git_config_int(prefixed_var, str_val, ctx->kvi);
+				if (value < 0)
+					return -1;
+				concern_stat_thresholds[i].threshold = value;
+				return 0;
+			}
+		}
+		warning(_("Invalid concern threshold name: '%s'"), concern_name);
+		return 0;
+	}
+	return 0;
+}
+
+static size_t approx_log2(long value) {
+	int position = 0;
+	if (value <= 0) {
+		return 0;
+	}
+	while (value > 1) {
+		position++;
+		value >>= 1;
+	}
+	return position;
+}
+
+static size_t repo_structure_concern_level(enum concern_stat stat_id,
+					   size_t value) {
+	size_t log_value;
+	if (stat_id >= ARRAY_SIZE(concern_stat_thresholds))
+		BUG("unexpected repository stat type");
+	log_value = approx_log2(value);
+	if (log_value > concern_stat_thresholds[stat_id].threshold)
+		return log_value -
+			concern_stat_thresholds[stat_id].threshold;
+	else
+		return 0;
+}
+
 /*
  * Holds column data that gets stored for each row.
  */
@@ -358,6 +468,7 @@ struct stats_table_entry {
 	const char *unit;
 	size_t index;
 	struct object_id *oid;
+	size_t concern;
 };
 
 static void stats_table_vaddf(struct stats_table *table,
@@ -391,6 +502,8 @@ static void stats_table_vaddf(struct stats_table *table,
 		if (value_width > table->value_col_width)
 			table->value_col_width = value_width;
 	}
+	if ((int)entry->concern > table->concern_col_width)
+		table->concern_col_width = entry->concern;
 	if (entry->unit) {
 		int unit_width = utf8_strwidth(entry->unit);
 		if (unit_width > table->unit_col_width)
@@ -410,13 +523,14 @@ static void stats_table_addf(struct stats_table *table, const char *format, ...)
 }
 
 static void stats_table_count_addf(struct stats_table *table, size_t value,
-				   const char *format, ...)
+				   size_t concern, const char *format, ...)
 {
 	struct stats_table_entry *entry;
 	va_list ap;
 
 	CALLOC_ARRAY(entry, 1);
 	humanise_count(value, &entry->value, &entry->unit);
+	entry->concern = concern;
 
 	va_start(ap, format);
 	stats_table_vaddf(table, entry, format, ap);
@@ -425,13 +539,14 @@ static void stats_table_count_addf(struct stats_table *table, size_t value,
 
 static void stats_table_object_count_addf(struct stats_table *table,
 					  struct object_id *oid, size_t value,
-					  const char *format, ...)
+					  size_t concern, const char *format, ...)
 {
 	struct stats_table_entry *entry;
 	va_list ap;
 
 	CALLOC_ARRAY(entry, 1);
 	humanise_count(value, &entry->value, &entry->unit);
+	entry->concern = concern;
 
 	/*
 	 * A NULL OID should not have a table annotation.
@@ -445,6 +560,7 @@ static void stats_table_object_count_addf(struct stats_table *table,
 }
 
 static void stats_table_size_addf(struct stats_table *table, size_t value,
+				  size_t concern,
 				  const char *format, ...)
 {
 	struct stats_table_entry *entry;
@@ -452,6 +568,7 @@ static void stats_table_size_addf(struct stats_table *table, size_t value,
 
 	CALLOC_ARRAY(entry, 1);
 	humanise_bytes(value, &entry->value, &entry->unit, HUMANISE_COMPACT);
+	entry->concern = concern;
 
 	va_start(ap, format);
 	stats_table_vaddf(table, entry, format, ap);
@@ -460,6 +577,7 @@ static void stats_table_size_addf(struct stats_table *table, size_t value,
 
 static void stats_table_object_size_addf(struct stats_table *table,
 					 struct object_id *oid, size_t value,
+					 size_t concern,
 					 const char *format, ...)
 {
 	struct stats_table_entry *entry;
@@ -467,6 +585,7 @@ static void stats_table_object_size_addf(struct stats_table *table,
 
 	CALLOC_ARRAY(entry, 1);
 	humanise_bytes(value, &entry->value, &entry->unit, HUMANISE_COMPACT);
+	entry->concern = concern;
 
 	/*
 	 * A NULL OID should not have a table annotation.
@@ -501,47 +620,92 @@ 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"));
-	stats_table_count_addf(table, refs->others, "    * %s", _("Others"));
+	stats_table_count_addf(table, ref_total, 0,
+			       "  * %s", _("Count"));
+	stats_table_count_addf(table, refs->branches, 0,
+			       "    * %s", _("Branches"));
+	stats_table_count_addf(table, refs->tags,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TAG_COUNT,
+				       refs->tags),
+			       "    * %s", _("Tags"));
+	stats_table_count_addf(table, refs->remotes, 0,
+			       "    * %s", _("Remotes"));
+	stats_table_count_addf(table, refs->others, 0,
+			       "    * %s", _("Others"));
 
 	object_count_total = get_total_object_values(&objects->type_counts);
 	stats_table_addf(table, "");
 	stats_table_addf(table, "* %s", _("Reachable objects"));
-	stats_table_count_addf(table, object_count_total, "  * %s", _("Count"));
+	stats_table_count_addf(table, object_count_total, 0,
+			       "  * %s", _("Count"));
 	stats_table_count_addf(table, objects->type_counts.commits,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_COMMIT_COUNT,
+				       objects->type_counts.commits),
 			       "    * %s", _("Commits"));
 	stats_table_count_addf(table, objects->type_counts.trees,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TREE_COUNT,
+				       objects->type_counts.trees),
 			       "    * %s", _("Trees"));
 	stats_table_count_addf(table, objects->type_counts.blobs,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_BLOB_COUNT,
+				       objects->type_counts.blobs),
 			       "    * %s", _("Blobs"));
 	stats_table_count_addf(table, objects->type_counts.tags,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TAG_COUNT,
+				       objects->type_counts.tags),
 			       "    * %s", _("Tags"));
 
 	inflated_object_total = get_total_object_values(&objects->inflated_sizes);
-	stats_table_size_addf(table, inflated_object_total,
+	stats_table_size_addf(table, inflated_object_total, 0,
 			      "  * %s", _("Inflated size"));
 	stats_table_size_addf(table, objects->inflated_sizes.commits,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_COMMIT_INFLATED_SIZE,
+				      objects->inflated_sizes.commits),
 			      "    * %s", _("Commits"));
 	stats_table_size_addf(table, objects->inflated_sizes.trees,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_TREE_INFLATED_SIZE,
+				      objects->inflated_sizes.trees),
 			      "    * %s", _("Trees"));
 	stats_table_size_addf(table, objects->inflated_sizes.blobs,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_BLOB_INFLATED_SIZE,
+				      objects->inflated_sizes.blobs),
 			      "    * %s", _("Blobs"));
 	stats_table_size_addf(table, objects->inflated_sizes.tags,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_TAG_INFLATED_SIZE,
+				      objects->inflated_sizes.tags),
 			      "    * %s", _("Tags"));
 
 	disk_object_total = get_total_object_values(&objects->disk_sizes);
-	stats_table_size_addf(table, disk_object_total,
+	stats_table_size_addf(table, disk_object_total, 0,
 			      "  * %s", _("Disk size"));
 	stats_table_size_addf(table, objects->disk_sizes.commits,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_COMMIT_SIZE,
+				      objects->disk_sizes.commits),
 			      "    * %s", _("Commits"));
 	stats_table_size_addf(table, objects->disk_sizes.trees,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_TREE_SIZE,
+				      objects->disk_sizes.trees),
 			      "    * %s", _("Trees"));
 	stats_table_size_addf(table, objects->disk_sizes.blobs,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_BLOB_SIZE,
+				      objects->disk_sizes.blobs),
 			      "    * %s", _("Blobs"));
 	stats_table_size_addf(table, objects->disk_sizes.tags,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_TAG_SIZE,
+				      objects->disk_sizes.tags),
 			      "    * %s", _("Tags"));
 
 	stats_table_addf(table, "");
@@ -550,51 +714,71 @@ static void stats_table_setup_structure(struct stats_table *table,
 	stats_table_object_size_addf(table,
 				     &objects->largest.commit_size.oid,
 				     objects->largest.commit_size.value,
+				     repo_structure_concern_level(
+					     CONCERN_STAT_COMMIT_LARGEST,
+					     objects->largest.commit_size.value),
 				     "    * %s", _("Maximum size"));
 	stats_table_object_count_addf(table,
 				      &objects->largest.parent_count.oid,
 				      objects->largest.parent_count.value,
+				      0,
 				      "    * %s", _("Maximum parents"));
 	stats_table_addf(table, "  * %s", _("Trees"));
 	stats_table_object_size_addf(table,
 				     &objects->largest.tree_size.oid,
 				     objects->largest.tree_size.value,
+				      repo_structure_concern_level(
+					      CONCERN_STAT_TREE_LARGEST,
+					      objects->largest.tree_size.value),
 				     "    * %s", _("Maximum size"));
 	stats_table_object_count_addf(table,
 				      &objects->largest.tree_entries.oid,
 				      objects->largest.tree_entries.value,
+				      0,
 				      "    * %s", _("Maximum entries"));
 	stats_table_addf(table, "  * %s", _("Blobs"));
 	stats_table_object_size_addf(table,
 				     &objects->largest.blob_size.oid,
 				     objects->largest.blob_size.value,
+				     repo_structure_concern_level(
+					     CONCERN_STAT_BLOB_LARGEST,
+					     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,
+				     repo_structure_concern_level(
+					     CONCERN_STAT_TAG_LARGEST,
+					     objects->largest.tag_size.value),
 				     "    * %s", _("Maximum size"));
 }
 
 #define INDEX_WIDTH 4
 
-static void stats_table_print_structure(const struct stats_table *table)
+static void stats_table_print_structure(const struct stats_table *table,
+					bool show_concern)
 {
 	const char *name_col_title = _("Repository structure");
 	const char *value_col_title = _("Value");
+	const char *concern_col_title = _("Concern");
 	int title_name_width = utf8_strwidth(name_col_title);
 	int title_value_width = utf8_strwidth(value_col_title);
+	int title_concern_width = utf8_strwidth(concern_col_title);
 	int name_col_width = table->name_col_width;
 	int value_col_width = table->value_col_width;
 	int unit_col_width = table->unit_col_width;
+	int concern_col_width = table->concern_col_width;
 	struct string_list_item *item;
 	struct strbuf buf = STRBUF_INIT;
+	struct strbuf concern_str = STRBUF_INIT;
 
 	if (title_name_width > name_col_width)
 		name_col_width = title_name_width;
 	if (title_value_width > value_col_width + unit_col_width + 1)
 		value_col_width = title_value_width - unit_col_width;
-
+	if (show_concern && title_concern_width > concern_col_width)
+		concern_col_width = title_concern_width;
 	strbuf_addstr(&buf, "| ");
 	strbuf_utf8_align(&buf, ALIGN_LEFT, name_col_width + INDEX_WIDTH,
 			  name_col_title);
@@ -602,6 +786,12 @@ static void stats_table_print_structure(const struct stats_table *table)
 	strbuf_utf8_align(&buf, ALIGN_LEFT,
 			  value_col_width + unit_col_width + 1, value_col_title);
 	strbuf_addstr(&buf, " |");
+	if (show_concern) {
+		strbuf_addch(&buf, ' ');
+		strbuf_utf8_align(&buf, ALIGN_LEFT, concern_col_width,
+			  concern_col_title);
+		strbuf_addstr(&buf, " |");
+	}
 	printf("%s\n", buf.buf);
 
 	printf("| ");
@@ -610,17 +800,28 @@ static void stats_table_print_structure(const struct stats_table *table)
 	printf(" | ");
 	for (int i = 0; i < value_col_width + unit_col_width + 1; i++)
 		putchar('-');
-	printf(" |\n");
+	printf(" |");
+	if (show_concern) {
+		putchar(' ');
+		for (int i = 0; i < concern_col_width; i++) {
+			putchar('-');
+		}
+		printf(" |");
+	}
+	putchar('\n');
 
 	for_each_string_list_item(item, &table->rows) {
 		struct stats_table_entry *entry = item->util;
 		const char *value = "";
 		const char *unit = "";
+		strbuf_reset(&concern_str);
 
 		if (entry) {
 			value = entry->value;
 			if (entry->unit)
 				unit = entry->unit;
+			if (entry->concern > 0)
+				strbuf_addchars(&concern_str, '*', entry->concern);
 		}
 
 		strbuf_reset(&buf);
@@ -638,6 +839,11 @@ static void stats_table_print_structure(const struct stats_table *table)
 		strbuf_addch(&buf, ' ');
 		strbuf_utf8_align(&buf, ALIGN_LEFT, unit_col_width, unit);
 		strbuf_addstr(&buf, " |");
+		if (show_concern) {
+			strbuf_addch(&buf, ' ');
+			strbuf_utf8_align(&buf, ALIGN_LEFT, concern_col_width, concern_str.buf);
+			strbuf_addstr(&buf, " |");
+		}
 		printf("%s\n", buf.buf);
 	}
 
@@ -681,12 +887,15 @@ 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,
+				     bool show_concern)
 {
 	print_keyvalue("references.branches.count", key_delim,
 		       stats->refs.branches, value_delim);
+
 	print_keyvalue("references.tags.count", key_delim,
 		       stats->refs.tags, value_delim);
+
 	print_keyvalue("references.remotes.count", key_delim,
 		       stats->refs.remotes, value_delim);
 	print_keyvalue("references.others.count", key_delim,
@@ -694,39 +903,120 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 
 	print_keyvalue("objects.commits.count", key_delim,
 		       stats->objects.type_counts.commits, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.commits.count.concern_level", key_delim,
+			      repo_structure_concern_level(CONCERN_STAT_COMMIT_COUNT,
+							   stats->objects.type_counts.commits),
+			      value_delim);
 	print_keyvalue("objects.trees.count", key_delim,
 		       stats->objects.type_counts.trees, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.trees.count.concern_level", key_delim,
+			      repo_structure_concern_level(CONCERN_STAT_TREE_COUNT,
+							   stats->objects.type_counts.trees),
+			      value_delim);
 	print_keyvalue("objects.blobs.count", key_delim,
 		       stats->objects.type_counts.blobs, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.blobs.count.concern_level", key_delim,
+			      repo_structure_concern_level(CONCERN_STAT_BLOB_COUNT,
+							   stats->objects.type_counts.blobs),
+			      value_delim);
 	print_keyvalue("objects.tags.count", key_delim,
 		       stats->objects.type_counts.tags, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.tags.count.concern_level", key_delim,
+			      repo_structure_concern_level(CONCERN_STAT_TAG_COUNT,
+							   stats->objects.type_counts.tags),
+			      value_delim);
 
 	print_keyvalue("objects.commits.inflated_size", key_delim,
 		       stats->objects.inflated_sizes.commits, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.commits.inflated_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_COMMIT_INFLATED_SIZE,
+							    stats->objects.inflated_sizes.commits),
+			      value_delim);
 	print_keyvalue("objects.trees.inflated_size", key_delim,
 		       stats->objects.inflated_sizes.trees, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.trees.inflated_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TREE_INFLATED_SIZE,
+							    stats->objects.inflated_sizes.trees),
+			      value_delim);
+
 	print_keyvalue("objects.blobs.inflated_size", key_delim,
 		       stats->objects.inflated_sizes.blobs, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.blobs.inflated_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_BLOB_INFLATED_SIZE,
+							    stats->objects.inflated_sizes.blobs),
+			      value_delim);
 	print_keyvalue("objects.tags.inflated_size", key_delim,
 		       stats->objects.inflated_sizes.tags, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.tags.inflated_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TAG_INFLATED_SIZE,
+							    stats->objects.inflated_sizes.tags),
+			      value_delim);
 
 	print_keyvalue("objects.commits.disk_size", key_delim,
 		       stats->objects.disk_sizes.commits, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.commits.disk_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_COMMIT_SIZE,
+							    stats->objects.disk_sizes.commits),
+			      value_delim);
 	print_keyvalue("objects.trees.disk_size", key_delim,
 		       stats->objects.disk_sizes.trees, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.trees.disk_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TREE_SIZE,
+							    stats->objects.disk_sizes.trees),
+			      value_delim);
 	print_keyvalue("objects.blobs.disk_size", key_delim,
 		       stats->objects.disk_sizes.blobs, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.blobs.blob_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_BLOB_SIZE,
+							    stats->objects.disk_sizes.blobs),
+			      value_delim);
 	print_keyvalue("objects.tags.disk_size", key_delim,
 		       stats->objects.disk_sizes.tags, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.tags.disk_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TAG_SIZE,
+							    stats->objects.disk_sizes.tags),
+			      value_delim);
 
 	print_object_data("objects.commits.max_size", key_delim,
 			  &stats->objects.largest.commit_size, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.commits.max_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_COMMIT_LARGEST,
+							    stats->objects.largest.commit_size.value),
+			      value_delim);
 	print_object_data("objects.trees.max_size", key_delim,
 			  &stats->objects.largest.tree_size, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.trees.max_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TREE_LARGEST,
+							    stats->objects.largest.tree_size.value),
+			      value_delim);
 	print_object_data("objects.blobs.max_size", key_delim,
 			  &stats->objects.largest.blob_size, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.blobs.max_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_BLOB_LARGEST,
+							    stats->objects.largest.blob_size.value),
+			      value_delim);
 	print_object_data("objects.tags.max_size", key_delim,
 			  &stats->objects.largest.tag_size, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.tags.max_size.concern_level", key_delim,
+			       repo_structure_concern_level(CONCERN_STAT_TAG_LARGEST,
+							    stats->objects.largest.tag_size.value),
+			      value_delim);
 
 	print_object_data("objects.commits.max_parents", key_delim,
 			  &stats->objects.largest.parent_count, value_delim);
@@ -935,6 +1225,8 @@ 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 show_concern = 0;
+
 	struct option options[] = {
 		OPT_CALLBACK_F(0, "format", &format, N_("format"),
 			       N_("output format"),
@@ -944,6 +1236,7 @@ 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_BOOL(0, "concern", &show_concern, N_("show level of concern")),
 		OPT_END()
 	};
 
@@ -962,13 +1255,13 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
 	switch (format) {
 	case FORMAT_TABLE:
 		stats_table_setup_structure(&table, &stats);
-		stats_table_print_structure(&table);
+		stats_table_print_structure(&table, show_concern);
 		break;
 	case FORMAT_NEWLINE_TERMINATED:
-		structure_keyvalue_print(&stats, '=', '\n');
+		structure_keyvalue_print(&stats, '=', '\n', show_concern);
 		break;
 	case FORMAT_NUL_TERMINATED:
-		structure_keyvalue_print(&stats, '\n', '\0');
+		structure_keyvalue_print(&stats, '\n', '\0', show_concern);
 		break;
 	default:
 		BUG("invalid output format");
diff --git a/sideband.c b/sideband.c
index 374187402e..0e75ab2317 100644
--- a/sideband.c
+++ b/sideband.c
@@ -1,4 +1,4 @@
-#define USE_THE_REPOSITORY_VARIABLE
+define USE_THE_REPOSITORY_VARIABLE
 #define DISABLE_SIGN_COMPARE_WARNINGS
 
 #include "git-compat-util.h"
diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index 02cc2b594a..f1e3fd2a75 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -67,77 +67,7 @@ test_expect_success 'empty repository' '
 		EOF
 
 		git repo structure >out 2>err &&
-
-		test_cmp expect out &&
-		test_line_count = 0 err
-	)
-'
-
-test_expect_success SHA1 'repository with references and objects' '
-	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" &&
-
-		# 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            |      1     |
-		|     * Tags                |      1     |
-		|     * Remotes             |      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.03 MiB |
-		|     * Commits             | 217.92 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] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
-		[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
-		[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
-		[5] 97d808e45116bf02103490294d3d46dad7a2ac62
-		[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
-		EOF
-
-		git repo structure >out 2>err &&
+		cp out /tmp/test1-out &&
 
 		test_cmp expect out &&
 		test_line_count = 0 err
@@ -204,6 +134,59 @@ test_expect_success SHA1 'lines and nul format' '
 	)
 '
 
+test_expect_success SHA1 'lines format with concerns' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		# configure thresholds to get concern indications
+		git config repo.structure.concern.commit-count 7 &&
+		git config repo.structure.concern.commit-size 14 &&
+		git config repo.structure.concern.tree-size 10 &&
+		git config repo.structure.concern.blob-size 8 &&
+		git config repo.structure.concern.blob-inflated-size 10 &&
+		git config repo.structure.concern.tag-size 2 &&
+		git config repo.structure.concern.commit-largest 4 &&
+		git config repo.structure.concern.blob-largest 2 &&
+		touch configured &&
+		test_commit_bulk 1005 &&
+		git tag -a foo -m bar &&
+		oid="$(git rev-parse HEAD)" &&
+		git update-ref refs/remotes/origin/foo "$oid" &&
+
+		# Also creates a commit, tree, and blob.
+		git notes add -m foo &&
+
+
+		# 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 &&
+		objects.commits.count.concern_level=2
+		objects.trees.count.concern_level=0
+		objects.blobs.count.concern_level=0
+		objects.tags.count.concern_level=0
+		objects.commits.inflated_size.concern_level=0
+		objects.trees.inflated_size.concern_level=0
+		objects.blobs.inflated_size.concern_level=3
+		objects.tags.inflated_size.concern_level=0
+		objects.commits.disk_size.concern_level=3
+		objects.trees.disk_size.concern_level=8
+		objects.blobs.blob_size.concern_level=6
+		objects.tags.disk_size.concern_level=4
+		objects.commits.max_size.concern_level=3
+		objects.trees.max_size.concern_level=0
+		objects.blobs.max_size.concern_level=1
+		objects.tags.max_size.concern_level=0
+		EOF
+
+		git repo structure --concern --format=lines >actual 2>err &&
+		test_grep "concern_level" actual >actual-concerns &&
+		test_cmp expect actual-concerns &&
+		test_line_count = 0 err
+	)
+'
+
 test_expect_success 'progress meter option' '
 	test_when_finished "rm -rf repo" &&
 	git init repo &&

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v3 2/2] repo: make concern thresholds configurable
  2026-10-27 15:39 ` [PATCH v3 0/2] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
  2026-10-27 15:39   ` [PATCH v3 1/2] repo: add level of concern reporting Mark C. Chu-Carroll
@ 2026-10-27 15:39   ` Mark C. Chu-Carroll
  1 sibling, 0 replies; 20+ messages in thread
From: Mark C. Chu-Carroll @ 2026-10-27 15:39 UTC (permalink / raw)
  To: git; +Cc: Mark C. Chu-Carroll

The concern reporting mechanism so far provides a
measure of concern based on heuristic values estimated
by analyzing a collection of (hopefully) representative
repositories. But for different projects/usecases, the
thresholds at which a value becomes concerning may vary.

Provide a mechanism using git config to allow users to
configure per-repo concern levels for "git repo structure"
concern reporting.
---
 Documentation/config/repo.adoc |  59 ++++++++++++++
 Documentation/git-repo.adoc    |  11 +++
 builtin/repo.c                 |  62 +++++++-------
 sideband.c                     |   2 +-
 t/t1901-repo-structure.sh      | 180 ++++++++++++++++++++++++++++++++++++++---
 5 files changed, 273 insertions(+), 41 deletions(-)

diff --git a/Documentation/config/repo.adoc b/Documentation/config/repo.adoc
new file mode 100644
index 0000000000..811b099d72
--- /dev/null
+++ b/Documentation/config/repo.adoc
@@ -0,0 +1,59 @@
+`repo.concern.commit-count`::
+	Sets the value of the concern threshold for commit count.
+
+`repo.concern.tree-count`::
+	Sets the value of the concern threshold for tree count.
+
+`repo.concern.blob-count`::
+	Sets the value of the concern threshold for blob count.
+
+`repo.concern.tag-count`::
+	Sets the value of the concern threshold for tag count.
+
+`repo.concern.commit-size`::
+	Sets the value of the concern threshold for total commit size.
+
+`repo.concern.tree-size`::
+	Sets the value of the concern threshold for total tree size.
+
+`repo.concern.blob-size`::
+	Sets the value of the concern threshold for total blob size.
+
+`repo.concern.tag-size`::
+	Sets the value of the concern threshold for total tag size.
+
+`repo.concern.commit-inflated-size`::
+	Sets the value of the concern threshold for total size
+	of all inflated commits.
+
+`repo.concern.tree-inflated-size`::
+	Sets the value of the concern threshold for total size of
+	all inflated trees.
+
+`repo.concern.blob-inflated-size`::
+	Sets the value of the concern threshold for total size of
+	all inflated blobs.
+
+`repo.concern.tag-inflated-size`::
+	Sets the value of the concern threshold for the total size
+	of all inflated tags.
+
+`repo.concern.commit-largest`::
+	Sets the value of the concern threshold the size of the largest
+	commit.
+
+`repo.concern.tree-largest`::
+	Sets the value of the concern threshold for the size of the
+	largest tree.
+
+`repo.concern.blob-largest`::
+	Sets the value of the concern threshold for the size of the
+	largest blob.
+
+`repo.concern.tag-largest`::
+	Sets the value of the concern threshold for the size of the
+	largest tag.
+
+All of the concern thresholds are integer values for the
+floor of the base-2 log of the threshold value. The default values
+of each threshold are set to heuristically determined values.
diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index f186b27d87..516403e17d 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -101,6 +101,8 @@ statistic value. The value is considered concerning if it exceeds a heuristicall
 chosen threshold value; the degree of concern is the difference between the
 log_2 of the value, and the log_2 of the corresponding threshold.
 
+The threshold values are configurable; see the configuration section below.
+
 INFO KEYS
 ---------
 In order to obtain a set of values from `git repo info`, you should provide
@@ -153,6 +155,15 @@ using the `nul` format:
 git repo info --format=nul layout.bare layout.shallow
 ------------
 
+CONFIGURATION
+-------------
+
+include::includes/cmd-config-section-all.adoc[]
+
+:git-repo: 1
+include::config/repo.adoc[]
+
+
 SEE ALSO
 --------
 linkgit:git-rev-parse[1]
diff --git a/builtin/repo.c b/builtin/repo.c
index a450497d4e..ed579e6685 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -2,6 +2,7 @@
 
 #include "builtin.h"
 #include "commit.h"
+#include "config.h"
 #include "environment.h"
 #include "hash.h"
 #include "hex.h"
@@ -29,6 +30,8 @@
 #define REPO_STRUCTURE_USAGE \
 	"git repo structure [--format=(table|lines|nul) | -z] [--[no-]concern]"
 
+#define REPO_STRUCTURE_CONFIG_PREFIX "repo.concern."
+
 static const char *const repo_usage[] = {
 	REPO_INFO_USAGE,
 	REPO_STRUCTURE_USAGE,
@@ -386,40 +389,43 @@ enum concern_stat {
 	CONCERN_STAT_TREE_INFLATED_SIZE,
 	CONCERN_STAT_BLOB_INFLATED_SIZE,
 	CONCERN_STAT_TAG_INFLATED_SIZE,
+	CONCERN_STAT_MAXIMUM
 };
 
-struct concern_stat_threshold repo_structure_concern_thresholds[] = {
-	{ .name = "commit_count", .threshold = 19 },
-	{.name = "tree_count", .threshold = 21 },
-	{ .name = "blob_count", .threshold = 22 },
-	{ .name = "tag_count", .threshold = 15 },
-	{ .name = "commit_size", .threshold = 28 },
-	{ .name = "tree_size", .threshold = 27 },
-	{ .name = "blob_size", .threshold = 36 },
-	{ .name = "tags_size", .threshold = 18 },
-	{ .name = "commit_largest", .threshold = 18  },
-	{ .name = "tree_largest", .threshold = 18 },
-	{ .name = "blob_largest", .threshold = 26 },
-	{ .name = "tag_largest", .threshold =  15 },
-	{ .name = "commit_inflated_size", .threshold = 28 },
-	{ .name = "tree_inflated_size", .threshold = 32 },
-	{ .name = "blob_inflated_size", .threshold = 34 },
-	{ .name = "tag_inflated_size", .threshold = 25}
+struct concern_stat_threshold concern_stat_thresholds[] = {
+	{ .name = "commit-count", .threshold = 19 },
+	{ .name = "tree-count", .threshold = 21 },
+	{ .name = "blob-count", .threshold = 22 },
+	{ .name = "tag-count", .threshold = 15 },
+	{ .name = "commit-size", .threshold = 28 },
+	{ .name = "tree-size", .threshold = 27 },
+	{ .name = "blob-size", .threshold = 36 },
+	{ .name = "tag-size", .threshold = 18 },
+	{ .name = "commit-largest", .threshold = 18  },
+	{ .name = "tree-largest", .threshold = 18 },
+	{ .name = "blob-largest", .threshold = 26 },
+	{ .name = "tag-largest", .threshold =  15 },
+	{ .name = "commit-inflated-size", .threshold = 28 },
+	{ .name = "tree-inflated-size", .threshold = 32 },
+	{ .name = "blob-inflated-size", .threshold = 34 },
+	{ .name = "tag-inflated-size", .threshold = 25},
+	{ 0 }
 };
 
 /*
  * Concern thresholds aren't universal. We try to set reasonable default
  * values, but projects may want to set these thresholds differently. To
  * support that, they can set the thresholds for their project in the
- * project's git config, in the section "repo.structure.concern".
+ * project's git config, in the section "repo.concern".
   */
 static int config_concern_thresholds(const char *prefixed_var,
 				     const char *str_val,
 				     const struct config_context *ctx,
 				     void *cb UNUSED) {
-	const char *config_prefix = "repo.structure.concern.";
-	if (!strncmp(config_prefix, prefixed_var, strlen(config_prefix))) {
-		const char *concern_name = &prefixed_var[strlen(config_prefix)];
+	if (!strncmp(REPO_STRUCTURE_CONFIG_PREFIX, prefixed_var,
+	      strlen(REPO_STRUCTURE_CONFIG_PREFIX))) {
+		const char *concern_name =
+			&prefixed_var[strlen(REPO_STRUCTURE_CONFIG_PREFIX)];
 		for (int i = 0; concern_stat_thresholds[i].name != 0; i++) {
 			if (!strcmp(concern_stat_thresholds[i].name, concern_name)) {
 				int value = git_config_int(prefixed_var, str_val, ctx->kvi);
@@ -435,7 +441,7 @@ static int config_concern_thresholds(const char *prefixed_var,
 	return 0;
 }
 
-static size_t approx_log2(long value) {
+static size_t approx_log2(size_t value) {
 	int position = 0;
 	if (value <= 0) {
 		return 0;
@@ -450,7 +456,7 @@ static size_t approx_log2(long value) {
 static size_t repo_structure_concern_level(enum concern_stat stat_id,
 					   size_t value) {
 	size_t log_value;
-	if (stat_id >= ARRAY_SIZE(concern_stat_thresholds))
+	if (stat_id >= CONCERN_STAT_MAXIMUM)
 		BUG("unexpected repository stat type");
 	log_value = approx_log2(value);
 	if (log_value > concern_stat_thresholds[stat_id].threshold)
@@ -844,6 +850,7 @@ static void stats_table_print_structure(const struct stats_table *table,
 			strbuf_utf8_align(&buf, ALIGN_LEFT, concern_col_width, concern_str.buf);
 			strbuf_addstr(&buf, " |");
 		}
+		strbuf_release(&concern_str);
 		printf("%s\n", buf.buf);
 	}
 
@@ -977,7 +984,7 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	print_keyvalue("objects.blobs.disk_size", key_delim,
 		       stats->objects.disk_sizes.blobs, value_delim);
 	if (show_concern)
-		print_keyvalue("objects.blobs.blob_size.concern_level", key_delim,
+		print_keyvalue("objects.blobs.disk_size.concern_level", key_delim,
 			       repo_structure_concern_level(CONCERN_STAT_BLOB_SIZE,
 							    stats->objects.disk_sizes.blobs),
 			      value_delim);
@@ -997,7 +1004,7 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 							    stats->objects.largest.commit_size.value),
 			      value_delim);
 	print_object_data("objects.trees.max_size", key_delim,
-			  &stats->objects.largest.tree_size, value_delim);
+			 &stats->objects.largest.tree_size, value_delim);
 	if (show_concern)
 		print_keyvalue("objects.trees.max_size.concern_level", key_delim,
 			       repo_structure_concern_level(CONCERN_STAT_TREE_LARGEST,
@@ -1011,7 +1018,7 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 							    stats->objects.largest.blob_size.value),
 			      value_delim);
 	print_object_data("objects.tags.max_size", key_delim,
-			  &stats->objects.largest.tag_size, value_delim);
+			   &stats->objects.largest.tag_size, value_delim);
 	if (show_concern)
 		print_keyvalue("objects.tags.max_size.concern_level", key_delim,
 			       repo_structure_concern_level(CONCERN_STAT_TAG_LARGEST,
@@ -1243,7 +1250,8 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
 	argc = parse_options(argc, argv, prefix, options, repo_structure_usage, 0);
 	if (argc)
 		usage(_("too many arguments"));
-
+	if (show_concern)
+		repo_config(repo, &config_concern_thresholds, NULL);
 	repo_init_revisions(repo, &revs, prefix);
 
 	if (show_progress < 0)
diff --git a/sideband.c b/sideband.c
index 0e75ab2317..374187402e 100644
--- a/sideband.c
+++ b/sideband.c
@@ -1,4 +1,4 @@
-define USE_THE_REPOSITORY_VARIABLE
+#define USE_THE_REPOSITORY_VARIABLE
 #define DISABLE_SIGN_COMPARE_WARNINGS
 
 #include "git-compat-util.h"
diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index f1e3fd2a75..e8f1269ad4 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -67,13 +67,166 @@ test_expect_success 'empty repository' '
 		EOF
 
 		git repo structure >out 2>err &&
-		cp out /tmp/test1-out &&
 
 		test_cmp expect out &&
 		test_line_count = 0 err
 	)
 '
 
+test_expect_success SHA1 'repository with references and objects' '
+	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" &&
+
+		# 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            |      1     |
+		|     * Tags                |      1     |
+		|     * Remotes             |      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.03 MiB |
+		|     * Commits             | 217.92 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] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
+		[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[5] 97d808e45116bf02103490294d3d46dad7a2ac62
+		[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
+		EOF
+
+		git repo structure >out 2>err &&
+
+		test_cmp expect out &&
+		test_line_count = 0 err
+	)
+'
+
+test_expect_success SHA1 'concern flagging' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		# differences between zlib and zlib-ng can cause this
+		# test to fail; so we just turn it off.
+		git config core.compression 0 &&
+		# configure thresholds to get concern indications
+		git config repo.concern.commit-count 7 &&
+		git config repo.concern.commit-size 14 &&
+		git config repo.concern.tree-size 10 &&
+		git config repo.concern.blob-size 8 &&
+		git config repo.concern.blob-inflated-size 10 &&
+		git config repo.concern.tag-size 2 &&
+		git config repo.concern.commit-largest 4 &&
+		git config repo.concern.blob-largest 2 &&
+		test_commit_bulk 1005 &&
+		git tag -a foo -m bar &&
+		oid="$(git rev-parse HEAD)" &&
+		git update-ref refs/remotes/origin/foo "$oid" &&
+
+		# Also creates a commit, tree, and blob.
+		git notes add -m foo &&
+
+
+		# 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      | Concern  |
+		| ------------------------- | ---------- | -------- |
+		| * References              |            |          |
+		|   * Count                 |      4     |          |
+		|     * Branches            |      1     |          |
+		|     * Tags                |      1     |          |
+		|     * Remotes             |      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.03 MiB |          |
+		|     * Commits             | 217.92 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] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
+		[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[5] 97d808e45116bf02103490294d3d46dad7a2ac62
+		[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
+		EOF
+
+		git repo structure --concern >actual 2>err &&
+
+		test_cmp expect actual &&
+		test_line_count = 0 err
+	)
+'
+
 test_expect_success SHA1 'lines and nul format' '
 	test_when_finished "rm -rf repo" &&
 	git init repo &&
@@ -139,16 +292,18 @@ test_expect_success SHA1 'lines format with concerns' '
 	git init repo &&
 	(
 		cd repo &&
+		# differences between zlib and zlib-ng can cause this
+		# test to fail; so we just turn it off.
+		git config core.compression 0 &&
 		# configure thresholds to get concern indications
-		git config repo.structure.concern.commit-count 7 &&
-		git config repo.structure.concern.commit-size 14 &&
-		git config repo.structure.concern.tree-size 10 &&
-		git config repo.structure.concern.blob-size 8 &&
-		git config repo.structure.concern.blob-inflated-size 10 &&
-		git config repo.structure.concern.tag-size 2 &&
-		git config repo.structure.concern.commit-largest 4 &&
-		git config repo.structure.concern.blob-largest 2 &&
-		touch configured &&
+		git config repo.concern.commit-count 7 &&
+		git config repo.concern.commit-size 14 &&
+		git config repo.concern.tree-size 10 &&
+		git config repo.concern.blob-size 8 &&
+		git config repo.concern.blob-inflated-size 10 &&
+		git config repo.concern.tag-size 2 &&
+		git config repo.concern.commit-largest 4 &&
+		git config repo.concern.blob-largest 2 &&
 		test_commit_bulk 1005 &&
 		git tag -a foo -m bar &&
 		oid="$(git rev-parse HEAD)" &&
@@ -157,7 +312,6 @@ test_expect_success SHA1 'lines format with concerns' '
 		# 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".
@@ -172,8 +326,8 @@ test_expect_success SHA1 'lines format with concerns' '
 		objects.tags.inflated_size.concern_level=0
 		objects.commits.disk_size.concern_level=3
 		objects.trees.disk_size.concern_level=8
-		objects.blobs.blob_size.concern_level=6
-		objects.tags.disk_size.concern_level=4
+		objects.blobs.disk_size.concern_level=6
+		objects.tags.disk_size.concern_level=5
 		objects.commits.max_size.concern_level=3
 		objects.trees.max_size.concern_level=0
 		objects.blobs.max_size.concern_level=1

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* Re: [PATCH v2 3/3] repo: add documentation for level-of-concern.
  2026-10-27 14:55     ` Patrick Steinhardt
@ 2026-10-28 13:23       ` Mark C. Chu-Carroll
  0 siblings, 0 replies; 20+ messages in thread
From: Mark C. Chu-Carroll @ 2026-10-28 13:23 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git


On Tue, 27 Oct 2026 15:55:31 +0100, Patrick Steinhardt <ps@pks.im> wrote:
> On Mon, Oct 26, 2026 at 01:43:56PM -0400, Mark C. Chu-Carroll wrote:
> > ---
> >  Documentation/config/repo.adoc | 59 ++++++++++++++++++++++++++++++++++++++++++
> >  Documentation/git-repo.adoc    | 27 +++++++++++++++++--
> >  2 files changed, 84 insertions(+), 2 deletions(-)
> > 
> > diff --git a/Documentation/config/repo.adoc b/Documentation/config/repo.adoc
> > new file mode 100644
> > index 0000000000..811b099d72
> > --- /dev/null
> > +++ b/Documentation/config/repo.adoc
> > @@ -0,0 +1,59 @@
> > +`repo.concern.commit-count`::
> > +	Sets the value of the concern threshold for commit count.
> > +
> > +`repo.concern.tree-count`::
> > +	Sets the value of the concern threshold for tree count.
> 
> I think this commit can be split and squashed into the other two
> commits:
> 
>   - The change to synopsis and documentation of "--concern" should be
>     squashed into the first commit.
> 
>   - These config values here should be squashed into the second commit.


Yeah, that makes sense. 
(Note, this will appear out of order; original send failed yesterday.)

       -Mark

^ permalink raw reply	[flat|nested] 20+ messages in thread

* [PATCH v4 0/2] repo: add degree-of-concern reporting to structure
  2026-10-21 13:55 [PATCH 0/3] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
                   ` (4 preceding siblings ...)
  2026-10-27 15:39 ` [PATCH v3 0/2] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
@ 2026-10-29 17:52 ` Mark C. Chu-Carroll
  2026-10-29 17:52   ` [PATCH v4 1/2] repo: add level of concern reporting Mark C. Chu-Carroll
  2026-10-29 17:52   ` [PATCH v4 2/2] repo: make concern thresholds configurable Mark C. Chu-Carroll
  5 siblings, 2 replies; 20+ messages in thread
From: Mark C. Chu-Carroll @ 2026-10-29 17:52 UTC (permalink / raw)
  To: git; +Cc: Mark C. Chu-Carroll

"git repo structure" computes a set of statistics that are useful for
diagnosing performance issues in a git repository. But for a typical
user who hasn't encountered any issues yet, it can be difficult to
interpret the data - when should a value be considered concerning?

This patchset introduces a feature to "git repo structure" that reports
on how concerning certain measures of storage structure  are for a
repository. In table mode, concern is displayed as a column containing
a number of stars indicating the degree of concern; in line or nul mode,
concerns are displayed as additional lines with a numeric value. This 
provides a functionality equivalent to the concern column provided 
by "git-sizer", although the implementation is different.

To select defaults for the thresholds where a value becomes concern,
a (hopefully) representative selection of repositories were analyzed.
Based on empirical exploration of the data, the measurement that seemed
closest to accurately assessing when a repository's structure becomes
concerning was based on the base 2 logarithm of the value. Across all of
the sample repositories, for all of the measurements, the point at which
things become concerning was when a base 2 logarithm of a statistic's
value was more than 1 standard deviation from the mean of the log_2
values of that stastistic in the sample set.  Based on this, the concern
threshold was set for each of the measures.

While this seems like a reasonable approach for setting default
thresholds, it's obviously still very ad-hoc. It's likely that over
time, the default values will evolve; and in any case, some projects
will want to set their own thresholds. To allow that, this includes
support for configuring the thresholds on a per-repository basis.

Overview of the change:
         
* The first commit adds the command-line option "--concern", and creates the basic implementation of concern reporting.
* The next commit adds configurability for the concern thresholds.

Signed-off-by: Mark C. Chu-Carroll <markchucarroll@fastmail.com>
---
Changes in v4:
- Reworked the history; the previous round had a history-rewrite error.
- Removed the duplication of configuration keys for the concern thresholds.
- Changed the prefix of the config keys.
- readability/quality of life naming changes.
- Link to v3: https://patch.msgid.link/20261027-b4-repo-structure-concern-v3-0-98d7a5704170@fastmail.com

Changes in v3:
- Merge the documentation commit backwards into the other two commits.
- Link to v2: https://patch.msgid.link/20261026-b4-repo-structure-concern-v2-0-e0df34228dc8@fastmail.com

Changes in v2:
- Fix memory leak in concern string generation.
- Fix CI/CD test issue involving zlib versions.
- Updating the naming of the config value prefix.
- Link to v1: https://patch.msgid.link/20261021-b4-repo-structure-concern-v1-0-e3010552292c@fastmail.com

To: git@vger.kernel.org

---
Mark C. Chu-Carroll (2):
      repo: add level of concern reporting
      repo: make concern thresholds configurable

 Documentation/config/repo.adoc |  59 ++++++
 Documentation/git-repo.adoc    |  27 ++-
 builtin/repo.c                 | 408 ++++++++++++++++++++++++++++++++++++++---
 repo.c                         |   1 +
 t/t1901-repo-structure.sh      | 137 ++++++++++++++
 5 files changed, 602 insertions(+), 30 deletions(-)

Range-diff versus v3:

1:  0f4e75e270 ! 1:  5f67be20e2 repo: add level of concern reporting
    @@ Commit message
         interpret the data - when should a value be considered
         concerning?
     
    -    Add an option to "git repo structure" that adds a "degree
    -    of concern" column to the structure report, which indicates
    -    when a stat values has reached a point where it might be
    -    concerning, and indicate how severe the concern should be.
    +    Add a new option to "git repo structure" which causes it
    +    to generate an additional column providing a level of concern
    +    for many values.
    +
    +    To select defaults for the thresholds where a value becomes concern,
    +    a (hopefully) representative selection of repositories were analyzed.
    +    Based on empirical exploration of the data, the measurement that seemed
    +    closest to accurately assessing when a repository's structure becomes
    +    concerning was based on the base 2 logarithm of the value. Across all of
    +    the sample repositories, for all of the measurements, the point at which
    +    things become problematic was when a base 2 logarithm of a value was
    +    more than 1 standard deviation from the mean of the log_2 values in
    +    the sample set.
    +
    +    Signed-off-by: Mark C. Chu-Carroll <markchucarroll@fastmail.com>
     
      ## Documentation/git-repo.adoc ##
     @@ Documentation/git-repo.adoc: SYNOPSIS
    @@ Documentation/git-repo.adoc: supported:
     
      ## builtin/repo.c ##
     @@
    + #include "tree.h"
    + #include "tree-walk.h"
    + #include "utf8.h"
    ++#include "wrapper.h"
    + 
    + #define REPO_INFO_USAGE \
    + 	"git repo info [--format=(lines|nul) | -z] [--all | <key>...]", \
      	"git repo info --keys [--format=(lines|nul) | -z]"
      
      #define REPO_STRUCTURE_USAGE \
    @@ builtin/repo.c: struct stats_table {
     +	size_t threshold;
     +};
     +
    -+enum concern_stat {
    -+	CONCERN_STAT_COMMIT_COUNT,
    -+	CONCERN_STAT_TREE_COUNT,
    -+	CONCERN_STAT_BLOB_COUNT,
    -+	CONCERN_STAT_TAG_COUNT,
    -+	CONCERN_STAT_COMMIT_SIZE,
    -+	CONCERN_STAT_TREE_SIZE,
    -+	CONCERN_STAT_BLOB_SIZE,
    -+	CONCERN_STAT_TAG_SIZE,
    -+	CONCERN_STAT_COMMIT_LARGEST,
    -+	CONCERN_STAT_TREE_LARGEST,
    -+	CONCERN_STAT_BLOB_LARGEST,
    -+	CONCERN_STAT_TAG_LARGEST,
    -+	CONCERN_STAT_COMMIT_INFLATED_SIZE,
    -+	CONCERN_STAT_TREE_INFLATED_SIZE,
    -+	CONCERN_STAT_BLOB_INFLATED_SIZE,
    -+	CONCERN_STAT_TAG_INFLATED_SIZE,
    -+};
    ++#define CONCERN_STAT_COMMIT_COUNT "commit-count"
    ++#define CONCERN_STAT_TREE_COUNT "tree-count"
    ++#define CONCERN_STAT_BLOB_COUNT "blob-count"
    ++#define CONCERN_STAT_TAG_COUNT "tag-count"
    ++#define CONCERN_STAT_COMMIT_SIZE "commit-size"
    ++#define CONCERN_STAT_TREE_SIZE "tree-size"
    ++#define CONCERN_STAT_BLOB_SIZE "blob-size"
    ++#define CONCERN_STAT_TAG_SIZE "tag-size"
    ++#define CONCERN_STAT_COMMIT_LARGEST "commit-largest"
    ++#define CONCERN_STAT_TREE_LARGEST "tree-largest"
    ++#define CONCERN_STAT_BLOB_LARGEST "blob-largest"
    ++#define CONCERN_STAT_TAG_LARGEST "tag-largest"
    ++#define CONCERN_STAT_COMMIT_INFLATED_SIZE "commit-inflated-size"
    ++#define CONCERN_STAT_TREE_INFLATED_SIZE "tree-inflated-size"
    ++#define CONCERN_STAT_BLOB_INFLATED_SIZE "blob-inflated-size"
    ++#define CONCERN_STAT_TAG_INFLATED_SIZE "tag-inflated-size"
     +
     +struct concern_stat_threshold repo_structure_concern_thresholds[] = {
    -+	{ .name = "commit_count", .threshold = 19 },
    -+	{.name = "tree_count", .threshold = 21 },
    -+	{ .name = "blob_count", .threshold = 22 },
    -+	{ .name = "tag_count", .threshold = 15 },
    -+	{ .name = "commit_size", .threshold = 28 },
    -+	{ .name = "tree_size", .threshold = 27 },
    -+	{ .name = "blob_size", .threshold = 36 },
    -+	{ .name = "tags_size", .threshold = 18 },
    -+	{ .name = "commit_largest", .threshold = 18  },
    -+	{ .name = "tree_largest", .threshold = 18 },
    -+	{ .name = "blob_largest", .threshold = 26 },
    -+	{ .name = "tag_largest", .threshold =  15 },
    -+	{ .name = "commit_inflated_size", .threshold = 28 },
    -+	{ .name = "tree_inflated_size", .threshold = 32 },
    -+	{ .name = "blob_inflated_size", .threshold = 34 },
    -+	{ .name = "tag_inflated_size", .threshold = 25}
    ++	{ .name = CONCERN_STAT_COMMIT_COUNT, .threshold = 19 },
    ++	{ .name = CONCERN_STAT_TREE_COUNT, .threshold = 21 },
    ++	{ .name = CONCERN_STAT_BLOB_COUNT, .threshold = 22 },
    ++	{ .name = CONCERN_STAT_TAG_COUNT, .threshold = 15 },
    ++	{ .name = CONCERN_STAT_COMMIT_SIZE, .threshold = 28 },
    ++	{ .name = CONCERN_STAT_TREE_SIZE, .threshold = 27 },
    ++	{ .name = CONCERN_STAT_BLOB_SIZE, .threshold = 36 },
    ++	{ .name = CONCERN_STAT_TAG_SIZE, .threshold = 18 },
    ++	{ .name = CONCERN_STAT_COMMIT_LARGEST, .threshold = 18  },
    ++	{ .name = CONCERN_STAT_TREE_LARGEST, .threshold = 18 },
    ++	{ .name = CONCERN_STAT_BLOB_LARGEST, .threshold = 26 },
    ++	{ .name = CONCERN_STAT_TAG_LARGEST, .threshold =  15 },
    ++	{ .name = CONCERN_STAT_COMMIT_INFLATED_SIZE, .threshold = 28 },
    ++	{ .name = CONCERN_STAT_TREE_INFLATED_SIZE, .threshold = 32 },
    ++	{ .name = CONCERN_STAT_BLOB_INFLATED_SIZE, .threshold = 34 },
    ++	{ .name = CONCERN_STAT_TAG_INFLATED_SIZE, .threshold = 25 },
    ++	{ 0 }
     +};
     +
    -+/*
    -+ * Concern thresholds aren't universal. We try to set reasonable default
    -+ * values, but projects may want to set these thresholds differently. To
    -+ * support that, they can set the thresholds for their project in the
    -+ * project's git config, in the section "repo.structure.concern".
    -+  */
    -+static int config_concern_thresholds(const char *prefixed_var,
    -+				     const char *str_val,
    -+				     const struct config_context *ctx,
    -+				     void *cb UNUSED) {
    -+	const char *config_prefix = "repo.structure.concern.";
    -+	if (!strncmp(config_prefix, prefixed_var, strlen(config_prefix))) {
    -+		const char *concern_name = &prefixed_var[strlen(config_prefix)];
    -+		for (int i = 0; concern_stat_thresholds[i].name != 0; i++) {
    -+			if (!strcmp(concern_stat_thresholds[i].name, concern_name)) {
    -+				int value = git_config_int(prefixed_var, str_val, ctx->kvi);
    -+				if (value < 0)
    -+					return -1;
    -+				concern_stat_thresholds[i].threshold = value;
    -+				return 0;
    -+			}
    ++static size_t get_concern_threshold(const char *name, struct concern_stat_threshold *thresholds) {
    ++	for (size_t i = 0; thresholds[i].name; i++) {
    ++		if (!strcmp(thresholds[i].name, name)) {
    ++			return thresholds[i].threshold;
     +		}
    -+		warning(_("Invalid concern threshold name: '%s'"), concern_name);
    -+		return 0;
     +	}
     +	return 0;
     +}
     +
    -+static size_t approx_log2(long value) {
    -+	int position = 0;
    -+	if (value <= 0) {
    -+		return 0;
    -+	}
    -+	while (value > 1) {
    -+		position++;
    -+		value >>= 1;
    -+	}
    -+	return position;
    -+}
    -+
    -+static size_t repo_structure_concern_level(enum concern_stat stat_id,
    -+					   size_t value) {
    ++static size_t repo_structure_concern_level(const char* concern_name,
    ++					   size_t value,
    ++					   struct concern_stat_threshold *thresholds) {
    ++	const size_t threshold = get_concern_threshold(concern_name, thresholds);
     +	size_t log_value;
    -+	if (stat_id >= ARRAY_SIZE(concern_stat_thresholds))
    -+		BUG("unexpected repository stat type");
    -+	log_value = approx_log2(value);
    -+	if (log_value > concern_stat_thresholds[stat_id].threshold)
    -+		return log_value -
    -+			concern_stat_thresholds[stat_id].threshold;
    ++	if (threshold == 0)
    ++		BUG("unexpected repository stat threshold: '%s'", concern_name);
    ++	log_value = log2u(value);
    ++	if (log_value > threshold)
    ++		return log_value - threshold;
     +	else
     +		return 0;
     +}
    @@ builtin/repo.c: static void stats_table_vaddf(struct stats_table *table,
      		int unit_width = utf8_strwidth(entry->unit);
      		if (unit_width > table->unit_col_width)
     @@ builtin/repo.c: static void stats_table_addf(struct stats_table *table, const char *format, ...)
    + 	va_end(ap);
      }
      
    ++// define a symbol for 'no concern value for this stat'; this will
    ++// make the code below easier to read.
    ++#define CONCERN_STAT_VALUE_NONE 0
    ++
      static void stats_table_count_addf(struct stats_table *table, size_t value,
     -				   const char *format, ...)
     +				   size_t concern, const char *format, ...)
    @@ builtin/repo.c: static void stats_table_setup_structure(struct stats_table *tabl
     -	stats_table_count_addf(table, refs->tags, "    * %s", _("Tags"));
     -	stats_table_count_addf(table, refs->remotes, "    * %s", _("Remotes"));
     -	stats_table_count_addf(table, refs->others, "    * %s", _("Others"));
    -+	stats_table_count_addf(table, ref_total, 0,
    +-
    ++	stats_table_count_addf(table, ref_total, CONCERN_STAT_VALUE_NONE,
     +			       "  * %s", _("Count"));
    -+	stats_table_count_addf(table, refs->branches, 0,
    ++	stats_table_count_addf(table, refs->branches, CONCERN_STAT_VALUE_NONE,
     +			       "    * %s", _("Branches"));
     +	stats_table_count_addf(table, refs->tags,
     +			       repo_structure_concern_level(
     +				       CONCERN_STAT_TAG_COUNT,
    -+				       refs->tags),
    ++				       refs->tags,
    ++    			               repo_structure_concern_thresholds),
     +			       "    * %s", _("Tags"));
    -+	stats_table_count_addf(table, refs->remotes, 0,
    ++	stats_table_count_addf(table, refs->remotes, CONCERN_STAT_VALUE_NONE,
     +			       "    * %s", _("Remotes"));
    -+	stats_table_count_addf(table, refs->others, 0,
    ++	stats_table_count_addf(table, refs->others, CONCERN_STAT_VALUE_NONE,
     +			       "    * %s", _("Others"));
    - 
      	object_count_total = get_total_object_values(&objects->type_counts);
      	stats_table_addf(table, "");
      	stats_table_addf(table, "* %s", _("Reachable objects"));
     -	stats_table_count_addf(table, object_count_total, "  * %s", _("Count"));
    -+	stats_table_count_addf(table, object_count_total, 0,
    ++	stats_table_count_addf(table, object_count_total, CONCERN_STAT_VALUE_NONE,
     +			       "  * %s", _("Count"));
      	stats_table_count_addf(table, objects->type_counts.commits,
     +			       repo_structure_concern_level(
     +				       CONCERN_STAT_COMMIT_COUNT,
    -+				       objects->type_counts.commits),
    ++				       objects->type_counts.commits,
    ++				       repo_structure_concern_thresholds),
      			       "    * %s", _("Commits"));
      	stats_table_count_addf(table, objects->type_counts.trees,
     +			       repo_structure_concern_level(
     +				       CONCERN_STAT_TREE_COUNT,
    -+				       objects->type_counts.trees),
    ++				       objects->type_counts.trees,
    ++				       repo_structure_concern_thresholds),
      			       "    * %s", _("Trees"));
      	stats_table_count_addf(table, objects->type_counts.blobs,
     +			       repo_structure_concern_level(
     +				       CONCERN_STAT_BLOB_COUNT,
    -+				       objects->type_counts.blobs),
    ++				       objects->type_counts.blobs,
    ++				       repo_structure_concern_thresholds),
      			       "    * %s", _("Blobs"));
      	stats_table_count_addf(table, objects->type_counts.tags,
     +			       repo_structure_concern_level(
     +				       CONCERN_STAT_TAG_COUNT,
    -+				       objects->type_counts.tags),
    ++				       objects->type_counts.tags,
    ++				       repo_structure_concern_thresholds),
      			       "    * %s", _("Tags"));
      
      	inflated_object_total = get_total_object_values(&objects->inflated_sizes);
     -	stats_table_size_addf(table, inflated_object_total,
    -+	stats_table_size_addf(table, inflated_object_total, 0,
    ++	stats_table_size_addf(table, inflated_object_total, CONCERN_STAT_VALUE_NONE,
      			      "  * %s", _("Inflated size"));
      	stats_table_size_addf(table, objects->inflated_sizes.commits,
     +			      repo_structure_concern_level(
     +				      CONCERN_STAT_COMMIT_INFLATED_SIZE,
    -+				      objects->inflated_sizes.commits),
    ++				      objects->inflated_sizes.commits,
    ++				      repo_structure_concern_thresholds),
      			      "    * %s", _("Commits"));
      	stats_table_size_addf(table, objects->inflated_sizes.trees,
     +			      repo_structure_concern_level(
     +				      CONCERN_STAT_TREE_INFLATED_SIZE,
    -+				      objects->inflated_sizes.trees),
    ++				      objects->inflated_sizes.trees,
    ++				      repo_structure_concern_thresholds),
      			      "    * %s", _("Trees"));
      	stats_table_size_addf(table, objects->inflated_sizes.blobs,
     +			      repo_structure_concern_level(
     +				      CONCERN_STAT_BLOB_INFLATED_SIZE,
    -+				      objects->inflated_sizes.blobs),
    ++				      objects->inflated_sizes.blobs,
    ++				      repo_structure_concern_thresholds),
      			      "    * %s", _("Blobs"));
      	stats_table_size_addf(table, objects->inflated_sizes.tags,
     +			      repo_structure_concern_level(
     +				      CONCERN_STAT_TAG_INFLATED_SIZE,
    -+				      objects->inflated_sizes.tags),
    ++				      objects->inflated_sizes.tags,
    ++				      repo_structure_concern_thresholds),
      			      "    * %s", _("Tags"));
      
      	disk_object_total = get_total_object_values(&objects->disk_sizes);
     -	stats_table_size_addf(table, disk_object_total,
    -+	stats_table_size_addf(table, disk_object_total, 0,
    ++	stats_table_size_addf(table, disk_object_total, CONCERN_STAT_VALUE_NONE,
      			      "  * %s", _("Disk size"));
      	stats_table_size_addf(table, objects->disk_sizes.commits,
     +			      repo_structure_concern_level(
     +				      CONCERN_STAT_COMMIT_SIZE,
    -+				      objects->disk_sizes.commits),
    ++				      objects->disk_sizes.commits,
    ++				      repo_structure_concern_thresholds),
      			      "    * %s", _("Commits"));
      	stats_table_size_addf(table, objects->disk_sizes.trees,
     +			      repo_structure_concern_level(
     +				      CONCERN_STAT_TREE_SIZE,
    -+				      objects->disk_sizes.trees),
    ++				      objects->disk_sizes.trees,
    ++				      repo_structure_concern_thresholds),
      			      "    * %s", _("Trees"));
      	stats_table_size_addf(table, objects->disk_sizes.blobs,
     +			      repo_structure_concern_level(
     +				      CONCERN_STAT_BLOB_SIZE,
    -+				      objects->disk_sizes.blobs),
    ++				      objects->disk_sizes.blobs,
    ++				      repo_structure_concern_thresholds),
      			      "    * %s", _("Blobs"));
      	stats_table_size_addf(table, objects->disk_sizes.tags,
     +			      repo_structure_concern_level(
     +				      CONCERN_STAT_TAG_SIZE,
    -+				      objects->disk_sizes.tags),
    ++				      objects->disk_sizes.tags,
    ++				      repo_structure_concern_thresholds),
      			      "    * %s", _("Tags"));
      
      	stats_table_addf(table, "");
    @@ builtin/repo.c: static void stats_table_setup_structure(struct stats_table *tabl
      				     objects->largest.commit_size.value,
     +				     repo_structure_concern_level(
     +					     CONCERN_STAT_COMMIT_LARGEST,
    -+					     objects->largest.commit_size.value),
    ++					     objects->largest.commit_size.value,
    ++					     repo_structure_concern_thresholds),
      				     "    * %s", _("Maximum size"));
      	stats_table_object_count_addf(table,
      				      &objects->largest.parent_count.oid,
      				      objects->largest.parent_count.value,
    -+				      0,
    ++				      CONCERN_STAT_VALUE_NONE,
      				      "    * %s", _("Maximum parents"));
      	stats_table_addf(table, "  * %s", _("Trees"));
      	stats_table_object_size_addf(table,
      				     &objects->largest.tree_size.oid,
      				     objects->largest.tree_size.value,
    -+				      repo_structure_concern_level(
    -+					      CONCERN_STAT_TREE_LARGEST,
    -+					      objects->largest.tree_size.value),
    ++				     repo_structure_concern_level(
    ++					     CONCERN_STAT_TREE_LARGEST,
    ++					     objects->largest.commit_size.value,
    ++					     repo_structure_concern_thresholds),
      				     "    * %s", _("Maximum size"));
    ++
      	stats_table_object_count_addf(table,
    - 				      &objects->largest.tree_entries.oid,
    - 				      objects->largest.tree_entries.value,
    -+				      0,
    - 				      "    * %s", _("Maximum entries"));
    +-				      &objects->largest.tree_entries.oid,
    +-				      objects->largest.tree_entries.value,
    +-				      "    * %s", _("Maximum entries"));
    ++				     &objects->largest.tree_entries.oid,
    ++				     objects->largest.tree_entries.value,
    ++			             CONCERN_STAT_VALUE_NONE,
    ++				     "    * %s", _("Maximum entries"));
      	stats_table_addf(table, "  * %s", _("Blobs"));
      	stats_table_object_size_addf(table,
      				     &objects->largest.blob_size.oid,
      				     objects->largest.blob_size.value,
     +				     repo_structure_concern_level(
     +					     CONCERN_STAT_BLOB_LARGEST,
    -+					     objects->largest.blob_size.value),
    ++					     objects->largest.blob_size.value,
    ++					     repo_structure_concern_thresholds),
      				     "    * %s", _("Maximum size"));
      	stats_table_addf(table, "  * %s", _("Tags"));
      	stats_table_object_size_addf(table,
    @@ builtin/repo.c: static void stats_table_setup_structure(struct stats_table *tabl
      				     objects->largest.tag_size.value,
     +				     repo_structure_concern_level(
     +					     CONCERN_STAT_TAG_LARGEST,
    -+					     objects->largest.tag_size.value),
    ++					     objects->largest.tag_size.value,
    ++					     repo_structure_concern_thresholds),
      				     "    * %s", _("Maximum size"));
      }
      
    @@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stat
      		       stats->objects.type_counts.commits, value_delim);
     +	if (show_concern)
     +		print_keyvalue("objects.commits.count.concern_level", key_delim,
    -+			      repo_structure_concern_level(CONCERN_STAT_COMMIT_COUNT,
    -+							   stats->objects.type_counts.commits),
    -+			      value_delim);
    ++			       repo_structure_concern_level(
    ++				       CONCERN_STAT_COMMIT_COUNT,
    ++				       stats->objects.type_counts.commits,
    ++				       repo_structure_concern_thresholds),
    ++			       value_delim);
      	print_keyvalue("objects.trees.count", key_delim,
      		       stats->objects.type_counts.trees, value_delim);
     +	if (show_concern)
     +		print_keyvalue("objects.trees.count.concern_level", key_delim,
    -+			      repo_structure_concern_level(CONCERN_STAT_TREE_COUNT,
    -+							   stats->objects.type_counts.trees),
    -+			      value_delim);
    ++			       repo_structure_concern_level(
    ++				       CONCERN_STAT_TREE_COUNT,
    ++				       stats->objects.type_counts.trees,
    ++				       repo_structure_concern_thresholds),
    ++			       value_delim);
      	print_keyvalue("objects.blobs.count", key_delim,
      		       stats->objects.type_counts.blobs, value_delim);
     +	if (show_concern)
     +		print_keyvalue("objects.blobs.count.concern_level", key_delim,
    -+			      repo_structure_concern_level(CONCERN_STAT_BLOB_COUNT,
    -+							   stats->objects.type_counts.blobs),
    -+			      value_delim);
    ++			       repo_structure_concern_level(
    ++				       CONCERN_STAT_BLOB_COUNT,
    ++				       stats->objects.type_counts.blobs,
    ++				       repo_structure_concern_thresholds),
    ++		 value_delim);
      	print_keyvalue("objects.tags.count", key_delim,
    - 		       stats->objects.type_counts.tags, value_delim);
    +-		       stats->objects.type_counts.tags, value_delim);
    ++		stats->objects.type_counts.tags, value_delim);
     +	if (show_concern)
     +		print_keyvalue("objects.tags.count.concern_level", key_delim,
    -+			      repo_structure_concern_level(CONCERN_STAT_TAG_COUNT,
    -+							   stats->objects.type_counts.tags),
    -+			      value_delim);
    ++			       repo_structure_concern_level(
    ++				       CONCERN_STAT_TAG_COUNT,
    ++				       stats->objects.type_counts.tags,
    ++				       repo_structure_concern_thresholds),
    ++		 	       value_delim);
      
      	print_keyvalue("objects.commits.inflated_size", key_delim,
    - 		       stats->objects.inflated_sizes.commits, value_delim);
    +-		       stats->objects.inflated_sizes.commits, value_delim);
    ++		stats->objects.inflated_sizes.commits, value_delim);
     +	if (show_concern)
     +		print_keyvalue("objects.commits.inflated_size.concern_level", key_delim,
    -+			       repo_structure_concern_level(CONCERN_STAT_COMMIT_INFLATED_SIZE,
    -+							    stats->objects.inflated_sizes.commits),
    -+			      value_delim);
    ++			       repo_structure_concern_level(
    ++				       CONCERN_STAT_COMMIT_INFLATED_SIZE,
    ++				       stats->objects.inflated_sizes.commits,
    ++				       repo_structure_concern_thresholds),
    ++			       value_delim);
      	print_keyvalue("objects.trees.inflated_size", key_delim,
    - 		       stats->objects.inflated_sizes.trees, value_delim);
    +-		       stats->objects.inflated_sizes.trees, value_delim);
    ++		stats->objects.inflated_sizes.trees, value_delim);
     +	if (show_concern)
     +		print_keyvalue("objects.trees.inflated_size.concern_level", key_delim,
    -+			       repo_structure_concern_level(CONCERN_STAT_TREE_INFLATED_SIZE,
    -+							    stats->objects.inflated_sizes.trees),
    -+			      value_delim);
    ++			       repo_structure_concern_level(
    ++				       CONCERN_STAT_TREE_INFLATED_SIZE,
    ++				       stats->objects.inflated_sizes.trees,
    ++				       repo_structure_concern_thresholds),
    ++			       value_delim);
     +
      	print_keyvalue("objects.blobs.inflated_size", key_delim,
      		       stats->objects.inflated_sizes.blobs, value_delim);
     +	if (show_concern)
     +		print_keyvalue("objects.blobs.inflated_size.concern_level", key_delim,
    -+			       repo_structure_concern_level(CONCERN_STAT_BLOB_INFLATED_SIZE,
    -+							    stats->objects.inflated_sizes.blobs),
    -+			      value_delim);
    ++			       repo_structure_concern_level(
    ++				       CONCERN_STAT_BLOB_INFLATED_SIZE,
    ++				       stats->objects.inflated_sizes.blobs,
    ++				       repo_structure_concern_thresholds),
    ++			       value_delim);
      	print_keyvalue("objects.tags.inflated_size", key_delim,
      		       stats->objects.inflated_sizes.tags, value_delim);
     +	if (show_concern)
     +		print_keyvalue("objects.tags.inflated_size.concern_level", key_delim,
    -+			       repo_structure_concern_level(CONCERN_STAT_TAG_INFLATED_SIZE,
    -+							    stats->objects.inflated_sizes.tags),
    -+			      value_delim);
    ++			       repo_structure_concern_level(
    ++				       CONCERN_STAT_TAG_INFLATED_SIZE,
    ++				       stats->objects.inflated_sizes.tags,
    ++				       repo_structure_concern_thresholds),
    ++			       value_delim);
      
      	print_keyvalue("objects.commits.disk_size", key_delim,
      		       stats->objects.disk_sizes.commits, value_delim);
     +	if (show_concern)
     +		print_keyvalue("objects.commits.disk_size.concern_level", key_delim,
    -+			       repo_structure_concern_level(CONCERN_STAT_COMMIT_SIZE,
    -+							    stats->objects.disk_sizes.commits),
    -+			      value_delim);
    ++			       repo_structure_concern_level(
    ++				       CONCERN_STAT_COMMIT_SIZE,
    ++				       stats->objects.disk_sizes.commits,
    ++				       repo_structure_concern_thresholds),
    ++			       value_delim);
      	print_keyvalue("objects.trees.disk_size", key_delim,
      		       stats->objects.disk_sizes.trees, value_delim);
     +	if (show_concern)
     +		print_keyvalue("objects.trees.disk_size.concern_level", key_delim,
    -+			       repo_structure_concern_level(CONCERN_STAT_TREE_SIZE,
    -+							    stats->objects.disk_sizes.trees),
    -+			      value_delim);
    ++			       repo_structure_concern_level(
    ++				       CONCERN_STAT_TREE_SIZE,
    ++				       stats->objects.disk_sizes.trees,
    ++				       repo_structure_concern_thresholds),
    ++			       value_delim);
      	print_keyvalue("objects.blobs.disk_size", key_delim,
      		       stats->objects.disk_sizes.blobs, value_delim);
     +	if (show_concern)
    -+		print_keyvalue("objects.blobs.blob_size.concern_level", key_delim,
    -+			       repo_structure_concern_level(CONCERN_STAT_BLOB_SIZE,
    -+							    stats->objects.disk_sizes.blobs),
    ++
    ++		print_keyvalue("objects.blobs.disk_size.concern_level", key_delim,
    ++			       repo_structure_concern_level(
    ++				       CONCERN_STAT_BLOB_SIZE,
    ++				       stats->objects.disk_sizes.blobs,
    ++				       repo_structure_concern_thresholds),
     +			      value_delim);
      	print_keyvalue("objects.tags.disk_size", key_delim,
      		       stats->objects.disk_sizes.tags, value_delim);
     +	if (show_concern)
     +		print_keyvalue("objects.tags.disk_size.concern_level", key_delim,
    -+			       repo_structure_concern_level(CONCERN_STAT_TAG_SIZE,
    -+							    stats->objects.disk_sizes.tags),
    ++			       repo_structure_concern_level(
    ++				       CONCERN_STAT_TAG_SIZE,
    ++				       stats->objects.disk_sizes.tags,
    ++				       repo_structure_concern_thresholds),
     +			      value_delim);
      
      	print_object_data("objects.commits.max_size", key_delim,
      			  &stats->objects.largest.commit_size, value_delim);
     +	if (show_concern)
     +		print_keyvalue("objects.commits.max_size.concern_level", key_delim,
    -+			       repo_structure_concern_level(CONCERN_STAT_COMMIT_LARGEST,
    -+							    stats->objects.largest.commit_size.value),
    -+			      value_delim);
    ++			       repo_structure_concern_level(
    ++				       CONCERN_STAT_COMMIT_LARGEST,
    ++				       stats->objects.largest.commit_size.value,
    ++				       repo_structure_concern_thresholds),
    ++			       value_delim);
      	print_object_data("objects.trees.max_size", key_delim,
      			  &stats->objects.largest.tree_size, value_delim);
     +	if (show_concern)
     +		print_keyvalue("objects.trees.max_size.concern_level", key_delim,
    -+			       repo_structure_concern_level(CONCERN_STAT_TREE_LARGEST,
    -+							    stats->objects.largest.tree_size.value),
    -+			      value_delim);
    ++			       repo_structure_concern_level(
    ++				       CONCERN_STAT_TREE_LARGEST,
    ++				       stats->objects.largest.tree_size.value,
    ++				       repo_structure_concern_thresholds),
    ++			       value_delim);
      	print_object_data("objects.blobs.max_size", key_delim,
      			  &stats->objects.largest.blob_size, value_delim);
     +	if (show_concern)
     +		print_keyvalue("objects.blobs.max_size.concern_level", key_delim,
    -+			       repo_structure_concern_level(CONCERN_STAT_BLOB_LARGEST,
    -+							    stats->objects.largest.blob_size.value),
    -+			      value_delim);
    ++			       repo_structure_concern_level(
    ++				       CONCERN_STAT_BLOB_LARGEST,
    ++				       stats->objects.largest.blob_size.value,
    ++				       repo_structure_concern_thresholds),
    ++			       value_delim);
      	print_object_data("objects.tags.max_size", key_delim,
    - 			  &stats->objects.largest.tag_size, value_delim);
    +-			  &stats->objects.largest.tag_size, value_delim);
    ++		          &stats->objects.largest.tag_size, value_delim);
     +	if (show_concern)
     +		print_keyvalue("objects.tags.max_size.concern_level", key_delim,
    -+			       repo_structure_concern_level(CONCERN_STAT_TAG_LARGEST,
    -+							    stats->objects.largest.tag_size.value),
    -+			      value_delim);
    ++			       repo_structure_concern_level(
    ++				       CONCERN_STAT_TAG_LARGEST,
    ++				       stats->objects.largest.tag_size.value,
    ++				       repo_structure_concern_thresholds),
    ++			       value_delim);
      
      	print_object_data("objects.commits.max_parents", key_delim,
      			  &stats->objects.largest.parent_count, value_delim);
    @@ builtin/repo.c: static int cmd_repo_structure(int argc, const char **argv, const
      		OPT_END()
      	};
      
    + 	argc = parse_options(argc, argv, prefix, options, repo_structure_usage, 0);
    + 	if (argc)
    + 		usage(_("too many arguments"));
    +-
    + 	repo_init_revisions(repo, &revs, prefix);
    + 
    + 	if (show_progress < 0)
     @@ builtin/repo.c: static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
      	switch (format) {
      	case FORMAT_TABLE:
    @@ builtin/repo.c: static int cmd_repo_structure(int argc, const char **argv, const
      	default:
      		BUG("invalid output format");
     
    - ## sideband.c ##
    + ## repo.c (new) ##
     @@
    --#define USE_THE_REPOSITORY_VARIABLE
    -+define USE_THE_REPOSITORY_VARIABLE
    - #define DISABLE_SIGN_COMPARE_WARNINGS
    - 
    - #include "git-compat-util.h"
    ++#include "repo.h"
     
      ## t/t1901-repo-structure.sh ##
     @@ t/t1901-repo-structure.sh: test_expect_success 'empty repository' '
      		EOF
      
      		git repo structure >out 2>err &&
    --
    --		test_cmp expect out &&
    --		test_line_count = 0 err
    --	)
    --'
    --
    --test_expect_success SHA1 'repository with references and objects' '
    --	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" &&
    --
    --		# 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            |      1     |
    --		|     * Tags                |      1     |
    --		|     * Remotes             |      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.03 MiB |
    --		|     * Commits             | 217.92 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] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
    --		[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
    --		[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
    --		[5] 97d808e45116bf02103490294d3d46dad7a2ac62
    --		[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
    --		EOF
    --
    --		git repo structure >out 2>err &&
     +		cp out /tmp/test1-out &&
      
      		test_cmp expect out &&
      		test_line_count = 0 err
    -@@ t/t1901-repo-structure.sh: test_expect_success SHA1 'lines and nul format' '
    - 	)
    - '
    - 
    -+test_expect_success SHA1 'lines format with concerns' '
    -+	test_when_finished "rm -rf repo" &&
    -+	git init repo &&
    -+	(
    -+		cd repo &&
    -+		# configure thresholds to get concern indications
    -+		git config repo.structure.concern.commit-count 7 &&
    -+		git config repo.structure.concern.commit-size 14 &&
    -+		git config repo.structure.concern.tree-size 10 &&
    -+		git config repo.structure.concern.blob-size 8 &&
    -+		git config repo.structure.concern.blob-inflated-size 10 &&
    -+		git config repo.structure.concern.tag-size 2 &&
    -+		git config repo.structure.concern.commit-largest 4 &&
    -+		git config repo.structure.concern.blob-largest 2 &&
    -+		touch configured &&
    -+		test_commit_bulk 1005 &&
    -+		git tag -a foo -m bar &&
    -+		oid="$(git rev-parse HEAD)" &&
    -+		git update-ref refs/remotes/origin/foo "$oid" &&
    -+
    -+		# Also creates a commit, tree, and blob.
    -+		git notes add -m foo &&
    -+
    -+
    -+		# 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 &&
    -+		objects.commits.count.concern_level=2
    -+		objects.trees.count.concern_level=0
    -+		objects.blobs.count.concern_level=0
    -+		objects.tags.count.concern_level=0
    -+		objects.commits.inflated_size.concern_level=0
    -+		objects.trees.inflated_size.concern_level=0
    -+		objects.blobs.inflated_size.concern_level=3
    -+		objects.tags.inflated_size.concern_level=0
    -+		objects.commits.disk_size.concern_level=3
    -+		objects.trees.disk_size.concern_level=8
    -+		objects.blobs.blob_size.concern_level=6
    -+		objects.tags.disk_size.concern_level=4
    -+		objects.commits.max_size.concern_level=3
    -+		objects.trees.max_size.concern_level=0
    -+		objects.blobs.max_size.concern_level=1
    -+		objects.tags.max_size.concern_level=0
    -+		EOF
    -+
    -+		git repo structure --concern --format=lines >actual 2>err &&
    -+		test_grep "concern_level" actual >actual-concerns &&
    -+		test_cmp expect actual-concerns &&
    -+		test_line_count = 0 err
    -+	)
    -+'
    -+
    - test_expect_success 'progress meter option' '
    - 	test_when_finished "rm -rf repo" &&
    - 	git init repo &&
2:  56fb5590ea ! 2:  f0e25fae09 repo: make concern thresholds configurable
    @@ Commit message
         configure per-repo concern levels for "git repo structure"
         concern reporting.
     
    +    Signed-off-by: Mark C. Chu-Carroll <markchucarroll@fastmail.com>
    +
      ## Documentation/config/repo.adoc (new) ##
     @@
     +`repo.concern.commit-count`::
    @@ builtin/repo.c
      static const char *const repo_usage[] = {
      	REPO_INFO_USAGE,
      	REPO_STRUCTURE_USAGE,
    -@@ builtin/repo.c: enum concern_stat {
    - 	CONCERN_STAT_TREE_INFLATED_SIZE,
    - 	CONCERN_STAT_BLOB_INFLATED_SIZE,
    - 	CONCERN_STAT_TAG_INFLATED_SIZE,
    -+	CONCERN_STAT_MAXIMUM
    - };
    - 
    --struct concern_stat_threshold repo_structure_concern_thresholds[] = {
    --	{ .name = "commit_count", .threshold = 19 },
    --	{.name = "tree_count", .threshold = 21 },
    --	{ .name = "blob_count", .threshold = 22 },
    --	{ .name = "tag_count", .threshold = 15 },
    --	{ .name = "commit_size", .threshold = 28 },
    --	{ .name = "tree_size", .threshold = 27 },
    --	{ .name = "blob_size", .threshold = 36 },
    --	{ .name = "tags_size", .threshold = 18 },
    --	{ .name = "commit_largest", .threshold = 18  },
    --	{ .name = "tree_largest", .threshold = 18 },
    --	{ .name = "blob_largest", .threshold = 26 },
    --	{ .name = "tag_largest", .threshold =  15 },
    --	{ .name = "commit_inflated_size", .threshold = 28 },
    --	{ .name = "tree_inflated_size", .threshold = 32 },
    --	{ .name = "blob_inflated_size", .threshold = 34 },
    --	{ .name = "tag_inflated_size", .threshold = 25}
    -+struct concern_stat_threshold concern_stat_thresholds[] = {
    -+	{ .name = "commit-count", .threshold = 19 },
    -+	{ .name = "tree-count", .threshold = 21 },
    -+	{ .name = "blob-count", .threshold = 22 },
    -+	{ .name = "tag-count", .threshold = 15 },
    -+	{ .name = "commit-size", .threshold = 28 },
    -+	{ .name = "tree-size", .threshold = 27 },
    -+	{ .name = "blob-size", .threshold = 36 },
    -+	{ .name = "tag-size", .threshold = 18 },
    -+	{ .name = "commit-largest", .threshold = 18  },
    -+	{ .name = "tree-largest", .threshold = 18 },
    -+	{ .name = "blob-largest", .threshold = 26 },
    -+	{ .name = "tag-largest", .threshold =  15 },
    -+	{ .name = "commit-inflated-size", .threshold = 28 },
    -+	{ .name = "tree-inflated-size", .threshold = 32 },
    -+	{ .name = "blob-inflated-size", .threshold = 34 },
    -+	{ .name = "tag-inflated-size", .threshold = 25},
    -+	{ 0 }
    - };
    - 
    - /*
    -  * Concern thresholds aren't universal. We try to set reasonable default
    -  * values, but projects may want to set these thresholds differently. To
    -  * support that, they can set the thresholds for their project in the
    -- * project's git config, in the section "repo.structure.concern".
    -+ * project's git config, in the section "repo.concern".
    -   */
    - static int config_concern_thresholds(const char *prefixed_var,
    - 				     const char *str_val,
    - 				     const struct config_context *ctx,
    - 				     void *cb UNUSED) {
    --	const char *config_prefix = "repo.structure.concern.";
    --	if (!strncmp(config_prefix, prefixed_var, strlen(config_prefix))) {
    --		const char *concern_name = &prefixed_var[strlen(config_prefix)];
    -+	if (!strncmp(REPO_STRUCTURE_CONFIG_PREFIX, prefixed_var,
    -+	      strlen(REPO_STRUCTURE_CONFIG_PREFIX))) {
    -+		const char *concern_name =
    -+			&prefixed_var[strlen(REPO_STRUCTURE_CONFIG_PREFIX)];
    - 		for (int i = 0; concern_stat_thresholds[i].name != 0; i++) {
    - 			if (!strcmp(concern_stat_thresholds[i].name, concern_name)) {
    - 				int value = git_config_int(prefixed_var, str_val, ctx->kvi);
    -@@ builtin/repo.c: static int config_concern_thresholds(const char *prefixed_var,
    - 	return 0;
    +@@ builtin/repo.c: static size_t repo_structure_concern_level(const char* concern_name,
    + 		return 0;
      }
      
    --static size_t approx_log2(long value) {
    -+static size_t approx_log2(size_t value) {
    - 	int position = 0;
    - 	if (value <= 0) {
    - 		return 0;
    -@@ builtin/repo.c: static size_t approx_log2(long value) {
    - static size_t repo_structure_concern_level(enum concern_stat stat_id,
    - 					   size_t value) {
    - 	size_t log_value;
    --	if (stat_id >= ARRAY_SIZE(concern_stat_thresholds))
    -+	if (stat_id >= CONCERN_STAT_MAXIMUM)
    - 		BUG("unexpected repository stat type");
    - 	log_value = approx_log2(value);
    - 	if (log_value > concern_stat_thresholds[stat_id].threshold)
    ++/*
    ++ * Concern thresholds aren't universal. We try to set reasonable default
    ++ * values, but projects may want to set these thresholds differently. To
    ++ * support that, they can set the thresholds for their project in the
    ++ * project's git config, in the section "repo.structure.concern".
    ++  */
    ++static int config_concern_thresholds(const char *prefixed_var,
    ++				     const char *str_val,
    ++				     const struct config_context *ctx,
    ++				     void *cb)
    ++{
    ++	struct concern_stat_threshold *thresholds = cb;
    ++	if (starts_with(prefixed_var, REPO_STRUCTURE_CONFIG_PREFIX)) {
    ++		const char *concern_name = &prefixed_var[strlen(REPO_STRUCTURE_CONFIG_PREFIX)];
    ++		for (int i = 0; thresholds[i].name; i++) {
    ++			if (!strcmp(thresholds[i].name, concern_name)) {
    ++				int value = git_config_int(prefixed_var, str_val, ctx->kvi);
    ++				if (value < 0)
    ++					return -1;
    ++				thresholds[i].threshold = value;
    ++				return 0;
    ++			}
    ++		}
    ++		warning(_("Invalid concern threshold name: '%s'"), concern_name);
    ++		return 0;
    ++	}
    ++	return 0;
    ++}
    ++
    + /*
    +  * Holds column data that gets stored for each row.
    +  */
     @@ builtin/repo.c: static void stats_table_print_structure(const struct stats_table *table,
      			strbuf_utf8_align(&buf, ALIGN_LEFT, concern_col_width, concern_str.buf);
      			strbuf_addstr(&buf, " |");
    @@ builtin/repo.c: static void stats_table_print_structure(const struct stats_table
      	}
      
     @@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stats,
    - 	print_keyvalue("objects.blobs.disk_size", key_delim,
    - 		       stats->objects.disk_sizes.blobs, value_delim);
    - 	if (show_concern)
    --		print_keyvalue("objects.blobs.blob_size.concern_level", key_delim,
    -+		print_keyvalue("objects.blobs.disk_size.concern_level", key_delim,
    - 			       repo_structure_concern_level(CONCERN_STAT_BLOB_SIZE,
    - 							    stats->objects.disk_sizes.blobs),
    - 			      value_delim);
    -@@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stats,
    - 							    stats->objects.largest.commit_size.value),
    - 			      value_delim);
    - 	print_object_data("objects.trees.max_size", key_delim,
    --			  &stats->objects.largest.tree_size, value_delim);
    -+			 &stats->objects.largest.tree_size, value_delim);
    - 	if (show_concern)
    - 		print_keyvalue("objects.trees.max_size.concern_level", key_delim,
    - 			       repo_structure_concern_level(CONCERN_STAT_TREE_LARGEST,
    -@@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stats,
    - 							    stats->objects.largest.blob_size.value),
    - 			      value_delim);
    - 	print_object_data("objects.tags.max_size", key_delim,
    --			  &stats->objects.largest.tag_size, value_delim);
    -+			   &stats->objects.largest.tag_size, value_delim);
    - 	if (show_concern)
    - 		print_keyvalue("objects.tags.max_size.concern_level", key_delim,
    - 			       repo_structure_concern_level(CONCERN_STAT_TAG_LARGEST,
    + 				       stats->objects.largest.tag_size.value,
    + 				       repo_structure_concern_thresholds),
    + 			       value_delim);
    +-
    + 	print_object_data("objects.commits.max_parents", key_delim,
    + 			  &stats->objects.largest.parent_count, value_delim);
    + 	print_object_data("objects.trees.max_entries", key_delim,
     @@ builtin/repo.c: static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
    - 	argc = parse_options(argc, argv, prefix, options, repo_structure_usage, 0);
    - 	if (argc)
      		usage(_("too many arguments"));
    --
    -+	if (show_concern)
    -+		repo_config(repo, &config_concern_thresholds, NULL);
      	repo_init_revisions(repo, &revs, prefix);
      
    ++	if (show_concern)
    ++                repo_config(repo, &config_concern_thresholds, &repo_structure_concern_thresholds);
    ++
    ++
      	if (show_progress < 0)
    -
    - ## sideband.c ##
    -@@
    --define USE_THE_REPOSITORY_VARIABLE
    -+#define USE_THE_REPOSITORY_VARIABLE
    - #define DISABLE_SIGN_COMPARE_WARNINGS
    + 		show_progress = isatty(2);
      
    - #include "git-compat-util.h"
     
      ## t/t1901-repo-structure.sh ##
     @@ t/t1901-repo-structure.sh: test_expect_success 'empty repository' '
    @@ t/t1901-repo-structure.sh: test_expect_success 'empty repository' '
      
      		test_cmp expect out &&
      		test_line_count = 0 err
    +@@ t/t1901-repo-structure.sh: test_expect_success SHA1 'repository with references and objects' '
      	)
      '
      
    -+test_expect_success SHA1 'repository with references and objects' '
    -+	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" &&
    -+
    -+		# 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            |      1     |
    -+		|     * Tags                |      1     |
    -+		|     * Remotes             |      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.03 MiB |
    -+		|     * Commits             | 217.92 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] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
    -+		[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
    -+		[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
    -+		[5] 97d808e45116bf02103490294d3d46dad7a2ac62
    -+		[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
    -+		EOF
    -+
    -+		git repo structure >out 2>err &&
    -+
    -+		test_cmp expect out &&
    -+		test_line_count = 0 err
    -+	)
    -+'
    -+
     +test_expect_success SHA1 'concern flagging' '
     +	test_when_finished "rm -rf repo" &&
     +	git init repo &&
    @@ t/t1901-repo-structure.sh: test_expect_success 'empty repository' '
      test_expect_success SHA1 'lines and nul format' '
      	test_when_finished "rm -rf repo" &&
      	git init repo &&
    -@@ t/t1901-repo-structure.sh: test_expect_success SHA1 'lines format with concerns' '
    - 	git init repo &&
    - 	(
    - 		cd repo &&
    -+		# differences between zlib and zlib-ng can cause this
    -+		# test to fail; so we just turn it off.
    +@@ t/t1901-repo-structure.sh: test_expect_success SHA1 'lines and nul format' '
    + 	)
    + '
    + 
    ++test_expect_success SHA1 'lines format with concerns' '
    ++	test_when_finished "rm -rf repo" &&
    ++	git init repo &&
    ++	(
    ++		cd repo &&
    ++		# configure thresholds to get concern indications
     +		git config core.compression 0 &&
    - 		# configure thresholds to get concern indications
    --		git config repo.structure.concern.commit-count 7 &&
    --		git config repo.structure.concern.commit-size 14 &&
    --		git config repo.structure.concern.tree-size 10 &&
    --		git config repo.structure.concern.blob-size 8 &&
    --		git config repo.structure.concern.blob-inflated-size 10 &&
    --		git config repo.structure.concern.tag-size 2 &&
    --		git config repo.structure.concern.commit-largest 4 &&
    --		git config repo.structure.concern.blob-largest 2 &&
    --		touch configured &&
     +		git config repo.concern.commit-count 7 &&
     +		git config repo.concern.commit-size 14 &&
     +		git config repo.concern.tree-size 10 &&
    @@ t/t1901-repo-structure.sh: test_expect_success SHA1 'lines format with concerns'
     +		git config repo.concern.tag-size 2 &&
     +		git config repo.concern.commit-largest 4 &&
     +		git config repo.concern.blob-largest 2 &&
    - 		test_commit_bulk 1005 &&
    - 		git tag -a foo -m bar &&
    - 		oid="$(git rev-parse HEAD)" &&
    -@@ t/t1901-repo-structure.sh: test_expect_success SHA1 'lines format with concerns' '
    - 		# 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".
    -@@ t/t1901-repo-structure.sh: test_expect_success SHA1 'lines format with concerns' '
    - 		objects.tags.inflated_size.concern_level=0
    - 		objects.commits.disk_size.concern_level=3
    - 		objects.trees.disk_size.concern_level=8
    --		objects.blobs.blob_size.concern_level=6
    --		objects.tags.disk_size.concern_level=4
    ++		touch configured &&
    ++		test_commit_bulk 1005 &&
    ++		git tag -a foo -m bar &&
    ++		oid="$(git rev-parse HEAD)" &&
    ++		git update-ref refs/remotes/origin/foo "$oid" &&
    ++
    ++		# Also creates a commit, tree, and blob.
    ++		git notes add -m foo &&
    ++
    ++
    ++		# 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 &&
    ++		objects.commits.count.concern_level=2
    ++		objects.trees.count.concern_level=0
    ++		objects.blobs.count.concern_level=0
    ++		objects.tags.count.concern_level=0
    ++		objects.commits.inflated_size.concern_level=0
    ++		objects.trees.inflated_size.concern_level=0
    ++		objects.blobs.inflated_size.concern_level=3
    ++		objects.tags.inflated_size.concern_level=0
    ++		objects.commits.disk_size.concern_level=3
    ++		objects.trees.disk_size.concern_level=8
     +		objects.blobs.disk_size.concern_level=6
     +		objects.tags.disk_size.concern_level=5
    - 		objects.commits.max_size.concern_level=3
    - 		objects.trees.max_size.concern_level=0
    - 		objects.blobs.max_size.concern_level=1
    ++		objects.commits.max_size.concern_level=3
    ++		objects.trees.max_size.concern_level=0
    ++		objects.blobs.max_size.concern_level=1
    ++		objects.tags.max_size.concern_level=0
    ++		EOF
    ++
    ++		git repo structure --concern --format=lines >actual 2>err &&
    ++		test_grep "concern_level" actual >actual-concerns &&
    ++		test_cmp expect actual-concerns &&
    ++		test_line_count = 0 err
    ++	)
    ++'
    ++
    + test_expect_success 'progress meter option' '
    + 	test_when_finished "rm -rf repo" &&
    + 	git init repo &&

---
base-commit: 3bc0341126508f78f5869cbfc0005e987efdf0c7
change-id: 20261019-b4-repo-structure-concern-f1d6f6326095


^ permalink raw reply	[flat|nested] 20+ messages in thread

* [PATCH v4 1/2] repo: add level of concern reporting
  2026-10-29 17:52 ` [PATCH v4 0/2] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
@ 2026-10-29 17:52   ` Mark C. Chu-Carroll
  2026-10-29 19:53     ` Junio C Hamano
  2026-10-29 17:52   ` [PATCH v4 2/2] repo: make concern thresholds configurable Mark C. Chu-Carroll
  1 sibling, 1 reply; 20+ messages in thread
From: Mark C. Chu-Carroll @ 2026-10-29 17:52 UTC (permalink / raw)
  To: git; +Cc: Mark C. Chu-Carroll

"git repo structure" computes a set of statistics
that are useful for diagnosing performance issues in
a git repository. But for a typical user who hasn't
encountered any issues yet, it can be difficult to
interpret the data - when should a value be considered
concerning?

Add a new option to "git repo structure" which causes it
to generate an additional column providing a level of concern
for many values.

To select defaults for the thresholds where a value becomes concern,
a (hopefully) representative selection of repositories were analyzed.
Based on empirical exploration of the data, the measurement that seemed
closest to accurately assessing when a repository's structure becomes
concerning was based on the base 2 logarithm of the value. Across all of
the sample repositories, for all of the measurements, the point at which
things become problematic was when a base 2 logarithm of a value was
more than 1 standard deviation from the mean of the log_2 values in
the sample set.

Signed-off-by: Mark C. Chu-Carroll <markchucarroll@fastmail.com>
---
 Documentation/git-repo.adoc |  16 +-
 builtin/repo.c              | 370 ++++++++++++++++++++++++++++++++++++++++----
 repo.c                      |   1 +
 t/t1901-repo-structure.sh   |   1 +
 4 files changed, 359 insertions(+), 29 deletions(-)

diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index ed7d80c690..f186b27d87 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-]concern]
 
 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-]concern]`::
 	Retrieve statistics about the current repository structure. The
 	following kinds of information are reported:
 +
@@ -88,6 +88,18 @@ supported:
 	quoted.
 +
 `-z` is an alias for `--format=nul`.
++
+To make it easier to know when one of the values is something that
+users should be concerned about, adding '--concern' will provide a
+level of concern to be added to the output. In 'table' mode, this will
+be a table column indicating the level of concern as a number of stars;
+in `lines` or `nul` mode, a `concern_level` line will be added for each
+statistic with a numeric value.
++
+The level of concern for each statistic is computed using the log_2 of the
+statistic value. The value is considered concerning if it exceeds a heuristically
+chosen threshold value; the degree of concern is the difference between the
+log_2 of the value, and the log_2 of the corresponding threshold.
 
 INFO KEYS
 ---------
diff --git a/builtin/repo.c b/builtin/repo.c
index 84e012f83f..bd1866c5fa 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -21,13 +21,14 @@
 #include "tree.h"
 #include "tree-walk.h"
 #include "utf8.h"
+#include "wrapper.h"
 
 #define REPO_INFO_USAGE \
 	"git repo info [--format=(lines|nul) | -z] [--all | <key>...]", \
 	"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] [--[no-]concern]"
 
 static const char *const repo_usage[] = {
 	REPO_INFO_USAGE,
@@ -348,8 +349,87 @@ struct stats_table {
 	int name_col_width;
 	int value_col_width;
 	int unit_col_width;
+	int concern_col_width;
 };
 
+/*
+ * For computing level of concern about a stat, we need to have some
+ * baseline values. For each of these, we'define a minimum level
+ * of concern, which will appear as 1 star. Then we scale exponentially:
+ * each time that value doubles, we add another star.
+ *
+ * The threshold values for concern are expressed as log_2 values of the size
+ * at which the statistic becomes concerning. The default values for the
+ * threshold were selected based on observed values from a collection of
+ * representative-ish repositories of varying size, by finding the mean log_2
+ * value and standard deviation (sigma) for each statistic over the collection.
+ * The threshold for each metric is then set at mean(log_2) + sigma.
+ */
+struct concern_stat_threshold {
+	const char *name;
+	size_t threshold;
+};
+
+#define CONCERN_STAT_COMMIT_COUNT "commit-count"
+#define CONCERN_STAT_TREE_COUNT "tree-count"
+#define CONCERN_STAT_BLOB_COUNT "blob-count"
+#define CONCERN_STAT_TAG_COUNT "tag-count"
+#define CONCERN_STAT_COMMIT_SIZE "commit-size"
+#define CONCERN_STAT_TREE_SIZE "tree-size"
+#define CONCERN_STAT_BLOB_SIZE "blob-size"
+#define CONCERN_STAT_TAG_SIZE "tag-size"
+#define CONCERN_STAT_COMMIT_LARGEST "commit-largest"
+#define CONCERN_STAT_TREE_LARGEST "tree-largest"
+#define CONCERN_STAT_BLOB_LARGEST "blob-largest"
+#define CONCERN_STAT_TAG_LARGEST "tag-largest"
+#define CONCERN_STAT_COMMIT_INFLATED_SIZE "commit-inflated-size"
+#define CONCERN_STAT_TREE_INFLATED_SIZE "tree-inflated-size"
+#define CONCERN_STAT_BLOB_INFLATED_SIZE "blob-inflated-size"
+#define CONCERN_STAT_TAG_INFLATED_SIZE "tag-inflated-size"
+
+struct concern_stat_threshold repo_structure_concern_thresholds[] = {
+	{ .name = CONCERN_STAT_COMMIT_COUNT, .threshold = 19 },
+	{ .name = CONCERN_STAT_TREE_COUNT, .threshold = 21 },
+	{ .name = CONCERN_STAT_BLOB_COUNT, .threshold = 22 },
+	{ .name = CONCERN_STAT_TAG_COUNT, .threshold = 15 },
+	{ .name = CONCERN_STAT_COMMIT_SIZE, .threshold = 28 },
+	{ .name = CONCERN_STAT_TREE_SIZE, .threshold = 27 },
+	{ .name = CONCERN_STAT_BLOB_SIZE, .threshold = 36 },
+	{ .name = CONCERN_STAT_TAG_SIZE, .threshold = 18 },
+	{ .name = CONCERN_STAT_COMMIT_LARGEST, .threshold = 18  },
+	{ .name = CONCERN_STAT_TREE_LARGEST, .threshold = 18 },
+	{ .name = CONCERN_STAT_BLOB_LARGEST, .threshold = 26 },
+	{ .name = CONCERN_STAT_TAG_LARGEST, .threshold =  15 },
+	{ .name = CONCERN_STAT_COMMIT_INFLATED_SIZE, .threshold = 28 },
+	{ .name = CONCERN_STAT_TREE_INFLATED_SIZE, .threshold = 32 },
+	{ .name = CONCERN_STAT_BLOB_INFLATED_SIZE, .threshold = 34 },
+	{ .name = CONCERN_STAT_TAG_INFLATED_SIZE, .threshold = 25 },
+	{ 0 }
+};
+
+static size_t get_concern_threshold(const char *name, struct concern_stat_threshold *thresholds) {
+	for (size_t i = 0; thresholds[i].name; i++) {
+		if (!strcmp(thresholds[i].name, name)) {
+			return thresholds[i].threshold;
+		}
+	}
+	return 0;
+}
+
+static size_t repo_structure_concern_level(const char* concern_name,
+					   size_t value,
+					   struct concern_stat_threshold *thresholds) {
+	const size_t threshold = get_concern_threshold(concern_name, thresholds);
+	size_t log_value;
+	if (threshold == 0)
+		BUG("unexpected repository stat threshold: '%s'", concern_name);
+	log_value = log2u(value);
+	if (log_value > threshold)
+		return log_value - threshold;
+	else
+		return 0;
+}
+
 /*
  * Holds column data that gets stored for each row.
  */
@@ -358,6 +438,7 @@ struct stats_table_entry {
 	const char *unit;
 	size_t index;
 	struct object_id *oid;
+	size_t concern;
 };
 
 static void stats_table_vaddf(struct stats_table *table,
@@ -391,6 +472,8 @@ static void stats_table_vaddf(struct stats_table *table,
 		if (value_width > table->value_col_width)
 			table->value_col_width = value_width;
 	}
+	if ((int)entry->concern > table->concern_col_width)
+		table->concern_col_width = entry->concern;
 	if (entry->unit) {
 		int unit_width = utf8_strwidth(entry->unit);
 		if (unit_width > table->unit_col_width)
@@ -409,14 +492,19 @@ static void stats_table_addf(struct stats_table *table, const char *format, ...)
 	va_end(ap);
 }
 
+// define a symbol for 'no concern value for this stat'; this will
+// make the code below easier to read.
+#define CONCERN_STAT_VALUE_NONE 0
+
 static void stats_table_count_addf(struct stats_table *table, size_t value,
-				   const char *format, ...)
+				   size_t concern, const char *format, ...)
 {
 	struct stats_table_entry *entry;
 	va_list ap;
 
 	CALLOC_ARRAY(entry, 1);
 	humanise_count(value, &entry->value, &entry->unit);
+	entry->concern = concern;
 
 	va_start(ap, format);
 	stats_table_vaddf(table, entry, format, ap);
@@ -425,13 +513,14 @@ static void stats_table_count_addf(struct stats_table *table, size_t value,
 
 static void stats_table_object_count_addf(struct stats_table *table,
 					  struct object_id *oid, size_t value,
-					  const char *format, ...)
+					  size_t concern, const char *format, ...)
 {
 	struct stats_table_entry *entry;
 	va_list ap;
 
 	CALLOC_ARRAY(entry, 1);
 	humanise_count(value, &entry->value, &entry->unit);
+	entry->concern = concern;
 
 	/*
 	 * A NULL OID should not have a table annotation.
@@ -445,6 +534,7 @@ static void stats_table_object_count_addf(struct stats_table *table,
 }
 
 static void stats_table_size_addf(struct stats_table *table, size_t value,
+				  size_t concern,
 				  const char *format, ...)
 {
 	struct stats_table_entry *entry;
@@ -452,6 +542,7 @@ static void stats_table_size_addf(struct stats_table *table, size_t value,
 
 	CALLOC_ARRAY(entry, 1);
 	humanise_bytes(value, &entry->value, &entry->unit, HUMANISE_COMPACT);
+	entry->concern = concern;
 
 	va_start(ap, format);
 	stats_table_vaddf(table, entry, format, ap);
@@ -460,6 +551,7 @@ static void stats_table_size_addf(struct stats_table *table, size_t value,
 
 static void stats_table_object_size_addf(struct stats_table *table,
 					 struct object_id *oid, size_t value,
+					 size_t concern,
 					 const char *format, ...)
 {
 	struct stats_table_entry *entry;
@@ -467,6 +559,7 @@ static void stats_table_object_size_addf(struct stats_table *table,
 
 	CALLOC_ARRAY(entry, 1);
 	humanise_bytes(value, &entry->value, &entry->unit, HUMANISE_COMPACT);
+	entry->concern = concern;
 
 	/*
 	 * A NULL OID should not have a table annotation.
@@ -501,47 +594,104 @@ 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"));
-	stats_table_count_addf(table, refs->others, "    * %s", _("Others"));
-
+	stats_table_count_addf(table, ref_total, CONCERN_STAT_VALUE_NONE,
+			       "  * %s", _("Count"));
+	stats_table_count_addf(table, refs->branches, CONCERN_STAT_VALUE_NONE,
+			       "    * %s", _("Branches"));
+	stats_table_count_addf(table, refs->tags,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TAG_COUNT,
+				       refs->tags,
+    			               repo_structure_concern_thresholds),
+			       "    * %s", _("Tags"));
+	stats_table_count_addf(table, refs->remotes, CONCERN_STAT_VALUE_NONE,
+			       "    * %s", _("Remotes"));
+	stats_table_count_addf(table, refs->others, CONCERN_STAT_VALUE_NONE,
+			       "    * %s", _("Others"));
 	object_count_total = get_total_object_values(&objects->type_counts);
 	stats_table_addf(table, "");
 	stats_table_addf(table, "* %s", _("Reachable objects"));
-	stats_table_count_addf(table, object_count_total, "  * %s", _("Count"));
+	stats_table_count_addf(table, object_count_total, CONCERN_STAT_VALUE_NONE,
+			       "  * %s", _("Count"));
 	stats_table_count_addf(table, objects->type_counts.commits,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_COMMIT_COUNT,
+				       objects->type_counts.commits,
+				       repo_structure_concern_thresholds),
 			       "    * %s", _("Commits"));
 	stats_table_count_addf(table, objects->type_counts.trees,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TREE_COUNT,
+				       objects->type_counts.trees,
+				       repo_structure_concern_thresholds),
 			       "    * %s", _("Trees"));
 	stats_table_count_addf(table, objects->type_counts.blobs,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_BLOB_COUNT,
+				       objects->type_counts.blobs,
+				       repo_structure_concern_thresholds),
 			       "    * %s", _("Blobs"));
 	stats_table_count_addf(table, objects->type_counts.tags,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TAG_COUNT,
+				       objects->type_counts.tags,
+				       repo_structure_concern_thresholds),
 			       "    * %s", _("Tags"));
 
 	inflated_object_total = get_total_object_values(&objects->inflated_sizes);
-	stats_table_size_addf(table, inflated_object_total,
+	stats_table_size_addf(table, inflated_object_total, CONCERN_STAT_VALUE_NONE,
 			      "  * %s", _("Inflated size"));
 	stats_table_size_addf(table, objects->inflated_sizes.commits,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_COMMIT_INFLATED_SIZE,
+				      objects->inflated_sizes.commits,
+				      repo_structure_concern_thresholds),
 			      "    * %s", _("Commits"));
 	stats_table_size_addf(table, objects->inflated_sizes.trees,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_TREE_INFLATED_SIZE,
+				      objects->inflated_sizes.trees,
+				      repo_structure_concern_thresholds),
 			      "    * %s", _("Trees"));
 	stats_table_size_addf(table, objects->inflated_sizes.blobs,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_BLOB_INFLATED_SIZE,
+				      objects->inflated_sizes.blobs,
+				      repo_structure_concern_thresholds),
 			      "    * %s", _("Blobs"));
 	stats_table_size_addf(table, objects->inflated_sizes.tags,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_TAG_INFLATED_SIZE,
+				      objects->inflated_sizes.tags,
+				      repo_structure_concern_thresholds),
 			      "    * %s", _("Tags"));
 
 	disk_object_total = get_total_object_values(&objects->disk_sizes);
-	stats_table_size_addf(table, disk_object_total,
+	stats_table_size_addf(table, disk_object_total, CONCERN_STAT_VALUE_NONE,
 			      "  * %s", _("Disk size"));
 	stats_table_size_addf(table, objects->disk_sizes.commits,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_COMMIT_SIZE,
+				      objects->disk_sizes.commits,
+				      repo_structure_concern_thresholds),
 			      "    * %s", _("Commits"));
 	stats_table_size_addf(table, objects->disk_sizes.trees,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_TREE_SIZE,
+				      objects->disk_sizes.trees,
+				      repo_structure_concern_thresholds),
 			      "    * %s", _("Trees"));
 	stats_table_size_addf(table, objects->disk_sizes.blobs,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_BLOB_SIZE,
+				      objects->disk_sizes.blobs,
+				      repo_structure_concern_thresholds),
 			      "    * %s", _("Blobs"));
 	stats_table_size_addf(table, objects->disk_sizes.tags,
+			      repo_structure_concern_level(
+				      CONCERN_STAT_TAG_SIZE,
+				      objects->disk_sizes.tags,
+				      repo_structure_concern_thresholds),
 			      "    * %s", _("Tags"));
 
 	stats_table_addf(table, "");
@@ -550,51 +700,76 @@ static void stats_table_setup_structure(struct stats_table *table,
 	stats_table_object_size_addf(table,
 				     &objects->largest.commit_size.oid,
 				     objects->largest.commit_size.value,
+				     repo_structure_concern_level(
+					     CONCERN_STAT_COMMIT_LARGEST,
+					     objects->largest.commit_size.value,
+					     repo_structure_concern_thresholds),
 				     "    * %s", _("Maximum size"));
 	stats_table_object_count_addf(table,
 				      &objects->largest.parent_count.oid,
 				      objects->largest.parent_count.value,
+				      CONCERN_STAT_VALUE_NONE,
 				      "    * %s", _("Maximum parents"));
 	stats_table_addf(table, "  * %s", _("Trees"));
 	stats_table_object_size_addf(table,
 				     &objects->largest.tree_size.oid,
 				     objects->largest.tree_size.value,
+				     repo_structure_concern_level(
+					     CONCERN_STAT_TREE_LARGEST,
+					     objects->largest.commit_size.value,
+					     repo_structure_concern_thresholds),
 				     "    * %s", _("Maximum size"));
+
 	stats_table_object_count_addf(table,
-				      &objects->largest.tree_entries.oid,
-				      objects->largest.tree_entries.value,
-				      "    * %s", _("Maximum entries"));
+				     &objects->largest.tree_entries.oid,
+				     objects->largest.tree_entries.value,
+			             CONCERN_STAT_VALUE_NONE,
+				     "    * %s", _("Maximum entries"));
 	stats_table_addf(table, "  * %s", _("Blobs"));
 	stats_table_object_size_addf(table,
 				     &objects->largest.blob_size.oid,
 				     objects->largest.blob_size.value,
+				     repo_structure_concern_level(
+					     CONCERN_STAT_BLOB_LARGEST,
+					     objects->largest.blob_size.value,
+					     repo_structure_concern_thresholds),
 				     "    * %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,
+				     repo_structure_concern_level(
+					     CONCERN_STAT_TAG_LARGEST,
+					     objects->largest.tag_size.value,
+					     repo_structure_concern_thresholds),
 				     "    * %s", _("Maximum size"));
 }
 
 #define INDEX_WIDTH 4
 
-static void stats_table_print_structure(const struct stats_table *table)
+static void stats_table_print_structure(const struct stats_table *table,
+					bool show_concern)
 {
 	const char *name_col_title = _("Repository structure");
 	const char *value_col_title = _("Value");
+	const char *concern_col_title = _("Concern");
 	int title_name_width = utf8_strwidth(name_col_title);
 	int title_value_width = utf8_strwidth(value_col_title);
+	int title_concern_width = utf8_strwidth(concern_col_title);
 	int name_col_width = table->name_col_width;
 	int value_col_width = table->value_col_width;
 	int unit_col_width = table->unit_col_width;
+	int concern_col_width = table->concern_col_width;
 	struct string_list_item *item;
 	struct strbuf buf = STRBUF_INIT;
+	struct strbuf concern_str = STRBUF_INIT;
 
 	if (title_name_width > name_col_width)
 		name_col_width = title_name_width;
 	if (title_value_width > value_col_width + unit_col_width + 1)
 		value_col_width = title_value_width - unit_col_width;
-
+	if (show_concern && title_concern_width > concern_col_width)
+		concern_col_width = title_concern_width;
 	strbuf_addstr(&buf, "| ");
 	strbuf_utf8_align(&buf, ALIGN_LEFT, name_col_width + INDEX_WIDTH,
 			  name_col_title);
@@ -602,6 +777,12 @@ static void stats_table_print_structure(const struct stats_table *table)
 	strbuf_utf8_align(&buf, ALIGN_LEFT,
 			  value_col_width + unit_col_width + 1, value_col_title);
 	strbuf_addstr(&buf, " |");
+	if (show_concern) {
+		strbuf_addch(&buf, ' ');
+		strbuf_utf8_align(&buf, ALIGN_LEFT, concern_col_width,
+			  concern_col_title);
+		strbuf_addstr(&buf, " |");
+	}
 	printf("%s\n", buf.buf);
 
 	printf("| ");
@@ -610,17 +791,28 @@ static void stats_table_print_structure(const struct stats_table *table)
 	printf(" | ");
 	for (int i = 0; i < value_col_width + unit_col_width + 1; i++)
 		putchar('-');
-	printf(" |\n");
+	printf(" |");
+	if (show_concern) {
+		putchar(' ');
+		for (int i = 0; i < concern_col_width; i++) {
+			putchar('-');
+		}
+		printf(" |");
+	}
+	putchar('\n');
 
 	for_each_string_list_item(item, &table->rows) {
 		struct stats_table_entry *entry = item->util;
 		const char *value = "";
 		const char *unit = "";
+		strbuf_reset(&concern_str);
 
 		if (entry) {
 			value = entry->value;
 			if (entry->unit)
 				unit = entry->unit;
+			if (entry->concern > 0)
+				strbuf_addchars(&concern_str, '*', entry->concern);
 		}
 
 		strbuf_reset(&buf);
@@ -638,6 +830,11 @@ static void stats_table_print_structure(const struct stats_table *table)
 		strbuf_addch(&buf, ' ');
 		strbuf_utf8_align(&buf, ALIGN_LEFT, unit_col_width, unit);
 		strbuf_addstr(&buf, " |");
+		if (show_concern) {
+			strbuf_addch(&buf, ' ');
+			strbuf_utf8_align(&buf, ALIGN_LEFT, concern_col_width, concern_str.buf);
+			strbuf_addstr(&buf, " |");
+		}
 		printf("%s\n", buf.buf);
 	}
 
@@ -681,12 +878,15 @@ 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,
+				     bool show_concern)
 {
 	print_keyvalue("references.branches.count", key_delim,
 		       stats->refs.branches, value_delim);
+
 	print_keyvalue("references.tags.count", key_delim,
 		       stats->refs.tags, value_delim);
+
 	print_keyvalue("references.remotes.count", key_delim,
 		       stats->refs.remotes, value_delim);
 	print_keyvalue("references.others.count", key_delim,
@@ -694,39 +894,153 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 
 	print_keyvalue("objects.commits.count", key_delim,
 		       stats->objects.type_counts.commits, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.commits.count.concern_level", key_delim,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_COMMIT_COUNT,
+				       stats->objects.type_counts.commits,
+				       repo_structure_concern_thresholds),
+			       value_delim);
 	print_keyvalue("objects.trees.count", key_delim,
 		       stats->objects.type_counts.trees, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.trees.count.concern_level", key_delim,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TREE_COUNT,
+				       stats->objects.type_counts.trees,
+				       repo_structure_concern_thresholds),
+			       value_delim);
 	print_keyvalue("objects.blobs.count", key_delim,
 		       stats->objects.type_counts.blobs, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.blobs.count.concern_level", key_delim,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_BLOB_COUNT,
+				       stats->objects.type_counts.blobs,
+				       repo_structure_concern_thresholds),
+		 value_delim);
 	print_keyvalue("objects.tags.count", key_delim,
-		       stats->objects.type_counts.tags, value_delim);
+		stats->objects.type_counts.tags, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.tags.count.concern_level", key_delim,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TAG_COUNT,
+				       stats->objects.type_counts.tags,
+				       repo_structure_concern_thresholds),
+		 	       value_delim);
 
 	print_keyvalue("objects.commits.inflated_size", key_delim,
-		       stats->objects.inflated_sizes.commits, value_delim);
+		stats->objects.inflated_sizes.commits, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.commits.inflated_size.concern_level", key_delim,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_COMMIT_INFLATED_SIZE,
+				       stats->objects.inflated_sizes.commits,
+				       repo_structure_concern_thresholds),
+			       value_delim);
 	print_keyvalue("objects.trees.inflated_size", key_delim,
-		       stats->objects.inflated_sizes.trees, value_delim);
+		stats->objects.inflated_sizes.trees, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.trees.inflated_size.concern_level", key_delim,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TREE_INFLATED_SIZE,
+				       stats->objects.inflated_sizes.trees,
+				       repo_structure_concern_thresholds),
+			       value_delim);
+
 	print_keyvalue("objects.blobs.inflated_size", key_delim,
 		       stats->objects.inflated_sizes.blobs, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.blobs.inflated_size.concern_level", key_delim,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_BLOB_INFLATED_SIZE,
+				       stats->objects.inflated_sizes.blobs,
+				       repo_structure_concern_thresholds),
+			       value_delim);
 	print_keyvalue("objects.tags.inflated_size", key_delim,
 		       stats->objects.inflated_sizes.tags, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.tags.inflated_size.concern_level", key_delim,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TAG_INFLATED_SIZE,
+				       stats->objects.inflated_sizes.tags,
+				       repo_structure_concern_thresholds),
+			       value_delim);
 
 	print_keyvalue("objects.commits.disk_size", key_delim,
 		       stats->objects.disk_sizes.commits, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.commits.disk_size.concern_level", key_delim,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_COMMIT_SIZE,
+				       stats->objects.disk_sizes.commits,
+				       repo_structure_concern_thresholds),
+			       value_delim);
 	print_keyvalue("objects.trees.disk_size", key_delim,
 		       stats->objects.disk_sizes.trees, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.trees.disk_size.concern_level", key_delim,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TREE_SIZE,
+				       stats->objects.disk_sizes.trees,
+				       repo_structure_concern_thresholds),
+			       value_delim);
 	print_keyvalue("objects.blobs.disk_size", key_delim,
 		       stats->objects.disk_sizes.blobs, value_delim);
+	if (show_concern)
+
+		print_keyvalue("objects.blobs.disk_size.concern_level", key_delim,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_BLOB_SIZE,
+				       stats->objects.disk_sizes.blobs,
+				       repo_structure_concern_thresholds),
+			      value_delim);
 	print_keyvalue("objects.tags.disk_size", key_delim,
 		       stats->objects.disk_sizes.tags, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.tags.disk_size.concern_level", key_delim,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TAG_SIZE,
+				       stats->objects.disk_sizes.tags,
+				       repo_structure_concern_thresholds),
+			      value_delim);
 
 	print_object_data("objects.commits.max_size", key_delim,
 			  &stats->objects.largest.commit_size, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.commits.max_size.concern_level", key_delim,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_COMMIT_LARGEST,
+				       stats->objects.largest.commit_size.value,
+				       repo_structure_concern_thresholds),
+			       value_delim);
 	print_object_data("objects.trees.max_size", key_delim,
 			  &stats->objects.largest.tree_size, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.trees.max_size.concern_level", key_delim,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TREE_LARGEST,
+				       stats->objects.largest.tree_size.value,
+				       repo_structure_concern_thresholds),
+			       value_delim);
 	print_object_data("objects.blobs.max_size", key_delim,
 			  &stats->objects.largest.blob_size, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.blobs.max_size.concern_level", key_delim,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_BLOB_LARGEST,
+				       stats->objects.largest.blob_size.value,
+				       repo_structure_concern_thresholds),
+			       value_delim);
 	print_object_data("objects.tags.max_size", key_delim,
-			  &stats->objects.largest.tag_size, value_delim);
+		          &stats->objects.largest.tag_size, value_delim);
+	if (show_concern)
+		print_keyvalue("objects.tags.max_size.concern_level", key_delim,
+			       repo_structure_concern_level(
+				       CONCERN_STAT_TAG_LARGEST,
+				       stats->objects.largest.tag_size.value,
+				       repo_structure_concern_thresholds),
+			       value_delim);
 
 	print_object_data("objects.commits.max_parents", key_delim,
 			  &stats->objects.largest.parent_count, value_delim);
@@ -935,6 +1249,8 @@ 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 show_concern = 0;
+
 	struct option options[] = {
 		OPT_CALLBACK_F(0, "format", &format, N_("format"),
 			       N_("output format"),
@@ -944,13 +1260,13 @@ 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_BOOL(0, "concern", &show_concern, N_("show level of concern")),
 		OPT_END()
 	};
 
 	argc = parse_options(argc, argv, prefix, options, repo_structure_usage, 0);
 	if (argc)
 		usage(_("too many arguments"));
-
 	repo_init_revisions(repo, &revs, prefix);
 
 	if (show_progress < 0)
@@ -962,13 +1278,13 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
 	switch (format) {
 	case FORMAT_TABLE:
 		stats_table_setup_structure(&table, &stats);
-		stats_table_print_structure(&table);
+		stats_table_print_structure(&table, show_concern);
 		break;
 	case FORMAT_NEWLINE_TERMINATED:
-		structure_keyvalue_print(&stats, '=', '\n');
+		structure_keyvalue_print(&stats, '=', '\n', show_concern);
 		break;
 	case FORMAT_NUL_TERMINATED:
-		structure_keyvalue_print(&stats, '\n', '\0');
+		structure_keyvalue_print(&stats, '\n', '\0', show_concern);
 		break;
 	default:
 		BUG("invalid output format");
diff --git a/repo.c b/repo.c
new file mode 100644
index 0000000000..51f8b7042e
--- /dev/null
+++ b/repo.c
@@ -0,0 +1 @@
+#include "repo.h"
diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index 02cc2b594a..f92168025b 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -67,6 +67,7 @@ test_expect_success 'empty repository' '
 		EOF
 
 		git repo structure >out 2>err &&
+		cp out /tmp/test1-out &&
 
 		test_cmp expect out &&
 		test_line_count = 0 err

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v4 2/2] repo: make concern thresholds configurable
  2026-10-29 17:52 ` [PATCH v4 0/2] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
  2026-10-29 17:52   ` [PATCH v4 1/2] repo: add level of concern reporting Mark C. Chu-Carroll
@ 2026-10-29 17:52   ` Mark C. Chu-Carroll
  1 sibling, 0 replies; 20+ messages in thread
From: Mark C. Chu-Carroll @ 2026-10-29 17:52 UTC (permalink / raw)
  To: git; +Cc: Mark C. Chu-Carroll

The concern reporting mechanism so far provides a
measure of concern based on heuristic values estimated
by analyzing a collection of (hopefully) representative
repositories. But for different projects/usecases, the
thresholds at which a value becomes concerning may vary.

Provide a mechanism using git config to allow users to
configure per-repo concern levels for "git repo structure"
concern reporting.

Signed-off-by: Mark C. Chu-Carroll <markchucarroll@fastmail.com>
---
 Documentation/config/repo.adoc |  59 ++++++++++++++++++
 Documentation/git-repo.adoc    |  11 ++++
 builtin/repo.c                 |  38 +++++++++++-
 t/t1901-repo-structure.sh      | 138 ++++++++++++++++++++++++++++++++++++++++-
 4 files changed, 244 insertions(+), 2 deletions(-)

diff --git a/Documentation/config/repo.adoc b/Documentation/config/repo.adoc
new file mode 100644
index 0000000000..811b099d72
--- /dev/null
+++ b/Documentation/config/repo.adoc
@@ -0,0 +1,59 @@
+`repo.concern.commit-count`::
+	Sets the value of the concern threshold for commit count.
+
+`repo.concern.tree-count`::
+	Sets the value of the concern threshold for tree count.
+
+`repo.concern.blob-count`::
+	Sets the value of the concern threshold for blob count.
+
+`repo.concern.tag-count`::
+	Sets the value of the concern threshold for tag count.
+
+`repo.concern.commit-size`::
+	Sets the value of the concern threshold for total commit size.
+
+`repo.concern.tree-size`::
+	Sets the value of the concern threshold for total tree size.
+
+`repo.concern.blob-size`::
+	Sets the value of the concern threshold for total blob size.
+
+`repo.concern.tag-size`::
+	Sets the value of the concern threshold for total tag size.
+
+`repo.concern.commit-inflated-size`::
+	Sets the value of the concern threshold for total size
+	of all inflated commits.
+
+`repo.concern.tree-inflated-size`::
+	Sets the value of the concern threshold for total size of
+	all inflated trees.
+
+`repo.concern.blob-inflated-size`::
+	Sets the value of the concern threshold for total size of
+	all inflated blobs.
+
+`repo.concern.tag-inflated-size`::
+	Sets the value of the concern threshold for the total size
+	of all inflated tags.
+
+`repo.concern.commit-largest`::
+	Sets the value of the concern threshold the size of the largest
+	commit.
+
+`repo.concern.tree-largest`::
+	Sets the value of the concern threshold for the size of the
+	largest tree.
+
+`repo.concern.blob-largest`::
+	Sets the value of the concern threshold for the size of the
+	largest blob.
+
+`repo.concern.tag-largest`::
+	Sets the value of the concern threshold for the size of the
+	largest tag.
+
+All of the concern thresholds are integer values for the
+floor of the base-2 log of the threshold value. The default values
+of each threshold are set to heuristically determined values.
diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index f186b27d87..516403e17d 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -101,6 +101,8 @@ statistic value. The value is considered concerning if it exceeds a heuristicall
 chosen threshold value; the degree of concern is the difference between the
 log_2 of the value, and the log_2 of the corresponding threshold.
 
+The threshold values are configurable; see the configuration section below.
+
 INFO KEYS
 ---------
 In order to obtain a set of values from `git repo info`, you should provide
@@ -153,6 +155,15 @@ using the `nul` format:
 git repo info --format=nul layout.bare layout.shallow
 ------------
 
+CONFIGURATION
+-------------
+
+include::includes/cmd-config-section-all.adoc[]
+
+:git-repo: 1
+include::config/repo.adoc[]
+
+
 SEE ALSO
 --------
 linkgit:git-rev-parse[1]
diff --git a/builtin/repo.c b/builtin/repo.c
index bd1866c5fa..4880c8b8f7 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -2,6 +2,7 @@
 
 #include "builtin.h"
 #include "commit.h"
+#include "config.h"
 #include "environment.h"
 #include "hash.h"
 #include "hex.h"
@@ -30,6 +31,8 @@
 #define REPO_STRUCTURE_USAGE \
 	"git repo structure [--format=(table|lines|nul) | -z] [--[no-]concern]"
 
+#define REPO_STRUCTURE_CONFIG_PREFIX "repo.concern."
+
 static const char *const repo_usage[] = {
 	REPO_INFO_USAGE,
 	REPO_STRUCTURE_USAGE,
@@ -430,6 +433,35 @@ static size_t repo_structure_concern_level(const char* concern_name,
 		return 0;
 }
 
+/*
+ * Concern thresholds aren't universal. We try to set reasonable default
+ * values, but projects may want to set these thresholds differently. To
+ * support that, they can set the thresholds for their project in the
+ * project's git config, in the section "repo.structure.concern".
+  */
+static int config_concern_thresholds(const char *prefixed_var,
+				     const char *str_val,
+				     const struct config_context *ctx,
+				     void *cb)
+{
+	struct concern_stat_threshold *thresholds = cb;
+	if (starts_with(prefixed_var, REPO_STRUCTURE_CONFIG_PREFIX)) {
+		const char *concern_name = &prefixed_var[strlen(REPO_STRUCTURE_CONFIG_PREFIX)];
+		for (int i = 0; thresholds[i].name; i++) {
+			if (!strcmp(thresholds[i].name, concern_name)) {
+				int value = git_config_int(prefixed_var, str_val, ctx->kvi);
+				if (value < 0)
+					return -1;
+				thresholds[i].threshold = value;
+				return 0;
+			}
+		}
+		warning(_("Invalid concern threshold name: '%s'"), concern_name);
+		return 0;
+	}
+	return 0;
+}
+
 /*
  * Holds column data that gets stored for each row.
  */
@@ -835,6 +867,7 @@ static void stats_table_print_structure(const struct stats_table *table,
 			strbuf_utf8_align(&buf, ALIGN_LEFT, concern_col_width, concern_str.buf);
 			strbuf_addstr(&buf, " |");
 		}
+		strbuf_release(&concern_str);
 		printf("%s\n", buf.buf);
 	}
 
@@ -1041,7 +1074,6 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 				       stats->objects.largest.tag_size.value,
 				       repo_structure_concern_thresholds),
 			       value_delim);
-
 	print_object_data("objects.commits.max_parents", key_delim,
 			  &stats->objects.largest.parent_count, value_delim);
 	print_object_data("objects.trees.max_entries", key_delim,
@@ -1269,6 +1301,10 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
 		usage(_("too many arguments"));
 	repo_init_revisions(repo, &revs, prefix);
 
+	if (show_concern)
+                repo_config(repo, &config_concern_thresholds, &repo_structure_concern_thresholds);
+
+
 	if (show_progress < 0)
 		show_progress = isatty(2);
 
diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index f92168025b..1db2cc5293 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -67,7 +67,6 @@ test_expect_success 'empty repository' '
 		EOF
 
 		git repo structure >out 2>err &&
-		cp out /tmp/test1-out &&
 
 		test_cmp expect out &&
 		test_line_count = 0 err
@@ -145,6 +144,89 @@ test_expect_success SHA1 'repository with references and objects' '
 	)
 '
 
+test_expect_success SHA1 'concern flagging' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		# differences between zlib and zlib-ng can cause this
+		# test to fail; so we just turn it off.
+		git config core.compression 0 &&
+		# configure thresholds to get concern indications
+		git config repo.concern.commit-count 7 &&
+		git config repo.concern.commit-size 14 &&
+		git config repo.concern.tree-size 10 &&
+		git config repo.concern.blob-size 8 &&
+		git config repo.concern.blob-inflated-size 10 &&
+		git config repo.concern.tag-size 2 &&
+		git config repo.concern.commit-largest 4 &&
+		git config repo.concern.blob-largest 2 &&
+		test_commit_bulk 1005 &&
+		git tag -a foo -m bar &&
+		oid="$(git rev-parse HEAD)" &&
+		git update-ref refs/remotes/origin/foo "$oid" &&
+
+		# Also creates a commit, tree, and blob.
+		git notes add -m foo &&
+
+
+		# 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      | Concern  |
+		| ------------------------- | ---------- | -------- |
+		| * References              |            |          |
+		|   * Count                 |      4     |          |
+		|     * Branches            |      1     |          |
+		|     * Tags                |      1     |          |
+		|     * Remotes             |      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.03 MiB |          |
+		|     * Commits             | 217.92 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] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
+		[3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[4] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
+		[5] 97d808e45116bf02103490294d3d46dad7a2ac62
+		[6] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
+		EOF
+
+		git repo structure --concern >actual 2>err &&
+
+		test_cmp expect actual &&
+		test_line_count = 0 err
+	)
+'
+
 test_expect_success SHA1 'lines and nul format' '
 	test_when_finished "rm -rf repo" &&
 	git init repo &&
@@ -205,6 +287,60 @@ test_expect_success SHA1 'lines and nul format' '
 	)
 '
 
+test_expect_success SHA1 'lines format with concerns' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		# configure thresholds to get concern indications
+		git config core.compression 0 &&
+		git config repo.concern.commit-count 7 &&
+		git config repo.concern.commit-size 14 &&
+		git config repo.concern.tree-size 10 &&
+		git config repo.concern.blob-size 8 &&
+		git config repo.concern.blob-inflated-size 10 &&
+		git config repo.concern.tag-size 2 &&
+		git config repo.concern.commit-largest 4 &&
+		git config repo.concern.blob-largest 2 &&
+		touch configured &&
+		test_commit_bulk 1005 &&
+		git tag -a foo -m bar &&
+		oid="$(git rev-parse HEAD)" &&
+		git update-ref refs/remotes/origin/foo "$oid" &&
+
+		# Also creates a commit, tree, and blob.
+		git notes add -m foo &&
+
+
+		# 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 &&
+		objects.commits.count.concern_level=2
+		objects.trees.count.concern_level=0
+		objects.blobs.count.concern_level=0
+		objects.tags.count.concern_level=0
+		objects.commits.inflated_size.concern_level=0
+		objects.trees.inflated_size.concern_level=0
+		objects.blobs.inflated_size.concern_level=3
+		objects.tags.inflated_size.concern_level=0
+		objects.commits.disk_size.concern_level=3
+		objects.trees.disk_size.concern_level=8
+		objects.blobs.disk_size.concern_level=6
+		objects.tags.disk_size.concern_level=5
+		objects.commits.max_size.concern_level=3
+		objects.trees.max_size.concern_level=0
+		objects.blobs.max_size.concern_level=1
+		objects.tags.max_size.concern_level=0
+		EOF
+
+		git repo structure --concern --format=lines >actual 2>err &&
+		test_grep "concern_level" actual >actual-concerns &&
+		test_cmp expect actual-concerns &&
+		test_line_count = 0 err
+	)
+'
+
 test_expect_success 'progress meter option' '
 	test_when_finished "rm -rf repo" &&
 	git init repo &&

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* Re: [PATCH v4 1/2] repo: add level of concern reporting
  2026-10-29 17:52   ` [PATCH v4 1/2] repo: add level of concern reporting Mark C. Chu-Carroll
@ 2026-10-29 19:53     ` Junio C Hamano
  2026-10-29 20:20       ` Mark C. Chu-Carroll
  0 siblings, 1 reply; 20+ messages in thread
From: Junio C Hamano @ 2026-10-29 19:53 UTC (permalink / raw)
  To: Mark C. Chu-Carroll; +Cc: git

"Mark C. Chu-Carroll" <markchucarroll@fastmail.com> writes:

> @@ -550,51 +700,76 @@ static void stats_table_setup_structure(struct stats_table *table,
>  	stats_table_object_size_addf(table,
>  				     &objects->largest.commit_size.oid,
>  				     objects->largest.commit_size.value,
> +				     repo_structure_concern_level(
> +					     CONCERN_STAT_COMMIT_LARGEST,
> +					     objects->largest.commit_size.value,
> +					     repo_structure_concern_thresholds),
>  				     "    * %s", _("Maximum size"));

So, objects->largest.commit_size is computed using the value in
objects->largest.commit_size.value field.

>  	stats_table_object_size_addf(table,
>  				     &objects->largest.tree_size.oid,
>  				     objects->largest.tree_size.value,
> +				     repo_structure_concern_level(
> +					     CONCERN_STAT_TREE_LARGEST,
> +					     objects->largest.commit_size.value,
> +					     repo_structure_concern_thresholds),
>  				     "    * %s", _("Maximum size"));

And the same objects->largest.commit_size.value is also used to
compute objects->largest.tree_size here.

Copy and paste mistake, perhaps?

Please do not take this "review" to mean that this is the only
glitch I spotted, or that I will advocate for its inclusion in
'next' once it is fixed.  I am merely pointing out what I happened
to notice while driving by.

I do not have a strong opinion for or against the feature itself,
nor have I read the entire patch carefully.

Thanks.

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v4 1/2] repo: add level of concern reporting
  2026-10-29 19:53     ` Junio C Hamano
@ 2026-10-29 20:20       ` Mark C. Chu-Carroll
  0 siblings, 0 replies; 20+ messages in thread
From: Mark C. Chu-Carroll @ 2026-10-29 20:20 UTC (permalink / raw)
  To: Junio C Hamano, Mark C. Chu-Carroll; +Cc: git

On Thu Oct 29, 2026 at 3:53 PM EDT, Junio C Hamano wrote:
> "Mark C. Chu-Carroll" <markchucarroll@fastmail.com> writes:
>
>> @@ -550,51 +700,76 @@ static void stats_table_setup_structure(struct stats_table *table,
>>  	stats_table_object_size_addf(table,
>>  				     &objects->largest.commit_size.oid,
>>  				     objects->largest.commit_size.value,
>> +				     repo_structure_concern_level(
>> +					     CONCERN_STAT_COMMIT_LARGEST,
>> +					     objects->largest.commit_size.value,
>> +					     repo_structure_concern_thresholds),
>>  				     "    * %s", _("Maximum size"));
>
> So, objects->largest.commit_size is computed using the value in
> objects->largest.commit_size.value field.
>
>>  	stats_table_object_size_addf(table,
>>  				     &objects->largest.tree_size.oid,
>>  				     objects->largest.tree_size.value,
>> +				     repo_structure_concern_level(
>> +					     CONCERN_STAT_TREE_LARGEST,
>> +					     objects->largest.commit_size.value,
>> +					     repo_structure_concern_thresholds),
>>  				     "    * %s", _("Maximum size"));
>
> And the same objects->largest.commit_size.value is also used to
> compute objects->largest.tree_size here.
>
> Copy and paste mistake, perhaps?

Argh, yeah. I road through this commit _so_ many times, and still 
managed to miss that. Grumble. Will, of course, fix.

    -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


^ permalink raw reply	[flat|nested] 20+ messages in thread

end of thread, other threads:[~2026-10-29 20:20 UTC | newest]

Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-21 13:55 [PATCH 0/3] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
2026-10-21 13:55 ` [PATCH 1/3] repo: add level of concern reporting Mark C. Chu-Carroll
2026-10-21 13:55 ` [PATCH 2/3] repo: make concern thresholds configurable Mark C. Chu-Carroll
2026-10-21 13:55 ` [PATCH 3/3] repo: add documentation for level-of-concern Mark C. Chu-Carroll
2026-10-26 17:43 ` [PATCH v2 0/3] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
2026-10-26 17:43   ` [PATCH v2 1/3] repo: add level of concern reporting Mark C. Chu-Carroll
2026-10-27 14:55     ` Patrick Steinhardt
2026-10-26 17:43   ` [PATCH v2 2/3] repo: make concern thresholds configurable Mark C. Chu-Carroll
2026-10-27 14:55     ` Patrick Steinhardt
2026-10-26 17:43   ` [PATCH v2 3/3] repo: add documentation for level-of-concern Mark C. Chu-Carroll
2026-10-27 14:55     ` Patrick Steinhardt
2026-10-28 13:23       ` Mark C. Chu-Carroll
2026-10-27 15:39 ` [PATCH v3 0/2] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
2026-10-27 15:39   ` [PATCH v3 1/2] repo: add level of concern reporting Mark C. Chu-Carroll
2026-10-27 15:39   ` [PATCH v3 2/2] repo: make concern thresholds configurable Mark C. Chu-Carroll
2026-10-29 17:52 ` [PATCH v4 0/2] repo: add degree-of-concern reporting to structure Mark C. Chu-Carroll
2026-10-29 17:52   ` [PATCH v4 1/2] repo: add level of concern reporting Mark C. Chu-Carroll
2026-10-29 19:53     ` Junio C Hamano
2026-10-29 20:20       ` Mark C. Chu-Carroll
2026-10-29 17:52   ` [PATCH v4 2/2] repo: make concern thresholds configurable Mark C. Chu-Carroll

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox