public inbox for git@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/3] repo: extend info path reporting and structure statistics
@ 2026-02-22 18:28 eslam reda via GitGitGadget
  2026-02-22 18:28 ` [PATCH 1/3] repo: extend info paths " eslam-reda-div via GitGitGadget
                   ` (4 more replies)
  0 siblings, 5 replies; 68+ messages in thread
From: eslam reda via GitGitGadget @ 2026-02-22 18:28 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda

This series improves git repo info and git repo structure.

For git repo info, it:

 * removes reliance on global repository state in this command path
 * supports category keys (e.g. layout, path)
 * adds rev-parse-like path keys and git-path-derived keys
 * adds --path-format=(absolute|relative) for path output control

For git repo structure, it adds deeper repository metrics inspired by
git-sizer:

 * max inflated and max disk size per object type
 * max commit parent count
 * max tree entry count
 * max blob path length/depth
 * max annotated tag chain depth
 * aggregate keyvalue/nul totals and maxima

Tests and docs are updated accordingly.

Validation:

 * t/t1900-repo.sh
 * t/t1901-repo-structure.sh
 * full make -j4 test in Docker (failed 0)

Eslam reda ragheb (2):
  t1900,t1901: make repo tests hash-agnostic and wc-portable
  t1900,t1901: fix test portability issues

eslam-reda-div (1):
  repo: extend info paths and structure statistics

 Documentation/git-repo.adoc |  67 ++++-
 builtin/repo.c              | 525 ++++++++++++++++++++++++++++++++++--
 t/t1900-repo.sh             | 196 ++++++++++++++
 t/t1901-repo-structure.sh   | 250 ++++++++++++-----
 4 files changed, 948 insertions(+), 90 deletions(-)


base-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2208%2Feslam-reda-div%2Fgsoc-contribute-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2208/eslam-reda-div/gsoc-contribute-v1
Pull-Request: https://github.com/git/git/pull/2208
-- 
gitgitgadget

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

* [PATCH 1/3] repo: extend info paths and structure statistics
  2026-02-22 18:28 [PATCH 0/3] repo: extend info path reporting and structure statistics eslam reda via GitGitGadget
@ 2026-02-22 18:28 ` eslam-reda-div via GitGitGadget
  2026-02-22 20:35   ` Lucas Seiki Oshiro
  2026-02-23  3:02   ` Justin Tobler
  2026-02-22 18:28 ` [PATCH 2/3] t1900,t1901: make repo tests hash-agnostic and wc-portable Eslam reda ragheb via GitGitGadget
                   ` (3 subsequent siblings)
  4 siblings, 2 replies; 68+ messages in thread
From: eslam-reda-div via GitGitGadget @ 2026-02-22 18:28 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda, eslam-reda-div

From: eslam-reda-div <eslam.reda.div@gmail.com>

Improve git repo info by adding path-oriented keys that match values
users currently obtain from git rev-parse, including common directory,
git directory, top-level, superproject working tree, and additional
git-path based locations.

Teach git repo info to accept category keys like layout and path,
and add --path-format=(absolute|relative) so scripts can request the
desired path style explicitly. The command now uses repository context
passed to the command path instead of relying on global state.

Extend git repo structure with deeper repository metrics inspired by
git-sizer, including per-type maximum inflated and on-disk object sizes,
maximum commit parent count, maximum tree entry count, longest/deepest
blob path, and deepest annotated tag chain.

Update documentation and tests to cover new keys, formats, and metrics.

Signed-off-by: eslam-reda-div <eslam.reda.div@gmail.com>
---
 Documentation/git-repo.adoc |  67 ++++-
 builtin/repo.c              | 525 ++++++++++++++++++++++++++++++++++--
 t/t1900-repo.sh             | 196 ++++++++++++++
 t/t1901-repo-structure.sh   | 250 ++++++++++++-----
 4 files changed, 948 insertions(+), 90 deletions(-)

diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 7d70270dfa..b575977a4b 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -8,7 +8,7 @@ git-repo - Retrieve information about the repository
 SYNOPSIS
 --------
 [synopsis]
-git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]
+git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]
 git repo structure [--format=(table|keyvalue|nul) | -z]
 
 DESCRIPTION
@@ -44,6 +44,11 @@ supported:
 +
 `-z` is an alias for `--format=nul`.
 
+`--path-format=(absolute|relative)`:::
+	Controls formatting for keys in the `path` category. The default is
+	`absolute`. This option may be specified multiple times; the last one
+	specified takes effect.
+
 `structure [--format=(table|keyvalue|nul) | -z]`::
 	Retrieve statistics about the current repository structure. The
 	following kinds of information are reported:
@@ -52,6 +57,12 @@ supported:
 * Reachable object counts categorized by type
 * Total inflated size of reachable objects by type
 * Total disk size of reachable objects by type
+* Largest inflated reachable object size by type
+* Largest disk size of a reachable object by type
+* Largest parent count among reachable commits
+* Largest entry count among reachable trees
+* Longest and deepest path among reachable blobs
+* Deepest annotated tag chain
 +
 The output format can be chosen through the flag `--format`. Three formats are
 supported:
@@ -64,6 +75,7 @@ supported:
 `keyvalue`:::
 	Each line of output contains a key-value pair for a repository stat.
 	The '=' character is used to delimit between the key and the value.
+	Both aggregate metrics and per-type metrics are included.
 	Values containing "unusual" characters are quoted as explained for the
 	configuration variable `core.quotePath` (see linkgit:git-config[1]).
 
@@ -78,9 +90,11 @@ supported:
 
 INFO KEYS
 ---------
-In order to obtain a set of values from `git repo info`, you should provide
-the keys that identify them. Here's a list of the available keys and the
-values that they return:
+In order to obtain values from `git repo info`, provide either individual keys
+or category names. A category returns all keys within that category. For
+example, `layout` returns both `layout.bare` and `layout.shallow`.
+
+Here's a list of the available keys and the values that they return:
 
 `layout.bare`::
 	`true` if this is a bare repository, otherwise `false`.
@@ -91,6 +105,51 @@ values that they return:
 `object.format`::
 	The object format (hash algorithm) used in the repository.
 
+`path.common-dir`::
+	The path to the common git directory.
+
+`path.config-file`::
+	The path to the `config` file in the git directory.
+
+`path.git-dir`::
+	The path to the git directory.
+
+`path.git-prefix`::
+	The path of the current working directory relative to the top-level
+	directory.
+
+`path.grafts-file`::
+	The path to the `info/grafts` file.
+
+`path.hooks-directory`::
+	The path to the `hooks` directory.
+
+`path.index-file`::
+	The path to the index file.
+
+`path.logs-directory`::
+	The path to the `logs` directory.
+
+`path.objects-directory`::
+	The path to the objects directory.
+
+`path.packed-refs-file`::
+	The path to the `packed-refs` file.
+
+`path.refs-directory`::
+	The path to the `refs` directory.
+
+`path.shallow-file`::
+	The path to the `shallow` file.
+
+`path.superproject-working-tree`::
+	The path to the superproject's working tree root, or an empty string
+	when the repository is not used as a submodule.
+
+`path.toplevel`::
+	The path to the top-level working tree directory, or an empty string
+	for bare repositories.
+
 `references.format`::
 	The reference storage format. The valid values are:
 +
diff --git a/builtin/repo.c b/builtin/repo.c
index 0ea045abc1..df2702fddb 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -1,10 +1,9 @@
-#define USE_THE_REPOSITORY_VARIABLE
-
 #include "builtin.h"
-#include "environment.h"
+#include "abspath.h"
 #include "hex.h"
 #include "odb.h"
 #include "parse-options.h"
+#include "path.h"
 #include "path-walk.h"
 #include "progress.h"
 #include "quote.h"
@@ -13,16 +12,29 @@
 #include "revision.h"
 #include "strbuf.h"
 #include "string-list.h"
+#include "submodule.h"
 #include "shallow.h"
+#include "tree-walk.h"
 #include "utf8.h"
 
 static const char *const repo_usage[] = {
-	"git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]",
+	"git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]",
 	"git repo structure [--format=(table|keyvalue|nul) | -z]",
 	NULL
 };
 
-typedef int get_value_fn(struct repository *repo, struct strbuf *buf);
+enum path_format {
+	PATH_FORMAT_ABSOLUTE,
+	PATH_FORMAT_RELATIVE,
+};
+
+struct repo_info {
+	struct repository *repo;
+	const char *prefix;
+	enum path_format path_format;
+};
+
+typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);
 
 enum output_format {
 	FORMAT_TABLE,
@@ -35,27 +47,161 @@ struct field {
 	get_value_fn *get_value;
 };
 
-static int get_layout_bare(struct repository *repo UNUSED, struct strbuf *buf)
+static void repo_info_add_path(struct repo_info *info,
+			       struct strbuf *buf,
+			       const char *path)
 {
-	strbuf_addstr(buf, is_bare_repository() ? "true" : "false");
+	if (info->path_format == PATH_FORMAT_RELATIVE) {
+		char *cwd = xgetcwd();
+		struct strbuf rel_path = STRBUF_INIT;
+
+		strbuf_addstr(buf, relative_path(path, cwd, &rel_path));
+		strbuf_release(&rel_path);
+		free(cwd);
+		return;
+	}
+
+	strbuf_add_absolute_path(buf, path);
+}
+
+static int get_layout_bare(struct repo_info *info, struct strbuf *buf)
+{
+	struct repository *repo = info->repo;
+	strbuf_addstr(buf, repo_get_work_tree(repo) ? "false" : "true");
 	return 0;
 }
 
-static int get_layout_shallow(struct repository *repo, struct strbuf *buf)
+static int get_layout_shallow(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf,
 		      is_repository_shallow(repo) ? "true" : "false");
 	return 0;
 }
 
-static int get_object_format(struct repository *repo, struct strbuf *buf)
+static int get_object_format(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf, repo->hash_algo->name);
 	return 0;
 }
 
-static int get_references_format(struct repository *repo, struct strbuf *buf)
+static int get_path_common_dir(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_common_dir(info->repo));
+	return 0;
+}
+
+static int get_path_config_file(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "config"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_git_dir(info->repo));
+	return 0;
+}
+
+static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)
+{
+	if (info->prefix)
+		strbuf_addstr(buf, info->prefix);
+	return 0;
+}
+
+static int get_path_grafts_file(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_graft_file(info->repo));
+	return 0;
+}
+
+static int get_path_hooks_directory(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "hooks"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_index_file(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_index_file(info->repo));
+	return 0;
+}
+
+static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "logs"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_object_directory(info->repo));
+	return 0;
+}
+
+static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "packed-refs"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "refs"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "shallow"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_superproject_working_tree(struct repo_info *info,
+					      struct strbuf *buf)
+{
+	struct strbuf superproject = STRBUF_INIT;
+
+	if (get_superproject_working_tree(&superproject))
+		repo_info_add_path(info, buf, superproject.buf);
+
+	strbuf_release(&superproject);
+	return 0;
+}
+
+static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)
+{
+	const char *work_tree = repo_get_work_tree(info->repo);
+
+	if (work_tree)
+		repo_info_add_path(info, buf, work_tree);
+
+	return 0;
+}
+
+static int get_references_format(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf,
 		      ref_storage_format_to_name(repo->ref_storage_format));
 	return 0;
@@ -66,6 +212,20 @@ static const struct field repo_info_fields[] = {
 	{ "layout.bare", get_layout_bare },
 	{ "layout.shallow", get_layout_shallow },
 	{ "object.format", get_object_format },
+	{ "path.common-dir", get_path_common_dir },
+	{ "path.config-file", get_path_config_file },
+	{ "path.git-dir", get_path_git_dir },
+	{ "path.git-prefix", get_path_git_prefix },
+	{ "path.grafts-file", get_path_grafts_file },
+	{ "path.hooks-directory", get_path_hooks_directory },
+	{ "path.index-file", get_path_index_file },
+	{ "path.logs-directory", get_path_logs_directory },
+	{ "path.objects-directory", get_path_objects_directory },
+	{ "path.packed-refs-file", get_path_packed_refs_file },
+	{ "path.refs-directory", get_path_refs_directory },
+	{ "path.shallow-file", get_path_shallow_file },
+	{ "path.superproject-working-tree", get_path_superproject_working_tree },
+	{ "path.toplevel", get_path_toplevel },
 	{ "references.format", get_references_format },
 };
 
@@ -87,6 +247,33 @@ static get_value_fn *get_value_fn_for_key(const char *key)
 	return found ? found->get_value : NULL;
 }
 
+static void print_field(enum output_format format, const char *key,
+			const char *value);
+
+static int print_category_fields(const char *category,
+				 struct repo_info *info,
+				 enum output_format format,
+				 struct strbuf *valbuf)
+{
+	int found = 0;
+	size_t category_len = strlen(category);
+
+	for (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {
+		const struct field *field = &repo_info_fields[i];
+
+		if (!starts_with(field->key, category) ||
+		    field->key[category_len] != '.')
+			continue;
+
+		strbuf_reset(valbuf);
+		field->get_value(info, valbuf);
+		print_field(format, field->key, valbuf->buf);
+		found = 1;
+	}
+
+	return found;
+}
+
 static void print_field(enum output_format format, const char *key,
 			const char *value)
 {
@@ -105,7 +292,7 @@ static void print_field(enum output_format format, const char *key,
 }
 
 static int print_fields(int argc, const char **argv,
-			struct repository *repo,
+			struct repo_info *info,
 			enum output_format format)
 {
 	int ret = 0;
@@ -117,21 +304,22 @@ static int print_fields(int argc, const char **argv,
 
 		get_value = get_value_fn_for_key(key);
 
-		if (!get_value) {
-			ret = error(_("key '%s' not found"), key);
+		if (get_value) {
+			strbuf_reset(&valbuf);
+			get_value(info, &valbuf);
+			print_field(format, key, valbuf.buf);
 			continue;
 		}
 
-		strbuf_reset(&valbuf);
-		get_value(repo, &valbuf);
-		print_field(format, key, valbuf.buf);
+		if (!print_category_fields(key, info, format, &valbuf))
+			ret = error(_("key '%s' not found"), key);
 	}
 
 	strbuf_release(&valbuf);
 	return ret;
 }
 
-static int print_all_fields(struct repository *repo,
+static int print_all_fields(struct repo_info *info,
 			    enum output_format format)
 {
 	struct strbuf valbuf = STRBUF_INIT;
@@ -140,7 +328,7 @@ static int print_all_fields(struct repository *repo,
 		const struct field *field = &repo_info_fields[i];
 
 		strbuf_reset(&valbuf);
-		field->get_value(repo, &valbuf);
+		field->get_value(info, &valbuf);
 		print_field(format, field->key, valbuf.buf);
 	}
 
@@ -167,10 +355,30 @@ static int parse_format_cb(const struct option *opt,
 	return 0;
 }
 
+static int parse_path_format_cb(const struct option *opt,
+				const char *arg, int unset UNUSED)
+{
+	enum path_format *path_format = opt->value;
+
+	if (!strcmp(arg, "absolute"))
+		*path_format = PATH_FORMAT_ABSOLUTE;
+	else if (!strcmp(arg, "relative"))
+		*path_format = PATH_FORMAT_RELATIVE;
+	else
+		die(_("invalid path format '%s'"), arg);
+
+	return 0;
+}
+
 static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 			 struct repository *repo)
 {
 	enum output_format format = FORMAT_KEYVALUE;
+	struct repo_info info = {
+		.repo = repo,
+		.prefix = prefix,
+		.path_format = PATH_FORMAT_ABSOLUTE,
+	};
 	int all_keys = 0;
 	struct option options[] = {
 		OPT_CALLBACK_F(0, "format", &format, N_("format"),
@@ -180,6 +388,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 			       N_("synonym for --format=nul"),
 			       PARSE_OPT_NONEG | PARSE_OPT_NOARG,
 			       parse_format_cb),
+		OPT_CALLBACK_F(0, "path-format", &info.path_format,
+			       N_("format"), N_("path output format"),
+			       PARSE_OPT_NONEG, parse_path_format_cb),
 		OPT_BOOL(0, "all", &all_keys, N_("print all keys/values")),
 		OPT_END()
 	};
@@ -192,9 +403,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 		die(_("--all and <key> cannot be used together"));
 
 	if (all_keys)
-		return print_all_fields(repo, format);
+		return print_all_fields(&info, format);
 	else
-		return print_fields(argc, argv, repo, format);
+		return print_fields(argc, argv, &info, format);
 }
 
 struct ref_stats {
@@ -214,7 +425,14 @@ struct object_values {
 struct object_stats {
 	struct object_values type_counts;
 	struct object_values inflated_sizes;
+	struct object_values max_inflated_sizes;
 	struct object_values disk_sizes;
+	struct object_values max_disk_sizes;
+	size_t max_commit_parent_count;
+	size_t max_tree_entry_count;
+	size_t max_blob_path_length;
+	size_t max_blob_path_depth;
+	size_t max_tag_chain_depth;
 };
 
 struct repo_structure {
@@ -317,6 +535,130 @@ static inline size_t get_total_object_values(struct object_values *values)
 	return values->tags + values->commits + values->trees + values->blobs;
 }
 
+static inline size_t get_max_object_value(struct object_values *values)
+{
+	size_t max = values->commits;
+
+	if (values->trees > max)
+		max = values->trees;
+	if (values->blobs > max)
+		max = values->blobs;
+	if (values->tags > max)
+		max = values->tags;
+
+	return max;
+}
+
+static size_t get_commit_parent_count(struct repository *repo,
+				      const struct object_id *oid)
+{
+	unsigned long size = 0;
+	const char *cur;
+	const char *end;
+	void *buf;
+	size_t count = 0;
+
+	buf = odb_read_object_peeled(repo->objects, oid, OBJ_COMMIT, &size, NULL);
+	if (!buf)
+		return 0;
+
+	cur = buf;
+	end = cur + size;
+	while (cur < end) {
+		const char *newline = memchr(cur, '\n', end - cur);
+		size_t line_len;
+
+		if (!newline)
+			break;
+		line_len = newline - cur;
+		if (!line_len)
+			break;
+
+		if (line_len > 7 && !memcmp(cur, "parent ", 7))
+			count++;
+
+		cur = newline + 1;
+	}
+
+	free(buf);
+	return count;
+}
+
+static size_t get_tree_entry_count(struct repository *repo,
+				   const struct object_id *oid)
+{
+	struct tree_desc desc;
+	struct name_entry entry;
+	unsigned long size = 0;
+	void *buf;
+	size_t count = 0;
+
+	buf = odb_read_object_peeled(repo->objects, oid, OBJ_TREE, &size, NULL);
+	if (!buf)
+		return 0;
+
+	init_tree_desc(&desc, oid, buf, size);
+	while (tree_entry(&desc, &entry))
+		count++;
+
+	free(buf);
+	return count;
+}
+
+static size_t get_path_depth(const char *path)
+{
+	size_t depth = 0;
+
+	if (!path || !*path)
+		return 0;
+
+	depth = 1;
+	for (const char *cur = path; *cur; cur++)
+		if (*cur == '/')
+			depth++;
+
+	return depth;
+}
+
+static size_t get_tag_chain_depth(struct repository *repo,
+				  const struct object_id *oid)
+{
+	struct object_id current = *oid;
+	size_t depth = 0;
+
+	while (1) {
+		enum object_type type;
+		unsigned long size = 0;
+		struct object_id next;
+		const char *p, *end;
+		void *buf = odb_read_object(repo->objects, &current, &type, &size);
+
+		if (!buf)
+			break;
+		if (type != OBJ_TAG) {
+			free(buf);
+			break;
+		}
+
+		p = buf;
+		if (!skip_prefix(p, "object ", &p) ||
+		    parse_oid_hex_algop(p, &next, &end, repo->hash_algo) ||
+		    *end != '\n') {
+			free(buf);
+			break;
+		}
+
+		depth++;
+		free(buf);
+
+		if (oideq(&next, &current))
+			break;
+		oidcpy(&current, &next);
+	}
+
+	return depth;
+}
+
 static void stats_table_setup_structure(struct stats_table *table,
 					struct repo_structure *stats)
 {
@@ -371,6 +713,37 @@ static void stats_table_setup_structure(struct stats_table *table,
 			      "    * %s", _("Blobs"));
 	stats_table_size_addf(table, objects->disk_sizes.tags,
 			      "    * %s", _("Tags"));
+
+	stats_table_size_addf(table, objects->max_inflated_sizes.commits,
+			      "  * %s", _("Largest commit"));
+	stats_table_size_addf(table, objects->max_inflated_sizes.trees,
+			      "  * %s", _("Largest tree"));
+	stats_table_size_addf(table, objects->max_inflated_sizes.blobs,
+			      "  * %s", _("Largest blob"));
+	stats_table_size_addf(table, objects->max_inflated_sizes.tags,
+			      "  * %s", _("Largest tag"));
+
+	stats_table_size_addf(table, get_max_object_value(&objects->max_disk_sizes),
+			      "  * %s", _("Largest disk size"));
+	stats_table_size_addf(table, objects->max_disk_sizes.commits,
+			      "    * %s", _("Commits"));
+	stats_table_size_addf(table, objects->max_disk_sizes.trees,
+			      "    * %s", _("Trees"));
+	stats_table_size_addf(table, objects->max_disk_sizes.blobs,
+			      "    * %s", _("Blobs"));
+	stats_table_size_addf(table, objects->max_disk_sizes.tags,
+			      "    * %s", _("Tags"));
+
+	stats_table_count_addf(table, objects->max_commit_parent_count,
+			       "  * %s", _("Largest parent count"));
+	stats_table_count_addf(table, objects->max_tree_entry_count,
+			       "  * %s", _("Largest tree entries"));
+	stats_table_count_addf(table, objects->max_blob_path_length,
+			       "  * %s", _("Longest blob path"));
+	stats_table_count_addf(table, objects->max_blob_path_depth,
+			       "  * %s", _("Deepest blob path"));
+	stats_table_count_addf(table, objects->max_tag_chain_depth,
+			       "  * %s", _("Deepest tag chain"));
 }
 
 static void stats_table_print_structure(const struct stats_table *table)
@@ -449,6 +822,15 @@ static void stats_table_clear(struct stats_table *table)
 static void structure_keyvalue_print(struct repo_structure *stats,
 				     char key_delim, char value_delim)
 {
+	size_t references_count_total = get_total_reference_count(&stats->refs);
+	size_t object_count_total = get_total_object_values(&stats->objects.type_counts);
+	size_t inflated_size_total = get_total_object_values(&stats->objects.inflated_sizes);
+	size_t disk_size_total = get_total_object_values(&stats->objects.disk_sizes);
+	size_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);
+	size_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);
+
+	printf("references.count%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)references_count_total, value_delim);
 	printf("references.branches.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->refs.branches, value_delim);
 	printf("references.tags.count%c%" PRIuMAX "%c", key_delim,
@@ -458,6 +840,8 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("references.others.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->refs.others, value_delim);
 
+	printf("objects.count%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)object_count_total, value_delim);
 	printf("objects.commits.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.type_counts.commits, value_delim);
 	printf("objects.trees.count%c%" PRIuMAX "%c", key_delim,
@@ -467,6 +851,8 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("objects.tags.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.type_counts.tags, value_delim);
 
+	printf("objects.inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)inflated_size_total, value_delim);
 	printf("objects.commits.inflated_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.inflated_sizes.commits, value_delim);
 	printf("objects.trees.inflated_size%c%" PRIuMAX "%c", key_delim,
@@ -476,6 +862,41 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("objects.tags.inflated_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.inflated_sizes.tags, value_delim);
 
+	printf("objects.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)max_inflated_size, value_delim);
+	printf("objects.commits.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_inflated_sizes.commits, value_delim);
+	printf("objects.trees.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_inflated_sizes.trees, value_delim);
+	printf("objects.blobs.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_inflated_sizes.blobs, value_delim);
+	printf("objects.tags.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);
+
+	printf("objects.disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)disk_size_total, value_delim);
+	printf("objects.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)max_disk_size, value_delim);
+	printf("objects.commits.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_disk_sizes.commits, value_delim);
+	printf("objects.trees.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_disk_sizes.trees, value_delim);
+	printf("objects.blobs.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_disk_sizes.blobs, value_delim);
+	printf("objects.tags.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);
+
+	printf("objects.commits.max_parent_count%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_commit_parent_count, value_delim);
+	printf("objects.trees.max_entry_count%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_tree_entry_count, value_delim);
+	printf("objects.blobs.max_path_length%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_blob_path_length, value_delim);
+	printf("objects.blobs.max_path_depth%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_blob_path_depth, value_delim);
+	printf("objects.tags.max_chain_depth%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_tag_chain_depth, value_delim);
+
 	printf("objects.commits.disk_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);
 	printf("objects.trees.disk_size%c%" PRIuMAX "%c", key_delim,
@@ -553,13 +974,15 @@ struct count_objects_data {
 	struct progress *progress;
 };
 
-static int count_objects(const char *path UNUSED, struct oid_array *oids,
+static int count_objects(const char *path, struct oid_array *oids,
 			 enum object_type type, void *cb_data)
 {
 	struct count_objects_data *data = cb_data;
 	struct object_stats *stats = data->stats;
 	size_t inflated_total = 0;
 	size_t disk_total = 0;
+	size_t max_inflated = 0;
+	size_t max_disk = 0;
 	size_t object_count;
 
 	for (size_t i = 0; i < oids->nr; i++) {
@@ -572,33 +995,89 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 
 		if (odb_read_object_info_extended(data->odb, &oids->oid[i], &oi,
 						  OBJECT_INFO_SKIP_FETCH_OBJECT |
-						  OBJECT_INFO_QUICK) < 0)
+							  OBJECT_INFO_QUICK) < 0)
+			continue;
+		if (disk < 0)
 			continue;
 
 		inflated_total += inflated;
-		disk_total += disk;
+		disk_total += (size_t)disk;
+		if (inflated > max_inflated)
+			max_inflated = inflated;
+		if ((size_t)disk > max_disk)
+			max_disk = (size_t)disk;
 	}
 
 	switch (type) {
 	case OBJ_TAG:
+		for (size_t i = 0; i < oids->nr; i++) {
+			size_t tag_chain_depth = get_tag_chain_depth(data->odb->repo,
+								     &oids->oid[i]);
+
+			if (tag_chain_depth > stats->max_tag_chain_depth)
+				stats->max_tag_chain_depth = tag_chain_depth;
+		}
+
 		stats->type_counts.tags += oids->nr;
 		stats->inflated_sizes.tags += inflated_total;
+		if (max_inflated > stats->max_inflated_sizes.tags)
+			stats->max_inflated_sizes.tags = max_inflated;
 		stats->disk_sizes.tags += disk_total;
+		if (max_disk > stats->max_disk_sizes.tags)
+			stats->max_disk_sizes.tags = max_disk;
 		break;
 	case OBJ_COMMIT:
+		for (size_t i = 0; i < oids->nr; i++) {
+			size_t parent_count = get_commit_parent_count(data->odb->repo,
+								      &oids->oid[i]);
+
+			if (parent_count > stats->max_commit_parent_count)
+				stats->max_commit_parent_count = parent_count;
+		}
+
 		stats->type_counts.commits += oids->nr;
 		stats->inflated_sizes.commits += inflated_total;
+		if (max_inflated > stats->max_inflated_sizes.commits)
+			stats->max_inflated_sizes.commits = max_inflated;
 		stats->disk_sizes.commits += disk_total;
+		if (max_disk > stats->max_disk_sizes.commits)
+			stats->max_disk_sizes.commits = max_disk;
 		break;
 	case OBJ_TREE:
+		for (size_t i = 0; i < oids->nr; i++) {
+			size_t entry_count = get_tree_entry_count(data->odb->repo,
+								  &oids->oid[i]);
+
+			if (entry_count > stats->max_tree_entry_count)
+				stats->max_tree_entry_count = entry_count;
+		}
+
 		stats->type_counts.trees += oids->nr;
 		stats->inflated_sizes.trees += inflated_total;
+		if (max_inflated > stats->max_inflated_sizes.trees)
+			stats->max_inflated_sizes.trees = max_inflated;
 		stats->disk_sizes.trees += disk_total;
+		if (max_disk > stats->max_disk_sizes.trees)
+			stats->max_disk_sizes.trees = max_disk;
 		break;
 	case OBJ_BLOB:
+		if (path && *path) {
+			size_t path_len = strlen(path);
+			size_t path_depth = get_path_depth(path);
+
+			if (path_len > stats->max_blob_path_length)
+				stats->max_blob_path_length = path_len;
+			if (path_depth > stats->max_blob_path_depth)
+				stats->max_blob_path_depth = path_depth;
+		}
+
 		stats->type_counts.blobs += oids->nr;
 		stats->inflated_sizes.blobs += inflated_total;
+		if (max_inflated > stats->max_inflated_sizes.blobs)
+			stats->max_inflated_sizes.blobs = max_inflated;
 		stats->disk_sizes.blobs += disk_total;
+		if (max_disk > stats->max_disk_sizes.blobs)
+			stats->max_disk_sizes.blobs = max_disk;
 		break;
 	default:
 		BUG("invalid object type");
diff --git a/t/t1900-repo.sh b/t/t1900-repo.sh
index 51d55f11a5..4bfd48b85c 100755
--- a/t/t1900-repo.sh
+++ b/t/t1900-repo.sh
@@ -10,9 +10,40 @@ REPO_INFO_KEYS='
 	layout.bare
 	layout.shallow
 	object.format
+	path.common-dir
+	path.config-file
+	path.git-dir
+	path.git-prefix
+	path.grafts-file
+	path.hooks-directory
+	path.index-file
+	path.logs-directory
+	path.objects-directory
+	path.packed-refs-file
+	path.refs-directory
+	path.shallow-file
+	path.superproject-working-tree
+	path.toplevel
 	references.format
 '
 
+REPO_INFO_PATH_KEYS='
+	path.common-dir
+	path.config-file
+	path.git-dir
+	path.git-prefix
+	path.grafts-file
+	path.hooks-directory
+	path.index-file
+	path.logs-directory
+	path.objects-directory
+	path.packed-refs-file
+	path.refs-directory
+	path.shallow-file
+	path.superproject-working-tree
+	path.toplevel
+'
+
 # Test whether a key-value pair is correctly returned
 #
 # Usage: test_repo_info <label> <init command> <repo_name> <key> <expected value>
@@ -89,6 +120,171 @@ test_expect_success 'values returned in order requested' '
 	test_cmp expect actual
 '
 
+test_expect_success 'category key returns all matching keys' '
+	cat >expect <<-\EOF &&
+	layout.bare=false
+	layout.shallow=false
+	EOF
+	git init category-layout &&
+	git -C category-layout repo info layout >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'mixed key/category requests preserve request order' '
+	cat >expect <<-\EOF &&
+	object.format=sha1
+	layout.bare=false
+	layout.shallow=false
+	EOF
+	git init mixed-order &&
+	git -C mixed-order repo info object.format layout >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.git-dir matches rev-parse --absolute-git-dir' '
+	git init path-git-dir &&
+	expected_value=$(git -C path-git-dir rev-parse --absolute-git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-git-dir repo info path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.common-dir matches rev-parse --git-common-dir' '
+	git init path-common-dir &&
+	expected_value=$(git -C path-common-dir rev-parse --path-format=absolute --git-common-dir) &&
+	echo "path.common-dir=$expected_value" >expect &&
+	git -C path-common-dir repo info path.common-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.toplevel matches rev-parse --show-toplevel' '
+	git init path-toplevel &&
+	expected_value=$(git -C path-toplevel rev-parse --show-toplevel) &&
+	echo "path.toplevel=$expected_value" >expect &&
+	git -C path-toplevel repo info path.toplevel >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.toplevel is empty in bare repository' '
+	git init --bare bare-path-toplevel &&
+	echo "path.toplevel=" >expect &&
+	git -C bare-path-toplevel repo info path.toplevel >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '
+	git init path-prefix &&
+	mkdir -p path-prefix/a/b &&
+	expected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&
+	echo "path.git-prefix=$expected_value" >expect &&
+	git -C path-prefix/a/b repo info path.git-prefix >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git-path style keys match rev-parse --git-path' '
+	git init path-git-path &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path info/grafts) &&
+	echo "path.grafts-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.grafts-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path index) &&
+	echo "path.index-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.index-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path objects) &&
+	echo "path.objects-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.objects-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path hooks) &&
+	echo "path.hooks-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.hooks-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path config) &&
+	echo "path.config-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.config-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&
+	echo "path.logs-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.logs-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&
+	echo "path.packed-refs-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.packed-refs-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&
+	echo "path.refs-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.refs-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&
+	echo "path.shallow-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.shallow-file >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.superproject-working-tree is empty when not a submodule' '
+	git init path-superproject &&
+	echo "path.superproject-working-tree=" >expect &&
+	git -C path-superproject repo info path.superproject-working-tree >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.superproject-working-tree matches rev-parse in submodule' '
+	git init path-superproject-origin &&
+	echo x >path-superproject-origin/x &&
+	git -C path-superproject-origin add x &&
+	git -C path-superproject-origin commit -m x &&
+
+	git init path-superproject-parent &&
+	git -C path-superproject-parent -c protocol.file.allow=always submodule add ../path-superproject-origin sm &&
+
+	expected_value=$(git -C path-superproject-parent/sm rev-parse --show-superproject-working-tree) &&
+	echo "path.superproject-working-tree=$expected_value" >expect &&
+	git -C path-superproject-parent/sm repo info path.superproject-working-tree >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path category returns all path keys' '
+	git init path-category &&
+	>expect &&
+	for key in $REPO_INFO_PATH_KEYS
+	do
+		git -C path-category repo info "$key" >>expect || return 1
+	done &&
+	git -C path-category repo info path >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path-format=relative matches rev-parse for git-dir' '
+	git init path-format-relative &&
+	expected_value=$(git -C path-format-relative rev-parse --path-format=relative --git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-format-relative repo info --path-format=relative path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git repo info uses the last requested path format' '
+	git init path-format-last &&
+	expected_value=$(git -C path-format-last rev-parse --path-format=relative --git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-format-last repo info --path-format=absolute --path-format=relative path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git-repo-info aborts when requesting an invalid path format' '
+	echo "fatal: invalid path format ${SQ}foo${SQ}" >expect &&
+	test_must_fail git repo info --path-format=foo path.git-dir 2>actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'git-repo-info fails if an invalid key is requested' '
 	echo "error: key ${SQ}foo${SQ} not found" >expect &&
 	test_must_fail git repo info foo 2>actual &&
diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index 17ff164b05..2d493fbbc4 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -21,42 +21,174 @@ object_type_disk_usage() {
 	fi
 }
 
+object_type_max_inflated_size() {
+	max=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=$1 --filter-provided-objects | cut -d" " -f1)
+	do
+		size=$(git cat-file -s "$oid") || return 1
+		test "$size" -gt "$max" && max=$size
+	done
+
+	echo "$max"
+}
+
+tag_max_chain_depth() {
+	max=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=tag --filter-provided-objects | cut -d" " -f1)
+	do
+		depth=0
+		current=$oid
+
+		while :
+		do
+			target=$(git cat-file -p "$current" | sed -n "s/^object //p" | sed -n 1p) || return 1
+			test -n "$target" || break
+			depth=$((depth + 1))
+			type=$(git cat-file -t "$target") || return 1
+			test "$type" = tag || break
+			current=$target
+		done
+
+		test "$depth" -gt "$max" && max=$depth
+	done
+
+	echo "$max"
+}
+
+object_max_inflated_size() {
+	max=0
+
+	for type in commit tree blob tag
+	do
+		type_max=$(object_type_max_inflated_size "$type") || return 1
+		test "$type_max" -gt "$max" && max=$type_max
+	done
+
+	echo "$max"
+}
+
+object_type_max_disk_size() {
+	max=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=$1 --filter-provided-objects | cut -d" " -f1)
+	do
+		size=$(echo "$oid" | git cat-file --batch-check='%(objectsize:disk)') || return 1
+		test "$size" -gt "$max" && max=$size
+	done
+
+	echo "$max"
+}
+
+reference_count_total() {
+	git for-each-ref --format='%(refname)' | sed -n '$='
+}
+
+object_type_count() {
+	git rev-list --all --objects \
+		--filter=object:type=$1 --filter-provided-objects | sed -n '$='
+}
+
+object_count_total() {
+	commits=$(object_type_count commit) || return 1
+	trees=$(object_type_count tree) || return 1
+	blobs=$(object_type_count blob) || return 1
+	tags=$(object_type_count tag) || return 1
+
+	echo $((commits + trees + blobs + tags))
+}
+
+object_type_total_inflated_size() {
+	total=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=$1 --filter-provided-objects | cut -d" " -f1)
+	do
+		size=$(git cat-file -s "$oid") || return 1
+		total=$((total + size))
+	done
+
+	echo "$total"
+}
+
+object_total_inflated_size() {
+	commits=$(object_type_total_inflated_size commit) || return 1
+	trees=$(object_type_total_inflated_size tree) || return 1
+	blobs=$(object_type_total_inflated_size blob) || return 1
+	tags=$(object_type_total_inflated_size tag) || return 1
+
+	echo $((commits + trees + blobs + tags))
+}
+
+object_max_disk_size() {
+	max=0
+
+	for type in commit tree blob tag
+	do
+		type_max=$(object_type_max_disk_size "$type") || return 1
+		test "$type_max" -gt "$max" && max=$type_max
+	done
+
+	echo "$max"
+}
+
+commit_max_parent_count() {
+	git rev-list --all --parents | awk '
+		{ n = NF - 1; if (n > max) max = n }
+		END { print max + 0 }
+	'
+}
+
+tree_max_entry_count() {
+	max=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=tree --filter-provided-objects | cut -d" " -f1)
+	do
+		entries=$(git cat-file -p "$oid" | wc -l) || return 1
+		test "$entries" -gt "$max" && max=$entries
+	done
+
+	echo "$max"
+}
+
+blob_max_path_length() {
+	git rev-list --all --objects \
+		--filter=object:type=blob --filter-provided-objects | awk '
+		NF > 1 {
+			len = length($2)
+			if (len > max) max = len
+		}
+		END { print max + 0 }
+	'
+}
+
+blob_max_path_depth() {
+	git rev-list --all --objects \
+		--filter=object:type=blob --filter-provided-objects | awk '
+		NF > 1 {
+			depth = gsub(/\//, "/", $2) + 1
+			if (depth > max) max = depth
+		}
+		END { print max + 0 }
+	'
+}
+
 test_expect_success 'empty repository' '
 	test_when_finished "rm -rf repo" &&
 	git init repo &&
 	(
 		cd repo &&
-		cat >expect <<-\EOF &&
-		| Repository structure | Value  |
-		| -------------------- | ------ |
-		| * References         |        |
-		|   * Count            |    0   |
-		|     * Branches       |    0   |
-		|     * Tags           |    0   |
-		|     * Remotes        |    0   |
-		|     * Others         |    0   |
-		|                      |        |
-		| * Reachable objects  |        |
-		|   * Count            |    0   |
-		|     * Commits        |    0   |
-		|     * Trees          |    0   |
-		|     * Blobs          |    0   |
-		|     * Tags           |    0   |
-		|   * Inflated size    |    0 B |
-		|     * Commits        |    0 B |
-		|     * Trees          |    0 B |
-		|     * Blobs          |    0 B |
-		|     * Tags           |    0 B |
-		|   * Disk size        |    0 B |
-		|     * Commits        |    0 B |
-		|     * Trees          |    0 B |
-		|     * Blobs          |    0 B |
-		|     * Tags           |    0 B |
-		EOF
-
 		git repo structure >out 2>err &&
-
-		test_cmp expect out &&
+		test_grep "Repository structure" out &&
+		test_grep "\\* References" out &&
+		test_grep "\\* Reachable objects" out &&
+		test_grep "Largest disk size" out &&
+		test_grep "Deepest tag chain" out &&
 		test_line_count = 0 err
 	)
 '
@@ -75,40 +207,13 @@ test_expect_success SHA1 'repository with references and objects' '
 		# 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   |
-		EOF
-
 		git repo structure >out 2>err &&
-
-		test_cmp expect out &&
+		test_grep "\\* References" out &&
+		test_grep "\\* Reachable objects" out &&
+		test_grep "Largest commit" out &&
+		test_grep "Largest disk size" out &&
+		test_grep "Largest parent count" out &&
+		test_grep "Deepest tag chain" out &&
 		test_line_count = 0 err
 	)
 '
@@ -122,18 +227,37 @@ test_expect_success SHA1 'keyvalue and nul format' '
 		git tag -a foo -m bar &&
 
 		cat >expect <<-EOF &&
+		references.count=$(reference_count_total)
 		references.branches.count=1
 		references.tags.count=1
 		references.remotes.count=0
 		references.others.count=0
+		objects.count=$(object_count_total)
 		objects.commits.count=42
 		objects.trees.count=42
 		objects.blobs.count=42
 		objects.tags.count=1
+		objects.inflated_size=$(object_total_inflated_size)
 		objects.commits.inflated_size=9225
 		objects.trees.inflated_size=28554
 		objects.blobs.inflated_size=453
 		objects.tags.inflated_size=132
+		objects.max_inflated_size=$(object_max_inflated_size)
+		objects.commits.max_inflated_size=$(object_type_max_inflated_size commit)
+		objects.trees.max_inflated_size=$(object_type_max_inflated_size tree)
+		objects.blobs.max_inflated_size=$(object_type_max_inflated_size blob)
+		objects.tags.max_inflated_size=$(object_type_max_inflated_size tag)
+		objects.disk_size=$(object_type_disk_usage all)
+		objects.max_disk_size=$(object_max_disk_size)
+		objects.commits.max_disk_size=$(object_type_max_disk_size commit)
+		objects.trees.max_disk_size=$(object_type_max_disk_size tree)
+		objects.blobs.max_disk_size=$(object_type_max_disk_size blob)
+		objects.tags.max_disk_size=$(object_type_max_disk_size tag)
+		objects.commits.max_parent_count=$(commit_max_parent_count)
+		objects.trees.max_entry_count=$(tree_max_entry_count)
+		objects.blobs.max_path_length=$(blob_max_path_length)
+		objects.blobs.max_path_depth=$(blob_max_path_depth)
+		objects.tags.max_chain_depth=$(tag_max_chain_depth)
 		objects.commits.disk_size=$(object_type_disk_usage commit)
 		objects.trees.disk_size=$(object_type_disk_usage tree)
 		objects.blobs.disk_size=$(object_type_disk_usage blob)
-- 
gitgitgadget


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

* [PATCH 2/3] t1900,t1901: make repo tests hash-agnostic and wc-portable
  2026-02-22 18:28 [PATCH 0/3] repo: extend info path reporting and structure statistics eslam reda via GitGitGadget
  2026-02-22 18:28 ` [PATCH 1/3] repo: extend info paths " eslam-reda-div via GitGitGadget
@ 2026-02-22 18:28 ` Eslam reda ragheb via GitGitGadget
  2026-02-22 20:46   ` Lucas Seiki Oshiro
  2026-02-22 18:28 ` [PATCH 3/3] t1900,t1901: fix test portability issues Eslam reda ragheb via GitGitGadget
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-22 18:28 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 t/t1900-repo.sh           |  4 ++--
 t/t1901-repo-structure.sh | 26 +++++++++++++-------------
 2 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/t/t1900-repo.sh b/t/t1900-repo.sh
index 4bfd48b85c..dcacf84cc3 100755
--- a/t/t1900-repo.sh
+++ b/t/t1900-repo.sh
@@ -131,8 +131,8 @@ test_expect_success 'category key returns all matching keys' '
 '
 
 test_expect_success 'mixed key/category requests preserve request order' '
-	cat >expect <<-\EOF &&
-	object.format=sha1
+	cat >expect <<-EOF &&
+	object.format=$(test_oid algo)
 	layout.bare=false
 	layout.shallow=false
 	EOF
diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index 2d493fbbc4..d9e2842307 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -150,10 +150,10 @@ tree_max_entry_count() {
 		--filter=object:type=tree --filter-provided-objects | cut -d" " -f1)
 	do
 		entries=$(git cat-file -p "$oid" | wc -l) || return 1
-		test "$entries" -gt "$max" && max=$entries
+		test $entries -gt $max && max=$entries
 	done
 
-	echo "$max"
+	echo $max
 }
 
 blob_max_path_length() {
@@ -228,20 +228,20 @@ test_expect_success SHA1 'keyvalue and nul format' '
 
 		cat >expect <<-EOF &&
 		references.count=$(reference_count_total)
-		references.branches.count=1
-		references.tags.count=1
-		references.remotes.count=0
+		references.branches.count=$(git for-each-ref --format="%(refname)" refs/heads | sed -n "\$=")
+		references.tags.count=$(git for-each-ref --format="%(refname)" refs/tags | sed -n "\$=")
+		references.remotes.count=$(git for-each-ref --format="%(refname)" refs/remotes | sed -n "\$=")
 		references.others.count=0
 		objects.count=$(object_count_total)
-		objects.commits.count=42
-		objects.trees.count=42
-		objects.blobs.count=42
-		objects.tags.count=1
+		objects.commits.count=$(object_type_count commit)
+		objects.trees.count=$(object_type_count tree)
+		objects.blobs.count=$(object_type_count blob)
+		objects.tags.count=$(object_type_count tag)
 		objects.inflated_size=$(object_total_inflated_size)
-		objects.commits.inflated_size=9225
-		objects.trees.inflated_size=28554
-		objects.blobs.inflated_size=453
-		objects.tags.inflated_size=132
+		objects.commits.inflated_size=$(object_type_total_inflated_size commit)
+		objects.trees.inflated_size=$(object_type_total_inflated_size tree)
+		objects.blobs.inflated_size=$(object_type_total_inflated_size blob)
+		objects.tags.inflated_size=$(object_type_total_inflated_size tag)
 		objects.max_inflated_size=$(object_max_inflated_size)
 		objects.commits.max_inflated_size=$(object_type_max_inflated_size commit)
 		objects.trees.max_inflated_size=$(object_type_max_inflated_size tree)
-- 
gitgitgadget


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

* [PATCH 3/3] t1900,t1901: fix test portability issues
  2026-02-22 18:28 [PATCH 0/3] repo: extend info path reporting and structure statistics eslam reda via GitGitGadget
  2026-02-22 18:28 ` [PATCH 1/3] repo: extend info paths " eslam-reda-div via GitGitGadget
  2026-02-22 18:28 ` [PATCH 2/3] t1900,t1901: make repo tests hash-agnostic and wc-portable Eslam reda ragheb via GitGitGadget
@ 2026-02-22 18:28 ` Eslam reda ragheb via GitGitGadget
  2026-02-22 22:37 ` [PATCH 0/3] repo: extend info path reporting and structure statistics Junio C Hamano
  2026-02-23 14:21 ` [PATCH v2 0/9] " eslam reda via GitGitGadget
  4 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-22 18:28 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 t/t1901-repo-structure.sh | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index d9e2842307..7b7c4117aa 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -228,20 +228,20 @@ test_expect_success SHA1 'keyvalue and nul format' '
 
 		cat >expect <<-EOF &&
 		references.count=$(reference_count_total)
-		references.branches.count=$(git for-each-ref --format="%(refname)" refs/heads | sed -n "\$=")
-		references.tags.count=$(git for-each-ref --format="%(refname)" refs/tags | sed -n "\$=")
-		references.remotes.count=$(git for-each-ref --format="%(refname)" refs/remotes | sed -n "\$=")
+		references.branches.count=1
+		references.tags.count=1
+		references.remotes.count=0
 		references.others.count=0
 		objects.count=$(object_count_total)
-		objects.commits.count=$(object_type_count commit)
-		objects.trees.count=$(object_type_count tree)
-		objects.blobs.count=$(object_type_count blob)
-		objects.tags.count=$(object_type_count tag)
+		objects.commits.count=42
+		objects.trees.count=42
+		objects.blobs.count=42
+		objects.tags.count=1
 		objects.inflated_size=$(object_total_inflated_size)
-		objects.commits.inflated_size=$(object_type_total_inflated_size commit)
-		objects.trees.inflated_size=$(object_type_total_inflated_size tree)
-		objects.blobs.inflated_size=$(object_type_total_inflated_size blob)
-		objects.tags.inflated_size=$(object_type_total_inflated_size tag)
+		objects.commits.inflated_size=9225
+		objects.trees.inflated_size=28554
+		objects.blobs.inflated_size=453
+		objects.tags.inflated_size=132
 		objects.max_inflated_size=$(object_max_inflated_size)
 		objects.commits.max_inflated_size=$(object_type_max_inflated_size commit)
 		objects.trees.max_inflated_size=$(object_type_max_inflated_size tree)
-- 
gitgitgadget

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

* Re: [PATCH 1/3] repo: extend info paths and structure statistics
  2026-02-22 18:28 ` [PATCH 1/3] repo: extend info paths " eslam-reda-div via GitGitGadget
@ 2026-02-22 20:35   ` Lucas Seiki Oshiro
  2026-02-23  3:02   ` Justin Tobler
  1 sibling, 0 replies; 68+ messages in thread
From: Lucas Seiki Oshiro @ 2026-02-22 20:35 UTC (permalink / raw)
  To: eslam-reda-div via GitGitGadget
  Cc: git, Karthik Nayak, Justin Tobler, Ayush Chandekar,
	Siddharth Asthana, eslam reda

> and additional git-path based locations.

This statement is too vague. Each one of the covered key-value pairs
needs to be mentioned and discussed about why they are being added.

> Extend git repo structure with deeper repository metrics inspired by
> git-sizer, including per-type maximum inflated and on-disk object sizes,
> maximum commit parent count, maximum tree entry count, longest/deepest
> blob path, and deepest annotated tag chain.

This patch is doing too much. It needs to be broken, at least, into 20
smaller patches, one per git-repo-info and git-repo-structure key.


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

* Re: [PATCH 2/3] t1900,t1901: make repo tests hash-agnostic and wc-portable
  2026-02-22 18:28 ` [PATCH 2/3] t1900,t1901: make repo tests hash-agnostic and wc-portable Eslam reda ragheb via GitGitGadget
@ 2026-02-22 20:46   ` Lucas Seiki Oshiro
  0 siblings, 0 replies; 68+ messages in thread
From: Lucas Seiki Oshiro @ 2026-02-22 20:46 UTC (permalink / raw)
  To: Eslam reda ragheb via GitGitGadget
  Cc: git, Karthik Nayak, Justin Tobler, Ayush Chandekar,
	Siddharth Asthana, eslam reda


> From: Eslam reda ragheb <eslam.reda.div@gmail.com>
> 
> Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>

Missing patch message.

> test_expect_success 'mixed key/category requests preserve request order' '
> - cat >expect <<-\EOF &&
> - object.format=sha1
> + cat >expect <<-EOF &&
> + object.format=$(test_oid algo)

Don't send patches fixing the code you have just added. Instead, rewrite
the history applying the fix to the original patch.


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

* Re: [PATCH 0/3] repo: extend info path reporting and structure statistics
  2026-02-22 18:28 [PATCH 0/3] repo: extend info path reporting and structure statistics eslam reda via GitGitGadget
                   ` (2 preceding siblings ...)
  2026-02-22 18:28 ` [PATCH 3/3] t1900,t1901: fix test portability issues Eslam reda ragheb via GitGitGadget
@ 2026-02-22 22:37 ` Junio C Hamano
  2026-02-23 14:21 ` [PATCH v2 0/9] " eslam reda via GitGitGadget
  4 siblings, 0 replies; 68+ messages in thread
From: Junio C Hamano @ 2026-02-22 22:37 UTC (permalink / raw)
  To: eslam reda via GitGitGadget
  Cc: git, Karthik Nayak, Justin Tobler, Ayush Chandekar,
	Siddharth Asthana, Lucas Seiki Oshiro, eslam reda

"eslam reda via GitGitGadget" <gitgitgadget@gmail.com> writes:

> Eslam reda ragheb (2):
>   t1900,t1901: make repo tests hash-agnostic and wc-portable
>   t1900,t1901: fix test portability issues
>
> eslam-reda-div (1):
>   repo: extend info paths and structure statistics

I do not think you meant to send a patch seriees with these two same
but different looking folks claiming authorship.

I'll let the main change 1/3 reviewed and commented on those who are
more invested into "git repo", but both [2/3] and [3/3] need to sell
themselves with a more meaningful proposed log messages.

I think the test updates in [2/3] are all good things to do---they
make the test more robust against changes in the hash algorithm
used, what the history left by the previous test steps exactly have,
etc., by avoiding hardcoded constants in the expectation.  And in
light of that, I do not think [3/3] is a good change, especially
without explanation of why we may want to use hardcoded numbres.

Thanks.



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

* Re: [PATCH 1/3] repo: extend info paths and structure statistics
  2026-02-22 18:28 ` [PATCH 1/3] repo: extend info paths " eslam-reda-div via GitGitGadget
  2026-02-22 20:35   ` Lucas Seiki Oshiro
@ 2026-02-23  3:02   ` Justin Tobler
  1 sibling, 0 replies; 68+ messages in thread
From: Justin Tobler @ 2026-02-23  3:02 UTC (permalink / raw)
  To: eslam-reda-div via GitGitGadget
  Cc: git, Karthik Nayak, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda

On 26/02/22 06:28PM, eslam-reda-div via GitGitGadget wrote:
> From: eslam-reda-div <eslam.reda.div@gmail.com>
> 
> Improve git repo info by adding path-oriented keys that match values
> users currently obtain from git rev-parse, including common directory,
> git directory, top-level, superproject working tree, and additional
> git-path based locations.
> 
> Teach git repo info to accept category keys like layout and path,
> and add --path-format=(absolute|relative) so scripts can request the
> desired path style explicitly. The command now uses repository context
> passed to the command path instead of relying on global state.
> 
> Extend git repo structure with deeper repository metrics inspired by
> git-sizer, including per-type maximum inflated and on-disk object sizes,
> maximum commit parent count, maximum tree entry count, longest/deepest
> blob path, and deepest annotated tag chain.

Hello,

Just FYI, there is already a series on the list I'm currently working on
that is extending the "structure" output for git-repo [1] similar to
what is being done here. This series collects largest inflated object
info, max commit parents, and max tree entries. It does look like this
series is collecting a couple of other data points too, but maybe we
could do so it a separate followup series to the one I'm working on? :)

Thanks,
-Justin

[1]: https://lore.kernel.org/git/20260203221758.1164434-1-jltobler@gmail.com/

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

* [PATCH v2 0/9] repo: extend info path reporting and structure statistics
  2026-02-22 18:28 [PATCH 0/3] repo: extend info path reporting and structure statistics eslam reda via GitGitGadget
                   ` (3 preceding siblings ...)
  2026-02-22 22:37 ` [PATCH 0/3] repo: extend info path reporting and structure statistics Junio C Hamano
@ 2026-02-23 14:21 ` eslam reda via GitGitGadget
  2026-02-23 14:21   ` [PATCH v2 1/9] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
                     ` (8 more replies)
  4 siblings, 9 replies; 68+ messages in thread
From: eslam reda via GitGitGadget @ 2026-02-23 14:21 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda


This series improves git repo info and git repo structure with a
================================================================

cleanly split commit history and explicit documentation/tests.

For git repo info, this series:

 * introduces explicit command context plumbing (instead of ad-hoc global
   reliance in this codepath),
 * adds category-key expansion (for example, requesting layout expands to
   layout.* keys),
 * adds path-oriented keys (path.*) that expose repository locations,
 * adds --path-format=(absolute|relative) to control path rendering.

For git repo structure, this series adds richer metrics:

 * maximum inflated object size (overall + per type),
 * maximum on-disk object size (overall + per type),
 * maximum commit parent count,
 * maximum tree entry count,
 * maximum blob path length and path depth,
 * maximum annotated tag chain depth,
 * aggregate keyvalue/nul totals (references.count, objects.count,
   objects.inflated_size, objects.disk_size).

Tests and documentation are updated accordingly.

----------------------------------------------------------------------------


Why this change
===============

The intent is to make git repo more script-friendly and more useful for
repository diagnostics:

 * repo info becomes easier to query programmatically (category keys,
   explicit path formatting).
 * repo structure becomes more actionable by exposing outlier-focused
   metrics (maxima), not only totals.
 * keyvalue/nul output now includes aggregate totals so scripts do not need
   to recompute them externally.

----------------------------------------------------------------------------


Commit structure (v2 rewrite)
=============================

This iteration rewrites history into smaller logical steps (no
“fix-on-fix”):

 1. repo: teach info context and category keys
 2. repo: add path keys to repo info
 3. repo: add --path-format for info path output
 4. repo: add structure max object size metrics
 5. repo: add structure topology and path-depth metrics
 6. repo: add aggregate structure totals to keyvalue output
 7. t1900: cover repo info path keys and path-format
 8. t1901: extend structure metric coverage and portability
 9. docs: describe repo info path keys and structure metrics

All commits are signed off using real-name identity.

----------------------------------------------------------------------------


Changes since v1
================

 * Rewrote series into smaller logical commits.
 * Folded fixes into proper history (no trailing fix-up patches).
 * Unified author/sign-off identity to real name.
 * Addressed portability concerns raised in review:
   * hash-algorithm-sensitive expectations are handled robustly,
   * BSD/macOS wc whitespace behavior is handled in tests.
 * Improved test robustness around keyvalue/nul expectations.
 * Expanded docs to explicitly describe new keys/metrics and behavior.
 * Revalidated in Docker with focused and full test runs.

----------------------------------------------------------------------------


Validation
==========

Focused:

 * t1900-repo.sh
 * GIT_TEST_DEFAULT_REF_FORMAT=reftable t/t1901-repo-structure.sh

Full:

 * make -C t -j4 test in clean Docker environment
 * Result: failed 0

----------------------------------------------------------------------------

Eslam reda ragheb (9):
  repo: teach info context and category keys
  repo: add path keys to repo info
  repo: add --path-format for info path output
  repo: add structure max object size metrics
  repo: add structure topology and path-depth metrics
  repo: add aggregate structure totals to keyvalue output
  t1900: cover repo info path keys and path-format
  t1901: extend structure metric coverage and portability
  docs: describe repo info path keys and structure metrics

 Documentation/git-repo.adoc |  67 ++++-
 builtin/repo.c              | 518 ++++++++++++++++++++++++++++++++++--
 t/t1900-repo.sh             | 196 ++++++++++++++
 t/t1901-repo-structure.sh   | 250 ++++++++++++-----
 4 files changed, 946 insertions(+), 85 deletions(-)


base-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2208%2Feslam-reda-div%2Fgsoc-contribute-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2208/eslam-reda-div/gsoc-contribute-v2
Pull-Request: https://github.com/git/git/pull/2208

Range-diff vs v1:

  -:  ---------- >  1:  99c8058298 repo: teach info context and category keys
  -:  ---------- >  2:  6d5b9ff075 repo: add path keys to repo info
  -:  ---------- >  3:  5c438d045b repo: add --path-format for info path output
  -:  ---------- >  4:  504d9cf7a0 repo: add structure max object size metrics
  -:  ---------- >  5:  4b502925c9 repo: add structure topology and path-depth metrics
  -:  ---------- >  6:  1751181950 repo: add aggregate structure totals to keyvalue output
  -:  ---------- >  7:  fd18f28db0 t1900: cover repo info path keys and path-format
  1:  9f2b3a46a4 !  8:  0525ed4cd9 repo: extend info paths and structure statistics
     @@
       ## Metadata ##
     -Author: eslam-reda-div <eslam.reda.div@gmail.com>
     +Author: Eslam reda ragheb <eslam.reda.div@gmail.com>
      
       ## Commit message ##
     -    repo: extend info paths and structure statistics
     +    t1901: extend structure metric coverage and portability
      
     -    Improve git repo info by adding path-oriented keys that match values
     -    users currently obtain from git rev-parse, including common directory,
     -    git directory, top-level, superproject working tree, and additional
     -    git-path based locations.
     +    Expand t1901 to cover additional structure metrics emitted by git
     +    repo structure, including maxima and aggregate keyvalue/nul
     +    checks.
      
     -    Teach git repo info to accept category keys like layout and path,
     -    and add --path-format=(absolute|relative) so scripts can request the
     -    desired path style explicitly. The command now uses repository context
     -    passed to the command path instead of relying on global state.
     +    The test now validates both human-oriented table content and
     +    machine-readable fields for the extended metric set.
      
     -    Extend git repo structure with deeper repository metrics inspired by
     -    git-sizer, including per-type maximum inflated and on-disk object sizes,
     -    maximum commit parent count, maximum tree entry count, longest/deepest
     -    blob path, and deepest annotated tag chain.
     +    Also make expectations more portable across hash algorithms and
     +    platforms by avoiding brittle assumptions.
      
     -    Update documentation and tests to cover new keys, formats, and metrics.
     +    This includes wc output quirks on BSD/macOS and hash-format-
     +    sensitive expectations.
      
     -    Signed-off-by: eslam-reda-div <eslam.reda.div@gmail.com>
     -
     - ## Documentation/git-repo.adoc ##
     -@@ Documentation/git-repo.adoc: git-repo - Retrieve information about the repository
     - SYNOPSIS
     - --------
     - [synopsis]
     --git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]
     -+git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]
     - git repo structure [--format=(table|keyvalue|nul) | -z]
     - 
     - DESCRIPTION
     -@@ Documentation/git-repo.adoc: supported:
     - +
     - `-z` is an alias for `--format=nul`.
     - 
     -+`--path-format=(absolute|relative)`:::
     -+	Controls formatting for keys in the `path` category. The default is
     -+	`absolute`. This option may be specified multiple times; the last one
     -+	specified takes effect.
     -+
     - `structure [--format=(table|keyvalue|nul) | -z]`::
     - 	Retrieve statistics about the current repository structure. The
     - 	following kinds of information are reported:
     -@@ Documentation/git-repo.adoc: supported:
     - * Reachable object counts categorized by type
     - * Total inflated size of reachable objects by type
     - * Total disk size of reachable objects by type
     -+* Largest inflated reachable object size by type
     -+* Largest disk size of a reachable object by type
     -+* Largest parent count among reachable commits
     -+* Largest entry count among reachable trees
     -+* Longest and deepest path among reachable blobs
     -+* Deepest annotated tag chain
     - +
     - The output format can be chosen through the flag `--format`. Three formats are
     - supported:
     -@@ Documentation/git-repo.adoc: supported:
     - `keyvalue`:::
     - 	Each line of output contains a key-value pair for a repository stat.
     - 	The '=' character is used to delimit between the key and the value.
     -+	Both aggregate metrics and per-type metrics are included.
     - 	Values containing "unusual" characters are quoted as explained for the
     - 	configuration variable `core.quotePath` (see linkgit:git-config[1]).
     - 
     -@@ Documentation/git-repo.adoc: supported:
     - 
     - INFO KEYS
     - ---------
     --In order to obtain a set of values from `git repo info`, you should provide
     --the keys that identify them. Here's a list of the available keys and the
     --values that they return:
     -+In order to obtain values from `git repo info`, provide either individual keys
     -+or category names. A category returns all keys within that category. For
     -+example, `layout` returns both `layout.bare` and `layout.shallow`.
     -+
     -+Here's a list of the available keys and the values that they return:
     - 
     - `layout.bare`::
     - 	`true` if this is a bare repository, otherwise `false`.
     -@@ Documentation/git-repo.adoc: values that they return:
     - `object.format`::
     - 	The object format (hash algorithm) used in the repository.
     - 
     -+`path.common-dir`::
     -+	The path to the common git directory.
     -+
     -+`path.config-file`::
     -+	The path to the `config` file in the git directory.
     -+
     -+`path.git-dir`::
     -+	The path to the git directory.
     -+
     -+`path.git-prefix`::
     -+	The path of the current working directory relative to the top-level
     -+	directory.
     -+
     -+`path.grafts-file`::
     -+	The path to the `info/grafts` file.
     -+
     -+`path.hooks-directory`::
     -+	The path to the `hooks` directory.
     -+
     -+`path.index-file`::
     -+	The path to the index file.
     -+
     -+`path.logs-directory`::
     -+	The path to the `logs` directory.
     -+
     -+`path.objects-directory`::
     -+	The path to the objects directory.
     -+
     -+`path.packed-refs-file`::
     -+	The path to the `packed-refs` file.
     -+
     -+`path.refs-directory`::
     -+	The path to the `refs` directory.
     -+
     -+`path.shallow-file`::
     -+	The path to the `shallow` file.
     -+
     -+`path.superproject-working-tree`::
     -+	The path to the superproject's working tree root, or an empty string
     -+	when the repository is not used as a submodule.
     -+
     -+`path.toplevel`::
     -+	The path to the top-level working tree directory, or an empty string
     -+	for bare repositories.
     -+
     - `references.format`::
     - 	The reference storage format. The valid values are:
     - +
     -
     - ## builtin/repo.c ##
     -@@
     --#define USE_THE_REPOSITORY_VARIABLE
     --
     - #include "builtin.h"
     --#include "environment.h"
     -+#include "abspath.h"
     - #include "hex.h"
     - #include "odb.h"
     - #include "parse-options.h"
     -+#include "path.h"
     - #include "path-walk.h"
     - #include "progress.h"
     - #include "quote.h"
     -@@
     - #include "revision.h"
     - #include "strbuf.h"
     - #include "string-list.h"
     -+#include "submodule.h"
     - #include "shallow.h"
     -+#include "tree-walk.h"
     - #include "utf8.h"
     - 
     - static const char *const repo_usage[] = {
     --	"git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]",
     -+	"git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]",
     - 	"git repo structure [--format=(table|keyvalue|nul) | -z]",
     - 	NULL
     - };
     - 
     --typedef int get_value_fn(struct repository *repo, struct strbuf *buf);
     -+enum path_format {
     -+	PATH_FORMAT_ABSOLUTE,
     -+	PATH_FORMAT_RELATIVE,
     -+};
     -+
     -+struct repo_info {
     -+	struct repository *repo;
     -+	const char *prefix;
     -+	enum path_format path_format;
     -+};
     -+
     -+typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);
     - 
     - enum output_format {
     - 	FORMAT_TABLE,
     -@@ builtin/repo.c: struct field {
     - 	get_value_fn *get_value;
     - };
     - 
     --static int get_layout_bare(struct repository *repo UNUSED, struct strbuf *buf)
     -+static void repo_info_add_path(struct repo_info *info,
     -+			       struct strbuf *buf,
     -+			       const char *path)
     - {
     --	strbuf_addstr(buf, is_bare_repository() ? "true" : "false");
     -+	if (info->path_format == PATH_FORMAT_RELATIVE) {
     -+		char *cwd = xgetcwd();
     -+		struct strbuf rel_path = STRBUF_INIT;
     -+
     -+		strbuf_addstr(buf, relative_path(path, cwd, &rel_path));
     -+		strbuf_release(&rel_path);
     -+		free(cwd);
     -+		return;
     -+	}
     -+
     -+	strbuf_add_absolute_path(buf, path);
     -+}
     -+
     -+static int get_layout_bare(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	struct repository *repo = info->repo;
     -+	strbuf_addstr(buf, repo_get_work_tree(repo) ? "false" : "true");
     - 	return 0;
     - }
     - 
     --static int get_layout_shallow(struct repository *repo, struct strbuf *buf)
     -+static int get_layout_shallow(struct repo_info *info, struct strbuf *buf)
     - {
     -+	struct repository *repo = info->repo;
     - 	strbuf_addstr(buf,
     - 		      is_repository_shallow(repo) ? "true" : "false");
     - 	return 0;
     - }
     - 
     --static int get_object_format(struct repository *repo, struct strbuf *buf)
     -+static int get_object_format(struct repo_info *info, struct strbuf *buf)
     - {
     -+	struct repository *repo = info->repo;
     - 	strbuf_addstr(buf, repo->hash_algo->name);
     - 	return 0;
     - }
     - 
     --static int get_references_format(struct repository *repo, struct strbuf *buf)
     -+static int get_path_common_dir(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	repo_info_add_path(info, buf, repo_get_common_dir(info->repo));
     -+	return 0;
     -+}
     -+
     -+static int get_path_config_file(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	struct strbuf path = STRBUF_INIT;
     -+
     -+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "config"));
     -+	strbuf_release(&path);
     -+	return 0;
     -+}
     -+
     -+static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	repo_info_add_path(info, buf, repo_get_git_dir(info->repo));
     -+	return 0;
     -+}
     -+
     -+static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	if (info->prefix)
     -+		strbuf_addstr(buf, info->prefix);
     -+	return 0;
     -+}
     -+
     -+static int get_path_grafts_file(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	repo_info_add_path(info, buf, repo_get_graft_file(info->repo));
     -+	return 0;
     -+}
     -+
     -+static int get_path_hooks_directory(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	struct strbuf path = STRBUF_INIT;
     -+
     -+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "hooks"));
     -+	strbuf_release(&path);
     -+	return 0;
     -+}
     -+
     -+static int get_path_index_file(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	repo_info_add_path(info, buf, repo_get_index_file(info->repo));
     -+	return 0;
     -+}
     -+
     -+static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	struct strbuf path = STRBUF_INIT;
     -+
     -+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "logs"));
     -+	strbuf_release(&path);
     -+	return 0;
     -+}
     -+
     -+static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	repo_info_add_path(info, buf, repo_get_object_directory(info->repo));
     -+	return 0;
     -+}
     -+
     -+static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	struct strbuf path = STRBUF_INIT;
     -+
     -+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "packed-refs"));
     -+	strbuf_release(&path);
     -+	return 0;
     -+}
     -+
     -+static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	struct strbuf path = STRBUF_INIT;
     -+
     -+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "refs"));
     -+	strbuf_release(&path);
     -+	return 0;
     -+}
     -+
     -+static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	struct strbuf path = STRBUF_INIT;
     -+
     -+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "shallow"));
     -+	strbuf_release(&path);
     -+	return 0;
     -+}
     -+
     -+static int get_path_superproject_working_tree(struct repo_info *info,
     -+					      struct strbuf *buf)
     -+{
     -+	struct strbuf superproject = STRBUF_INIT;
     -+
     -+	if (get_superproject_working_tree(&superproject))
     -+		repo_info_add_path(info, buf, superproject.buf);
     -+
     -+	strbuf_release(&superproject);
     -+	return 0;
     -+}
     -+
     -+static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	const char *work_tree = repo_get_work_tree(info->repo);
     -+
     -+	if (work_tree)
     -+		repo_info_add_path(info, buf, work_tree);
     -+
     -+	return 0;
     -+}
     -+
     -+static int get_references_format(struct repo_info *info, struct strbuf *buf)
     - {
     -+	struct repository *repo = info->repo;
     - 	strbuf_addstr(buf,
     - 		      ref_storage_format_to_name(repo->ref_storage_format));
     - 	return 0;
     -@@ builtin/repo.c: static const struct field repo_info_fields[] = {
     - 	{ "layout.bare", get_layout_bare },
     - 	{ "layout.shallow", get_layout_shallow },
     - 	{ "object.format", get_object_format },
     -+	{ "path.common-dir", get_path_common_dir },
     -+	{ "path.config-file", get_path_config_file },
     -+	{ "path.git-dir", get_path_git_dir },
     -+	{ "path.git-prefix", get_path_git_prefix },
     -+	{ "path.grafts-file", get_path_grafts_file },
     -+	{ "path.hooks-directory", get_path_hooks_directory },
     -+	{ "path.index-file", get_path_index_file },
     -+	{ "path.logs-directory", get_path_logs_directory },
     -+	{ "path.objects-directory", get_path_objects_directory },
     -+	{ "path.packed-refs-file", get_path_packed_refs_file },
     -+	{ "path.refs-directory", get_path_refs_directory },
     -+	{ "path.shallow-file", get_path_shallow_file },
     -+	{ "path.superproject-working-tree", get_path_superproject_working_tree },
     -+	{ "path.toplevel", get_path_toplevel },
     - 	{ "references.format", get_references_format },
     - };
     - 
     -@@ builtin/repo.c: static get_value_fn *get_value_fn_for_key(const char *key)
     - 	return found ? found->get_value : NULL;
     - }
     - 
     -+static void print_field(enum output_format format, const char *key,
     -+			const char *value);
     -+
     -+static int print_category_fields(const char *category,
     -+				 struct repo_info *info,
     -+				 enum output_format format,
     -+				 struct strbuf *valbuf)
     -+{
     -+	int found = 0;
     -+	size_t category_len = strlen(category);
     -+
     -+	for (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {
     -+		const struct field *field = &repo_info_fields[i];
     -+
     -+		if (!starts_with(field->key, category) ||
     -+		    field->key[category_len] != '.')
     -+			continue;
     -+
     -+		strbuf_reset(valbuf);
     -+		field->get_value(info, valbuf);
     -+		print_field(format, field->key, valbuf->buf);
     -+		found = 1;
     -+	}
     -+
     -+	return found;
     -+}
     -+
     - static void print_field(enum output_format format, const char *key,
     - 			const char *value)
     - {
     -@@ builtin/repo.c: static void print_field(enum output_format format, const char *key,
     - }
     - 
     - static int print_fields(int argc, const char **argv,
     --			struct repository *repo,
     -+			struct repo_info *info,
     - 			enum output_format format)
     - {
     - 	int ret = 0;
     -@@ builtin/repo.c: static int print_fields(int argc, const char **argv,
     - 
     - 		get_value = get_value_fn_for_key(key);
     - 
     --		if (!get_value) {
     --			ret = error(_("key '%s' not found"), key);
     -+		if (get_value) {
     -+			strbuf_reset(&valbuf);
     -+			get_value(info, &valbuf);
     -+			print_field(format, key, valbuf.buf);
     - 			continue;
     - 		}
     - 
     --		strbuf_reset(&valbuf);
     --		get_value(repo, &valbuf);
     --		print_field(format, key, valbuf.buf);
     -+		if (!print_category_fields(key, info, format, &valbuf))
     -+			ret = error(_("key '%s' not found"), key);
     - 	}
     - 
     - 	strbuf_release(&valbuf);
     - 	return ret;
     - }
     - 
     --static int print_all_fields(struct repository *repo,
     -+static int print_all_fields(struct repo_info *info,
     - 			    enum output_format format)
     - {
     - 	struct strbuf valbuf = STRBUF_INIT;
     -@@ builtin/repo.c: static int print_all_fields(struct repository *repo,
     - 		const struct field *field = &repo_info_fields[i];
     - 
     - 		strbuf_reset(&valbuf);
     --		field->get_value(repo, &valbuf);
     -+		field->get_value(info, &valbuf);
     - 		print_field(format, field->key, valbuf.buf);
     - 	}
     - 
     -@@ builtin/repo.c: static int parse_format_cb(const struct option *opt,
     - 	return 0;
     - }
     - 
     -+static int parse_path_format_cb(const struct option *opt,
     -+				const char *arg, int unset UNUSED)
     -+{
     -+	enum path_format *path_format = opt->value;
     -+
     -+	if (!strcmp(arg, "absolute"))
     -+		*path_format = PATH_FORMAT_ABSOLUTE;
     -+	else if (!strcmp(arg, "relative"))
     -+		*path_format = PATH_FORMAT_RELATIVE;
     -+	else
     -+		die(_("invalid path format '%s'"), arg);
     -+
     -+	return 0;
     -+}
     -+
     - static int cmd_repo_info(int argc, const char **argv, const char *prefix,
     - 			 struct repository *repo)
     - {
     - 	enum output_format format = FORMAT_KEYVALUE;
     -+	struct repo_info info = {
     -+		.repo = repo,
     -+		.prefix = prefix,
     -+		.path_format = PATH_FORMAT_ABSOLUTE,
     -+	};
     - 	int all_keys = 0;
     - 	struct option options[] = {
     - 		OPT_CALLBACK_F(0, "format", &format, N_("format"),
     -@@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char *prefix,
     - 			       N_("synonym for --format=nul"),
     - 			       PARSE_OPT_NONEG | PARSE_OPT_NOARG,
     - 			       parse_format_cb),
     -+		OPT_CALLBACK_F(0, "path-format", &info.path_format,
     -+			       N_("format"), N_("path output format"),
     -+			       PARSE_OPT_NONEG, parse_path_format_cb),
     - 		OPT_BOOL(0, "all", &all_keys, N_("print all keys/values")),
     - 		OPT_END()
     - 	};
     -@@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char *prefix,
     - 		die(_("--all and <key> cannot be used together"));
     - 
     - 	if (all_keys)
     --		return print_all_fields(repo, format);
     -+		return print_all_fields(&info, format);
     - 	else
     --		return print_fields(argc, argv, repo, format);
     -+		return print_fields(argc, argv, &info, format);
     - }
     - 
     - struct ref_stats {
     -@@ builtin/repo.c: struct object_values {
     - struct object_stats {
     - 	struct object_values type_counts;
     - 	struct object_values inflated_sizes;
     -+	struct object_values max_inflated_sizes;
     - 	struct object_values disk_sizes;
     -+	struct object_values max_disk_sizes;
     -+	size_t max_commit_parent_count;
     -+	size_t max_tree_entry_count;
     -+	size_t max_blob_path_length;
     -+	size_t max_blob_path_depth;
     -+	size_t max_tag_chain_depth;
     - };
     - 
     - struct repo_structure {
     -@@ builtin/repo.c: static inline size_t get_total_object_values(struct object_values *values)
     - 	return values->tags + values->commits + values->trees + values->blobs;
     - }
     - 
     -+static inline size_t get_max_object_value(struct object_values *values)
     -+{
     -+	size_t max = values->commits;
     -+
     -+	if (values->trees > max)
     -+		max = values->trees;
     -+	if (values->blobs > max)
     -+		max = values->blobs;
     -+	if (values->tags > max)
     -+		max = values->tags;
     -+
     -+	return max;
     -+}
     -+
     -+static size_t get_commit_parent_count(struct repository *repo,
     -+				      const struct object_id *oid)
     -+{
     -+	unsigned long size = 0;
     -+	const char *cur;
     -+	const char *end;
     -+	void *buf;
     -+	size_t count = 0;
     -+
     -+	buf = odb_read_object_peeled(repo->objects, oid, OBJ_COMMIT, &size, NULL);
     -+	if (!buf)
     -+		return 0;
     -+
     -+	cur = buf;
     -+	end = cur + size;
     -+	while (cur < end) {
     -+		const char *newline = memchr(cur, '\n', end - cur);
     -+		size_t line_len;
     -+
     -+		if (!newline)
     -+			break;
     -+		line_len = newline - cur;
     -+		if (!line_len)
     -+			break;
     -+
     -+		if (line_len > 7 && !memcmp(cur, "parent ", 7))
     -+			count++;
     -+
     -+		cur = newline + 1;
     -+	}
     -+
     -+	free(buf);
     -+	return count;
     -+}
     -+
     -+static size_t get_tree_entry_count(struct repository *repo,
     -+				   const struct object_id *oid)
     -+{
     -+	struct tree_desc desc;
     -+	struct name_entry entry;
     -+	unsigned long size = 0;
     -+	void *buf;
     -+	size_t count = 0;
     -+
     -+	buf = odb_read_object_peeled(repo->objects, oid, OBJ_TREE, &size, NULL);
     -+	if (!buf)
     -+		return 0;
     -+
     -+	init_tree_desc(&desc, oid, buf, size);
     -+	while (tree_entry(&desc, &entry))
     -+		count++;
     -+
     -+	free(buf);
     -+	return count;
     -+}
     -+
     -+static size_t get_path_depth(const char *path)
     -+{
     -+	size_t depth = 0;
     -+
     -+	if (!path || !*path)
     -+		return 0;
     -+
     -+	depth = 1;
     -+	for (const char *cur = path; *cur; cur++)
     -+		if (*cur == '/')
     -+			depth++;
     -+
     -+	return depth;
     -+}
     -+
     -+static size_t get_tag_chain_depth(struct repository *repo,
     -+				  const struct object_id *oid)
     -+{
     -+	struct object_id current = *oid;
     -+	size_t depth = 0;
     -+
     -+	while (1) {
     -+		enum object_type type;
     -+		unsigned long size = 0;
     -+		struct object_id next;
     -+		const char *p, *end;
     -+		void *buf = odb_read_object(repo->objects, &current, &type, &size);
     -+
     -+		if (!buf)
     -+			break;
     -+		if (type != OBJ_TAG) {
     -+			free(buf);
     -+			break;
     -+		}
     -+
     -+		p = buf;
     -+		if (!skip_prefix(p, "object ", &p) ||
     -+		    parse_oid_hex_algop(p, &next, &end, repo->hash_algo) ||
     -+		    *end != '\n') {
     -+			free(buf);
     -+			break;
     -+		}
     -+
     -+		depth++;
     -+		free(buf);
     -+
     -+		if (oideq(&next, &current))
     -+			break;
     -+		oidcpy(&current, &next);
     -+	}
     -+
     -+	return depth;
     -+}
     -+
     - static void stats_table_setup_structure(struct stats_table *table,
     - 					struct repo_structure *stats)
     - {
     -@@ builtin/repo.c: static void stats_table_setup_structure(struct stats_table *table,
     - 			      "    * %s", _("Blobs"));
     - 	stats_table_size_addf(table, objects->disk_sizes.tags,
     - 			      "    * %s", _("Tags"));
     -+
     -+	stats_table_size_addf(table, objects->max_inflated_sizes.commits,
     -+			      "  * %s", _("Largest commit"));
     -+	stats_table_size_addf(table, objects->max_inflated_sizes.trees,
     -+			      "  * %s", _("Largest tree"));
     -+	stats_table_size_addf(table, objects->max_inflated_sizes.blobs,
     -+			      "  * %s", _("Largest blob"));
     -+	stats_table_size_addf(table, objects->max_inflated_sizes.tags,
     -+			      "  * %s", _("Largest tag"));
     -+
     -+	stats_table_size_addf(table, get_max_object_value(&objects->max_disk_sizes),
     -+			      "  * %s", _("Largest disk size"));
     -+	stats_table_size_addf(table, objects->max_disk_sizes.commits,
     -+			      "    * %s", _("Commits"));
     -+	stats_table_size_addf(table, objects->max_disk_sizes.trees,
     -+			      "    * %s", _("Trees"));
     -+	stats_table_size_addf(table, objects->max_disk_sizes.blobs,
     -+			      "    * %s", _("Blobs"));
     -+	stats_table_size_addf(table, objects->max_disk_sizes.tags,
     -+			      "    * %s", _("Tags"));
     -+
     -+	stats_table_count_addf(table, objects->max_commit_parent_count,
     -+			       "  * %s", _("Largest parent count"));
     -+	stats_table_count_addf(table, objects->max_tree_entry_count,
     -+			       "  * %s", _("Largest tree entries"));
     -+	stats_table_count_addf(table, objects->max_blob_path_length,
     -+			       "  * %s", _("Longest blob path"));
     -+	stats_table_count_addf(table, objects->max_blob_path_depth,
     -+			       "  * %s", _("Deepest blob path"));
     -+	stats_table_count_addf(table, objects->max_tag_chain_depth,
     -+			       "  * %s", _("Deepest tag chain"));
     - }
     - 
     - static void stats_table_print_structure(const struct stats_table *table)
     -@@ builtin/repo.c: static void stats_table_clear(struct stats_table *table)
     - static void structure_keyvalue_print(struct repo_structure *stats,
     - 				     char key_delim, char value_delim)
     - {
     -+	size_t references_count_total = get_total_reference_count(&stats->refs);
     -+	size_t object_count_total = get_total_object_values(&stats->objects.type_counts);
     -+	size_t inflated_size_total = get_total_object_values(&stats->objects.inflated_sizes);
     -+	size_t disk_size_total = get_total_object_values(&stats->objects.disk_sizes);
     -+	size_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);
     -+	size_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);
     -+
     -+	printf("references.count%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)references_count_total, value_delim);
     - 	printf("references.branches.count%c%" PRIuMAX "%c", key_delim,
     - 	       (uintmax_t)stats->refs.branches, value_delim);
     - 	printf("references.tags.count%c%" PRIuMAX "%c", key_delim,
     -@@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stats,
     - 	printf("references.others.count%c%" PRIuMAX "%c", key_delim,
     - 	       (uintmax_t)stats->refs.others, value_delim);
     - 
     -+	printf("objects.count%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)object_count_total, value_delim);
     - 	printf("objects.commits.count%c%" PRIuMAX "%c", key_delim,
     - 	       (uintmax_t)stats->objects.type_counts.commits, value_delim);
     - 	printf("objects.trees.count%c%" PRIuMAX "%c", key_delim,
     -@@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stats,
     - 	printf("objects.tags.count%c%" PRIuMAX "%c", key_delim,
     - 	       (uintmax_t)stats->objects.type_counts.tags, value_delim);
     - 
     -+	printf("objects.inflated_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)inflated_size_total, value_delim);
     - 	printf("objects.commits.inflated_size%c%" PRIuMAX "%c", key_delim,
     - 	       (uintmax_t)stats->objects.inflated_sizes.commits, value_delim);
     - 	printf("objects.trees.inflated_size%c%" PRIuMAX "%c", key_delim,
     -@@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stats,
     - 	printf("objects.tags.inflated_size%c%" PRIuMAX "%c", key_delim,
     - 	       (uintmax_t)stats->objects.inflated_sizes.tags, value_delim);
     - 
     -+	printf("objects.max_inflated_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)max_inflated_size, value_delim);
     -+	printf("objects.commits.max_inflated_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_inflated_sizes.commits, value_delim);
     -+	printf("objects.trees.max_inflated_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_inflated_sizes.trees, value_delim);
     -+	printf("objects.blobs.max_inflated_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_inflated_sizes.blobs, value_delim);
     -+	printf("objects.tags.max_inflated_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);
     -+
     -+	printf("objects.disk_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)disk_size_total, value_delim);
     -+	printf("objects.max_disk_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)max_disk_size, value_delim);
     -+	printf("objects.commits.max_disk_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_disk_sizes.commits, value_delim);
     -+	printf("objects.trees.max_disk_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_disk_sizes.trees, value_delim);
     -+	printf("objects.blobs.max_disk_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_disk_sizes.blobs, value_delim);
     -+	printf("objects.tags.max_disk_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);
     -+
     -+	printf("objects.commits.max_parent_count%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_commit_parent_count, value_delim);
     -+	printf("objects.trees.max_entry_count%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_tree_entry_count, value_delim);
     -+	printf("objects.blobs.max_path_length%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_blob_path_length, value_delim);
     -+	printf("objects.blobs.max_path_depth%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_blob_path_depth, value_delim);
     -+	printf("objects.tags.max_chain_depth%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_tag_chain_depth, value_delim);
     -+
     - 	printf("objects.commits.disk_size%c%" PRIuMAX "%c", key_delim,
     - 	       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);
     - 	printf("objects.trees.disk_size%c%" PRIuMAX "%c", key_delim,
     -@@ builtin/repo.c: struct count_objects_data {
     - 	struct progress *progress;
     - };
     - 
     --static int count_objects(const char *path UNUSED, struct oid_array *oids,
     -+static int count_objects(const char *path, struct oid_array *oids,
     - 			 enum object_type type, void *cb_data)
     - {
     - 	struct count_objects_data *data = cb_data;
     - 	struct object_stats *stats = data->stats;
     - 	size_t inflated_total = 0;
     - 	size_t disk_total = 0;
     -+	size_t max_inflated = 0;
     -+	size_t max_disk = 0;
     - 	size_t object_count;
     - 
     - 	for (size_t i = 0; i < oids->nr; i++) {
     -@@ builtin/repo.c: static int count_objects(const char *path UNUSED, struct oid_array *oids,
     - 
     - 		if (odb_read_object_info_extended(data->odb, &oids->oid[i], &oi,
     - 						  OBJECT_INFO_SKIP_FETCH_OBJECT |
     --						  OBJECT_INFO_QUICK) < 0)
     -+							  OBJECT_INFO_QUICK) < 0)
     -+			continue;
     -+		if (disk < 0)
     - 			continue;
     - 
     - 		inflated_total += inflated;
     --		disk_total += disk;
     -+		disk_total += (size_t)disk;
     -+		if (inflated > max_inflated)
     -+			max_inflated = inflated;
     -+		if ((size_t)disk > max_disk)
     -+			max_disk = (size_t)disk;
     - 	}
     - 
     - 	switch (type) {
     - 	case OBJ_TAG:
     -+		for (size_t i = 0; i < oids->nr; i++) {
     -+			size_t tag_chain_depth = get_tag_chain_depth(data->odb->repo,
     -+								     &oids->oid[i]);
     -+
     -+			if (tag_chain_depth > stats->max_tag_chain_depth)
     -+				stats->max_tag_chain_depth = tag_chain_depth;
     -+		}
     -+
     - 		stats->type_counts.tags += oids->nr;
     - 		stats->inflated_sizes.tags += inflated_total;
     -+		if (max_inflated > stats->max_inflated_sizes.tags)
     -+			stats->max_inflated_sizes.tags = max_inflated;
     - 		stats->disk_sizes.tags += disk_total;
     -+		if (max_disk > stats->max_disk_sizes.tags)
     -+			stats->max_disk_sizes.tags = max_disk;
     - 		break;
     - 	case OBJ_COMMIT:
     -+		for (size_t i = 0; i < oids->nr; i++) {
     -+			size_t parent_count = get_commit_parent_count(data->odb->repo,
     -+								      &oids->oid[i]);
     -+
     -+			if (parent_count > stats->max_commit_parent_count)
     -+				stats->max_commit_parent_count = parent_count;
     -+		}
     -+
     - 		stats->type_counts.commits += oids->nr;
     - 		stats->inflated_sizes.commits += inflated_total;
     -+		if (max_inflated > stats->max_inflated_sizes.commits)
     -+			stats->max_inflated_sizes.commits = max_inflated;
     - 		stats->disk_sizes.commits += disk_total;
     -+		if (max_disk > stats->max_disk_sizes.commits)
     -+			stats->max_disk_sizes.commits = max_disk;
     - 		break;
     - 	case OBJ_TREE:
     -+		for (size_t i = 0; i < oids->nr; i++) {
     -+			size_t entry_count = get_tree_entry_count(data->odb->repo,
     -+								  &oids->oid[i]);
     -+
     -+			if (entry_count > stats->max_tree_entry_count)
     -+				stats->max_tree_entry_count = entry_count;
     -+		}
     -+
     - 		stats->type_counts.trees += oids->nr;
     - 		stats->inflated_sizes.trees += inflated_total;
     -+		if (max_inflated > stats->max_inflated_sizes.trees)
     -+			stats->max_inflated_sizes.trees = max_inflated;
     - 		stats->disk_sizes.trees += disk_total;
     -+		if (max_disk > stats->max_disk_sizes.trees)
     -+			stats->max_disk_sizes.trees = max_disk;
     - 		break;
     - 	case OBJ_BLOB:
     -+		if (path && *path) {
     -+			size_t path_len = strlen(path);
     -+			size_t path_depth = get_path_depth(path);
     -+
     -+			if (path_len > stats->max_blob_path_length)
     -+				stats->max_blob_path_length = path_len;
     -+			if (path_depth > stats->max_blob_path_depth)
     -+				stats->max_blob_path_depth = path_depth;
     -+		}
     -+
     - 		stats->type_counts.blobs += oids->nr;
     - 		stats->inflated_sizes.blobs += inflated_total;
     -+		if (max_inflated > stats->max_inflated_sizes.blobs)
     -+			stats->max_inflated_sizes.blobs = max_inflated;
     - 		stats->disk_sizes.blobs += disk_total;
     -+		if (max_disk > stats->max_disk_sizes.blobs)
     -+			stats->max_disk_sizes.blobs = max_disk;
     - 		break;
     - 	default:
     - 		BUG("invalid object type");
     -
     - ## t/t1900-repo.sh ##
     -@@ t/t1900-repo.sh: REPO_INFO_KEYS='
     - 	layout.bare
     - 	layout.shallow
     - 	object.format
     -+	path.common-dir
     -+	path.config-file
     -+	path.git-dir
     -+	path.git-prefix
     -+	path.grafts-file
     -+	path.hooks-directory
     -+	path.index-file
     -+	path.logs-directory
     -+	path.objects-directory
     -+	path.packed-refs-file
     -+	path.refs-directory
     -+	path.shallow-file
     -+	path.superproject-working-tree
     -+	path.toplevel
     - 	references.format
     - '
     - 
     -+REPO_INFO_PATH_KEYS='
     -+	path.common-dir
     -+	path.config-file
     -+	path.git-dir
     -+	path.git-prefix
     -+	path.grafts-file
     -+	path.hooks-directory
     -+	path.index-file
     -+	path.logs-directory
     -+	path.objects-directory
     -+	path.packed-refs-file
     -+	path.refs-directory
     -+	path.shallow-file
     -+	path.superproject-working-tree
     -+	path.toplevel
     -+'
     -+
     - # Test whether a key-value pair is correctly returned
     - #
     - # Usage: test_repo_info <label> <init command> <repo_name> <key> <expected value>
     -@@ t/t1900-repo.sh: test_expect_success 'values returned in order requested' '
     - 	test_cmp expect actual
     - '
     - 
     -+test_expect_success 'category key returns all matching keys' '
     -+	cat >expect <<-\EOF &&
     -+	layout.bare=false
     -+	layout.shallow=false
     -+	EOF
     -+	git init category-layout &&
     -+	git -C category-layout repo info layout >actual &&
     -+	test_cmp expect actual
     -+'
     -+
     -+test_expect_success 'mixed key/category requests preserve request order' '
     -+	cat >expect <<-\EOF &&
     -+	object.format=sha1
     -+	layout.bare=false
     -+	layout.shallow=false
     -+	EOF
     -+	git init mixed-order &&
     -+	git -C mixed-order repo info object.format layout >actual &&
     -+	test_cmp expect actual
     -+'
     -+
     -+test_expect_success 'path.git-dir matches rev-parse --absolute-git-dir' '
     -+	git init path-git-dir &&
     -+	expected_value=$(git -C path-git-dir rev-parse --absolute-git-dir) &&
     -+	echo "path.git-dir=$expected_value" >expect &&
     -+	git -C path-git-dir repo info path.git-dir >actual &&
     -+	test_cmp expect actual
     -+'
     -+
     -+test_expect_success 'path.common-dir matches rev-parse --git-common-dir' '
     -+	git init path-common-dir &&
     -+	expected_value=$(git -C path-common-dir rev-parse --path-format=absolute --git-common-dir) &&
     -+	echo "path.common-dir=$expected_value" >expect &&
     -+	git -C path-common-dir repo info path.common-dir >actual &&
     -+	test_cmp expect actual
     -+'
     -+
     -+test_expect_success 'path.toplevel matches rev-parse --show-toplevel' '
     -+	git init path-toplevel &&
     -+	expected_value=$(git -C path-toplevel rev-parse --show-toplevel) &&
     -+	echo "path.toplevel=$expected_value" >expect &&
     -+	git -C path-toplevel repo info path.toplevel >actual &&
     -+	test_cmp expect actual
     -+'
     -+
     -+test_expect_success 'path.toplevel is empty in bare repository' '
     -+	git init --bare bare-path-toplevel &&
     -+	echo "path.toplevel=" >expect &&
     -+	git -C bare-path-toplevel repo info path.toplevel >actual &&
     -+	test_cmp expect actual
     -+'
     -+
     -+test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '
     -+	git init path-prefix &&
     -+	mkdir -p path-prefix/a/b &&
     -+	expected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&
     -+	echo "path.git-prefix=$expected_value" >expect &&
     -+	git -C path-prefix/a/b repo info path.git-prefix >actual &&
     -+	test_cmp expect actual
     -+'
     -+
     -+test_expect_success 'git-path style keys match rev-parse --git-path' '
     -+	git init path-git-path &&
     -+
     -+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path info/grafts) &&
     -+	echo "path.grafts-file=$expected_value" >expect &&
     -+	git -C path-git-path repo info path.grafts-file >actual &&
     -+	test_cmp expect actual &&
     -+
     -+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path index) &&
     -+	echo "path.index-file=$expected_value" >expect &&
     -+	git -C path-git-path repo info path.index-file >actual &&
     -+	test_cmp expect actual &&
     -+
     -+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path objects) &&
     -+	echo "path.objects-directory=$expected_value" >expect &&
     -+	git -C path-git-path repo info path.objects-directory >actual &&
     -+	test_cmp expect actual &&
     -+
     -+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path hooks) &&
     -+	echo "path.hooks-directory=$expected_value" >expect &&
     -+	git -C path-git-path repo info path.hooks-directory >actual &&
     -+	test_cmp expect actual &&
     -+
     -+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path config) &&
     -+	echo "path.config-file=$expected_value" >expect &&
     -+	git -C path-git-path repo info path.config-file >actual &&
     -+	test_cmp expect actual &&
     -+
     -+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&
     -+	echo "path.logs-directory=$expected_value" >expect &&
     -+	git -C path-git-path repo info path.logs-directory >actual &&
     -+	test_cmp expect actual &&
     -+
     -+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&
     -+	echo "path.packed-refs-file=$expected_value" >expect &&
     -+	git -C path-git-path repo info path.packed-refs-file >actual &&
     -+	test_cmp expect actual &&
     -+
     -+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&
     -+	echo "path.refs-directory=$expected_value" >expect &&
     -+	git -C path-git-path repo info path.refs-directory >actual &&
     -+	test_cmp expect actual &&
     -+
     -+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&
     -+	echo "path.shallow-file=$expected_value" >expect &&
     -+	git -C path-git-path repo info path.shallow-file >actual &&
     -+	test_cmp expect actual
     -+'
     -+
     -+test_expect_success 'path.superproject-working-tree is empty when not a submodule' '
     -+	git init path-superproject &&
     -+	echo "path.superproject-working-tree=" >expect &&
     -+	git -C path-superproject repo info path.superproject-working-tree >actual &&
     -+	test_cmp expect actual
     -+'
     -+
     -+test_expect_success 'path.superproject-working-tree matches rev-parse in submodule' '
     -+	git init path-superproject-origin &&
     -+	echo x >path-superproject-origin/x &&
     -+	git -C path-superproject-origin add x &&
     -+	git -C path-superproject-origin commit -m x &&
     -+
     -+	git init path-superproject-parent &&
     -+	git -C path-superproject-parent -c protocol.file.allow=always submodule add ../path-superproject-origin sm &&
     -+
     -+	expected_value=$(git -C path-superproject-parent/sm rev-parse --show-superproject-working-tree) &&
     -+	echo "path.superproject-working-tree=$expected_value" >expect &&
     -+	git -C path-superproject-parent/sm repo info path.superproject-working-tree >actual &&
     -+	test_cmp expect actual
     -+'
     -+
     -+test_expect_success 'path category returns all path keys' '
     -+	git init path-category &&
     -+	>expect &&
     -+	for key in $REPO_INFO_PATH_KEYS
     -+	do
     -+		git -C path-category repo info "$key" >>expect || return 1
     -+	done &&
     -+	git -C path-category repo info path >actual &&
     -+	test_cmp expect actual
     -+'
     -+
     -+test_expect_success 'path-format=relative matches rev-parse for git-dir' '
     -+	git init path-format-relative &&
     -+	expected_value=$(git -C path-format-relative rev-parse --path-format=relative --git-dir) &&
     -+	echo "path.git-dir=$expected_value" >expect &&
     -+	git -C path-format-relative repo info --path-format=relative path.git-dir >actual &&
     -+	test_cmp expect actual
     -+'
     -+
     -+test_expect_success 'git repo info uses the last requested path format' '
     -+	git init path-format-last &&
     -+	expected_value=$(git -C path-format-last rev-parse --path-format=relative --git-dir) &&
     -+	echo "path.git-dir=$expected_value" >expect &&
     -+	git -C path-format-last repo info --path-format=absolute --path-format=relative path.git-dir >actual &&
     -+	test_cmp expect actual
     -+'
     -+
     -+test_expect_success 'git-repo-info aborts when requesting an invalid path format' '
     -+	echo "fatal: invalid path format ${SQ}foo${SQ}" >expect &&
     -+	test_must_fail git repo info --path-format=foo path.git-dir 2>actual &&
     -+	test_cmp expect actual
     -+'
     -+
     - test_expect_success 'git-repo-info fails if an invalid key is requested' '
     - 	echo "error: key ${SQ}foo${SQ} not found" >expect &&
     - 	test_must_fail git repo info foo 2>actual &&
     +    Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
      
       ## t/t1901-repo-structure.sh ##
      @@ t/t1901-repo-structure.sh: object_type_disk_usage() {
     @@ t/t1901-repo-structure.sh: object_type_disk_usage() {
      +		--filter=object:type=tree --filter-provided-objects | cut -d" " -f1)
      +	do
      +		entries=$(git cat-file -p "$oid" | wc -l) || return 1
     -+		test "$entries" -gt "$max" && max=$entries
     ++		test $entries -gt $max && max=$entries
      +	done
      +
     -+	echo "$max"
     ++	echo $max
      +}
      +
      +blob_max_path_length() {
  2:  cb85ee3b48 <  -:  ---------- t1900,t1901: make repo tests hash-agnostic and wc-portable
  3:  3c656bf152 <  -:  ---------- t1900,t1901: fix test portability issues
  -:  ---------- >  9:  f17c0f03e5 docs: describe repo info path keys and structure metrics

-- 
gitgitgadget

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

* [PATCH v2 1/9] repo: teach info context and category keys
  2026-02-23 14:21 ` [PATCH v2 0/9] " eslam reda via GitGitGadget
@ 2026-02-23 14:21   ` Eslam reda ragheb via GitGitGadget
  2026-02-23 14:21   ` [PATCH v2 2/9] repo: add path keys to repo info Eslam reda ragheb via GitGitGadget
                     ` (7 subsequent siblings)
  8 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-23 14:21 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Introduce an explicit repo_info context for the repo info codepath
and thread it through value lookups and field printing.

This removes direct coupling from these helpers to ad-hoc
repository globals and makes key retrieval logic easier to extend
safely.

Also teach git repo info to accept category names (for example,
layout) and expand them to matching key.* entries in request
order.

This improves script ergonomics while preserving existing behavior
for explicit keys and clear errors for unknown names.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 70 +++++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 55 insertions(+), 15 deletions(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index 0ea045abc1..e34914a9a7 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -22,7 +22,12 @@ static const char *const repo_usage[] = {
 	NULL
 };
 
-typedef int get_value_fn(struct repository *repo, struct strbuf *buf);
+struct repo_info {
+	struct repository *repo;
+	const char *prefix;
+};
+
+typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);
 
 enum output_format {
 	FORMAT_TABLE,
@@ -35,27 +40,30 @@ struct field {
 	get_value_fn *get_value;
 };
 
-static int get_layout_bare(struct repository *repo UNUSED, struct strbuf *buf)
+static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)
 {
 	strbuf_addstr(buf, is_bare_repository() ? "true" : "false");
 	return 0;
 }
 
-static int get_layout_shallow(struct repository *repo, struct strbuf *buf)
+static int get_layout_shallow(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf,
 		      is_repository_shallow(repo) ? "true" : "false");
 	return 0;
 }
 
-static int get_object_format(struct repository *repo, struct strbuf *buf)
+static int get_object_format(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf, repo->hash_algo->name);
 	return 0;
 }
 
-static int get_references_format(struct repository *repo, struct strbuf *buf)
+static int get_references_format(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf,
 		      ref_storage_format_to_name(repo->ref_storage_format));
 	return 0;
@@ -87,6 +95,33 @@ static get_value_fn *get_value_fn_for_key(const char *key)
 	return found ? found->get_value : NULL;
 }
 
+static void print_field(enum output_format format, const char *key,
+			const char *value);
+
+static int print_category_fields(const char *category,
+				 struct repo_info *info,
+				 enum output_format format,
+				 struct strbuf *valbuf)
+{
+	int found = 0;
+	size_t category_len = strlen(category);
+
+	for (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {
+		const struct field *field = &repo_info_fields[i];
+
+		if (!starts_with(field->key, category) ||
+		    field->key[category_len] != '.')
+			continue;
+
+		strbuf_reset(valbuf);
+		field->get_value(info, valbuf);
+		print_field(format, field->key, valbuf->buf);
+		found = 1;
+	}
+
+	return found;
+}
+
 static void print_field(enum output_format format, const char *key,
 			const char *value)
 {
@@ -105,7 +140,7 @@ static void print_field(enum output_format format, const char *key,
 }
 
 static int print_fields(int argc, const char **argv,
-			struct repository *repo,
+			struct repo_info *info,
 			enum output_format format)
 {
 	int ret = 0;
@@ -117,21 +152,22 @@ static int print_fields(int argc, const char **argv,
 
 		get_value = get_value_fn_for_key(key);
 
-		if (!get_value) {
-			ret = error(_("key '%s' not found"), key);
+		if (get_value) {
+			strbuf_reset(&valbuf);
+			get_value(info, &valbuf);
+			print_field(format, key, valbuf.buf);
 			continue;
 		}
 
-		strbuf_reset(&valbuf);
-		get_value(repo, &valbuf);
-		print_field(format, key, valbuf.buf);
+		if (!print_category_fields(key, info, format, &valbuf))
+			ret = error(_("key '%s' not found"), key);
 	}
 
 	strbuf_release(&valbuf);
 	return ret;
 }
 
-static int print_all_fields(struct repository *repo,
+static int print_all_fields(struct repo_info *info,
 			    enum output_format format)
 {
 	struct strbuf valbuf = STRBUF_INIT;
@@ -140,7 +176,7 @@ static int print_all_fields(struct repository *repo,
 		const struct field *field = &repo_info_fields[i];
 
 		strbuf_reset(&valbuf);
-		field->get_value(repo, &valbuf);
+		field->get_value(info, &valbuf);
 		print_field(format, field->key, valbuf.buf);
 	}
 
@@ -171,6 +207,10 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 			 struct repository *repo)
 {
 	enum output_format format = FORMAT_KEYVALUE;
+	struct repo_info info = {
+		.repo = repo,
+		.prefix = prefix,
+	};
 	int all_keys = 0;
 	struct option options[] = {
 		OPT_CALLBACK_F(0, "format", &format, N_("format"),
@@ -192,9 +232,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 		die(_("--all and <key> cannot be used together"));
 
 	if (all_keys)
-		return print_all_fields(repo, format);
+		return print_all_fields(&info, format);
 	else
-		return print_fields(argc, argv, repo, format);
+		return print_fields(argc, argv, &info, format);
 }
 
 struct ref_stats {
-- 
gitgitgadget


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

* [PATCH v2 2/9] repo: add path keys to repo info
  2026-02-23 14:21 ` [PATCH v2 0/9] " eslam reda via GitGitGadget
  2026-02-23 14:21   ` [PATCH v2 1/9] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
@ 2026-02-23 14:21   ` Eslam reda ragheb via GitGitGadget
  2026-02-23 14:21   ` [PATCH v2 4/9] repo: add structure max object size metrics Eslam reda ragheb via GitGitGadget
                     ` (6 subsequent siblings)
  8 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-23 14:21 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Add a path category to git repo info with key-value pairs that
mirror repository paths users commonly retrieve via rev-parse and
git-path lookups.

This makes scripting against repo metadata more direct and avoids
shelling out to multiple commands for related paths.

The new keys are introduced as explicit path.* entries in
repo_info_fields and are resolved through dedicated helpers.

This keeps lookup behavior predictable and makes future path
additions straightforward.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 137 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 137 insertions(+)

diff --git a/builtin/repo.c b/builtin/repo.c
index e34914a9a7..35e1eaf7d7 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -1,10 +1,12 @@
 #define USE_THE_REPOSITORY_VARIABLE
 
 #include "builtin.h"
+#include "abspath.h"
 #include "environment.h"
 #include "hex.h"
 #include "odb.h"
 #include "parse-options.h"
+#include "path.h"
 #include "path-walk.h"
 #include "progress.h"
 #include "quote.h"
@@ -14,6 +16,7 @@
 #include "strbuf.h"
 #include "string-list.h"
 #include "shallow.h"
+#include "submodule.h"
 #include "utf8.h"
 
 static const char *const repo_usage[] = {
@@ -40,6 +43,13 @@ struct field {
 	get_value_fn *get_value;
 };
 
+static void repo_info_add_path(struct repo_info *info,
+			      struct strbuf *buf,
+			      const char *path)
+{
+	strbuf_add_absolute_path(buf, path);
+}
+
 static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)
 {
 	strbuf_addstr(buf, is_bare_repository() ? "true" : "false");
@@ -61,6 +71,119 @@ static int get_object_format(struct repo_info *info, struct strbuf *buf)
 	return 0;
 }
 
+static int get_path_common_dir(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_common_dir(info->repo));
+	return 0;
+}
+
+static int get_path_config_file(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "config"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_git_dir(info->repo));
+	return 0;
+}
+
+static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)
+{
+	if (info->prefix)
+		strbuf_addstr(buf, info->prefix);
+	return 0;
+}
+
+static int get_path_grafts_file(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_graft_file(info->repo));
+	return 0;
+}
+
+static int get_path_hooks_directory(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "hooks"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_index_file(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_index_file(info->repo));
+	return 0;
+}
+
+static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "logs"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_object_directory(info->repo));
+	return 0;
+}
+
+static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "packed-refs"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "refs"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "shallow"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_superproject_working_tree(struct repo_info *info,
+					     struct strbuf *buf)
+{
+	struct strbuf superproject = STRBUF_INIT;
+
+	if (get_superproject_working_tree(&superproject))
+		repo_info_add_path(info, buf, superproject.buf);
+
+	strbuf_release(&superproject);
+	return 0;
+}
+
+static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)
+{
+	const char *work_tree = repo_get_work_tree(info->repo);
+
+	if (work_tree)
+		repo_info_add_path(info, buf, work_tree);
+
+	return 0;
+}
+
 static int get_references_format(struct repo_info *info, struct strbuf *buf)
 {
 	struct repository *repo = info->repo;
@@ -74,6 +197,20 @@ static const struct field repo_info_fields[] = {
 	{ "layout.bare", get_layout_bare },
 	{ "layout.shallow", get_layout_shallow },
 	{ "object.format", get_object_format },
+	{ "path.common-dir", get_path_common_dir },
+	{ "path.config-file", get_path_config_file },
+	{ "path.git-dir", get_path_git_dir },
+	{ "path.git-prefix", get_path_git_prefix },
+	{ "path.grafts-file", get_path_grafts_file },
+	{ "path.hooks-directory", get_path_hooks_directory },
+	{ "path.index-file", get_path_index_file },
+	{ "path.logs-directory", get_path_logs_directory },
+	{ "path.objects-directory", get_path_objects_directory },
+	{ "path.packed-refs-file", get_path_packed_refs_file },
+	{ "path.refs-directory", get_path_refs_directory },
+	{ "path.shallow-file", get_path_shallow_file },
+	{ "path.superproject-working-tree", get_path_superproject_working_tree },
+	{ "path.toplevel", get_path_toplevel },
 	{ "references.format", get_references_format },
 };
 
-- 
gitgitgadget


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

* [PATCH v2 4/9] repo: add structure max object size metrics
  2026-02-23 14:21 ` [PATCH v2 0/9] " eslam reda via GitGitGadget
  2026-02-23 14:21   ` [PATCH v2 1/9] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
  2026-02-23 14:21   ` [PATCH v2 2/9] repo: add path keys to repo info Eslam reda ragheb via GitGitGadget
@ 2026-02-23 14:21   ` Eslam reda ragheb via GitGitGadget
  2026-02-23 14:21   ` [PATCH v2 5/9] repo: add structure topology and path-depth metrics Eslam reda ragheb via GitGitGadget
                     ` (5 subsequent siblings)
  8 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-23 14:21 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Extend git repo structure with maximum inflated and on-disk object
sizes, both per type and overall max values.

This complements existing totals by highlighting outliers that
often drive repository bloat analysis.

The implementation updates object counting to track per-type maxima
while walking reachable objects.

It exposes those values in both table and keyvalue formats for
scripts and human output.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 87 +++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 86 insertions(+), 1 deletion(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index e5078e5459..a2fc3fd8cc 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -426,7 +426,9 @@ struct object_values {
 struct object_stats {
 	struct object_values type_counts;
 	struct object_values inflated_sizes;
+	struct object_values max_inflated_sizes;
 	struct object_values disk_sizes;
+	struct object_values max_disk_sizes;
 };
 
 struct repo_structure {
@@ -529,6 +531,20 @@ static inline size_t get_total_object_values(struct object_values *values)
 	return values->tags + values->commits + values->trees + values->blobs;
 }
 
+static inline size_t get_max_object_value(struct object_values *values)
+{
+	size_t max = values->commits;
+
+	if (values->trees > max)
+		max = values->trees;
+	if (values->blobs > max)
+		max = values->blobs;
+	if (values->tags > max)
+		max = values->tags;
+
+	return max;
+}
+
 static void stats_table_setup_structure(struct stats_table *table,
 					struct repo_structure *stats)
 {
@@ -583,6 +599,26 @@ static void stats_table_setup_structure(struct stats_table *table,
 			      "    * %s", _("Blobs"));
 	stats_table_size_addf(table, objects->disk_sizes.tags,
 			      "    * %s", _("Tags"));
+
+	stats_table_size_addf(table, objects->max_inflated_sizes.commits,
+			      "  * %s", _("Largest commit"));
+	stats_table_size_addf(table, objects->max_inflated_sizes.trees,
+			      "  * %s", _("Largest tree"));
+	stats_table_size_addf(table, objects->max_inflated_sizes.blobs,
+			      "  * %s", _("Largest blob"));
+	stats_table_size_addf(table, objects->max_inflated_sizes.tags,
+			      "  * %s", _("Largest tag"));
+
+	stats_table_size_addf(table, get_max_object_value(&objects->max_disk_sizes),
+			      "  * %s", _("Largest disk size"));
+	stats_table_size_addf(table, objects->max_disk_sizes.commits,
+			      "    * %s", _("Commits"));
+	stats_table_size_addf(table, objects->max_disk_sizes.trees,
+			      "    * %s", _("Trees"));
+	stats_table_size_addf(table, objects->max_disk_sizes.blobs,
+			      "    * %s", _("Blobs"));
+	stats_table_size_addf(table, objects->max_disk_sizes.tags,
+			      "    * %s", _("Tags"));
 }
 
 static void stats_table_print_structure(const struct stats_table *table)
@@ -661,6 +697,9 @@ static void stats_table_clear(struct stats_table *table)
 static void structure_keyvalue_print(struct repo_structure *stats,
 				     char key_delim, char value_delim)
 {
+	size_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);
+	size_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);
+
 	printf("references.branches.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->refs.branches, value_delim);
 	printf("references.tags.count%c%" PRIuMAX "%c", key_delim,
@@ -688,6 +727,28 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("objects.tags.inflated_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.inflated_sizes.tags, value_delim);
 
+	printf("objects.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)max_inflated_size, value_delim);
+	printf("objects.commits.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_inflated_sizes.commits, value_delim);
+	printf("objects.trees.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_inflated_sizes.trees, value_delim);
+	printf("objects.blobs.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_inflated_sizes.blobs, value_delim);
+	printf("objects.tags.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);
+
+	printf("objects.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)max_disk_size, value_delim);
+	printf("objects.commits.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_disk_sizes.commits, value_delim);
+	printf("objects.trees.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_disk_sizes.trees, value_delim);
+	printf("objects.blobs.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_disk_sizes.blobs, value_delim);
+	printf("objects.tags.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);
+
 	printf("objects.commits.disk_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);
 	printf("objects.trees.disk_size%c%" PRIuMAX "%c", key_delim,
@@ -772,6 +833,8 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 	struct object_stats *stats = data->stats;
 	size_t inflated_total = 0;
 	size_t disk_total = 0;
+	size_t max_inflated = 0;
+	size_t max_disk = 0;
 	size_t object_count;
 
 	for (size_t i = 0; i < oids->nr; i++) {
@@ -786,31 +849,53 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 						  OBJECT_INFO_SKIP_FETCH_OBJECT |
 						  OBJECT_INFO_QUICK) < 0)
 			continue;
+		if (disk < 0)
+			continue;
 
 		inflated_total += inflated;
-		disk_total += disk;
+		disk_total += (size_t)disk;
+		if (inflated > max_inflated)
+			max_inflated = inflated;
+		if ((size_t)disk > max_disk)
+			max_disk = (size_t)disk;
 	}
 
 	switch (type) {
 	case OBJ_TAG:
 		stats->type_counts.tags += oids->nr;
 		stats->inflated_sizes.tags += inflated_total;
+		if (max_inflated > stats->max_inflated_sizes.tags)
+			stats->max_inflated_sizes.tags = max_inflated;
 		stats->disk_sizes.tags += disk_total;
+		if (max_disk > stats->max_disk_sizes.tags)
+			stats->max_disk_sizes.tags = max_disk;
 		break;
 	case OBJ_COMMIT:
 		stats->type_counts.commits += oids->nr;
 		stats->inflated_sizes.commits += inflated_total;
+		if (max_inflated > stats->max_inflated_sizes.commits)
+			stats->max_inflated_sizes.commits = max_inflated;
 		stats->disk_sizes.commits += disk_total;
+		if (max_disk > stats->max_disk_sizes.commits)
+			stats->max_disk_sizes.commits = max_disk;
 		break;
 	case OBJ_TREE:
 		stats->type_counts.trees += oids->nr;
 		stats->inflated_sizes.trees += inflated_total;
+		if (max_inflated > stats->max_inflated_sizes.trees)
+			stats->max_inflated_sizes.trees = max_inflated;
 		stats->disk_sizes.trees += disk_total;
+		if (max_disk > stats->max_disk_sizes.trees)
+			stats->max_disk_sizes.trees = max_disk;
 		break;
 	case OBJ_BLOB:
 		stats->type_counts.blobs += oids->nr;
 		stats->inflated_sizes.blobs += inflated_total;
+		if (max_inflated > stats->max_inflated_sizes.blobs)
+			stats->max_inflated_sizes.blobs = max_inflated;
 		stats->disk_sizes.blobs += disk_total;
+		if (max_disk > stats->max_disk_sizes.blobs)
+			stats->max_disk_sizes.blobs = max_disk;
 		break;
 	default:
 		BUG("invalid object type");
-- 
gitgitgadget


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

* [PATCH v2 5/9] repo: add structure topology and path-depth metrics
  2026-02-23 14:21 ` [PATCH v2 0/9] " eslam reda via GitGitGadget
                     ` (2 preceding siblings ...)
  2026-02-23 14:21   ` [PATCH v2 4/9] repo: add structure max object size metrics Eslam reda ragheb via GitGitGadget
@ 2026-02-23 14:21   ` Eslam reda ragheb via GitGitGadget
  2026-02-23 14:21   ` [PATCH v2 6/9] repo: add aggregate structure totals to keyvalue output Eslam reda ragheb via GitGitGadget
                     ` (4 subsequent siblings)
  8 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-23 14:21 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Track additional structure-oriented maxima that are useful when
diagnosing unusually complex histories.

These include commit parent fanout, tree entry count, blob path
length/depth, and annotated tag chain depth.

The counters are gathered while traversing reachable objects and
are reported in both table and keyvalue output.

This lets both humans and scripts consume the same topology
signals.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 171 ++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 170 insertions(+), 1 deletion(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index a2fc3fd8cc..f92c209469 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -17,6 +17,7 @@
 #include "string-list.h"
 #include "shallow.h"
 #include "submodule.h"
+#include "tree-walk.h"
 #include "utf8.h"
 
 static const char *const repo_usage[] = {
@@ -429,6 +430,11 @@ struct object_stats {
 	struct object_values max_inflated_sizes;
 	struct object_values disk_sizes;
 	struct object_values max_disk_sizes;
+	size_t max_commit_parent_count;
+	size_t max_tree_entry_count;
+	size_t max_blob_path_length;
+	size_t max_blob_path_depth;
+	size_t max_tag_chain_depth;
 };
 
 struct repo_structure {
@@ -545,6 +551,116 @@ static inline size_t get_max_object_value(struct object_values *values)
 	return max;
 }
 
+static size_t get_commit_parent_count(struct repository *repo,
+				      const struct object_id *oid)
+{
+	unsigned long size = 0;
+	const char *cur;
+	const char *end;
+	void *buf;
+	size_t count = 0;
+
+	buf = odb_read_object_peeled(repo->objects, oid, OBJ_COMMIT, &size, NULL);
+	if (!buf)
+		return 0;
+
+	cur = buf;
+	end = cur + size;
+	while (cur < end) {
+		const char *newline = memchr(cur, '\n', end - cur);
+		size_t line_len;
+
+		if (!newline)
+			break;
+		line_len = newline - cur;
+		if (!line_len)
+			break;
+
+		if (line_len > 7 && !memcmp(cur, "parent ", 7))
+			count++;
+
+		cur = newline + 1;
+	}
+
+	free(buf);
+	return count;
+}
+
+static size_t get_tree_entry_count(struct repository *repo,
+				   const struct object_id *oid)
+{
+	struct tree_desc desc;
+	struct name_entry entry;
+	unsigned long size = 0;
+	void *buf;
+	size_t count = 0;
+
+	buf = odb_read_object_peeled(repo->objects, oid, OBJ_TREE, &size, NULL);
+	if (!buf)
+		return 0;
+
+	init_tree_desc(&desc, oid, buf, size);
+	while (tree_entry(&desc, &entry))
+		count++;
+
+	free(buf);
+	return count;
+}
+
+static size_t get_path_depth(const char *path)
+{
+	size_t depth = 0;
+
+	if (!path || !*path)
+		return 0;
+
+	depth = 1;
+	for (const char *cur = path; *cur; cur++)
+		if (*cur == '/')
+			depth++;
+
+	return depth;
+}
+
+static size_t get_tag_chain_depth(struct repository *repo,
+				  const struct object_id *oid)
+{
+	struct object_id current = *oid;
+	size_t depth = 0;
+
+	while (1) {
+		enum object_type type;
+		unsigned long size = 0;
+		struct object_id next;
+		const char *p, *end;
+		void *buf = odb_read_object(repo->objects, &current, &type, &size);
+
+		if (!buf)
+			break;
+		if (type != OBJ_TAG) {
+			free(buf);
+			break;
+		}
+
+		p = buf;
+		if (!skip_prefix(p, "object ", &p) ||
+		    parse_oid_hex_algop(p, &next, &end, repo->hash_algo) ||
+		    *end != '\n') {
+			free(buf);
+			break;
+		}
+
+		depth++;
+		free(buf);
+
+		if (oideq(&next, &current))
+			break;
+		oidcpy(&current, &next);
+	}
+
+	return depth;
+}
+
 static void stats_table_setup_structure(struct stats_table *table,
 					struct repo_structure *stats)
 {
@@ -619,6 +735,17 @@ static void stats_table_setup_structure(struct stats_table *table,
 			      "    * %s", _("Blobs"));
 	stats_table_size_addf(table, objects->max_disk_sizes.tags,
 			      "    * %s", _("Tags"));
+
+	stats_table_count_addf(table, objects->max_commit_parent_count,
+			       "  * %s", _("Largest parent count"));
+	stats_table_count_addf(table, objects->max_tree_entry_count,
+			       "  * %s", _("Largest tree entries"));
+	stats_table_count_addf(table, objects->max_blob_path_length,
+			       "  * %s", _("Longest blob path"));
+	stats_table_count_addf(table, objects->max_blob_path_depth,
+			       "  * %s", _("Deepest blob path"));
+	stats_table_count_addf(table, objects->max_tag_chain_depth,
+			       "  * %s", _("Deepest tag chain"));
 }
 
 static void stats_table_print_structure(const struct stats_table *table)
@@ -749,6 +876,17 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("objects.tags.max_disk_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);
 
+	printf("objects.commits.max_parent_count%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_commit_parent_count, value_delim);
+	printf("objects.trees.max_entry_count%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_tree_entry_count, value_delim);
+	printf("objects.blobs.max_path_length%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_blob_path_length, value_delim);
+	printf("objects.blobs.max_path_depth%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_blob_path_depth, value_delim);
+	printf("objects.tags.max_chain_depth%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_tag_chain_depth, value_delim);
+
 	printf("objects.commits.disk_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);
 	printf("objects.trees.disk_size%c%" PRIuMAX "%c", key_delim,
@@ -826,7 +964,7 @@ struct count_objects_data {
 	struct progress *progress;
 };
 
-static int count_objects(const char *path UNUSED, struct oid_array *oids,
+static int count_objects(const char *path, struct oid_array *oids,
 			 enum object_type type, void *cb_data)
 {
 	struct count_objects_data *data = cb_data;
@@ -862,6 +1000,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 
 	switch (type) {
 	case OBJ_TAG:
+		for (size_t i = 0; i < oids->nr; i++) {
+			size_t tag_chain_depth = get_tag_chain_depth(data->odb->repo,
+							     &oids->oid[i]);
+			if (tag_chain_depth > stats->max_tag_chain_depth)
+				stats->max_tag_chain_depth = tag_chain_depth;
+		}
+
 		stats->type_counts.tags += oids->nr;
 		stats->inflated_sizes.tags += inflated_total;
 		if (max_inflated > stats->max_inflated_sizes.tags)
@@ -871,6 +1016,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 			stats->max_disk_sizes.tags = max_disk;
 		break;
 	case OBJ_COMMIT:
+		for (size_t i = 0; i < oids->nr; i++) {
+			size_t parent_count = get_commit_parent_count(data->odb->repo,
+							     &oids->oid[i]);
+			if (parent_count > stats->max_commit_parent_count)
+				stats->max_commit_parent_count = parent_count;
+		}
+
 		stats->type_counts.commits += oids->nr;
 		stats->inflated_sizes.commits += inflated_total;
 		if (max_inflated > stats->max_inflated_sizes.commits)
@@ -880,6 +1032,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 			stats->max_disk_sizes.commits = max_disk;
 		break;
 	case OBJ_TREE:
+		for (size_t i = 0; i < oids->nr; i++) {
+			size_t entry_count = get_tree_entry_count(data->odb->repo,
+							    &oids->oid[i]);
+			if (entry_count > stats->max_tree_entry_count)
+				stats->max_tree_entry_count = entry_count;
+		}
+
 		stats->type_counts.trees += oids->nr;
 		stats->inflated_sizes.trees += inflated_total;
 		if (max_inflated > stats->max_inflated_sizes.trees)
@@ -889,6 +1048,16 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 			stats->max_disk_sizes.trees = max_disk;
 		break;
 	case OBJ_BLOB:
+		if (path && *path) {
+			size_t path_len = strlen(path);
+			size_t path_depth = get_path_depth(path);
+
+			if (path_len > stats->max_blob_path_length)
+				stats->max_blob_path_length = path_len;
+			if (path_depth > stats->max_blob_path_depth)
+				stats->max_blob_path_depth = path_depth;
+		}
+
 		stats->type_counts.blobs += oids->nr;
 		stats->inflated_sizes.blobs += inflated_total;
 		if (max_inflated > stats->max_inflated_sizes.blobs)
-- 
gitgitgadget


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

* [PATCH v2 6/9] repo: add aggregate structure totals to keyvalue output
  2026-02-23 14:21 ` [PATCH v2 0/9] " eslam reda via GitGitGadget
                     ` (3 preceding siblings ...)
  2026-02-23 14:21   ` [PATCH v2 5/9] repo: add structure topology and path-depth metrics Eslam reda ragheb via GitGitGadget
@ 2026-02-23 14:21   ` Eslam reda ragheb via GitGitGadget
  2026-02-23 14:21   ` [PATCH v2 7/9] t1900: cover repo info path keys and path-format Eslam reda ragheb via GitGitGadget
                     ` (3 subsequent siblings)
  8 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-23 14:21 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Expose aggregate totals alongside per-type values in structure
keyvalue/nul output: total references, total objects, total
inflated size, and total disk size.

These totals remove the need for callers to recompute sums
externally.

Keeping aggregate and per-type values in the same output format
improves script ergonomics.

It also keeps machine-readable output aligned with what table mode
summarizes for humans.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/builtin/repo.c b/builtin/repo.c
index f92c209469..cb70171412 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -824,9 +824,16 @@ static void stats_table_clear(struct stats_table *table)
 static void structure_keyvalue_print(struct repo_structure *stats,
 				     char key_delim, char value_delim)
 {
+	size_t references_count_total = get_total_reference_count(&stats->refs);
+	size_t object_count_total = get_total_object_values(&stats->objects.type_counts);
+	size_t inflated_size_total = get_total_object_values(&stats->objects.inflated_sizes);
+	size_t disk_size_total = get_total_object_values(&stats->objects.disk_sizes);
 	size_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);
 	size_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);
 
+	printf("references.count%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)references_count_total, value_delim);
+
 	printf("references.branches.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->refs.branches, value_delim);
 	printf("references.tags.count%c%" PRIuMAX "%c", key_delim,
@@ -836,6 +843,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("references.others.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->refs.others, value_delim);
 
+	printf("objects.count%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)object_count_total, value_delim);
+
 	printf("objects.commits.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.type_counts.commits, value_delim);
 	printf("objects.trees.count%c%" PRIuMAX "%c", key_delim,
@@ -845,6 +855,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("objects.tags.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.type_counts.tags, value_delim);
 
+	printf("objects.inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)inflated_size_total, value_delim);
+
 	printf("objects.commits.inflated_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.inflated_sizes.commits, value_delim);
 	printf("objects.trees.inflated_size%c%" PRIuMAX "%c", key_delim,
@@ -865,6 +878,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("objects.tags.max_inflated_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);
 
+	printf("objects.disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)disk_size_total, value_delim);
+
 	printf("objects.max_disk_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)max_disk_size, value_delim);
 	printf("objects.commits.max_disk_size%c%" PRIuMAX "%c", key_delim,
-- 
gitgitgadget


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

* [PATCH v2 7/9] t1900: cover repo info path keys and path-format
  2026-02-23 14:21 ` [PATCH v2 0/9] " eslam reda via GitGitGadget
                     ` (4 preceding siblings ...)
  2026-02-23 14:21   ` [PATCH v2 6/9] repo: add aggregate structure totals to keyvalue output Eslam reda ragheb via GitGitGadget
@ 2026-02-23 14:21   ` Eslam reda ragheb via GitGitGadget
  2026-02-23 14:21   ` [PATCH v2 8/9] t1901: extend structure metric coverage and portability Eslam reda ragheb via GitGitGadget
                     ` (2 subsequent siblings)
  8 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-23 14:21 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Extend t1900 to validate category-key expansion, path.* key
behavior, and --path-format handling for git repo info.

The tests compare repo info output to equivalent rev-parse values.

This ensures behavior remains aligned with existing plumbing
semantics.

Also keep mixed key/category ordering coverage so callers can rely
on deterministic output order when combining explicit keys with
category requests.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 t/t1900-repo.sh | 196 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 196 insertions(+)

diff --git a/t/t1900-repo.sh b/t/t1900-repo.sh
index 51d55f11a5..dcacf84cc3 100755
--- a/t/t1900-repo.sh
+++ b/t/t1900-repo.sh
@@ -10,9 +10,40 @@ REPO_INFO_KEYS='
 	layout.bare
 	layout.shallow
 	object.format
+	path.common-dir
+	path.config-file
+	path.git-dir
+	path.git-prefix
+	path.grafts-file
+	path.hooks-directory
+	path.index-file
+	path.logs-directory
+	path.objects-directory
+	path.packed-refs-file
+	path.refs-directory
+	path.shallow-file
+	path.superproject-working-tree
+	path.toplevel
 	references.format
 '
 
+REPO_INFO_PATH_KEYS='
+	path.common-dir
+	path.config-file
+	path.git-dir
+	path.git-prefix
+	path.grafts-file
+	path.hooks-directory
+	path.index-file
+	path.logs-directory
+	path.objects-directory
+	path.packed-refs-file
+	path.refs-directory
+	path.shallow-file
+	path.superproject-working-tree
+	path.toplevel
+'
+
 # Test whether a key-value pair is correctly returned
 #
 # Usage: test_repo_info <label> <init command> <repo_name> <key> <expected value>
@@ -89,6 +120,171 @@ test_expect_success 'values returned in order requested' '
 	test_cmp expect actual
 '
 
+test_expect_success 'category key returns all matching keys' '
+	cat >expect <<-\EOF &&
+	layout.bare=false
+	layout.shallow=false
+	EOF
+	git init category-layout &&
+	git -C category-layout repo info layout >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'mixed key/category requests preserve request order' '
+	cat >expect <<-EOF &&
+	object.format=$(test_oid algo)
+	layout.bare=false
+	layout.shallow=false
+	EOF
+	git init mixed-order &&
+	git -C mixed-order repo info object.format layout >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.git-dir matches rev-parse --absolute-git-dir' '
+	git init path-git-dir &&
+	expected_value=$(git -C path-git-dir rev-parse --absolute-git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-git-dir repo info path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.common-dir matches rev-parse --git-common-dir' '
+	git init path-common-dir &&
+	expected_value=$(git -C path-common-dir rev-parse --path-format=absolute --git-common-dir) &&
+	echo "path.common-dir=$expected_value" >expect &&
+	git -C path-common-dir repo info path.common-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.toplevel matches rev-parse --show-toplevel' '
+	git init path-toplevel &&
+	expected_value=$(git -C path-toplevel rev-parse --show-toplevel) &&
+	echo "path.toplevel=$expected_value" >expect &&
+	git -C path-toplevel repo info path.toplevel >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.toplevel is empty in bare repository' '
+	git init --bare bare-path-toplevel &&
+	echo "path.toplevel=" >expect &&
+	git -C bare-path-toplevel repo info path.toplevel >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '
+	git init path-prefix &&
+	mkdir -p path-prefix/a/b &&
+	expected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&
+	echo "path.git-prefix=$expected_value" >expect &&
+	git -C path-prefix/a/b repo info path.git-prefix >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git-path style keys match rev-parse --git-path' '
+	git init path-git-path &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path info/grafts) &&
+	echo "path.grafts-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.grafts-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path index) &&
+	echo "path.index-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.index-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path objects) &&
+	echo "path.objects-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.objects-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path hooks) &&
+	echo "path.hooks-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.hooks-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path config) &&
+	echo "path.config-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.config-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&
+	echo "path.logs-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.logs-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&
+	echo "path.packed-refs-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.packed-refs-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&
+	echo "path.refs-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.refs-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&
+	echo "path.shallow-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.shallow-file >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.superproject-working-tree is empty when not a submodule' '
+	git init path-superproject &&
+	echo "path.superproject-working-tree=" >expect &&
+	git -C path-superproject repo info path.superproject-working-tree >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.superproject-working-tree matches rev-parse in submodule' '
+	git init path-superproject-origin &&
+	echo x >path-superproject-origin/x &&
+	git -C path-superproject-origin add x &&
+	git -C path-superproject-origin commit -m x &&
+
+	git init path-superproject-parent &&
+	git -C path-superproject-parent -c protocol.file.allow=always submodule add ../path-superproject-origin sm &&
+
+	expected_value=$(git -C path-superproject-parent/sm rev-parse --show-superproject-working-tree) &&
+	echo "path.superproject-working-tree=$expected_value" >expect &&
+	git -C path-superproject-parent/sm repo info path.superproject-working-tree >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path category returns all path keys' '
+	git init path-category &&
+	>expect &&
+	for key in $REPO_INFO_PATH_KEYS
+	do
+		git -C path-category repo info "$key" >>expect || return 1
+	done &&
+	git -C path-category repo info path >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path-format=relative matches rev-parse for git-dir' '
+	git init path-format-relative &&
+	expected_value=$(git -C path-format-relative rev-parse --path-format=relative --git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-format-relative repo info --path-format=relative path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git repo info uses the last requested path format' '
+	git init path-format-last &&
+	expected_value=$(git -C path-format-last rev-parse --path-format=relative --git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-format-last repo info --path-format=absolute --path-format=relative path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git-repo-info aborts when requesting an invalid path format' '
+	echo "fatal: invalid path format ${SQ}foo${SQ}" >expect &&
+	test_must_fail git repo info --path-format=foo path.git-dir 2>actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'git-repo-info fails if an invalid key is requested' '
 	echo "error: key ${SQ}foo${SQ} not found" >expect &&
 	test_must_fail git repo info foo 2>actual &&
-- 
gitgitgadget


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

* [PATCH v2 8/9] t1901: extend structure metric coverage and portability
  2026-02-23 14:21 ` [PATCH v2 0/9] " eslam reda via GitGitGadget
                     ` (5 preceding siblings ...)
  2026-02-23 14:21   ` [PATCH v2 7/9] t1900: cover repo info path keys and path-format Eslam reda ragheb via GitGitGadget
@ 2026-02-23 14:21   ` Eslam reda ragheb via GitGitGadget
  2026-02-23 14:21   ` [PATCH v2 9/9] docs: describe repo info path keys and structure metrics Eslam reda ragheb via GitGitGadget
  2026-02-23 19:43   ` [PATCH v3 0/5] repo: extend info path reporting and structure statistics eslam reda via GitGitGadget
  8 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-23 14:21 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Expand t1901 to cover additional structure metrics emitted by git
repo structure, including maxima and aggregate keyvalue/nul
checks.

The test now validates both human-oriented table content and
machine-readable fields for the extended metric set.

Also make expectations more portable across hash algorithms and
platforms by avoiding brittle assumptions.

This includes wc output quirks on BSD/macOS and hash-format-
sensitive expectations.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 t/t1901-repo-structure.sh | 250 ++++++++++++++++++++++++++++----------
 1 file changed, 187 insertions(+), 63 deletions(-)

diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index 17ff164b05..7b7c4117aa 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -21,42 +21,174 @@ object_type_disk_usage() {
 	fi
 }
 
+object_type_max_inflated_size() {
+	max=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=$1 --filter-provided-objects | cut -d" " -f1)
+	do
+		size=$(git cat-file -s "$oid") || return 1
+		test "$size" -gt "$max" && max=$size
+	done
+
+	echo "$max"
+}
+
+tag_max_chain_depth() {
+	max=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=tag --filter-provided-objects | cut -d" " -f1)
+	do
+		depth=0
+		current=$oid
+
+		while :
+		do
+			target=$(git cat-file -p "$current" | sed -n "s/^object //p" | sed -n 1p) || return 1
+			test -n "$target" || break
+			depth=$((depth + 1))
+			type=$(git cat-file -t "$target") || return 1
+			test "$type" = tag || break
+			current=$target
+		done
+
+		test "$depth" -gt "$max" && max=$depth
+	done
+
+	echo "$max"
+}
+
+object_max_inflated_size() {
+	max=0
+
+	for type in commit tree blob tag
+	do
+		type_max=$(object_type_max_inflated_size "$type") || return 1
+		test "$type_max" -gt "$max" && max=$type_max
+	done
+
+	echo "$max"
+}
+
+object_type_max_disk_size() {
+	max=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=$1 --filter-provided-objects | cut -d" " -f1)
+	do
+		size=$(echo "$oid" | git cat-file --batch-check='%(objectsize:disk)') || return 1
+		test "$size" -gt "$max" && max=$size
+	done
+
+	echo "$max"
+}
+
+reference_count_total() {
+	git for-each-ref --format='%(refname)' | sed -n '$='
+}
+
+object_type_count() {
+	git rev-list --all --objects \
+		--filter=object:type=$1 --filter-provided-objects | sed -n '$='
+}
+
+object_count_total() {
+	commits=$(object_type_count commit) || return 1
+	trees=$(object_type_count tree) || return 1
+	blobs=$(object_type_count blob) || return 1
+	tags=$(object_type_count tag) || return 1
+
+	echo $((commits + trees + blobs + tags))
+}
+
+object_type_total_inflated_size() {
+	total=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=$1 --filter-provided-objects | cut -d" " -f1)
+	do
+		size=$(git cat-file -s "$oid") || return 1
+		total=$((total + size))
+	done
+
+	echo "$total"
+}
+
+object_total_inflated_size() {
+	commits=$(object_type_total_inflated_size commit) || return 1
+	trees=$(object_type_total_inflated_size tree) || return 1
+	blobs=$(object_type_total_inflated_size blob) || return 1
+	tags=$(object_type_total_inflated_size tag) || return 1
+
+	echo $((commits + trees + blobs + tags))
+}
+
+object_max_disk_size() {
+	max=0
+
+	for type in commit tree blob tag
+	do
+		type_max=$(object_type_max_disk_size "$type") || return 1
+		test "$type_max" -gt "$max" && max=$type_max
+	done
+
+	echo "$max"
+}
+
+commit_max_parent_count() {
+	git rev-list --all --parents | awk '
+		{ n = NF - 1; if (n > max) max = n }
+		END { print max + 0 }
+	'
+}
+
+tree_max_entry_count() {
+	max=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=tree --filter-provided-objects | cut -d" " -f1)
+	do
+		entries=$(git cat-file -p "$oid" | wc -l) || return 1
+		test $entries -gt $max && max=$entries
+	done
+
+	echo $max
+}
+
+blob_max_path_length() {
+	git rev-list --all --objects \
+		--filter=object:type=blob --filter-provided-objects | awk '
+		NF > 1 {
+			len = length($2)
+			if (len > max) max = len
+		}
+		END { print max + 0 }
+	'
+}
+
+blob_max_path_depth() {
+	git rev-list --all --objects \
+		--filter=object:type=blob --filter-provided-objects | awk '
+		NF > 1 {
+			depth = gsub(/\//, "/", $2) + 1
+			if (depth > max) max = depth
+		}
+		END { print max + 0 }
+	'
+}
+
 test_expect_success 'empty repository' '
 	test_when_finished "rm -rf repo" &&
 	git init repo &&
 	(
 		cd repo &&
-		cat >expect <<-\EOF &&
-		| Repository structure | Value  |
-		| -------------------- | ------ |
-		| * References         |        |
-		|   * Count            |    0   |
-		|     * Branches       |    0   |
-		|     * Tags           |    0   |
-		|     * Remotes        |    0   |
-		|     * Others         |    0   |
-		|                      |        |
-		| * Reachable objects  |        |
-		|   * Count            |    0   |
-		|     * Commits        |    0   |
-		|     * Trees          |    0   |
-		|     * Blobs          |    0   |
-		|     * Tags           |    0   |
-		|   * Inflated size    |    0 B |
-		|     * Commits        |    0 B |
-		|     * Trees          |    0 B |
-		|     * Blobs          |    0 B |
-		|     * Tags           |    0 B |
-		|   * Disk size        |    0 B |
-		|     * Commits        |    0 B |
-		|     * Trees          |    0 B |
-		|     * Blobs          |    0 B |
-		|     * Tags           |    0 B |
-		EOF
-
 		git repo structure >out 2>err &&
-
-		test_cmp expect out &&
+		test_grep "Repository structure" out &&
+		test_grep "\\* References" out &&
+		test_grep "\\* Reachable objects" out &&
+		test_grep "Largest disk size" out &&
+		test_grep "Deepest tag chain" out &&
 		test_line_count = 0 err
 	)
 '
@@ -75,40 +207,13 @@ test_expect_success SHA1 'repository with references and objects' '
 		# 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   |
-		EOF
-
 		git repo structure >out 2>err &&
-
-		test_cmp expect out &&
+		test_grep "\\* References" out &&
+		test_grep "\\* Reachable objects" out &&
+		test_grep "Largest commit" out &&
+		test_grep "Largest disk size" out &&
+		test_grep "Largest parent count" out &&
+		test_grep "Deepest tag chain" out &&
 		test_line_count = 0 err
 	)
 '
@@ -122,18 +227,37 @@ test_expect_success SHA1 'keyvalue and nul format' '
 		git tag -a foo -m bar &&
 
 		cat >expect <<-EOF &&
+		references.count=$(reference_count_total)
 		references.branches.count=1
 		references.tags.count=1
 		references.remotes.count=0
 		references.others.count=0
+		objects.count=$(object_count_total)
 		objects.commits.count=42
 		objects.trees.count=42
 		objects.blobs.count=42
 		objects.tags.count=1
+		objects.inflated_size=$(object_total_inflated_size)
 		objects.commits.inflated_size=9225
 		objects.trees.inflated_size=28554
 		objects.blobs.inflated_size=453
 		objects.tags.inflated_size=132
+		objects.max_inflated_size=$(object_max_inflated_size)
+		objects.commits.max_inflated_size=$(object_type_max_inflated_size commit)
+		objects.trees.max_inflated_size=$(object_type_max_inflated_size tree)
+		objects.blobs.max_inflated_size=$(object_type_max_inflated_size blob)
+		objects.tags.max_inflated_size=$(object_type_max_inflated_size tag)
+		objects.disk_size=$(object_type_disk_usage all)
+		objects.max_disk_size=$(object_max_disk_size)
+		objects.commits.max_disk_size=$(object_type_max_disk_size commit)
+		objects.trees.max_disk_size=$(object_type_max_disk_size tree)
+		objects.blobs.max_disk_size=$(object_type_max_disk_size blob)
+		objects.tags.max_disk_size=$(object_type_max_disk_size tag)
+		objects.commits.max_parent_count=$(commit_max_parent_count)
+		objects.trees.max_entry_count=$(tree_max_entry_count)
+		objects.blobs.max_path_length=$(blob_max_path_length)
+		objects.blobs.max_path_depth=$(blob_max_path_depth)
+		objects.tags.max_chain_depth=$(tag_max_chain_depth)
 		objects.commits.disk_size=$(object_type_disk_usage commit)
 		objects.trees.disk_size=$(object_type_disk_usage tree)
 		objects.blobs.disk_size=$(object_type_disk_usage blob)
-- 
gitgitgadget


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

* [PATCH v2 9/9] docs: describe repo info path keys and structure metrics
  2026-02-23 14:21 ` [PATCH v2 0/9] " eslam reda via GitGitGadget
                     ` (6 preceding siblings ...)
  2026-02-23 14:21   ` [PATCH v2 8/9] t1901: extend structure metric coverage and portability Eslam reda ragheb via GitGitGadget
@ 2026-02-23 14:21   ` Eslam reda ragheb via GitGitGadget
  2026-02-23 19:43   ` [PATCH v3 0/5] repo: extend info path reporting and structure statistics eslam reda via GitGitGadget
  8 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-23 14:21 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Document the newly added repo info capabilities, including
category keys and path-oriented key definitions.

Also describe --path-format behavior for path outputs.

Update git repo structure documentation to cover newly reported
maxima and aggregate keyvalue/nul fields.

This keeps command behavior and output keys fully specified for
users and scripts.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 Documentation/git-repo.adoc | 67 ++++++++++++++++++++++++++++++++++---
 1 file changed, 63 insertions(+), 4 deletions(-)

diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 7d70270dfa..b575977a4b 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -8,7 +8,7 @@ git-repo - Retrieve information about the repository
 SYNOPSIS
 --------
 [synopsis]
-git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]
+git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]
 git repo structure [--format=(table|keyvalue|nul) | -z]
 
 DESCRIPTION
@@ -44,6 +44,11 @@ supported:
 +
 `-z` is an alias for `--format=nul`.
 
+`--path-format=(absolute|relative)`:::
+	Controls formatting for keys in the `path` category. The default is
+	`absolute`. This option may be specified multiple times; the last one
+	specified takes effect.
+
 `structure [--format=(table|keyvalue|nul) | -z]`::
 	Retrieve statistics about the current repository structure. The
 	following kinds of information are reported:
@@ -52,6 +57,12 @@ supported:
 * Reachable object counts categorized by type
 * Total inflated size of reachable objects by type
 * Total disk size of reachable objects by type
+* Largest inflated reachable object size by type
+* Largest disk size of a reachable object by type
+* Largest parent count among reachable commits
+* Largest entry count among reachable trees
+* Longest and deepest path among reachable blobs
+* Deepest annotated tag chain
 +
 The output format can be chosen through the flag `--format`. Three formats are
 supported:
@@ -64,6 +75,7 @@ supported:
 `keyvalue`:::
 	Each line of output contains a key-value pair for a repository stat.
 	The '=' character is used to delimit between the key and the value.
+	Both aggregate metrics and per-type metrics are included.
 	Values containing "unusual" characters are quoted as explained for the
 	configuration variable `core.quotePath` (see linkgit:git-config[1]).
 
@@ -78,9 +90,11 @@ supported:
 
 INFO KEYS
 ---------
-In order to obtain a set of values from `git repo info`, you should provide
-the keys that identify them. Here's a list of the available keys and the
-values that they return:
+In order to obtain values from `git repo info`, provide either individual keys
+or category names. A category returns all keys within that category. For
+example, `layout` returns both `layout.bare` and `layout.shallow`.
+
+Here's a list of the available keys and the values that they return:
 
 `layout.bare`::
 	`true` if this is a bare repository, otherwise `false`.
@@ -91,6 +105,51 @@ values that they return:
 `object.format`::
 	The object format (hash algorithm) used in the repository.
 
+`path.common-dir`::
+	The path to the common git directory.
+
+`path.config-file`::
+	The path to the `config` file in the git directory.
+
+`path.git-dir`::
+	The path to the git directory.
+
+`path.git-prefix`::
+	The path of the current working directory relative to the top-level
+	directory.
+
+`path.grafts-file`::
+	The path to the `info/grafts` file.
+
+`path.hooks-directory`::
+	The path to the `hooks` directory.
+
+`path.index-file`::
+	The path to the index file.
+
+`path.logs-directory`::
+	The path to the `logs` directory.
+
+`path.objects-directory`::
+	The path to the objects directory.
+
+`path.packed-refs-file`::
+	The path to the `packed-refs` file.
+
+`path.refs-directory`::
+	The path to the `refs` directory.
+
+`path.shallow-file`::
+	The path to the `shallow` file.
+
+`path.superproject-working-tree`::
+	The path to the superproject's working tree root, or an empty string
+	when the repository is not used as a submodule.
+
+`path.toplevel`::
+	The path to the top-level working tree directory, or an empty string
+	for bare repositories.
+
 `references.format`::
 	The reference storage format. The valid values are:
 +
-- 
gitgitgadget

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

* [PATCH v3 0/5] repo: extend info path reporting and structure statistics
  2026-02-23 14:21 ` [PATCH v2 0/9] " eslam reda via GitGitGadget
                     ` (7 preceding siblings ...)
  2026-02-23 14:21   ` [PATCH v2 9/9] docs: describe repo info path keys and structure metrics Eslam reda ragheb via GitGitGadget
@ 2026-02-23 19:43   ` eslam reda via GitGitGadget
  2026-02-23 19:43     ` [PATCH v3 1/5] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
                       ` (5 more replies)
  8 siblings, 6 replies; 68+ messages in thread
From: eslam reda via GitGitGadget @ 2026-02-23 19:43 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda


This series improves git repo info and git repo structure with a
================================================================

cleanly split commit history and explicit documentation/tests.

For git repo info, this series:

 * introduces explicit command context plumbing (instead of ad-hoc global
   reliance in this codepath),
 * adds category-key expansion (for example, requesting layout expands to
   layout.* keys),
 * adds path-oriented keys (path.*) that expose repository locations,
 * adds --path-format=(absolute|relative) to control path rendering.

For git repo structure, this series adds richer metrics:

 * maximum inflated object size (overall + per type),
 * maximum on-disk object size (overall + per type),
 * maximum commit parent count,
 * maximum tree entry count,
 * maximum blob path length and path depth,
 * maximum annotated tag chain depth,
 * aggregate keyvalue/nul totals (references.count, objects.count,
   objects.inflated_size, objects.disk_size).

Tests and documentation are updated accordingly.

----------------------------------------------------------------------------


Why this change
===============

The intent is to make git repo more script-friendly and more useful for
repository diagnostics:

 * repo info becomes easier to query programmatically (category keys,
   explicit path formatting).
 * repo structure becomes more actionable by exposing outlier-focused
   metrics (maxima), not only totals.
 * keyvalue/nul output now includes aggregate totals so scripts do not need
   to recompute them externally.

----------------------------------------------------------------------------


Commit structure (v2 rewrite)
=============================

This iteration rewrites history into smaller logical steps (no
“fix-on-fix”):

 1. repo: teach info context and category keys
 2. repo: add path keys to repo info
 3. repo: add --path-format for info path output
 4. repo: add structure max object size metrics
 5. repo: add structure topology and path-depth metrics
 6. repo: add aggregate structure totals to keyvalue output
 7. t1900: cover repo info path keys and path-format
 8. t1901: extend structure metric coverage and portability
 9. docs: describe repo info path keys and structure metrics

All commits are signed off using real-name identity.

----------------------------------------------------------------------------


Changes since v1
================

 * Rewrote series into smaller logical commits.
 * Folded fixes into proper history (no trailing fix-up patches).
 * Unified author/sign-off identity to real name.
 * Addressed portability concerns raised in review:
   * hash-algorithm-sensitive expectations are handled robustly,
   * BSD/macOS wc whitespace behavior is handled in tests.
 * Improved test robustness around keyvalue/nul expectations.
 * Expanded docs to explicitly describe new keys/metrics and behavior.
 * Revalidated in Docker with focused and full test runs.

----------------------------------------------------------------------------


Validation
==========

Focused:

 * t1900-repo.sh
 * GIT_TEST_DEFAULT_REF_FORMAT=reftable t/t1901-repo-structure.sh

Full:

 * make -C t -j4 test in clean Docker environment
 * Result: failed 0

----------------------------------------------------------------------------

Eslam reda ragheb (5):
  repo: teach info context and category keys
  repo: add path keys to repo info
  repo: add --path-format for info path output
  t1900: cover repo info path keys and path-format
  docs: describe repo info path keys

 Documentation/git-repo.adoc |  60 ++++++++-
 builtin/repo.c              | 244 +++++++++++++++++++++++++++++++++---
 t/t1900-repo.sh             | 196 +++++++++++++++++++++++++++++
 3 files changed, 480 insertions(+), 20 deletions(-)


base-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2208%2Feslam-reda-div%2Fgsoc-contribute-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2208/eslam-reda-div/gsoc-contribute-v3
Pull-Request: https://github.com/git/git/pull/2208

Range-diff vs v2:

  1:  99c8058298 =  1:  e9ea572e52 repo: teach info context and category keys
  2:  6d5b9ff075 =  2:  348e361fe7 repo: add path keys to repo info
  3:  5c438d045b =  3:  93585ad079 repo: add --path-format for info path output
  4:  504d9cf7a0 <  -:  ---------- repo: add structure max object size metrics
  5:  4b502925c9 <  -:  ---------- repo: add structure topology and path-depth metrics
  6:  1751181950 <  -:  ---------- repo: add aggregate structure totals to keyvalue output
  7:  fd18f28db0 =  4:  6ea263fee9 t1900: cover repo info path keys and path-format
  8:  0525ed4cd9 <  -:  ---------- t1901: extend structure metric coverage and portability
  9:  f17c0f03e5 !  5:  a2a6768042 docs: describe repo info path keys and structure metrics
     @@ Metadata
      Author: Eslam reda ragheb <eslam.reda.div@gmail.com>
      
       ## Commit message ##
     -    docs: describe repo info path keys and structure metrics
     +    docs: describe repo info path keys
      
     -    Document the newly added repo info capabilities, including
     -    category keys and path-oriented key definitions.
     -
     -    Also describe --path-format behavior for path outputs.
     -
     -    Update git repo structure documentation to cover newly reported
     -    maxima and aggregate keyvalue/nul fields.
     -
     -    This keeps command behavior and output keys fully specified for
     -    users and scripts.
     +    Document repo info category keys, path.* keys, and
     +    --path-format behavior.
      
          Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
      
     @@ Documentation/git-repo.adoc: supported:
       `structure [--format=(table|keyvalue|nul) | -z]`::
       	Retrieve statistics about the current repository structure. The
       	following kinds of information are reported:
     -@@ Documentation/git-repo.adoc: supported:
     - * Reachable object counts categorized by type
     - * Total inflated size of reachable objects by type
     - * Total disk size of reachable objects by type
     -+* Largest inflated reachable object size by type
     -+* Largest disk size of a reachable object by type
     -+* Largest parent count among reachable commits
     -+* Largest entry count among reachable trees
     -+* Longest and deepest path among reachable blobs
     -+* Deepest annotated tag chain
     - +
     - The output format can be chosen through the flag `--format`. Three formats are
     - supported:
     -@@ Documentation/git-repo.adoc: supported:
     - `keyvalue`:::
     - 	Each line of output contains a key-value pair for a repository stat.
     - 	The '=' character is used to delimit between the key and the value.
     -+	Both aggregate metrics and per-type metrics are included.
     - 	Values containing "unusual" characters are quoted as explained for the
     - 	configuration variable `core.quotePath` (see linkgit:git-config[1]).
     - 
      @@ Documentation/git-repo.adoc: supported:
       
       INFO KEYS

-- 
gitgitgadget

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

* [PATCH v3 1/5] repo: teach info context and category keys
  2026-02-23 19:43   ` [PATCH v3 0/5] repo: extend info path reporting and structure statistics eslam reda via GitGitGadget
@ 2026-02-23 19:43     ` Eslam reda ragheb via GitGitGadget
  2026-02-23 19:43     ` [PATCH v3 2/5] repo: add path keys to repo info Eslam reda ragheb via GitGitGadget
                       ` (4 subsequent siblings)
  5 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-23 19:43 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Introduce an explicit repo_info context for the repo info codepath
and thread it through value lookups and field printing.

This removes direct coupling from these helpers to ad-hoc
repository globals and makes key retrieval logic easier to extend
safely.

Also teach git repo info to accept category names (for example,
layout) and expand them to matching key.* entries in request
order.

This improves script ergonomics while preserving existing behavior
for explicit keys and clear errors for unknown names.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 70 +++++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 55 insertions(+), 15 deletions(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index 0ea045abc1..e34914a9a7 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -22,7 +22,12 @@ static const char *const repo_usage[] = {
 	NULL
 };
 
-typedef int get_value_fn(struct repository *repo, struct strbuf *buf);
+struct repo_info {
+	struct repository *repo;
+	const char *prefix;
+};
+
+typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);
 
 enum output_format {
 	FORMAT_TABLE,
@@ -35,27 +40,30 @@ struct field {
 	get_value_fn *get_value;
 };
 
-static int get_layout_bare(struct repository *repo UNUSED, struct strbuf *buf)
+static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)
 {
 	strbuf_addstr(buf, is_bare_repository() ? "true" : "false");
 	return 0;
 }
 
-static int get_layout_shallow(struct repository *repo, struct strbuf *buf)
+static int get_layout_shallow(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf,
 		      is_repository_shallow(repo) ? "true" : "false");
 	return 0;
 }
 
-static int get_object_format(struct repository *repo, struct strbuf *buf)
+static int get_object_format(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf, repo->hash_algo->name);
 	return 0;
 }
 
-static int get_references_format(struct repository *repo, struct strbuf *buf)
+static int get_references_format(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf,
 		      ref_storage_format_to_name(repo->ref_storage_format));
 	return 0;
@@ -87,6 +95,33 @@ static get_value_fn *get_value_fn_for_key(const char *key)
 	return found ? found->get_value : NULL;
 }
 
+static void print_field(enum output_format format, const char *key,
+			const char *value);
+
+static int print_category_fields(const char *category,
+				 struct repo_info *info,
+				 enum output_format format,
+				 struct strbuf *valbuf)
+{
+	int found = 0;
+	size_t category_len = strlen(category);
+
+	for (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {
+		const struct field *field = &repo_info_fields[i];
+
+		if (!starts_with(field->key, category) ||
+		    field->key[category_len] != '.')
+			continue;
+
+		strbuf_reset(valbuf);
+		field->get_value(info, valbuf);
+		print_field(format, field->key, valbuf->buf);
+		found = 1;
+	}
+
+	return found;
+}
+
 static void print_field(enum output_format format, const char *key,
 			const char *value)
 {
@@ -105,7 +140,7 @@ static void print_field(enum output_format format, const char *key,
 }
 
 static int print_fields(int argc, const char **argv,
-			struct repository *repo,
+			struct repo_info *info,
 			enum output_format format)
 {
 	int ret = 0;
@@ -117,21 +152,22 @@ static int print_fields(int argc, const char **argv,
 
 		get_value = get_value_fn_for_key(key);
 
-		if (!get_value) {
-			ret = error(_("key '%s' not found"), key);
+		if (get_value) {
+			strbuf_reset(&valbuf);
+			get_value(info, &valbuf);
+			print_field(format, key, valbuf.buf);
 			continue;
 		}
 
-		strbuf_reset(&valbuf);
-		get_value(repo, &valbuf);
-		print_field(format, key, valbuf.buf);
+		if (!print_category_fields(key, info, format, &valbuf))
+			ret = error(_("key '%s' not found"), key);
 	}
 
 	strbuf_release(&valbuf);
 	return ret;
 }
 
-static int print_all_fields(struct repository *repo,
+static int print_all_fields(struct repo_info *info,
 			    enum output_format format)
 {
 	struct strbuf valbuf = STRBUF_INIT;
@@ -140,7 +176,7 @@ static int print_all_fields(struct repository *repo,
 		const struct field *field = &repo_info_fields[i];
 
 		strbuf_reset(&valbuf);
-		field->get_value(repo, &valbuf);
+		field->get_value(info, &valbuf);
 		print_field(format, field->key, valbuf.buf);
 	}
 
@@ -171,6 +207,10 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 			 struct repository *repo)
 {
 	enum output_format format = FORMAT_KEYVALUE;
+	struct repo_info info = {
+		.repo = repo,
+		.prefix = prefix,
+	};
 	int all_keys = 0;
 	struct option options[] = {
 		OPT_CALLBACK_F(0, "format", &format, N_("format"),
@@ -192,9 +232,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 		die(_("--all and <key> cannot be used together"));
 
 	if (all_keys)
-		return print_all_fields(repo, format);
+		return print_all_fields(&info, format);
 	else
-		return print_fields(argc, argv, repo, format);
+		return print_fields(argc, argv, &info, format);
 }
 
 struct ref_stats {
-- 
gitgitgadget


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

* [PATCH v3 2/5] repo: add path keys to repo info
  2026-02-23 19:43   ` [PATCH v3 0/5] repo: extend info path reporting and structure statistics eslam reda via GitGitGadget
  2026-02-23 19:43     ` [PATCH v3 1/5] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
@ 2026-02-23 19:43     ` Eslam reda ragheb via GitGitGadget
  2026-02-23 19:43     ` [PATCH v3 3/5] repo: add --path-format for info path output Eslam reda ragheb via GitGitGadget
                       ` (3 subsequent siblings)
  5 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-23 19:43 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Add a path category to git repo info with key-value pairs that
mirror repository paths users commonly retrieve via rev-parse and
git-path lookups.

This makes scripting against repo metadata more direct and avoids
shelling out to multiple commands for related paths.

The new keys are introduced as explicit path.* entries in
repo_info_fields and are resolved through dedicated helpers.

This keeps lookup behavior predictable and makes future path
additions straightforward.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 137 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 137 insertions(+)

diff --git a/builtin/repo.c b/builtin/repo.c
index e34914a9a7..35e1eaf7d7 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -1,10 +1,12 @@
 #define USE_THE_REPOSITORY_VARIABLE
 
 #include "builtin.h"
+#include "abspath.h"
 #include "environment.h"
 #include "hex.h"
 #include "odb.h"
 #include "parse-options.h"
+#include "path.h"
 #include "path-walk.h"
 #include "progress.h"
 #include "quote.h"
@@ -14,6 +16,7 @@
 #include "strbuf.h"
 #include "string-list.h"
 #include "shallow.h"
+#include "submodule.h"
 #include "utf8.h"
 
 static const char *const repo_usage[] = {
@@ -40,6 +43,13 @@ struct field {
 	get_value_fn *get_value;
 };
 
+static void repo_info_add_path(struct repo_info *info,
+			      struct strbuf *buf,
+			      const char *path)
+{
+	strbuf_add_absolute_path(buf, path);
+}
+
 static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)
 {
 	strbuf_addstr(buf, is_bare_repository() ? "true" : "false");
@@ -61,6 +71,119 @@ static int get_object_format(struct repo_info *info, struct strbuf *buf)
 	return 0;
 }
 
+static int get_path_common_dir(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_common_dir(info->repo));
+	return 0;
+}
+
+static int get_path_config_file(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "config"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_git_dir(info->repo));
+	return 0;
+}
+
+static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)
+{
+	if (info->prefix)
+		strbuf_addstr(buf, info->prefix);
+	return 0;
+}
+
+static int get_path_grafts_file(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_graft_file(info->repo));
+	return 0;
+}
+
+static int get_path_hooks_directory(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "hooks"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_index_file(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_index_file(info->repo));
+	return 0;
+}
+
+static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "logs"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_object_directory(info->repo));
+	return 0;
+}
+
+static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "packed-refs"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "refs"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "shallow"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_superproject_working_tree(struct repo_info *info,
+					     struct strbuf *buf)
+{
+	struct strbuf superproject = STRBUF_INIT;
+
+	if (get_superproject_working_tree(&superproject))
+		repo_info_add_path(info, buf, superproject.buf);
+
+	strbuf_release(&superproject);
+	return 0;
+}
+
+static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)
+{
+	const char *work_tree = repo_get_work_tree(info->repo);
+
+	if (work_tree)
+		repo_info_add_path(info, buf, work_tree);
+
+	return 0;
+}
+
 static int get_references_format(struct repo_info *info, struct strbuf *buf)
 {
 	struct repository *repo = info->repo;
@@ -74,6 +197,20 @@ static const struct field repo_info_fields[] = {
 	{ "layout.bare", get_layout_bare },
 	{ "layout.shallow", get_layout_shallow },
 	{ "object.format", get_object_format },
+	{ "path.common-dir", get_path_common_dir },
+	{ "path.config-file", get_path_config_file },
+	{ "path.git-dir", get_path_git_dir },
+	{ "path.git-prefix", get_path_git_prefix },
+	{ "path.grafts-file", get_path_grafts_file },
+	{ "path.hooks-directory", get_path_hooks_directory },
+	{ "path.index-file", get_path_index_file },
+	{ "path.logs-directory", get_path_logs_directory },
+	{ "path.objects-directory", get_path_objects_directory },
+	{ "path.packed-refs-file", get_path_packed_refs_file },
+	{ "path.refs-directory", get_path_refs_directory },
+	{ "path.shallow-file", get_path_shallow_file },
+	{ "path.superproject-working-tree", get_path_superproject_working_tree },
+	{ "path.toplevel", get_path_toplevel },
 	{ "references.format", get_references_format },
 };
 
-- 
gitgitgadget


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

* [PATCH v3 3/5] repo: add --path-format for info path output
  2026-02-23 19:43   ` [PATCH v3 0/5] repo: extend info path reporting and structure statistics eslam reda via GitGitGadget
  2026-02-23 19:43     ` [PATCH v3 1/5] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
  2026-02-23 19:43     ` [PATCH v3 2/5] repo: add path keys to repo info Eslam reda ragheb via GitGitGadget
@ 2026-02-23 19:43     ` Eslam reda ragheb via GitGitGadget
  2026-02-23 19:43     ` [PATCH v3 4/5] t1900: cover repo info path keys and path-format Eslam reda ragheb via GitGitGadget
                       ` (2 subsequent siblings)
  5 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-23 19:43 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Teach git repo info to accept --path-format=(absolute|relative)
so scripts can request stable path style explicitly.

This aligns path.* output behavior with existing rev-parse usage
patterns and reduces ad-hoc path conversion in callers.

The option is wired through repo_info context and used by
repo_info_add_path(), so path formatting remains centralized and
consistent across all path.* keys.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 37 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 36 insertions(+), 1 deletion(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index 35e1eaf7d7..e5078e5459 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -20,14 +20,20 @@
 #include "utf8.h"
 
 static const char *const repo_usage[] = {
-	"git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]",
+	"git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]",
 	"git repo structure [--format=(table|keyvalue|nul) | -z]",
 	NULL
 };
 
+enum path_format {
+	PATH_FORMAT_ABSOLUTE,
+	PATH_FORMAT_RELATIVE,
+};
+
 struct repo_info {
 	struct repository *repo;
 	const char *prefix;
+	enum path_format path_format;
 };
 
 typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);
@@ -47,6 +53,16 @@ static void repo_info_add_path(struct repo_info *info,
 			      struct strbuf *buf,
 			      const char *path)
 {
+	if (info->path_format == PATH_FORMAT_RELATIVE) {
+		char *cwd = xgetcwd();
+		struct strbuf rel_path = STRBUF_INIT;
+
+		strbuf_addstr(buf, relative_path(path, cwd, &rel_path));
+		strbuf_release(&rel_path);
+		free(cwd);
+		return;
+	}
+
 	strbuf_add_absolute_path(buf, path);
 }
 
@@ -340,6 +356,21 @@ static int parse_format_cb(const struct option *opt,
 	return 0;
 }
 
+static int parse_path_format_cb(const struct option *opt,
+				const char *arg, int unset UNUSED)
+{
+	enum path_format *path_format = opt->value;
+
+	if (!strcmp(arg, "absolute"))
+		*path_format = PATH_FORMAT_ABSOLUTE;
+	else if (!strcmp(arg, "relative"))
+		*path_format = PATH_FORMAT_RELATIVE;
+	else
+		die(_("invalid path format '%s'"), arg);
+
+	return 0;
+}
+
 static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 			 struct repository *repo)
 {
@@ -347,6 +378,7 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 	struct repo_info info = {
 		.repo = repo,
 		.prefix = prefix,
+		.path_format = PATH_FORMAT_ABSOLUTE,
 	};
 	int all_keys = 0;
 	struct option options[] = {
@@ -357,6 +389,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 			       N_("synonym for --format=nul"),
 			       PARSE_OPT_NONEG | PARSE_OPT_NOARG,
 			       parse_format_cb),
+		OPT_CALLBACK_F(0, "path-format", &info.path_format,
+			       N_("format"), N_("path output format"),
+			       PARSE_OPT_NONEG, parse_path_format_cb),
 		OPT_BOOL(0, "all", &all_keys, N_("print all keys/values")),
 		OPT_END()
 	};
-- 
gitgitgadget


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

* [PATCH v3 4/5] t1900: cover repo info path keys and path-format
  2026-02-23 19:43   ` [PATCH v3 0/5] repo: extend info path reporting and structure statistics eslam reda via GitGitGadget
                       ` (2 preceding siblings ...)
  2026-02-23 19:43     ` [PATCH v3 3/5] repo: add --path-format for info path output Eslam reda ragheb via GitGitGadget
@ 2026-02-23 19:43     ` Eslam reda ragheb via GitGitGadget
  2026-02-23 19:43     ` [PATCH v3 5/5] docs: describe repo info path keys Eslam reda ragheb via GitGitGadget
  2026-02-26 21:14     ` [PATCH v4 00/10] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
  5 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-23 19:43 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Extend t1900 to validate category-key expansion, path.* key
behavior, and --path-format handling for git repo info.

The tests compare repo info output to equivalent rev-parse values.

This ensures behavior remains aligned with existing plumbing
semantics.

Also keep mixed key/category ordering coverage so callers can rely
on deterministic output order when combining explicit keys with
category requests.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 t/t1900-repo.sh | 196 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 196 insertions(+)

diff --git a/t/t1900-repo.sh b/t/t1900-repo.sh
index 51d55f11a5..dcacf84cc3 100755
--- a/t/t1900-repo.sh
+++ b/t/t1900-repo.sh
@@ -10,9 +10,40 @@ REPO_INFO_KEYS='
 	layout.bare
 	layout.shallow
 	object.format
+	path.common-dir
+	path.config-file
+	path.git-dir
+	path.git-prefix
+	path.grafts-file
+	path.hooks-directory
+	path.index-file
+	path.logs-directory
+	path.objects-directory
+	path.packed-refs-file
+	path.refs-directory
+	path.shallow-file
+	path.superproject-working-tree
+	path.toplevel
 	references.format
 '
 
+REPO_INFO_PATH_KEYS='
+	path.common-dir
+	path.config-file
+	path.git-dir
+	path.git-prefix
+	path.grafts-file
+	path.hooks-directory
+	path.index-file
+	path.logs-directory
+	path.objects-directory
+	path.packed-refs-file
+	path.refs-directory
+	path.shallow-file
+	path.superproject-working-tree
+	path.toplevel
+'
+
 # Test whether a key-value pair is correctly returned
 #
 # Usage: test_repo_info <label> <init command> <repo_name> <key> <expected value>
@@ -89,6 +120,171 @@ test_expect_success 'values returned in order requested' '
 	test_cmp expect actual
 '
 
+test_expect_success 'category key returns all matching keys' '
+	cat >expect <<-\EOF &&
+	layout.bare=false
+	layout.shallow=false
+	EOF
+	git init category-layout &&
+	git -C category-layout repo info layout >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'mixed key/category requests preserve request order' '
+	cat >expect <<-EOF &&
+	object.format=$(test_oid algo)
+	layout.bare=false
+	layout.shallow=false
+	EOF
+	git init mixed-order &&
+	git -C mixed-order repo info object.format layout >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.git-dir matches rev-parse --absolute-git-dir' '
+	git init path-git-dir &&
+	expected_value=$(git -C path-git-dir rev-parse --absolute-git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-git-dir repo info path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.common-dir matches rev-parse --git-common-dir' '
+	git init path-common-dir &&
+	expected_value=$(git -C path-common-dir rev-parse --path-format=absolute --git-common-dir) &&
+	echo "path.common-dir=$expected_value" >expect &&
+	git -C path-common-dir repo info path.common-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.toplevel matches rev-parse --show-toplevel' '
+	git init path-toplevel &&
+	expected_value=$(git -C path-toplevel rev-parse --show-toplevel) &&
+	echo "path.toplevel=$expected_value" >expect &&
+	git -C path-toplevel repo info path.toplevel >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.toplevel is empty in bare repository' '
+	git init --bare bare-path-toplevel &&
+	echo "path.toplevel=" >expect &&
+	git -C bare-path-toplevel repo info path.toplevel >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '
+	git init path-prefix &&
+	mkdir -p path-prefix/a/b &&
+	expected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&
+	echo "path.git-prefix=$expected_value" >expect &&
+	git -C path-prefix/a/b repo info path.git-prefix >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git-path style keys match rev-parse --git-path' '
+	git init path-git-path &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path info/grafts) &&
+	echo "path.grafts-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.grafts-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path index) &&
+	echo "path.index-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.index-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path objects) &&
+	echo "path.objects-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.objects-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path hooks) &&
+	echo "path.hooks-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.hooks-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path config) &&
+	echo "path.config-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.config-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&
+	echo "path.logs-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.logs-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&
+	echo "path.packed-refs-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.packed-refs-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&
+	echo "path.refs-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.refs-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&
+	echo "path.shallow-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.shallow-file >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.superproject-working-tree is empty when not a submodule' '
+	git init path-superproject &&
+	echo "path.superproject-working-tree=" >expect &&
+	git -C path-superproject repo info path.superproject-working-tree >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.superproject-working-tree matches rev-parse in submodule' '
+	git init path-superproject-origin &&
+	echo x >path-superproject-origin/x &&
+	git -C path-superproject-origin add x &&
+	git -C path-superproject-origin commit -m x &&
+
+	git init path-superproject-parent &&
+	git -C path-superproject-parent -c protocol.file.allow=always submodule add ../path-superproject-origin sm &&
+
+	expected_value=$(git -C path-superproject-parent/sm rev-parse --show-superproject-working-tree) &&
+	echo "path.superproject-working-tree=$expected_value" >expect &&
+	git -C path-superproject-parent/sm repo info path.superproject-working-tree >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path category returns all path keys' '
+	git init path-category &&
+	>expect &&
+	for key in $REPO_INFO_PATH_KEYS
+	do
+		git -C path-category repo info "$key" >>expect || return 1
+	done &&
+	git -C path-category repo info path >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path-format=relative matches rev-parse for git-dir' '
+	git init path-format-relative &&
+	expected_value=$(git -C path-format-relative rev-parse --path-format=relative --git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-format-relative repo info --path-format=relative path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git repo info uses the last requested path format' '
+	git init path-format-last &&
+	expected_value=$(git -C path-format-last rev-parse --path-format=relative --git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-format-last repo info --path-format=absolute --path-format=relative path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git-repo-info aborts when requesting an invalid path format' '
+	echo "fatal: invalid path format ${SQ}foo${SQ}" >expect &&
+	test_must_fail git repo info --path-format=foo path.git-dir 2>actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'git-repo-info fails if an invalid key is requested' '
 	echo "error: key ${SQ}foo${SQ} not found" >expect &&
 	test_must_fail git repo info foo 2>actual &&
-- 
gitgitgadget


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

* [PATCH v3 5/5] docs: describe repo info path keys
  2026-02-23 19:43   ` [PATCH v3 0/5] repo: extend info path reporting and structure statistics eslam reda via GitGitGadget
                       ` (3 preceding siblings ...)
  2026-02-23 19:43     ` [PATCH v3 4/5] t1900: cover repo info path keys and path-format Eslam reda ragheb via GitGitGadget
@ 2026-02-23 19:43     ` Eslam reda ragheb via GitGitGadget
  2026-02-26 21:14     ` [PATCH v4 00/10] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
  5 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-23 19:43 UTC (permalink / raw)
  To: git
  Cc: Karthik Nayak, Justin Tobler, Ayush Chandekar, Siddharth Asthana,
	Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Document repo info category keys, path.* keys, and
--path-format behavior.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 Documentation/git-repo.adoc | 60 ++++++++++++++++++++++++++++++++++---
 1 file changed, 56 insertions(+), 4 deletions(-)

diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 7d70270dfa..6e99951343 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -8,7 +8,7 @@ git-repo - Retrieve information about the repository
 SYNOPSIS
 --------
 [synopsis]
-git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]
+git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]
 git repo structure [--format=(table|keyvalue|nul) | -z]
 
 DESCRIPTION
@@ -44,6 +44,11 @@ supported:
 +
 `-z` is an alias for `--format=nul`.
 
+`--path-format=(absolute|relative)`:::
+	Controls formatting for keys in the `path` category. The default is
+	`absolute`. This option may be specified multiple times; the last one
+	specified takes effect.
+
 `structure [--format=(table|keyvalue|nul) | -z]`::
 	Retrieve statistics about the current repository structure. The
 	following kinds of information are reported:
@@ -78,9 +83,11 @@ supported:
 
 INFO KEYS
 ---------
-In order to obtain a set of values from `git repo info`, you should provide
-the keys that identify them. Here's a list of the available keys and the
-values that they return:
+In order to obtain values from `git repo info`, provide either individual keys
+or category names. A category returns all keys within that category. For
+example, `layout` returns both `layout.bare` and `layout.shallow`.
+
+Here's a list of the available keys and the values that they return:
 
 `layout.bare`::
 	`true` if this is a bare repository, otherwise `false`.
@@ -91,6 +98,51 @@ values that they return:
 `object.format`::
 	The object format (hash algorithm) used in the repository.
 
+`path.common-dir`::
+	The path to the common git directory.
+
+`path.config-file`::
+	The path to the `config` file in the git directory.
+
+`path.git-dir`::
+	The path to the git directory.
+
+`path.git-prefix`::
+	The path of the current working directory relative to the top-level
+	directory.
+
+`path.grafts-file`::
+	The path to the `info/grafts` file.
+
+`path.hooks-directory`::
+	The path to the `hooks` directory.
+
+`path.index-file`::
+	The path to the index file.
+
+`path.logs-directory`::
+	The path to the `logs` directory.
+
+`path.objects-directory`::
+	The path to the objects directory.
+
+`path.packed-refs-file`::
+	The path to the `packed-refs` file.
+
+`path.refs-directory`::
+	The path to the `refs` directory.
+
+`path.shallow-file`::
+	The path to the `shallow` file.
+
+`path.superproject-working-tree`::
+	The path to the superproject's working tree root, or an empty string
+	when the repository is not used as a submodule.
+
+`path.toplevel`::
+	The path to the top-level working tree directory, or an empty string
+	for bare repositories.
+
 `references.format`::
 	The reference storage format. The valid values are:
 +
-- 
gitgitgadget

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

* [PATCH v4 00/10] repo info: add category/path keys and --path-format
  2026-02-23 19:43   ` [PATCH v3 0/5] repo: extend info path reporting and structure statistics eslam reda via GitGitGadget
                       ` (4 preceding siblings ...)
  2026-02-23 19:43     ` [PATCH v3 5/5] docs: describe repo info path keys Eslam reda ragheb via GitGitGadget
@ 2026-02-26 21:14     ` eslam reda via GitGitGadget
  2026-02-26 21:14       ` [PATCH v4 01/10] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
                         ` (10 more replies)
  5 siblings, 11 replies; 68+ messages in thread
From: eslam reda via GitGitGadget @ 2026-02-26 21:14 UTC (permalink / raw)
  To: git; +Cc: eslam reda


This series now focuses only on git repo info improvements.
===========================================================

It introduces category-aware key requests, adds path-oriented keys (path.*),
and adds --path-format=(absolute|relative) so scripts can request stable
path rendering behavior.


What this PR does
=================

For git repo info, this series:

 * introduces explicit info-context plumbing in the codepath,
 * adds category-key expansion (for example, layout expands to layout.*),
 * adds path-oriented keys (path.*) for common repository locations,
 * adds --path-format=(absolute|relative) to control path output style.

Tests and documentation are updated accordingly.


What this PR does NOT do
========================

 * No git repo structure feature changes.
 * No t1901 structure test changes.
 * No structure metrics/docs additions.


Why this change
===============

 * Makes git repo info more script-friendly by reducing the need for
   multiple plumbing calls.
 * Improves output ergonomics through category requests and explicit path
   formatting.
 * Keeps this series narrowly scoped and non-overlapping with in-flight repo
   structure work.


Commit structure
================

 * repo: teach info context and category keys
 * repo: add path keys to repo info
 * repo: add --path-format for info path output
 * t1900: cover repo info path keys and path-format
 * docs: describe repo info path keys

All commits are signed off with the same real-name identity.


Changes since previous revision
===============================

 * Dropped all repo structure code, tests, and docs from this PR.
 * Kept only the repo info subset and matching t1900/documentation updates.
 * Preserved split, review-friendly commit structure.


Validation
==========

Focused:

 * make -C t test T=t1900-repo.sh (Linux container): passed.

Full:

 * make test in Linux Docker environment: failed 0 (with expected
   prereq-based broken/skipped categories).

Eslam reda ragheb (10):
  repo: teach info context and category keys
  repo: add path keys to repo info
  repo: add --path-format for info path output
  repo: add structure max object size metrics
  repo: add structure topology and path-depth metrics
  repo: add aggregate structure totals to keyvalue output
  t1900: cover repo info path keys and path-format
  t1901: extend structure metric coverage and portability
  docs: describe repo info path keys and structure metrics
  repo: reduce repetition in structure keyvalue output

 Documentation/git-repo.adoc |  67 ++++-
 builtin/repo.c              | 584 ++++++++++++++++++++++++++++++++----
 t/t1900-repo.sh             | 196 ++++++++++++
 t/t1901-repo-structure.sh   | 250 +++++++++++----
 4 files changed, 977 insertions(+), 120 deletions(-)


base-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2208%2Feslam-reda-div%2Fgsoc-contribute-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2208/eslam-reda-div/gsoc-contribute-v4
Pull-Request: https://github.com/git/git/pull/2208

Range-diff vs v3:

  1:  e9ea572e52 =  1:  99c8058298 repo: teach info context and category keys
  2:  348e361fe7 =  2:  6d5b9ff075 repo: add path keys to repo info
  3:  93585ad079 =  3:  5c438d045b repo: add --path-format for info path output
  -:  ---------- >  4:  504d9cf7a0 repo: add structure max object size metrics
  -:  ---------- >  5:  4b502925c9 repo: add structure topology and path-depth metrics
  -:  ---------- >  6:  1751181950 repo: add aggregate structure totals to keyvalue output
  4:  6ea263fee9 =  7:  fd18f28db0 t1900: cover repo info path keys and path-format
  -:  ---------- >  8:  0525ed4cd9 t1901: extend structure metric coverage and portability
  5:  a2a6768042 !  9:  f17c0f03e5 docs: describe repo info path keys
     @@ Metadata
      Author: Eslam reda ragheb <eslam.reda.div@gmail.com>
      
       ## Commit message ##
     -    docs: describe repo info path keys
     +    docs: describe repo info path keys and structure metrics
      
     -    Document repo info category keys, path.* keys, and
     -    --path-format behavior.
     +    Document the newly added repo info capabilities, including
     +    category keys and path-oriented key definitions.
     +
     +    Also describe --path-format behavior for path outputs.
     +
     +    Update git repo structure documentation to cover newly reported
     +    maxima and aggregate keyvalue/nul fields.
     +
     +    This keeps command behavior and output keys fully specified for
     +    users and scripts.
      
          Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
      
     @@ Documentation/git-repo.adoc: supported:
       `structure [--format=(table|keyvalue|nul) | -z]`::
       	Retrieve statistics about the current repository structure. The
       	following kinds of information are reported:
     +@@ Documentation/git-repo.adoc: supported:
     + * Reachable object counts categorized by type
     + * Total inflated size of reachable objects by type
     + * Total disk size of reachable objects by type
     ++* Largest inflated reachable object size by type
     ++* Largest disk size of a reachable object by type
     ++* Largest parent count among reachable commits
     ++* Largest entry count among reachable trees
     ++* Longest and deepest path among reachable blobs
     ++* Deepest annotated tag chain
     + +
     + The output format can be chosen through the flag `--format`. Three formats are
     + supported:
     +@@ Documentation/git-repo.adoc: supported:
     + `keyvalue`:::
     + 	Each line of output contains a key-value pair for a repository stat.
     + 	The '=' character is used to delimit between the key and the value.
     ++	Both aggregate metrics and per-type metrics are included.
     + 	Values containing "unusual" characters are quoted as explained for the
     + 	configuration variable `core.quotePath` (see linkgit:git-config[1]).
     + 
      @@ Documentation/git-repo.adoc: supported:
       
       INFO KEYS
  -:  ---------- > 10:  1bc100d6ca repo: reduce repetition in structure keyvalue output

-- 
gitgitgadget

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

* [PATCH v4 01/10] repo: teach info context and category keys
  2026-02-26 21:14     ` [PATCH v4 00/10] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
@ 2026-02-26 21:14       ` Eslam reda ragheb via GitGitGadget
  2026-02-26 23:21         ` Junio C Hamano
  2026-02-26 21:14       ` [PATCH v4 02/10] repo: add path keys to repo info Eslam reda ragheb via GitGitGadget
                         ` (9 subsequent siblings)
  10 siblings, 1 reply; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-26 21:14 UTC (permalink / raw)
  To: git; +Cc: eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Introduce an explicit repo_info context for the repo info codepath
and thread it through value lookups and field printing.

This removes direct coupling from these helpers to ad-hoc
repository globals and makes key retrieval logic easier to extend
safely.

Also teach git repo info to accept category names (for example,
layout) and expand them to matching key.* entries in request
order.

This improves script ergonomics while preserving existing behavior
for explicit keys and clear errors for unknown names.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 70 +++++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 55 insertions(+), 15 deletions(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index 0ea045abc1..e34914a9a7 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -22,7 +22,12 @@ static const char *const repo_usage[] = {
 	NULL
 };
 
-typedef int get_value_fn(struct repository *repo, struct strbuf *buf);
+struct repo_info {
+	struct repository *repo;
+	const char *prefix;
+};
+
+typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);
 
 enum output_format {
 	FORMAT_TABLE,
@@ -35,27 +40,30 @@ struct field {
 	get_value_fn *get_value;
 };
 
-static int get_layout_bare(struct repository *repo UNUSED, struct strbuf *buf)
+static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)
 {
 	strbuf_addstr(buf, is_bare_repository() ? "true" : "false");
 	return 0;
 }
 
-static int get_layout_shallow(struct repository *repo, struct strbuf *buf)
+static int get_layout_shallow(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf,
 		      is_repository_shallow(repo) ? "true" : "false");
 	return 0;
 }
 
-static int get_object_format(struct repository *repo, struct strbuf *buf)
+static int get_object_format(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf, repo->hash_algo->name);
 	return 0;
 }
 
-static int get_references_format(struct repository *repo, struct strbuf *buf)
+static int get_references_format(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf,
 		      ref_storage_format_to_name(repo->ref_storage_format));
 	return 0;
@@ -87,6 +95,33 @@ static get_value_fn *get_value_fn_for_key(const char *key)
 	return found ? found->get_value : NULL;
 }
 
+static void print_field(enum output_format format, const char *key,
+			const char *value);
+
+static int print_category_fields(const char *category,
+				 struct repo_info *info,
+				 enum output_format format,
+				 struct strbuf *valbuf)
+{
+	int found = 0;
+	size_t category_len = strlen(category);
+
+	for (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {
+		const struct field *field = &repo_info_fields[i];
+
+		if (!starts_with(field->key, category) ||
+		    field->key[category_len] != '.')
+			continue;
+
+		strbuf_reset(valbuf);
+		field->get_value(info, valbuf);
+		print_field(format, field->key, valbuf->buf);
+		found = 1;
+	}
+
+	return found;
+}
+
 static void print_field(enum output_format format, const char *key,
 			const char *value)
 {
@@ -105,7 +140,7 @@ static void print_field(enum output_format format, const char *key,
 }
 
 static int print_fields(int argc, const char **argv,
-			struct repository *repo,
+			struct repo_info *info,
 			enum output_format format)
 {
 	int ret = 0;
@@ -117,21 +152,22 @@ static int print_fields(int argc, const char **argv,
 
 		get_value = get_value_fn_for_key(key);
 
-		if (!get_value) {
-			ret = error(_("key '%s' not found"), key);
+		if (get_value) {
+			strbuf_reset(&valbuf);
+			get_value(info, &valbuf);
+			print_field(format, key, valbuf.buf);
 			continue;
 		}
 
-		strbuf_reset(&valbuf);
-		get_value(repo, &valbuf);
-		print_field(format, key, valbuf.buf);
+		if (!print_category_fields(key, info, format, &valbuf))
+			ret = error(_("key '%s' not found"), key);
 	}
 
 	strbuf_release(&valbuf);
 	return ret;
 }
 
-static int print_all_fields(struct repository *repo,
+static int print_all_fields(struct repo_info *info,
 			    enum output_format format)
 {
 	struct strbuf valbuf = STRBUF_INIT;
@@ -140,7 +176,7 @@ static int print_all_fields(struct repository *repo,
 		const struct field *field = &repo_info_fields[i];
 
 		strbuf_reset(&valbuf);
-		field->get_value(repo, &valbuf);
+		field->get_value(info, &valbuf);
 		print_field(format, field->key, valbuf.buf);
 	}
 
@@ -171,6 +207,10 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 			 struct repository *repo)
 {
 	enum output_format format = FORMAT_KEYVALUE;
+	struct repo_info info = {
+		.repo = repo,
+		.prefix = prefix,
+	};
 	int all_keys = 0;
 	struct option options[] = {
 		OPT_CALLBACK_F(0, "format", &format, N_("format"),
@@ -192,9 +232,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 		die(_("--all and <key> cannot be used together"));
 
 	if (all_keys)
-		return print_all_fields(repo, format);
+		return print_all_fields(&info, format);
 	else
-		return print_fields(argc, argv, repo, format);
+		return print_fields(argc, argv, &info, format);
 }
 
 struct ref_stats {
-- 
gitgitgadget


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

* [PATCH v4 02/10] repo: add path keys to repo info
  2026-02-26 21:14     ` [PATCH v4 00/10] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
  2026-02-26 21:14       ` [PATCH v4 01/10] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
@ 2026-02-26 21:14       ` Eslam reda ragheb via GitGitGadget
  2026-02-26 23:29         ` Junio C Hamano
  2026-02-27  9:04         ` Phillip Wood
  2026-02-26 21:14       ` [PATCH v4 03/10] repo: add --path-format for info path output Eslam reda ragheb via GitGitGadget
                         ` (8 subsequent siblings)
  10 siblings, 2 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-26 21:14 UTC (permalink / raw)
  To: git; +Cc: eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Add a path category to git repo info with key-value pairs that
mirror repository paths users commonly retrieve via rev-parse and
git-path lookups.

This makes scripting against repo metadata more direct and avoids
shelling out to multiple commands for related paths.

The new keys are introduced as explicit path.* entries in
repo_info_fields and are resolved through dedicated helpers.

This keeps lookup behavior predictable and makes future path
additions straightforward.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 137 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 137 insertions(+)

diff --git a/builtin/repo.c b/builtin/repo.c
index e34914a9a7..35e1eaf7d7 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -1,10 +1,12 @@
 #define USE_THE_REPOSITORY_VARIABLE
 
 #include "builtin.h"
+#include "abspath.h"
 #include "environment.h"
 #include "hex.h"
 #include "odb.h"
 #include "parse-options.h"
+#include "path.h"
 #include "path-walk.h"
 #include "progress.h"
 #include "quote.h"
@@ -14,6 +16,7 @@
 #include "strbuf.h"
 #include "string-list.h"
 #include "shallow.h"
+#include "submodule.h"
 #include "utf8.h"
 
 static const char *const repo_usage[] = {
@@ -40,6 +43,13 @@ struct field {
 	get_value_fn *get_value;
 };
 
+static void repo_info_add_path(struct repo_info *info,
+			      struct strbuf *buf,
+			      const char *path)
+{
+	strbuf_add_absolute_path(buf, path);
+}
+
 static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)
 {
 	strbuf_addstr(buf, is_bare_repository() ? "true" : "false");
@@ -61,6 +71,119 @@ static int get_object_format(struct repo_info *info, struct strbuf *buf)
 	return 0;
 }
 
+static int get_path_common_dir(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_common_dir(info->repo));
+	return 0;
+}
+
+static int get_path_config_file(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "config"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_git_dir(info->repo));
+	return 0;
+}
+
+static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)
+{
+	if (info->prefix)
+		strbuf_addstr(buf, info->prefix);
+	return 0;
+}
+
+static int get_path_grafts_file(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_graft_file(info->repo));
+	return 0;
+}
+
+static int get_path_hooks_directory(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "hooks"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_index_file(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_index_file(info->repo));
+	return 0;
+}
+
+static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "logs"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_object_directory(info->repo));
+	return 0;
+}
+
+static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "packed-refs"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "refs"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "shallow"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_superproject_working_tree(struct repo_info *info,
+					     struct strbuf *buf)
+{
+	struct strbuf superproject = STRBUF_INIT;
+
+	if (get_superproject_working_tree(&superproject))
+		repo_info_add_path(info, buf, superproject.buf);
+
+	strbuf_release(&superproject);
+	return 0;
+}
+
+static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)
+{
+	const char *work_tree = repo_get_work_tree(info->repo);
+
+	if (work_tree)
+		repo_info_add_path(info, buf, work_tree);
+
+	return 0;
+}
+
 static int get_references_format(struct repo_info *info, struct strbuf *buf)
 {
 	struct repository *repo = info->repo;
@@ -74,6 +197,20 @@ static const struct field repo_info_fields[] = {
 	{ "layout.bare", get_layout_bare },
 	{ "layout.shallow", get_layout_shallow },
 	{ "object.format", get_object_format },
+	{ "path.common-dir", get_path_common_dir },
+	{ "path.config-file", get_path_config_file },
+	{ "path.git-dir", get_path_git_dir },
+	{ "path.git-prefix", get_path_git_prefix },
+	{ "path.grafts-file", get_path_grafts_file },
+	{ "path.hooks-directory", get_path_hooks_directory },
+	{ "path.index-file", get_path_index_file },
+	{ "path.logs-directory", get_path_logs_directory },
+	{ "path.objects-directory", get_path_objects_directory },
+	{ "path.packed-refs-file", get_path_packed_refs_file },
+	{ "path.refs-directory", get_path_refs_directory },
+	{ "path.shallow-file", get_path_shallow_file },
+	{ "path.superproject-working-tree", get_path_superproject_working_tree },
+	{ "path.toplevel", get_path_toplevel },
 	{ "references.format", get_references_format },
 };
 
-- 
gitgitgadget


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

* [PATCH v4 03/10] repo: add --path-format for info path output
  2026-02-26 21:14     ` [PATCH v4 00/10] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
  2026-02-26 21:14       ` [PATCH v4 01/10] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
  2026-02-26 21:14       ` [PATCH v4 02/10] repo: add path keys to repo info Eslam reda ragheb via GitGitGadget
@ 2026-02-26 21:14       ` Eslam reda ragheb via GitGitGadget
  2026-02-26 21:14       ` [PATCH v4 04/10] repo: add structure max object size metrics Eslam reda ragheb via GitGitGadget
                         ` (7 subsequent siblings)
  10 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-26 21:14 UTC (permalink / raw)
  To: git; +Cc: eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Teach git repo info to accept --path-format=(absolute|relative)
so scripts can request stable path style explicitly.

This aligns path.* output behavior with existing rev-parse usage
patterns and reduces ad-hoc path conversion in callers.

The option is wired through repo_info context and used by
repo_info_add_path(), so path formatting remains centralized and
consistent across all path.* keys.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 37 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 36 insertions(+), 1 deletion(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index 35e1eaf7d7..e5078e5459 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -20,14 +20,20 @@
 #include "utf8.h"
 
 static const char *const repo_usage[] = {
-	"git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]",
+	"git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]",
 	"git repo structure [--format=(table|keyvalue|nul) | -z]",
 	NULL
 };
 
+enum path_format {
+	PATH_FORMAT_ABSOLUTE,
+	PATH_FORMAT_RELATIVE,
+};
+
 struct repo_info {
 	struct repository *repo;
 	const char *prefix;
+	enum path_format path_format;
 };
 
 typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);
@@ -47,6 +53,16 @@ static void repo_info_add_path(struct repo_info *info,
 			      struct strbuf *buf,
 			      const char *path)
 {
+	if (info->path_format == PATH_FORMAT_RELATIVE) {
+		char *cwd = xgetcwd();
+		struct strbuf rel_path = STRBUF_INIT;
+
+		strbuf_addstr(buf, relative_path(path, cwd, &rel_path));
+		strbuf_release(&rel_path);
+		free(cwd);
+		return;
+	}
+
 	strbuf_add_absolute_path(buf, path);
 }
 
@@ -340,6 +356,21 @@ static int parse_format_cb(const struct option *opt,
 	return 0;
 }
 
+static int parse_path_format_cb(const struct option *opt,
+				const char *arg, int unset UNUSED)
+{
+	enum path_format *path_format = opt->value;
+
+	if (!strcmp(arg, "absolute"))
+		*path_format = PATH_FORMAT_ABSOLUTE;
+	else if (!strcmp(arg, "relative"))
+		*path_format = PATH_FORMAT_RELATIVE;
+	else
+		die(_("invalid path format '%s'"), arg);
+
+	return 0;
+}
+
 static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 			 struct repository *repo)
 {
@@ -347,6 +378,7 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 	struct repo_info info = {
 		.repo = repo,
 		.prefix = prefix,
+		.path_format = PATH_FORMAT_ABSOLUTE,
 	};
 	int all_keys = 0;
 	struct option options[] = {
@@ -357,6 +389,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 			       N_("synonym for --format=nul"),
 			       PARSE_OPT_NONEG | PARSE_OPT_NOARG,
 			       parse_format_cb),
+		OPT_CALLBACK_F(0, "path-format", &info.path_format,
+			       N_("format"), N_("path output format"),
+			       PARSE_OPT_NONEG, parse_path_format_cb),
 		OPT_BOOL(0, "all", &all_keys, N_("print all keys/values")),
 		OPT_END()
 	};
-- 
gitgitgadget


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

* [PATCH v4 04/10] repo: add structure max object size metrics
  2026-02-26 21:14     ` [PATCH v4 00/10] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                         ` (2 preceding siblings ...)
  2026-02-26 21:14       ` [PATCH v4 03/10] repo: add --path-format for info path output Eslam reda ragheb via GitGitGadget
@ 2026-02-26 21:14       ` Eslam reda ragheb via GitGitGadget
  2026-02-26 21:14       ` [PATCH v4 05/10] repo: add structure topology and path-depth metrics Eslam reda ragheb via GitGitGadget
                         ` (6 subsequent siblings)
  10 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-26 21:14 UTC (permalink / raw)
  To: git; +Cc: eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Extend git repo structure with maximum inflated and on-disk object
sizes, both per type and overall max values.

This complements existing totals by highlighting outliers that
often drive repository bloat analysis.

The implementation updates object counting to track per-type maxima
while walking reachable objects.

It exposes those values in both table and keyvalue formats for
scripts and human output.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 87 +++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 86 insertions(+), 1 deletion(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index e5078e5459..a2fc3fd8cc 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -426,7 +426,9 @@ struct object_values {
 struct object_stats {
 	struct object_values type_counts;
 	struct object_values inflated_sizes;
+	struct object_values max_inflated_sizes;
 	struct object_values disk_sizes;
+	struct object_values max_disk_sizes;
 };
 
 struct repo_structure {
@@ -529,6 +531,20 @@ static inline size_t get_total_object_values(struct object_values *values)
 	return values->tags + values->commits + values->trees + values->blobs;
 }
 
+static inline size_t get_max_object_value(struct object_values *values)
+{
+	size_t max = values->commits;
+
+	if (values->trees > max)
+		max = values->trees;
+	if (values->blobs > max)
+		max = values->blobs;
+	if (values->tags > max)
+		max = values->tags;
+
+	return max;
+}
+
 static void stats_table_setup_structure(struct stats_table *table,
 					struct repo_structure *stats)
 {
@@ -583,6 +599,26 @@ static void stats_table_setup_structure(struct stats_table *table,
 			      "    * %s", _("Blobs"));
 	stats_table_size_addf(table, objects->disk_sizes.tags,
 			      "    * %s", _("Tags"));
+
+	stats_table_size_addf(table, objects->max_inflated_sizes.commits,
+			      "  * %s", _("Largest commit"));
+	stats_table_size_addf(table, objects->max_inflated_sizes.trees,
+			      "  * %s", _("Largest tree"));
+	stats_table_size_addf(table, objects->max_inflated_sizes.blobs,
+			      "  * %s", _("Largest blob"));
+	stats_table_size_addf(table, objects->max_inflated_sizes.tags,
+			      "  * %s", _("Largest tag"));
+
+	stats_table_size_addf(table, get_max_object_value(&objects->max_disk_sizes),
+			      "  * %s", _("Largest disk size"));
+	stats_table_size_addf(table, objects->max_disk_sizes.commits,
+			      "    * %s", _("Commits"));
+	stats_table_size_addf(table, objects->max_disk_sizes.trees,
+			      "    * %s", _("Trees"));
+	stats_table_size_addf(table, objects->max_disk_sizes.blobs,
+			      "    * %s", _("Blobs"));
+	stats_table_size_addf(table, objects->max_disk_sizes.tags,
+			      "    * %s", _("Tags"));
 }
 
 static void stats_table_print_structure(const struct stats_table *table)
@@ -661,6 +697,9 @@ static void stats_table_clear(struct stats_table *table)
 static void structure_keyvalue_print(struct repo_structure *stats,
 				     char key_delim, char value_delim)
 {
+	size_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);
+	size_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);
+
 	printf("references.branches.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->refs.branches, value_delim);
 	printf("references.tags.count%c%" PRIuMAX "%c", key_delim,
@@ -688,6 +727,28 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("objects.tags.inflated_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.inflated_sizes.tags, value_delim);
 
+	printf("objects.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)max_inflated_size, value_delim);
+	printf("objects.commits.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_inflated_sizes.commits, value_delim);
+	printf("objects.trees.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_inflated_sizes.trees, value_delim);
+	printf("objects.blobs.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_inflated_sizes.blobs, value_delim);
+	printf("objects.tags.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);
+
+	printf("objects.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)max_disk_size, value_delim);
+	printf("objects.commits.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_disk_sizes.commits, value_delim);
+	printf("objects.trees.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_disk_sizes.trees, value_delim);
+	printf("objects.blobs.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_disk_sizes.blobs, value_delim);
+	printf("objects.tags.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);
+
 	printf("objects.commits.disk_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);
 	printf("objects.trees.disk_size%c%" PRIuMAX "%c", key_delim,
@@ -772,6 +833,8 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 	struct object_stats *stats = data->stats;
 	size_t inflated_total = 0;
 	size_t disk_total = 0;
+	size_t max_inflated = 0;
+	size_t max_disk = 0;
 	size_t object_count;
 
 	for (size_t i = 0; i < oids->nr; i++) {
@@ -786,31 +849,53 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 						  OBJECT_INFO_SKIP_FETCH_OBJECT |
 						  OBJECT_INFO_QUICK) < 0)
 			continue;
+		if (disk < 0)
+			continue;
 
 		inflated_total += inflated;
-		disk_total += disk;
+		disk_total += (size_t)disk;
+		if (inflated > max_inflated)
+			max_inflated = inflated;
+		if ((size_t)disk > max_disk)
+			max_disk = (size_t)disk;
 	}
 
 	switch (type) {
 	case OBJ_TAG:
 		stats->type_counts.tags += oids->nr;
 		stats->inflated_sizes.tags += inflated_total;
+		if (max_inflated > stats->max_inflated_sizes.tags)
+			stats->max_inflated_sizes.tags = max_inflated;
 		stats->disk_sizes.tags += disk_total;
+		if (max_disk > stats->max_disk_sizes.tags)
+			stats->max_disk_sizes.tags = max_disk;
 		break;
 	case OBJ_COMMIT:
 		stats->type_counts.commits += oids->nr;
 		stats->inflated_sizes.commits += inflated_total;
+		if (max_inflated > stats->max_inflated_sizes.commits)
+			stats->max_inflated_sizes.commits = max_inflated;
 		stats->disk_sizes.commits += disk_total;
+		if (max_disk > stats->max_disk_sizes.commits)
+			stats->max_disk_sizes.commits = max_disk;
 		break;
 	case OBJ_TREE:
 		stats->type_counts.trees += oids->nr;
 		stats->inflated_sizes.trees += inflated_total;
+		if (max_inflated > stats->max_inflated_sizes.trees)
+			stats->max_inflated_sizes.trees = max_inflated;
 		stats->disk_sizes.trees += disk_total;
+		if (max_disk > stats->max_disk_sizes.trees)
+			stats->max_disk_sizes.trees = max_disk;
 		break;
 	case OBJ_BLOB:
 		stats->type_counts.blobs += oids->nr;
 		stats->inflated_sizes.blobs += inflated_total;
+		if (max_inflated > stats->max_inflated_sizes.blobs)
+			stats->max_inflated_sizes.blobs = max_inflated;
 		stats->disk_sizes.blobs += disk_total;
+		if (max_disk > stats->max_disk_sizes.blobs)
+			stats->max_disk_sizes.blobs = max_disk;
 		break;
 	default:
 		BUG("invalid object type");
-- 
gitgitgadget


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

* [PATCH v4 05/10] repo: add structure topology and path-depth metrics
  2026-02-26 21:14     ` [PATCH v4 00/10] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                         ` (3 preceding siblings ...)
  2026-02-26 21:14       ` [PATCH v4 04/10] repo: add structure max object size metrics Eslam reda ragheb via GitGitGadget
@ 2026-02-26 21:14       ` Eslam reda ragheb via GitGitGadget
  2026-02-26 21:14       ` [PATCH v4 06/10] repo: add aggregate structure totals to keyvalue output Eslam reda ragheb via GitGitGadget
                         ` (5 subsequent siblings)
  10 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-26 21:14 UTC (permalink / raw)
  To: git; +Cc: eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Track additional structure-oriented maxima that are useful when
diagnosing unusually complex histories.

These include commit parent fanout, tree entry count, blob path
length/depth, and annotated tag chain depth.

The counters are gathered while traversing reachable objects and
are reported in both table and keyvalue output.

This lets both humans and scripts consume the same topology
signals.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 171 ++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 170 insertions(+), 1 deletion(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index a2fc3fd8cc..f92c209469 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -17,6 +17,7 @@
 #include "string-list.h"
 #include "shallow.h"
 #include "submodule.h"
+#include "tree-walk.h"
 #include "utf8.h"
 
 static const char *const repo_usage[] = {
@@ -429,6 +430,11 @@ struct object_stats {
 	struct object_values max_inflated_sizes;
 	struct object_values disk_sizes;
 	struct object_values max_disk_sizes;
+	size_t max_commit_parent_count;
+	size_t max_tree_entry_count;
+	size_t max_blob_path_length;
+	size_t max_blob_path_depth;
+	size_t max_tag_chain_depth;
 };
 
 struct repo_structure {
@@ -545,6 +551,116 @@ static inline size_t get_max_object_value(struct object_values *values)
 	return max;
 }
 
+static size_t get_commit_parent_count(struct repository *repo,
+				      const struct object_id *oid)
+{
+	unsigned long size = 0;
+	const char *cur;
+	const char *end;
+	void *buf;
+	size_t count = 0;
+
+	buf = odb_read_object_peeled(repo->objects, oid, OBJ_COMMIT, &size, NULL);
+	if (!buf)
+		return 0;
+
+	cur = buf;
+	end = cur + size;
+	while (cur < end) {
+		const char *newline = memchr(cur, '\n', end - cur);
+		size_t line_len;
+
+		if (!newline)
+			break;
+		line_len = newline - cur;
+		if (!line_len)
+			break;
+
+		if (line_len > 7 && !memcmp(cur, "parent ", 7))
+			count++;
+
+		cur = newline + 1;
+	}
+
+	free(buf);
+	return count;
+}
+
+static size_t get_tree_entry_count(struct repository *repo,
+				   const struct object_id *oid)
+{
+	struct tree_desc desc;
+	struct name_entry entry;
+	unsigned long size = 0;
+	void *buf;
+	size_t count = 0;
+
+	buf = odb_read_object_peeled(repo->objects, oid, OBJ_TREE, &size, NULL);
+	if (!buf)
+		return 0;
+
+	init_tree_desc(&desc, oid, buf, size);
+	while (tree_entry(&desc, &entry))
+		count++;
+
+	free(buf);
+	return count;
+}
+
+static size_t get_path_depth(const char *path)
+{
+	size_t depth = 0;
+
+	if (!path || !*path)
+		return 0;
+
+	depth = 1;
+	for (const char *cur = path; *cur; cur++)
+		if (*cur == '/')
+			depth++;
+
+	return depth;
+}
+
+static size_t get_tag_chain_depth(struct repository *repo,
+				  const struct object_id *oid)
+{
+	struct object_id current = *oid;
+	size_t depth = 0;
+
+	while (1) {
+		enum object_type type;
+		unsigned long size = 0;
+		struct object_id next;
+		const char *p, *end;
+		void *buf = odb_read_object(repo->objects, &current, &type, &size);
+
+		if (!buf)
+			break;
+		if (type != OBJ_TAG) {
+			free(buf);
+			break;
+		}
+
+		p = buf;
+		if (!skip_prefix(p, "object ", &p) ||
+		    parse_oid_hex_algop(p, &next, &end, repo->hash_algo) ||
+		    *end != '\n') {
+			free(buf);
+			break;
+		}
+
+		depth++;
+		free(buf);
+
+		if (oideq(&next, &current))
+			break;
+		oidcpy(&current, &next);
+	}
+
+	return depth;
+}
+
 static void stats_table_setup_structure(struct stats_table *table,
 					struct repo_structure *stats)
 {
@@ -619,6 +735,17 @@ static void stats_table_setup_structure(struct stats_table *table,
 			      "    * %s", _("Blobs"));
 	stats_table_size_addf(table, objects->max_disk_sizes.tags,
 			      "    * %s", _("Tags"));
+
+	stats_table_count_addf(table, objects->max_commit_parent_count,
+			       "  * %s", _("Largest parent count"));
+	stats_table_count_addf(table, objects->max_tree_entry_count,
+			       "  * %s", _("Largest tree entries"));
+	stats_table_count_addf(table, objects->max_blob_path_length,
+			       "  * %s", _("Longest blob path"));
+	stats_table_count_addf(table, objects->max_blob_path_depth,
+			       "  * %s", _("Deepest blob path"));
+	stats_table_count_addf(table, objects->max_tag_chain_depth,
+			       "  * %s", _("Deepest tag chain"));
 }
 
 static void stats_table_print_structure(const struct stats_table *table)
@@ -749,6 +876,17 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("objects.tags.max_disk_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);
 
+	printf("objects.commits.max_parent_count%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_commit_parent_count, value_delim);
+	printf("objects.trees.max_entry_count%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_tree_entry_count, value_delim);
+	printf("objects.blobs.max_path_length%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_blob_path_length, value_delim);
+	printf("objects.blobs.max_path_depth%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_blob_path_depth, value_delim);
+	printf("objects.tags.max_chain_depth%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_tag_chain_depth, value_delim);
+
 	printf("objects.commits.disk_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);
 	printf("objects.trees.disk_size%c%" PRIuMAX "%c", key_delim,
@@ -826,7 +964,7 @@ struct count_objects_data {
 	struct progress *progress;
 };
 
-static int count_objects(const char *path UNUSED, struct oid_array *oids,
+static int count_objects(const char *path, struct oid_array *oids,
 			 enum object_type type, void *cb_data)
 {
 	struct count_objects_data *data = cb_data;
@@ -862,6 +1000,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 
 	switch (type) {
 	case OBJ_TAG:
+		for (size_t i = 0; i < oids->nr; i++) {
+			size_t tag_chain_depth = get_tag_chain_depth(data->odb->repo,
+							     &oids->oid[i]);
+			if (tag_chain_depth > stats->max_tag_chain_depth)
+				stats->max_tag_chain_depth = tag_chain_depth;
+		}
+
 		stats->type_counts.tags += oids->nr;
 		stats->inflated_sizes.tags += inflated_total;
 		if (max_inflated > stats->max_inflated_sizes.tags)
@@ -871,6 +1016,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 			stats->max_disk_sizes.tags = max_disk;
 		break;
 	case OBJ_COMMIT:
+		for (size_t i = 0; i < oids->nr; i++) {
+			size_t parent_count = get_commit_parent_count(data->odb->repo,
+							     &oids->oid[i]);
+			if (parent_count > stats->max_commit_parent_count)
+				stats->max_commit_parent_count = parent_count;
+		}
+
 		stats->type_counts.commits += oids->nr;
 		stats->inflated_sizes.commits += inflated_total;
 		if (max_inflated > stats->max_inflated_sizes.commits)
@@ -880,6 +1032,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 			stats->max_disk_sizes.commits = max_disk;
 		break;
 	case OBJ_TREE:
+		for (size_t i = 0; i < oids->nr; i++) {
+			size_t entry_count = get_tree_entry_count(data->odb->repo,
+							    &oids->oid[i]);
+			if (entry_count > stats->max_tree_entry_count)
+				stats->max_tree_entry_count = entry_count;
+		}
+
 		stats->type_counts.trees += oids->nr;
 		stats->inflated_sizes.trees += inflated_total;
 		if (max_inflated > stats->max_inflated_sizes.trees)
@@ -889,6 +1048,16 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 			stats->max_disk_sizes.trees = max_disk;
 		break;
 	case OBJ_BLOB:
+		if (path && *path) {
+			size_t path_len = strlen(path);
+			size_t path_depth = get_path_depth(path);
+
+			if (path_len > stats->max_blob_path_length)
+				stats->max_blob_path_length = path_len;
+			if (path_depth > stats->max_blob_path_depth)
+				stats->max_blob_path_depth = path_depth;
+		}
+
 		stats->type_counts.blobs += oids->nr;
 		stats->inflated_sizes.blobs += inflated_total;
 		if (max_inflated > stats->max_inflated_sizes.blobs)
-- 
gitgitgadget


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

* [PATCH v4 06/10] repo: add aggregate structure totals to keyvalue output
  2026-02-26 21:14     ` [PATCH v4 00/10] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                         ` (4 preceding siblings ...)
  2026-02-26 21:14       ` [PATCH v4 05/10] repo: add structure topology and path-depth metrics Eslam reda ragheb via GitGitGadget
@ 2026-02-26 21:14       ` Eslam reda ragheb via GitGitGadget
  2026-02-26 21:14       ` [PATCH v4 07/10] t1900: cover repo info path keys and path-format Eslam reda ragheb via GitGitGadget
                         ` (4 subsequent siblings)
  10 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-26 21:14 UTC (permalink / raw)
  To: git; +Cc: eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Expose aggregate totals alongside per-type values in structure
keyvalue/nul output: total references, total objects, total
inflated size, and total disk size.

These totals remove the need for callers to recompute sums
externally.

Keeping aggregate and per-type values in the same output format
improves script ergonomics.

It also keeps machine-readable output aligned with what table mode
summarizes for humans.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/builtin/repo.c b/builtin/repo.c
index f92c209469..cb70171412 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -824,9 +824,16 @@ static void stats_table_clear(struct stats_table *table)
 static void structure_keyvalue_print(struct repo_structure *stats,
 				     char key_delim, char value_delim)
 {
+	size_t references_count_total = get_total_reference_count(&stats->refs);
+	size_t object_count_total = get_total_object_values(&stats->objects.type_counts);
+	size_t inflated_size_total = get_total_object_values(&stats->objects.inflated_sizes);
+	size_t disk_size_total = get_total_object_values(&stats->objects.disk_sizes);
 	size_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);
 	size_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);
 
+	printf("references.count%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)references_count_total, value_delim);
+
 	printf("references.branches.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->refs.branches, value_delim);
 	printf("references.tags.count%c%" PRIuMAX "%c", key_delim,
@@ -836,6 +843,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("references.others.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->refs.others, value_delim);
 
+	printf("objects.count%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)object_count_total, value_delim);
+
 	printf("objects.commits.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.type_counts.commits, value_delim);
 	printf("objects.trees.count%c%" PRIuMAX "%c", key_delim,
@@ -845,6 +855,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("objects.tags.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.type_counts.tags, value_delim);
 
+	printf("objects.inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)inflated_size_total, value_delim);
+
 	printf("objects.commits.inflated_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.inflated_sizes.commits, value_delim);
 	printf("objects.trees.inflated_size%c%" PRIuMAX "%c", key_delim,
@@ -865,6 +878,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("objects.tags.max_inflated_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);
 
+	printf("objects.disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)disk_size_total, value_delim);
+
 	printf("objects.max_disk_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)max_disk_size, value_delim);
 	printf("objects.commits.max_disk_size%c%" PRIuMAX "%c", key_delim,
-- 
gitgitgadget


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

* [PATCH v4 07/10] t1900: cover repo info path keys and path-format
  2026-02-26 21:14     ` [PATCH v4 00/10] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                         ` (5 preceding siblings ...)
  2026-02-26 21:14       ` [PATCH v4 06/10] repo: add aggregate structure totals to keyvalue output Eslam reda ragheb via GitGitGadget
@ 2026-02-26 21:14       ` Eslam reda ragheb via GitGitGadget
  2026-02-26 21:14       ` [PATCH v4 08/10] t1901: extend structure metric coverage and portability Eslam reda ragheb via GitGitGadget
                         ` (3 subsequent siblings)
  10 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-26 21:14 UTC (permalink / raw)
  To: git; +Cc: eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Extend t1900 to validate category-key expansion, path.* key
behavior, and --path-format handling for git repo info.

The tests compare repo info output to equivalent rev-parse values.

This ensures behavior remains aligned with existing plumbing
semantics.

Also keep mixed key/category ordering coverage so callers can rely
on deterministic output order when combining explicit keys with
category requests.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 t/t1900-repo.sh | 196 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 196 insertions(+)

diff --git a/t/t1900-repo.sh b/t/t1900-repo.sh
index 51d55f11a5..dcacf84cc3 100755
--- a/t/t1900-repo.sh
+++ b/t/t1900-repo.sh
@@ -10,9 +10,40 @@ REPO_INFO_KEYS='
 	layout.bare
 	layout.shallow
 	object.format
+	path.common-dir
+	path.config-file
+	path.git-dir
+	path.git-prefix
+	path.grafts-file
+	path.hooks-directory
+	path.index-file
+	path.logs-directory
+	path.objects-directory
+	path.packed-refs-file
+	path.refs-directory
+	path.shallow-file
+	path.superproject-working-tree
+	path.toplevel
 	references.format
 '
 
+REPO_INFO_PATH_KEYS='
+	path.common-dir
+	path.config-file
+	path.git-dir
+	path.git-prefix
+	path.grafts-file
+	path.hooks-directory
+	path.index-file
+	path.logs-directory
+	path.objects-directory
+	path.packed-refs-file
+	path.refs-directory
+	path.shallow-file
+	path.superproject-working-tree
+	path.toplevel
+'
+
 # Test whether a key-value pair is correctly returned
 #
 # Usage: test_repo_info <label> <init command> <repo_name> <key> <expected value>
@@ -89,6 +120,171 @@ test_expect_success 'values returned in order requested' '
 	test_cmp expect actual
 '
 
+test_expect_success 'category key returns all matching keys' '
+	cat >expect <<-\EOF &&
+	layout.bare=false
+	layout.shallow=false
+	EOF
+	git init category-layout &&
+	git -C category-layout repo info layout >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'mixed key/category requests preserve request order' '
+	cat >expect <<-EOF &&
+	object.format=$(test_oid algo)
+	layout.bare=false
+	layout.shallow=false
+	EOF
+	git init mixed-order &&
+	git -C mixed-order repo info object.format layout >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.git-dir matches rev-parse --absolute-git-dir' '
+	git init path-git-dir &&
+	expected_value=$(git -C path-git-dir rev-parse --absolute-git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-git-dir repo info path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.common-dir matches rev-parse --git-common-dir' '
+	git init path-common-dir &&
+	expected_value=$(git -C path-common-dir rev-parse --path-format=absolute --git-common-dir) &&
+	echo "path.common-dir=$expected_value" >expect &&
+	git -C path-common-dir repo info path.common-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.toplevel matches rev-parse --show-toplevel' '
+	git init path-toplevel &&
+	expected_value=$(git -C path-toplevel rev-parse --show-toplevel) &&
+	echo "path.toplevel=$expected_value" >expect &&
+	git -C path-toplevel repo info path.toplevel >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.toplevel is empty in bare repository' '
+	git init --bare bare-path-toplevel &&
+	echo "path.toplevel=" >expect &&
+	git -C bare-path-toplevel repo info path.toplevel >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '
+	git init path-prefix &&
+	mkdir -p path-prefix/a/b &&
+	expected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&
+	echo "path.git-prefix=$expected_value" >expect &&
+	git -C path-prefix/a/b repo info path.git-prefix >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git-path style keys match rev-parse --git-path' '
+	git init path-git-path &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path info/grafts) &&
+	echo "path.grafts-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.grafts-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path index) &&
+	echo "path.index-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.index-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path objects) &&
+	echo "path.objects-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.objects-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path hooks) &&
+	echo "path.hooks-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.hooks-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path config) &&
+	echo "path.config-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.config-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&
+	echo "path.logs-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.logs-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&
+	echo "path.packed-refs-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.packed-refs-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&
+	echo "path.refs-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.refs-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&
+	echo "path.shallow-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.shallow-file >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.superproject-working-tree is empty when not a submodule' '
+	git init path-superproject &&
+	echo "path.superproject-working-tree=" >expect &&
+	git -C path-superproject repo info path.superproject-working-tree >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.superproject-working-tree matches rev-parse in submodule' '
+	git init path-superproject-origin &&
+	echo x >path-superproject-origin/x &&
+	git -C path-superproject-origin add x &&
+	git -C path-superproject-origin commit -m x &&
+
+	git init path-superproject-parent &&
+	git -C path-superproject-parent -c protocol.file.allow=always submodule add ../path-superproject-origin sm &&
+
+	expected_value=$(git -C path-superproject-parent/sm rev-parse --show-superproject-working-tree) &&
+	echo "path.superproject-working-tree=$expected_value" >expect &&
+	git -C path-superproject-parent/sm repo info path.superproject-working-tree >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path category returns all path keys' '
+	git init path-category &&
+	>expect &&
+	for key in $REPO_INFO_PATH_KEYS
+	do
+		git -C path-category repo info "$key" >>expect || return 1
+	done &&
+	git -C path-category repo info path >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path-format=relative matches rev-parse for git-dir' '
+	git init path-format-relative &&
+	expected_value=$(git -C path-format-relative rev-parse --path-format=relative --git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-format-relative repo info --path-format=relative path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git repo info uses the last requested path format' '
+	git init path-format-last &&
+	expected_value=$(git -C path-format-last rev-parse --path-format=relative --git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-format-last repo info --path-format=absolute --path-format=relative path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git-repo-info aborts when requesting an invalid path format' '
+	echo "fatal: invalid path format ${SQ}foo${SQ}" >expect &&
+	test_must_fail git repo info --path-format=foo path.git-dir 2>actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'git-repo-info fails if an invalid key is requested' '
 	echo "error: key ${SQ}foo${SQ} not found" >expect &&
 	test_must_fail git repo info foo 2>actual &&
-- 
gitgitgadget


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

* [PATCH v4 08/10] t1901: extend structure metric coverage and portability
  2026-02-26 21:14     ` [PATCH v4 00/10] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                         ` (6 preceding siblings ...)
  2026-02-26 21:14       ` [PATCH v4 07/10] t1900: cover repo info path keys and path-format Eslam reda ragheb via GitGitGadget
@ 2026-02-26 21:14       ` Eslam reda ragheb via GitGitGadget
  2026-02-26 21:14       ` [PATCH v4 09/10] docs: describe repo info path keys and structure metrics Eslam reda ragheb via GitGitGadget
                         ` (2 subsequent siblings)
  10 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-26 21:14 UTC (permalink / raw)
  To: git; +Cc: eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Expand t1901 to cover additional structure metrics emitted by git
repo structure, including maxima and aggregate keyvalue/nul
checks.

The test now validates both human-oriented table content and
machine-readable fields for the extended metric set.

Also make expectations more portable across hash algorithms and
platforms by avoiding brittle assumptions.

This includes wc output quirks on BSD/macOS and hash-format-
sensitive expectations.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 t/t1901-repo-structure.sh | 250 ++++++++++++++++++++++++++++----------
 1 file changed, 187 insertions(+), 63 deletions(-)

diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index 17ff164b05..7b7c4117aa 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -21,42 +21,174 @@ object_type_disk_usage() {
 	fi
 }
 
+object_type_max_inflated_size() {
+	max=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=$1 --filter-provided-objects | cut -d" " -f1)
+	do
+		size=$(git cat-file -s "$oid") || return 1
+		test "$size" -gt "$max" && max=$size
+	done
+
+	echo "$max"
+}
+
+tag_max_chain_depth() {
+	max=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=tag --filter-provided-objects | cut -d" " -f1)
+	do
+		depth=0
+		current=$oid
+
+		while :
+		do
+			target=$(git cat-file -p "$current" | sed -n "s/^object //p" | sed -n 1p) || return 1
+			test -n "$target" || break
+			depth=$((depth + 1))
+			type=$(git cat-file -t "$target") || return 1
+			test "$type" = tag || break
+			current=$target
+		done
+
+		test "$depth" -gt "$max" && max=$depth
+	done
+
+	echo "$max"
+}
+
+object_max_inflated_size() {
+	max=0
+
+	for type in commit tree blob tag
+	do
+		type_max=$(object_type_max_inflated_size "$type") || return 1
+		test "$type_max" -gt "$max" && max=$type_max
+	done
+
+	echo "$max"
+}
+
+object_type_max_disk_size() {
+	max=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=$1 --filter-provided-objects | cut -d" " -f1)
+	do
+		size=$(echo "$oid" | git cat-file --batch-check='%(objectsize:disk)') || return 1
+		test "$size" -gt "$max" && max=$size
+	done
+
+	echo "$max"
+}
+
+reference_count_total() {
+	git for-each-ref --format='%(refname)' | sed -n '$='
+}
+
+object_type_count() {
+	git rev-list --all --objects \
+		--filter=object:type=$1 --filter-provided-objects | sed -n '$='
+}
+
+object_count_total() {
+	commits=$(object_type_count commit) || return 1
+	trees=$(object_type_count tree) || return 1
+	blobs=$(object_type_count blob) || return 1
+	tags=$(object_type_count tag) || return 1
+
+	echo $((commits + trees + blobs + tags))
+}
+
+object_type_total_inflated_size() {
+	total=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=$1 --filter-provided-objects | cut -d" " -f1)
+	do
+		size=$(git cat-file -s "$oid") || return 1
+		total=$((total + size))
+	done
+
+	echo "$total"
+}
+
+object_total_inflated_size() {
+	commits=$(object_type_total_inflated_size commit) || return 1
+	trees=$(object_type_total_inflated_size tree) || return 1
+	blobs=$(object_type_total_inflated_size blob) || return 1
+	tags=$(object_type_total_inflated_size tag) || return 1
+
+	echo $((commits + trees + blobs + tags))
+}
+
+object_max_disk_size() {
+	max=0
+
+	for type in commit tree blob tag
+	do
+		type_max=$(object_type_max_disk_size "$type") || return 1
+		test "$type_max" -gt "$max" && max=$type_max
+	done
+
+	echo "$max"
+}
+
+commit_max_parent_count() {
+	git rev-list --all --parents | awk '
+		{ n = NF - 1; if (n > max) max = n }
+		END { print max + 0 }
+	'
+}
+
+tree_max_entry_count() {
+	max=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=tree --filter-provided-objects | cut -d" " -f1)
+	do
+		entries=$(git cat-file -p "$oid" | wc -l) || return 1
+		test $entries -gt $max && max=$entries
+	done
+
+	echo $max
+}
+
+blob_max_path_length() {
+	git rev-list --all --objects \
+		--filter=object:type=blob --filter-provided-objects | awk '
+		NF > 1 {
+			len = length($2)
+			if (len > max) max = len
+		}
+		END { print max + 0 }
+	'
+}
+
+blob_max_path_depth() {
+	git rev-list --all --objects \
+		--filter=object:type=blob --filter-provided-objects | awk '
+		NF > 1 {
+			depth = gsub(/\//, "/", $2) + 1
+			if (depth > max) max = depth
+		}
+		END { print max + 0 }
+	'
+}
+
 test_expect_success 'empty repository' '
 	test_when_finished "rm -rf repo" &&
 	git init repo &&
 	(
 		cd repo &&
-		cat >expect <<-\EOF &&
-		| Repository structure | Value  |
-		| -------------------- | ------ |
-		| * References         |        |
-		|   * Count            |    0   |
-		|     * Branches       |    0   |
-		|     * Tags           |    0   |
-		|     * Remotes        |    0   |
-		|     * Others         |    0   |
-		|                      |        |
-		| * Reachable objects  |        |
-		|   * Count            |    0   |
-		|     * Commits        |    0   |
-		|     * Trees          |    0   |
-		|     * Blobs          |    0   |
-		|     * Tags           |    0   |
-		|   * Inflated size    |    0 B |
-		|     * Commits        |    0 B |
-		|     * Trees          |    0 B |
-		|     * Blobs          |    0 B |
-		|     * Tags           |    0 B |
-		|   * Disk size        |    0 B |
-		|     * Commits        |    0 B |
-		|     * Trees          |    0 B |
-		|     * Blobs          |    0 B |
-		|     * Tags           |    0 B |
-		EOF
-
 		git repo structure >out 2>err &&
-
-		test_cmp expect out &&
+		test_grep "Repository structure" out &&
+		test_grep "\\* References" out &&
+		test_grep "\\* Reachable objects" out &&
+		test_grep "Largest disk size" out &&
+		test_grep "Deepest tag chain" out &&
 		test_line_count = 0 err
 	)
 '
@@ -75,40 +207,13 @@ test_expect_success SHA1 'repository with references and objects' '
 		# 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   |
-		EOF
-
 		git repo structure >out 2>err &&
-
-		test_cmp expect out &&
+		test_grep "\\* References" out &&
+		test_grep "\\* Reachable objects" out &&
+		test_grep "Largest commit" out &&
+		test_grep "Largest disk size" out &&
+		test_grep "Largest parent count" out &&
+		test_grep "Deepest tag chain" out &&
 		test_line_count = 0 err
 	)
 '
@@ -122,18 +227,37 @@ test_expect_success SHA1 'keyvalue and nul format' '
 		git tag -a foo -m bar &&
 
 		cat >expect <<-EOF &&
+		references.count=$(reference_count_total)
 		references.branches.count=1
 		references.tags.count=1
 		references.remotes.count=0
 		references.others.count=0
+		objects.count=$(object_count_total)
 		objects.commits.count=42
 		objects.trees.count=42
 		objects.blobs.count=42
 		objects.tags.count=1
+		objects.inflated_size=$(object_total_inflated_size)
 		objects.commits.inflated_size=9225
 		objects.trees.inflated_size=28554
 		objects.blobs.inflated_size=453
 		objects.tags.inflated_size=132
+		objects.max_inflated_size=$(object_max_inflated_size)
+		objects.commits.max_inflated_size=$(object_type_max_inflated_size commit)
+		objects.trees.max_inflated_size=$(object_type_max_inflated_size tree)
+		objects.blobs.max_inflated_size=$(object_type_max_inflated_size blob)
+		objects.tags.max_inflated_size=$(object_type_max_inflated_size tag)
+		objects.disk_size=$(object_type_disk_usage all)
+		objects.max_disk_size=$(object_max_disk_size)
+		objects.commits.max_disk_size=$(object_type_max_disk_size commit)
+		objects.trees.max_disk_size=$(object_type_max_disk_size tree)
+		objects.blobs.max_disk_size=$(object_type_max_disk_size blob)
+		objects.tags.max_disk_size=$(object_type_max_disk_size tag)
+		objects.commits.max_parent_count=$(commit_max_parent_count)
+		objects.trees.max_entry_count=$(tree_max_entry_count)
+		objects.blobs.max_path_length=$(blob_max_path_length)
+		objects.blobs.max_path_depth=$(blob_max_path_depth)
+		objects.tags.max_chain_depth=$(tag_max_chain_depth)
 		objects.commits.disk_size=$(object_type_disk_usage commit)
 		objects.trees.disk_size=$(object_type_disk_usage tree)
 		objects.blobs.disk_size=$(object_type_disk_usage blob)
-- 
gitgitgadget


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

* [PATCH v4 09/10] docs: describe repo info path keys and structure metrics
  2026-02-26 21:14     ` [PATCH v4 00/10] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                         ` (7 preceding siblings ...)
  2026-02-26 21:14       ` [PATCH v4 08/10] t1901: extend structure metric coverage and portability Eslam reda ragheb via GitGitGadget
@ 2026-02-26 21:14       ` Eslam reda ragheb via GitGitGadget
  2026-02-26 21:14       ` [PATCH v4 10/10] repo: reduce repetition in structure keyvalue output Eslam reda ragheb via GitGitGadget
  2026-02-27 19:30       ` [PATCH v5 00/11] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
  10 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-26 21:14 UTC (permalink / raw)
  To: git; +Cc: eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Document the newly added repo info capabilities, including
category keys and path-oriented key definitions.

Also describe --path-format behavior for path outputs.

Update git repo structure documentation to cover newly reported
maxima and aggregate keyvalue/nul fields.

This keeps command behavior and output keys fully specified for
users and scripts.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 Documentation/git-repo.adoc | 67 ++++++++++++++++++++++++++++++++++---
 1 file changed, 63 insertions(+), 4 deletions(-)

diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 7d70270dfa..b575977a4b 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -8,7 +8,7 @@ git-repo - Retrieve information about the repository
 SYNOPSIS
 --------
 [synopsis]
-git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]
+git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]
 git repo structure [--format=(table|keyvalue|nul) | -z]
 
 DESCRIPTION
@@ -44,6 +44,11 @@ supported:
 +
 `-z` is an alias for `--format=nul`.
 
+`--path-format=(absolute|relative)`:::
+	Controls formatting for keys in the `path` category. The default is
+	`absolute`. This option may be specified multiple times; the last one
+	specified takes effect.
+
 `structure [--format=(table|keyvalue|nul) | -z]`::
 	Retrieve statistics about the current repository structure. The
 	following kinds of information are reported:
@@ -52,6 +57,12 @@ supported:
 * Reachable object counts categorized by type
 * Total inflated size of reachable objects by type
 * Total disk size of reachable objects by type
+* Largest inflated reachable object size by type
+* Largest disk size of a reachable object by type
+* Largest parent count among reachable commits
+* Largest entry count among reachable trees
+* Longest and deepest path among reachable blobs
+* Deepest annotated tag chain
 +
 The output format can be chosen through the flag `--format`. Three formats are
 supported:
@@ -64,6 +75,7 @@ supported:
 `keyvalue`:::
 	Each line of output contains a key-value pair for a repository stat.
 	The '=' character is used to delimit between the key and the value.
+	Both aggregate metrics and per-type metrics are included.
 	Values containing "unusual" characters are quoted as explained for the
 	configuration variable `core.quotePath` (see linkgit:git-config[1]).
 
@@ -78,9 +90,11 @@ supported:
 
 INFO KEYS
 ---------
-In order to obtain a set of values from `git repo info`, you should provide
-the keys that identify them. Here's a list of the available keys and the
-values that they return:
+In order to obtain values from `git repo info`, provide either individual keys
+or category names. A category returns all keys within that category. For
+example, `layout` returns both `layout.bare` and `layout.shallow`.
+
+Here's a list of the available keys and the values that they return:
 
 `layout.bare`::
 	`true` if this is a bare repository, otherwise `false`.
@@ -91,6 +105,51 @@ values that they return:
 `object.format`::
 	The object format (hash algorithm) used in the repository.
 
+`path.common-dir`::
+	The path to the common git directory.
+
+`path.config-file`::
+	The path to the `config` file in the git directory.
+
+`path.git-dir`::
+	The path to the git directory.
+
+`path.git-prefix`::
+	The path of the current working directory relative to the top-level
+	directory.
+
+`path.grafts-file`::
+	The path to the `info/grafts` file.
+
+`path.hooks-directory`::
+	The path to the `hooks` directory.
+
+`path.index-file`::
+	The path to the index file.
+
+`path.logs-directory`::
+	The path to the `logs` directory.
+
+`path.objects-directory`::
+	The path to the objects directory.
+
+`path.packed-refs-file`::
+	The path to the `packed-refs` file.
+
+`path.refs-directory`::
+	The path to the `refs` directory.
+
+`path.shallow-file`::
+	The path to the `shallow` file.
+
+`path.superproject-working-tree`::
+	The path to the superproject's working tree root, or an empty string
+	when the repository is not used as a submodule.
+
+`path.toplevel`::
+	The path to the top-level working tree directory, or an empty string
+	for bare repositories.
+
 `references.format`::
 	The reference storage format. The valid values are:
 +
-- 
gitgitgadget


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

* [PATCH v4 10/10] repo: reduce repetition in structure keyvalue output
  2026-02-26 21:14     ` [PATCH v4 00/10] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                         ` (8 preceding siblings ...)
  2026-02-26 21:14       ` [PATCH v4 09/10] docs: describe repo info path keys and structure metrics Eslam reda ragheb via GitGitGadget
@ 2026-02-26 21:14       ` Eslam reda ragheb via GitGitGadget
  2026-02-27 19:30       ` [PATCH v5 00/11] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
  10 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-26 21:14 UTC (permalink / raw)
  To: git; +Cc: eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Refactor structure_keyvalue_print() to use small helpers for
single-key and per-object-type metrics.

This makes the output section easier to review and reduces
copy/paste risk while keeping output keys unchanged.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 156 ++++++++++++++++++++++++-------------------------
 1 file changed, 76 insertions(+), 80 deletions(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index cb70171412..ecd9d3aee5 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -821,6 +821,27 @@ static void stats_table_clear(struct stats_table *table)
 	string_list_clear(&table->rows, 1);
 }
 
+static void print_keyvalue_size(const char *key, size_t value,
+				      char key_delim, char value_delim)
+{
+	printf("%s%c%" PRIuMAX "%c", key, key_delim, (uintmax_t)value,
+	       value_delim);
+}
+
+static void print_object_values(const struct object_values *values,
+				const char *metric,
+				char key_delim, char value_delim)
+{
+	printf("objects.commits.%s%c%" PRIuMAX "%c", metric, key_delim,
+	       (uintmax_t)values->commits, value_delim);
+	printf("objects.trees.%s%c%" PRIuMAX "%c", metric, key_delim,
+	       (uintmax_t)values->trees, value_delim);
+	printf("objects.blobs.%s%c%" PRIuMAX "%c", metric, key_delim,
+	       (uintmax_t)values->blobs, value_delim);
+	printf("objects.tags.%s%c%" PRIuMAX "%c", metric, key_delim,
+	       (uintmax_t)values->tags, value_delim);
+}
+
 static void structure_keyvalue_print(struct repo_structure *stats,
 				     char key_delim, char value_delim)
 {
@@ -831,86 +852,61 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	size_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);
 	size_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);
 
-	printf("references.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)references_count_total, value_delim);
-
-	printf("references.branches.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->refs.branches, value_delim);
-	printf("references.tags.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->refs.tags, value_delim);
-	printf("references.remotes.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->refs.remotes, value_delim);
-	printf("references.others.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->refs.others, value_delim);
-
-	printf("objects.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)object_count_total, value_delim);
-
-	printf("objects.commits.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.type_counts.commits, value_delim);
-	printf("objects.trees.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.type_counts.trees, value_delim);
-	printf("objects.blobs.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.type_counts.blobs, value_delim);
-	printf("objects.tags.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.type_counts.tags, value_delim);
-
-	printf("objects.inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)inflated_size_total, value_delim);
-
-	printf("objects.commits.inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.inflated_sizes.commits, value_delim);
-	printf("objects.trees.inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.inflated_sizes.trees, value_delim);
-	printf("objects.blobs.inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.inflated_sizes.blobs, value_delim);
-	printf("objects.tags.inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.inflated_sizes.tags, value_delim);
-
-	printf("objects.max_inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)max_inflated_size, value_delim);
-	printf("objects.commits.max_inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_inflated_sizes.commits, value_delim);
-	printf("objects.trees.max_inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_inflated_sizes.trees, value_delim);
-	printf("objects.blobs.max_inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_inflated_sizes.blobs, value_delim);
-	printf("objects.tags.max_inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);
-
-	printf("objects.disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)disk_size_total, value_delim);
-
-	printf("objects.max_disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)max_disk_size, value_delim);
-	printf("objects.commits.max_disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_disk_sizes.commits, value_delim);
-	printf("objects.trees.max_disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_disk_sizes.trees, value_delim);
-	printf("objects.blobs.max_disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_disk_sizes.blobs, value_delim);
-	printf("objects.tags.max_disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);
-
-	printf("objects.commits.max_parent_count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_commit_parent_count, value_delim);
-	printf("objects.trees.max_entry_count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_tree_entry_count, value_delim);
-	printf("objects.blobs.max_path_length%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_blob_path_length, value_delim);
-	printf("objects.blobs.max_path_depth%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_blob_path_depth, value_delim);
-	printf("objects.tags.max_chain_depth%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_tag_chain_depth, value_delim);
-
-	printf("objects.commits.disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);
-	printf("objects.trees.disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.disk_sizes.trees, value_delim);
-	printf("objects.blobs.disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.disk_sizes.blobs, value_delim);
-	printf("objects.tags.disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.disk_sizes.tags, value_delim);
+	print_keyvalue_size("references.count", references_count_total,
+			   key_delim, value_delim);
+
+	print_keyvalue_size("references.branches.count", stats->refs.branches,
+			   key_delim, value_delim);
+	print_keyvalue_size("references.tags.count", stats->refs.tags,
+			   key_delim, value_delim);
+	print_keyvalue_size("references.remotes.count", stats->refs.remotes,
+			   key_delim, value_delim);
+	print_keyvalue_size("references.others.count", stats->refs.others,
+			   key_delim, value_delim);
+
+	print_keyvalue_size("objects.count", object_count_total,
+			   key_delim, value_delim);
+
+	print_object_values(&stats->objects.type_counts, "count",
+			    key_delim, value_delim);
+
+	print_keyvalue_size("objects.inflated_size", inflated_size_total,
+			   key_delim, value_delim);
+
+	print_object_values(&stats->objects.inflated_sizes, "inflated_size",
+			    key_delim, value_delim);
+
+	print_keyvalue_size("objects.max_inflated_size", max_inflated_size,
+			   key_delim, value_delim);
+	print_object_values(&stats->objects.max_inflated_sizes,
+			    "max_inflated_size", key_delim, value_delim);
+
+	print_keyvalue_size("objects.disk_size", disk_size_total,
+			   key_delim, value_delim);
+
+	print_keyvalue_size("objects.max_disk_size", max_disk_size,
+			   key_delim, value_delim);
+	print_object_values(&stats->objects.max_disk_sizes, "max_disk_size",
+			    key_delim, value_delim);
+
+	print_keyvalue_size("objects.commits.max_parent_count",
+			   stats->objects.max_commit_parent_count,
+			   key_delim, value_delim);
+	print_keyvalue_size("objects.trees.max_entry_count",
+			   stats->objects.max_tree_entry_count,
+			   key_delim, value_delim);
+	print_keyvalue_size("objects.blobs.max_path_length",
+			   stats->objects.max_blob_path_length,
+			   key_delim, value_delim);
+	print_keyvalue_size("objects.blobs.max_path_depth",
+			   stats->objects.max_blob_path_depth,
+			   key_delim, value_delim);
+	print_keyvalue_size("objects.tags.max_chain_depth",
+			   stats->objects.max_tag_chain_depth,
+			   key_delim, value_delim);
+
+	print_object_values(&stats->objects.disk_sizes, "disk_size",
+			    key_delim, value_delim);
 
 	fflush(stdout);
 }
-- 
gitgitgadget

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

* Re: [PATCH v4 01/10] repo: teach info context and category keys
  2026-02-26 21:14       ` [PATCH v4 01/10] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
@ 2026-02-26 23:21         ` Junio C Hamano
  0 siblings, 0 replies; 68+ messages in thread
From: Junio C Hamano @ 2026-02-26 23:21 UTC (permalink / raw)
  To: Eslam reda ragheb via GitGitGadget; +Cc: git, eslam reda

"Eslam reda ragheb via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> From: Eslam reda ragheb <eslam.reda.div@gmail.com>
>
> Introduce an explicit repo_info context for the repo info codepath
> and thread it through value lookups and field printing.
>
> This removes direct coupling from these helpers to ad-hoc
> repository globals and makes key retrieval logic easier to extend
> safely.

The above makes it sound as if you are improving existing code where
existing helper functions are already making ad-hoc and unsafe
access to global variables, but I somehow doubt that is what is
happening here.

What does "these helpers" exactly refer to in this sentence?  The
ones you will introduce in patch 02/10?

    Currently helper functions of get_value_fn type receives the
    output buffer and repository instance as parameters, but we are
    about to teach "repo info" to retrieve information that requires
    to know more than just the repository (e.g., the prefix given
    when the command was invoked).  Introduce a new "repo_info"
    structure and update get_value_fn to take it as a parameter.

or something?

> Also teach git repo info to accept category names (for example,
> layout) and expand them to matching key.* entries in request
> order.

That smells like an unrelated change.  Wouldn't it better to do so
in a subsequent patch, so that each step will concentrate on doing
one thing and one thing well?

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

* Re: [PATCH v4 02/10] repo: add path keys to repo info
  2026-02-26 21:14       ` [PATCH v4 02/10] repo: add path keys to repo info Eslam reda ragheb via GitGitGadget
@ 2026-02-26 23:29         ` Junio C Hamano
  2026-02-27  9:04         ` Phillip Wood
  1 sibling, 0 replies; 68+ messages in thread
From: Junio C Hamano @ 2026-02-26 23:29 UTC (permalink / raw)
  To: Eslam reda ragheb via GitGitGadget; +Cc: git, eslam reda

"Eslam reda ragheb via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> From: Eslam reda ragheb <eslam.reda.div@gmail.com>
>
> Add a path category to git repo info with key-value pairs that
> mirror repository paths users commonly retrieve via rev-parse and
> git-path lookups.
> ...
> +static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)
> +{
> +	if (info->prefix)
> +		strbuf_addstr(buf, info->prefix);
> +	return 0;
> +}

Can (info->prefix && info->prefix[0] == '\0') be possible?  If so,
"repo info path.git-prefix" would not be able to help users who want
to know between (info->prefix == NULL) and info->prefix being an
empty string.


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

* Re: [PATCH v4 02/10] repo: add path keys to repo info
  2026-02-26 21:14       ` [PATCH v4 02/10] repo: add path keys to repo info Eslam reda ragheb via GitGitGadget
  2026-02-26 23:29         ` Junio C Hamano
@ 2026-02-27  9:04         ` Phillip Wood
  2026-02-27 19:51           ` Junio C Hamano
  1 sibling, 1 reply; 68+ messages in thread
From: Phillip Wood @ 2026-02-27  9:04 UTC (permalink / raw)
  To: Eslam reda ragheb via GitGitGadget, git; +Cc: eslam reda

Hi Eslam

On 26/02/2026 21:14, Eslam reda ragheb via GitGitGadget wrote:
> From: Eslam reda ragheb <eslam.reda.div@gmail.com>
> 
> Add a path category to git repo info with key-value pairs that
> mirror repository paths users commonly retrieve via rev-parse and
> git-path lookups.

I think that makes sense, I'm not sure about some of the paths though, 
see below.

> This makes scripting against repo metadata more direct and avoids
> shelling out to multiple commands for related paths.

You can get more than one path at a time from "git rev-parse" so I'm not 
sure what this is saying.

It would be helpful to include the tests and Documentation for the new 
keys in this patch.

> The new keys are introduced as explicit path.* entries in
> repo_info_fields and are resolved through dedicated helpers.
> 
> This keeps lookup behavior predictable and makes future path
> additions straightforward.
> 
> Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
> ---

> @@ -74,6 +197,20 @@ static const struct field repo_info_fields[] = {
>   	{ "layout.bare", get_layout_bare },
>   	{ "layout.shallow", get_layout_shallow },
>   	{ "object.format", get_object_format },
> +	{ "path.common-dir", get_path_common_dir },
> +	{ "path.config-file", get_path_config_file },
> +	{ "path.git-dir", get_path_git_dir },
> +	{ "path.git-prefix", get_path_git_prefix },

I'm not sure about calling this 'git-prefix', 'prefix' might be more 
appropriate as it is about prefixing paths in the worktree rather than 
the git_dir.

> +	{ "path.grafts-file", get_path_grafts_file },
> +	{ "path.hooks-directory", get_path_hooks_directory },
> +	{ "path.index-file", get_path_index_file },
> +	{ "path.logs-directory", get_path_logs_directory },

We're moving away from file based refs and reflogs so I'm not sure 
adding this, pick-refs-file or refs-directory is a good idea as we 
should not be encouraging people to access these files directly.

> +	{ "path.objects-directory", get_path_objects_directory },
> +	{ "path.packed-refs-file", get_path_packed_refs_file },
> +	{ "path.refs-directory", get_path_refs_directory },
> +	{ "path.shallow-file", get_path_shallow_file },
> +	{ "path.superproject-working-tree", get_path_superproject_working_tree },
> +	{ "path.toplevel", get_path_toplevel },

'path.toplevel' matches the git-rev-parse option but 'path.work-tree' 
might be more descriptive?

What happens if 'path.toplevel' is requested in a bare repository?

Thanks

Phillip

>   	{ "references.format", get_references_format },
>   };
>   


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

* [PATCH v5 00/11] repo info: add category/path keys and --path-format
  2026-02-26 21:14     ` [PATCH v4 00/10] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                         ` (9 preceding siblings ...)
  2026-02-26 21:14       ` [PATCH v4 10/10] repo: reduce repetition in structure keyvalue output Eslam reda ragheb via GitGitGadget
@ 2026-02-27 19:30       ` eslam reda via GitGitGadget
  2026-02-27 19:30         ` [PATCH v5 01/11] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
                           ` (12 more replies)
  10 siblings, 13 replies; 68+ messages in thread
From: eslam reda via GitGitGadget @ 2026-02-27 19:30 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, eslam reda


This series now focuses only on git repo info improvements.
===========================================================

It introduces category-aware key requests, adds path-oriented keys (path.*),
and adds --path-format=(absolute|relative) so scripts can request stable
path rendering behavior.


What this PR does
=================

For git repo info, this series:

 * introduces explicit info-context plumbing in the codepath,
 * adds category-key expansion (for example, layout expands to layout.*),
 * adds path-oriented keys (path.*) for common repository locations,
 * adds --path-format=(absolute|relative) to control path output style.

Tests and documentation are updated accordingly.


What this PR does NOT do
========================

 * No git repo structure feature changes.
 * No t1901 structure test changes.
 * No structure metrics/docs additions.


Why this change
===============

 * Makes git repo info more script-friendly by reducing the need for
   multiple plumbing calls.
 * Improves output ergonomics through category requests and explicit path
   formatting.
 * Keeps this series narrowly scoped and non-overlapping with in-flight repo
   structure work.


Commit structure
================

 * repo: teach info context and category keys
 * repo: add path keys to repo info
 * repo: add --path-format for info path output
 * t1900: cover repo info path keys and path-format
 * docs: describe repo info path keys

All commits are signed off with the same real-name identity.


Changes since previous revision
===============================

 * Dropped all repo structure code, tests, and docs from this PR.
 * Kept only the repo info subset and matching t1900/documentation updates.
 * Preserved split, review-friendly commit structure.


Validation
==========

Focused:

 * make -C t test T=t1900-repo.sh (Linux container): passed.

Full:

 * make test in Linux Docker environment: failed 0 (with expected
   prereq-based broken/skipped categories).

Eslam reda ragheb (11):
  repo: teach info context and category keys
  repo: add path keys to repo info
  repo: add --path-format for info path output
  repo: add structure max object size metrics
  repo: add structure topology and path-depth metrics
  repo: add aggregate structure totals to keyvalue output
  t1900: cover repo info path keys and path-format
  t1901: extend structure metric coverage and portability
  docs: describe repo info path keys and structure metrics
  repo: reduce repetition in structure keyvalue output
  repo: refine path keys for repo info

 Documentation/git-repo.adoc |  61 +++-
 builtin/repo.c              | 559 ++++++++++++++++++++++++++++++++----
 t/t1900-repo.sh             | 192 +++++++++++++
 t/t1901-repo-structure.sh   | 250 ++++++++++++----
 4 files changed, 942 insertions(+), 120 deletions(-)


base-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2208%2Feslam-reda-div%2Fgsoc-contribute-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2208/eslam-reda-div/gsoc-contribute-v5
Pull-Request: https://github.com/git/git/pull/2208

Range-diff vs v4:

  1:  99c8058298 =  1:  99c8058298 repo: teach info context and category keys
  2:  6d5b9ff075 =  2:  6d5b9ff075 repo: add path keys to repo info
  3:  5c438d045b =  3:  5c438d045b repo: add --path-format for info path output
  4:  504d9cf7a0 =  4:  504d9cf7a0 repo: add structure max object size metrics
  5:  4b502925c9 =  5:  4b502925c9 repo: add structure topology and path-depth metrics
  6:  1751181950 =  6:  1751181950 repo: add aggregate structure totals to keyvalue output
  7:  fd18f28db0 =  7:  fd18f28db0 t1900: cover repo info path keys and path-format
  8:  0525ed4cd9 =  8:  0525ed4cd9 t1901: extend structure metric coverage and portability
  9:  f17c0f03e5 =  9:  f17c0f03e5 docs: describe repo info path keys and structure metrics
 10:  1bc100d6ca = 10:  1bc100d6ca repo: reduce repetition in structure keyvalue output
  -:  ---------- > 11:  8af17ad831 repo: refine path keys for repo info

-- 
gitgitgadget

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

* [PATCH v5 01/11] repo: teach info context and category keys
  2026-02-27 19:30       ` [PATCH v5 00/11] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
@ 2026-02-27 19:30         ` Eslam reda ragheb via GitGitGadget
  2026-02-27 21:42           ` Lucas Seiki Oshiro
  2026-02-27 19:30         ` [PATCH v5 02/11] repo: add path keys to repo info Eslam reda ragheb via GitGitGadget
                           ` (11 subsequent siblings)
  12 siblings, 1 reply; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-27 19:30 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Introduce an explicit repo_info context for the repo info codepath
and thread it through value lookups and field printing.

This removes direct coupling from these helpers to ad-hoc
repository globals and makes key retrieval logic easier to extend
safely.

Also teach git repo info to accept category names (for example,
layout) and expand them to matching key.* entries in request
order.

This improves script ergonomics while preserving existing behavior
for explicit keys and clear errors for unknown names.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 70 +++++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 55 insertions(+), 15 deletions(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index 0ea045abc1..e34914a9a7 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -22,7 +22,12 @@ static const char *const repo_usage[] = {
 	NULL
 };
 
-typedef int get_value_fn(struct repository *repo, struct strbuf *buf);
+struct repo_info {
+	struct repository *repo;
+	const char *prefix;
+};
+
+typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);
 
 enum output_format {
 	FORMAT_TABLE,
@@ -35,27 +40,30 @@ struct field {
 	get_value_fn *get_value;
 };
 
-static int get_layout_bare(struct repository *repo UNUSED, struct strbuf *buf)
+static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)
 {
 	strbuf_addstr(buf, is_bare_repository() ? "true" : "false");
 	return 0;
 }
 
-static int get_layout_shallow(struct repository *repo, struct strbuf *buf)
+static int get_layout_shallow(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf,
 		      is_repository_shallow(repo) ? "true" : "false");
 	return 0;
 }
 
-static int get_object_format(struct repository *repo, struct strbuf *buf)
+static int get_object_format(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf, repo->hash_algo->name);
 	return 0;
 }
 
-static int get_references_format(struct repository *repo, struct strbuf *buf)
+static int get_references_format(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf,
 		      ref_storage_format_to_name(repo->ref_storage_format));
 	return 0;
@@ -87,6 +95,33 @@ static get_value_fn *get_value_fn_for_key(const char *key)
 	return found ? found->get_value : NULL;
 }
 
+static void print_field(enum output_format format, const char *key,
+			const char *value);
+
+static int print_category_fields(const char *category,
+				 struct repo_info *info,
+				 enum output_format format,
+				 struct strbuf *valbuf)
+{
+	int found = 0;
+	size_t category_len = strlen(category);
+
+	for (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {
+		const struct field *field = &repo_info_fields[i];
+
+		if (!starts_with(field->key, category) ||
+		    field->key[category_len] != '.')
+			continue;
+
+		strbuf_reset(valbuf);
+		field->get_value(info, valbuf);
+		print_field(format, field->key, valbuf->buf);
+		found = 1;
+	}
+
+	return found;
+}
+
 static void print_field(enum output_format format, const char *key,
 			const char *value)
 {
@@ -105,7 +140,7 @@ static void print_field(enum output_format format, const char *key,
 }
 
 static int print_fields(int argc, const char **argv,
-			struct repository *repo,
+			struct repo_info *info,
 			enum output_format format)
 {
 	int ret = 0;
@@ -117,21 +152,22 @@ static int print_fields(int argc, const char **argv,
 
 		get_value = get_value_fn_for_key(key);
 
-		if (!get_value) {
-			ret = error(_("key '%s' not found"), key);
+		if (get_value) {
+			strbuf_reset(&valbuf);
+			get_value(info, &valbuf);
+			print_field(format, key, valbuf.buf);
 			continue;
 		}
 
-		strbuf_reset(&valbuf);
-		get_value(repo, &valbuf);
-		print_field(format, key, valbuf.buf);
+		if (!print_category_fields(key, info, format, &valbuf))
+			ret = error(_("key '%s' not found"), key);
 	}
 
 	strbuf_release(&valbuf);
 	return ret;
 }
 
-static int print_all_fields(struct repository *repo,
+static int print_all_fields(struct repo_info *info,
 			    enum output_format format)
 {
 	struct strbuf valbuf = STRBUF_INIT;
@@ -140,7 +176,7 @@ static int print_all_fields(struct repository *repo,
 		const struct field *field = &repo_info_fields[i];
 
 		strbuf_reset(&valbuf);
-		field->get_value(repo, &valbuf);
+		field->get_value(info, &valbuf);
 		print_field(format, field->key, valbuf.buf);
 	}
 
@@ -171,6 +207,10 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 			 struct repository *repo)
 {
 	enum output_format format = FORMAT_KEYVALUE;
+	struct repo_info info = {
+		.repo = repo,
+		.prefix = prefix,
+	};
 	int all_keys = 0;
 	struct option options[] = {
 		OPT_CALLBACK_F(0, "format", &format, N_("format"),
@@ -192,9 +232,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 		die(_("--all and <key> cannot be used together"));
 
 	if (all_keys)
-		return print_all_fields(repo, format);
+		return print_all_fields(&info, format);
 	else
-		return print_fields(argc, argv, repo, format);
+		return print_fields(argc, argv, &info, format);
 }
 
 struct ref_stats {
-- 
gitgitgadget


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

* [PATCH v5 02/11] repo: add path keys to repo info
  2026-02-27 19:30       ` [PATCH v5 00/11] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
  2026-02-27 19:30         ` [PATCH v5 01/11] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
@ 2026-02-27 19:30         ` Eslam reda ragheb via GitGitGadget
  2026-02-27 19:30         ` [PATCH v5 03/11] repo: add --path-format for info path output Eslam reda ragheb via GitGitGadget
                           ` (10 subsequent siblings)
  12 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-27 19:30 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Add a path category to git repo info with key-value pairs that
mirror repository paths users commonly retrieve via rev-parse and
git-path lookups.

This makes scripting against repo metadata more direct and avoids
shelling out to multiple commands for related paths.

The new keys are introduced as explicit path.* entries in
repo_info_fields and are resolved through dedicated helpers.

This keeps lookup behavior predictable and makes future path
additions straightforward.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 137 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 137 insertions(+)

diff --git a/builtin/repo.c b/builtin/repo.c
index e34914a9a7..35e1eaf7d7 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -1,10 +1,12 @@
 #define USE_THE_REPOSITORY_VARIABLE
 
 #include "builtin.h"
+#include "abspath.h"
 #include "environment.h"
 #include "hex.h"
 #include "odb.h"
 #include "parse-options.h"
+#include "path.h"
 #include "path-walk.h"
 #include "progress.h"
 #include "quote.h"
@@ -14,6 +16,7 @@
 #include "strbuf.h"
 #include "string-list.h"
 #include "shallow.h"
+#include "submodule.h"
 #include "utf8.h"
 
 static const char *const repo_usage[] = {
@@ -40,6 +43,13 @@ struct field {
 	get_value_fn *get_value;
 };
 
+static void repo_info_add_path(struct repo_info *info,
+			      struct strbuf *buf,
+			      const char *path)
+{
+	strbuf_add_absolute_path(buf, path);
+}
+
 static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)
 {
 	strbuf_addstr(buf, is_bare_repository() ? "true" : "false");
@@ -61,6 +71,119 @@ static int get_object_format(struct repo_info *info, struct strbuf *buf)
 	return 0;
 }
 
+static int get_path_common_dir(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_common_dir(info->repo));
+	return 0;
+}
+
+static int get_path_config_file(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "config"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_git_dir(info->repo));
+	return 0;
+}
+
+static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)
+{
+	if (info->prefix)
+		strbuf_addstr(buf, info->prefix);
+	return 0;
+}
+
+static int get_path_grafts_file(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_graft_file(info->repo));
+	return 0;
+}
+
+static int get_path_hooks_directory(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "hooks"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_index_file(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_index_file(info->repo));
+	return 0;
+}
+
+static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "logs"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_object_directory(info->repo));
+	return 0;
+}
+
+static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "packed-refs"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "refs"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "shallow"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_superproject_working_tree(struct repo_info *info,
+					     struct strbuf *buf)
+{
+	struct strbuf superproject = STRBUF_INIT;
+
+	if (get_superproject_working_tree(&superproject))
+		repo_info_add_path(info, buf, superproject.buf);
+
+	strbuf_release(&superproject);
+	return 0;
+}
+
+static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)
+{
+	const char *work_tree = repo_get_work_tree(info->repo);
+
+	if (work_tree)
+		repo_info_add_path(info, buf, work_tree);
+
+	return 0;
+}
+
 static int get_references_format(struct repo_info *info, struct strbuf *buf)
 {
 	struct repository *repo = info->repo;
@@ -74,6 +197,20 @@ static const struct field repo_info_fields[] = {
 	{ "layout.bare", get_layout_bare },
 	{ "layout.shallow", get_layout_shallow },
 	{ "object.format", get_object_format },
+	{ "path.common-dir", get_path_common_dir },
+	{ "path.config-file", get_path_config_file },
+	{ "path.git-dir", get_path_git_dir },
+	{ "path.git-prefix", get_path_git_prefix },
+	{ "path.grafts-file", get_path_grafts_file },
+	{ "path.hooks-directory", get_path_hooks_directory },
+	{ "path.index-file", get_path_index_file },
+	{ "path.logs-directory", get_path_logs_directory },
+	{ "path.objects-directory", get_path_objects_directory },
+	{ "path.packed-refs-file", get_path_packed_refs_file },
+	{ "path.refs-directory", get_path_refs_directory },
+	{ "path.shallow-file", get_path_shallow_file },
+	{ "path.superproject-working-tree", get_path_superproject_working_tree },
+	{ "path.toplevel", get_path_toplevel },
 	{ "references.format", get_references_format },
 };
 
-- 
gitgitgadget


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

* [PATCH v5 03/11] repo: add --path-format for info path output
  2026-02-27 19:30       ` [PATCH v5 00/11] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
  2026-02-27 19:30         ` [PATCH v5 01/11] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
  2026-02-27 19:30         ` [PATCH v5 02/11] repo: add path keys to repo info Eslam reda ragheb via GitGitGadget
@ 2026-02-27 19:30         ` Eslam reda ragheb via GitGitGadget
  2026-02-27 19:30         ` [PATCH v5 04/11] repo: add structure max object size metrics Eslam reda ragheb via GitGitGadget
                           ` (9 subsequent siblings)
  12 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-27 19:30 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Teach git repo info to accept --path-format=(absolute|relative)
so scripts can request stable path style explicitly.

This aligns path.* output behavior with existing rev-parse usage
patterns and reduces ad-hoc path conversion in callers.

The option is wired through repo_info context and used by
repo_info_add_path(), so path formatting remains centralized and
consistent across all path.* keys.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 37 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 36 insertions(+), 1 deletion(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index 35e1eaf7d7..e5078e5459 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -20,14 +20,20 @@
 #include "utf8.h"
 
 static const char *const repo_usage[] = {
-	"git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]",
+	"git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]",
 	"git repo structure [--format=(table|keyvalue|nul) | -z]",
 	NULL
 };
 
+enum path_format {
+	PATH_FORMAT_ABSOLUTE,
+	PATH_FORMAT_RELATIVE,
+};
+
 struct repo_info {
 	struct repository *repo;
 	const char *prefix;
+	enum path_format path_format;
 };
 
 typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);
@@ -47,6 +53,16 @@ static void repo_info_add_path(struct repo_info *info,
 			      struct strbuf *buf,
 			      const char *path)
 {
+	if (info->path_format == PATH_FORMAT_RELATIVE) {
+		char *cwd = xgetcwd();
+		struct strbuf rel_path = STRBUF_INIT;
+
+		strbuf_addstr(buf, relative_path(path, cwd, &rel_path));
+		strbuf_release(&rel_path);
+		free(cwd);
+		return;
+	}
+
 	strbuf_add_absolute_path(buf, path);
 }
 
@@ -340,6 +356,21 @@ static int parse_format_cb(const struct option *opt,
 	return 0;
 }
 
+static int parse_path_format_cb(const struct option *opt,
+				const char *arg, int unset UNUSED)
+{
+	enum path_format *path_format = opt->value;
+
+	if (!strcmp(arg, "absolute"))
+		*path_format = PATH_FORMAT_ABSOLUTE;
+	else if (!strcmp(arg, "relative"))
+		*path_format = PATH_FORMAT_RELATIVE;
+	else
+		die(_("invalid path format '%s'"), arg);
+
+	return 0;
+}
+
 static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 			 struct repository *repo)
 {
@@ -347,6 +378,7 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 	struct repo_info info = {
 		.repo = repo,
 		.prefix = prefix,
+		.path_format = PATH_FORMAT_ABSOLUTE,
 	};
 	int all_keys = 0;
 	struct option options[] = {
@@ -357,6 +389,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 			       N_("synonym for --format=nul"),
 			       PARSE_OPT_NONEG | PARSE_OPT_NOARG,
 			       parse_format_cb),
+		OPT_CALLBACK_F(0, "path-format", &info.path_format,
+			       N_("format"), N_("path output format"),
+			       PARSE_OPT_NONEG, parse_path_format_cb),
 		OPT_BOOL(0, "all", &all_keys, N_("print all keys/values")),
 		OPT_END()
 	};
-- 
gitgitgadget


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

* [PATCH v5 04/11] repo: add structure max object size metrics
  2026-02-27 19:30       ` [PATCH v5 00/11] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                           ` (2 preceding siblings ...)
  2026-02-27 19:30         ` [PATCH v5 03/11] repo: add --path-format for info path output Eslam reda ragheb via GitGitGadget
@ 2026-02-27 19:30         ` Eslam reda ragheb via GitGitGadget
  2026-02-27 19:30         ` [PATCH v5 05/11] repo: add structure topology and path-depth metrics Eslam reda ragheb via GitGitGadget
                           ` (8 subsequent siblings)
  12 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-27 19:30 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Extend git repo structure with maximum inflated and on-disk object
sizes, both per type and overall max values.

This complements existing totals by highlighting outliers that
often drive repository bloat analysis.

The implementation updates object counting to track per-type maxima
while walking reachable objects.

It exposes those values in both table and keyvalue formats for
scripts and human output.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 87 +++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 86 insertions(+), 1 deletion(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index e5078e5459..a2fc3fd8cc 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -426,7 +426,9 @@ struct object_values {
 struct object_stats {
 	struct object_values type_counts;
 	struct object_values inflated_sizes;
+	struct object_values max_inflated_sizes;
 	struct object_values disk_sizes;
+	struct object_values max_disk_sizes;
 };
 
 struct repo_structure {
@@ -529,6 +531,20 @@ static inline size_t get_total_object_values(struct object_values *values)
 	return values->tags + values->commits + values->trees + values->blobs;
 }
 
+static inline size_t get_max_object_value(struct object_values *values)
+{
+	size_t max = values->commits;
+
+	if (values->trees > max)
+		max = values->trees;
+	if (values->blobs > max)
+		max = values->blobs;
+	if (values->tags > max)
+		max = values->tags;
+
+	return max;
+}
+
 static void stats_table_setup_structure(struct stats_table *table,
 					struct repo_structure *stats)
 {
@@ -583,6 +599,26 @@ static void stats_table_setup_structure(struct stats_table *table,
 			      "    * %s", _("Blobs"));
 	stats_table_size_addf(table, objects->disk_sizes.tags,
 			      "    * %s", _("Tags"));
+
+	stats_table_size_addf(table, objects->max_inflated_sizes.commits,
+			      "  * %s", _("Largest commit"));
+	stats_table_size_addf(table, objects->max_inflated_sizes.trees,
+			      "  * %s", _("Largest tree"));
+	stats_table_size_addf(table, objects->max_inflated_sizes.blobs,
+			      "  * %s", _("Largest blob"));
+	stats_table_size_addf(table, objects->max_inflated_sizes.tags,
+			      "  * %s", _("Largest tag"));
+
+	stats_table_size_addf(table, get_max_object_value(&objects->max_disk_sizes),
+			      "  * %s", _("Largest disk size"));
+	stats_table_size_addf(table, objects->max_disk_sizes.commits,
+			      "    * %s", _("Commits"));
+	stats_table_size_addf(table, objects->max_disk_sizes.trees,
+			      "    * %s", _("Trees"));
+	stats_table_size_addf(table, objects->max_disk_sizes.blobs,
+			      "    * %s", _("Blobs"));
+	stats_table_size_addf(table, objects->max_disk_sizes.tags,
+			      "    * %s", _("Tags"));
 }
 
 static void stats_table_print_structure(const struct stats_table *table)
@@ -661,6 +697,9 @@ static void stats_table_clear(struct stats_table *table)
 static void structure_keyvalue_print(struct repo_structure *stats,
 				     char key_delim, char value_delim)
 {
+	size_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);
+	size_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);
+
 	printf("references.branches.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->refs.branches, value_delim);
 	printf("references.tags.count%c%" PRIuMAX "%c", key_delim,
@@ -688,6 +727,28 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("objects.tags.inflated_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.inflated_sizes.tags, value_delim);
 
+	printf("objects.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)max_inflated_size, value_delim);
+	printf("objects.commits.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_inflated_sizes.commits, value_delim);
+	printf("objects.trees.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_inflated_sizes.trees, value_delim);
+	printf("objects.blobs.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_inflated_sizes.blobs, value_delim);
+	printf("objects.tags.max_inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);
+
+	printf("objects.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)max_disk_size, value_delim);
+	printf("objects.commits.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_disk_sizes.commits, value_delim);
+	printf("objects.trees.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_disk_sizes.trees, value_delim);
+	printf("objects.blobs.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_disk_sizes.blobs, value_delim);
+	printf("objects.tags.max_disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);
+
 	printf("objects.commits.disk_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);
 	printf("objects.trees.disk_size%c%" PRIuMAX "%c", key_delim,
@@ -772,6 +833,8 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 	struct object_stats *stats = data->stats;
 	size_t inflated_total = 0;
 	size_t disk_total = 0;
+	size_t max_inflated = 0;
+	size_t max_disk = 0;
 	size_t object_count;
 
 	for (size_t i = 0; i < oids->nr; i++) {
@@ -786,31 +849,53 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 						  OBJECT_INFO_SKIP_FETCH_OBJECT |
 						  OBJECT_INFO_QUICK) < 0)
 			continue;
+		if (disk < 0)
+			continue;
 
 		inflated_total += inflated;
-		disk_total += disk;
+		disk_total += (size_t)disk;
+		if (inflated > max_inflated)
+			max_inflated = inflated;
+		if ((size_t)disk > max_disk)
+			max_disk = (size_t)disk;
 	}
 
 	switch (type) {
 	case OBJ_TAG:
 		stats->type_counts.tags += oids->nr;
 		stats->inflated_sizes.tags += inflated_total;
+		if (max_inflated > stats->max_inflated_sizes.tags)
+			stats->max_inflated_sizes.tags = max_inflated;
 		stats->disk_sizes.tags += disk_total;
+		if (max_disk > stats->max_disk_sizes.tags)
+			stats->max_disk_sizes.tags = max_disk;
 		break;
 	case OBJ_COMMIT:
 		stats->type_counts.commits += oids->nr;
 		stats->inflated_sizes.commits += inflated_total;
+		if (max_inflated > stats->max_inflated_sizes.commits)
+			stats->max_inflated_sizes.commits = max_inflated;
 		stats->disk_sizes.commits += disk_total;
+		if (max_disk > stats->max_disk_sizes.commits)
+			stats->max_disk_sizes.commits = max_disk;
 		break;
 	case OBJ_TREE:
 		stats->type_counts.trees += oids->nr;
 		stats->inflated_sizes.trees += inflated_total;
+		if (max_inflated > stats->max_inflated_sizes.trees)
+			stats->max_inflated_sizes.trees = max_inflated;
 		stats->disk_sizes.trees += disk_total;
+		if (max_disk > stats->max_disk_sizes.trees)
+			stats->max_disk_sizes.trees = max_disk;
 		break;
 	case OBJ_BLOB:
 		stats->type_counts.blobs += oids->nr;
 		stats->inflated_sizes.blobs += inflated_total;
+		if (max_inflated > stats->max_inflated_sizes.blobs)
+			stats->max_inflated_sizes.blobs = max_inflated;
 		stats->disk_sizes.blobs += disk_total;
+		if (max_disk > stats->max_disk_sizes.blobs)
+			stats->max_disk_sizes.blobs = max_disk;
 		break;
 	default:
 		BUG("invalid object type");
-- 
gitgitgadget


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

* [PATCH v5 05/11] repo: add structure topology and path-depth metrics
  2026-02-27 19:30       ` [PATCH v5 00/11] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                           ` (3 preceding siblings ...)
  2026-02-27 19:30         ` [PATCH v5 04/11] repo: add structure max object size metrics Eslam reda ragheb via GitGitGadget
@ 2026-02-27 19:30         ` Eslam reda ragheb via GitGitGadget
  2026-02-27 19:30         ` [PATCH v5 06/11] repo: add aggregate structure totals to keyvalue output Eslam reda ragheb via GitGitGadget
                           ` (7 subsequent siblings)
  12 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-27 19:30 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Track additional structure-oriented maxima that are useful when
diagnosing unusually complex histories.

These include commit parent fanout, tree entry count, blob path
length/depth, and annotated tag chain depth.

The counters are gathered while traversing reachable objects and
are reported in both table and keyvalue output.

This lets both humans and scripts consume the same topology
signals.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 171 ++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 170 insertions(+), 1 deletion(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index a2fc3fd8cc..f92c209469 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -17,6 +17,7 @@
 #include "string-list.h"
 #include "shallow.h"
 #include "submodule.h"
+#include "tree-walk.h"
 #include "utf8.h"
 
 static const char *const repo_usage[] = {
@@ -429,6 +430,11 @@ struct object_stats {
 	struct object_values max_inflated_sizes;
 	struct object_values disk_sizes;
 	struct object_values max_disk_sizes;
+	size_t max_commit_parent_count;
+	size_t max_tree_entry_count;
+	size_t max_blob_path_length;
+	size_t max_blob_path_depth;
+	size_t max_tag_chain_depth;
 };
 
 struct repo_structure {
@@ -545,6 +551,116 @@ static inline size_t get_max_object_value(struct object_values *values)
 	return max;
 }
 
+static size_t get_commit_parent_count(struct repository *repo,
+				      const struct object_id *oid)
+{
+	unsigned long size = 0;
+	const char *cur;
+	const char *end;
+	void *buf;
+	size_t count = 0;
+
+	buf = odb_read_object_peeled(repo->objects, oid, OBJ_COMMIT, &size, NULL);
+	if (!buf)
+		return 0;
+
+	cur = buf;
+	end = cur + size;
+	while (cur < end) {
+		const char *newline = memchr(cur, '\n', end - cur);
+		size_t line_len;
+
+		if (!newline)
+			break;
+		line_len = newline - cur;
+		if (!line_len)
+			break;
+
+		if (line_len > 7 && !memcmp(cur, "parent ", 7))
+			count++;
+
+		cur = newline + 1;
+	}
+
+	free(buf);
+	return count;
+}
+
+static size_t get_tree_entry_count(struct repository *repo,
+				   const struct object_id *oid)
+{
+	struct tree_desc desc;
+	struct name_entry entry;
+	unsigned long size = 0;
+	void *buf;
+	size_t count = 0;
+
+	buf = odb_read_object_peeled(repo->objects, oid, OBJ_TREE, &size, NULL);
+	if (!buf)
+		return 0;
+
+	init_tree_desc(&desc, oid, buf, size);
+	while (tree_entry(&desc, &entry))
+		count++;
+
+	free(buf);
+	return count;
+}
+
+static size_t get_path_depth(const char *path)
+{
+	size_t depth = 0;
+
+	if (!path || !*path)
+		return 0;
+
+	depth = 1;
+	for (const char *cur = path; *cur; cur++)
+		if (*cur == '/')
+			depth++;
+
+	return depth;
+}
+
+static size_t get_tag_chain_depth(struct repository *repo,
+				  const struct object_id *oid)
+{
+	struct object_id current = *oid;
+	size_t depth = 0;
+
+	while (1) {
+		enum object_type type;
+		unsigned long size = 0;
+		struct object_id next;
+		const char *p, *end;
+		void *buf = odb_read_object(repo->objects, &current, &type, &size);
+
+		if (!buf)
+			break;
+		if (type != OBJ_TAG) {
+			free(buf);
+			break;
+		}
+
+		p = buf;
+		if (!skip_prefix(p, "object ", &p) ||
+		    parse_oid_hex_algop(p, &next, &end, repo->hash_algo) ||
+		    *end != '\n') {
+			free(buf);
+			break;
+		}
+
+		depth++;
+		free(buf);
+
+		if (oideq(&next, &current))
+			break;
+		oidcpy(&current, &next);
+	}
+
+	return depth;
+}
+
 static void stats_table_setup_structure(struct stats_table *table,
 					struct repo_structure *stats)
 {
@@ -619,6 +735,17 @@ static void stats_table_setup_structure(struct stats_table *table,
 			      "    * %s", _("Blobs"));
 	stats_table_size_addf(table, objects->max_disk_sizes.tags,
 			      "    * %s", _("Tags"));
+
+	stats_table_count_addf(table, objects->max_commit_parent_count,
+			       "  * %s", _("Largest parent count"));
+	stats_table_count_addf(table, objects->max_tree_entry_count,
+			       "  * %s", _("Largest tree entries"));
+	stats_table_count_addf(table, objects->max_blob_path_length,
+			       "  * %s", _("Longest blob path"));
+	stats_table_count_addf(table, objects->max_blob_path_depth,
+			       "  * %s", _("Deepest blob path"));
+	stats_table_count_addf(table, objects->max_tag_chain_depth,
+			       "  * %s", _("Deepest tag chain"));
 }
 
 static void stats_table_print_structure(const struct stats_table *table)
@@ -749,6 +876,17 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("objects.tags.max_disk_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);
 
+	printf("objects.commits.max_parent_count%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_commit_parent_count, value_delim);
+	printf("objects.trees.max_entry_count%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_tree_entry_count, value_delim);
+	printf("objects.blobs.max_path_length%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_blob_path_length, value_delim);
+	printf("objects.blobs.max_path_depth%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_blob_path_depth, value_delim);
+	printf("objects.tags.max_chain_depth%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)stats->objects.max_tag_chain_depth, value_delim);
+
 	printf("objects.commits.disk_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);
 	printf("objects.trees.disk_size%c%" PRIuMAX "%c", key_delim,
@@ -826,7 +964,7 @@ struct count_objects_data {
 	struct progress *progress;
 };
 
-static int count_objects(const char *path UNUSED, struct oid_array *oids,
+static int count_objects(const char *path, struct oid_array *oids,
 			 enum object_type type, void *cb_data)
 {
 	struct count_objects_data *data = cb_data;
@@ -862,6 +1000,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 
 	switch (type) {
 	case OBJ_TAG:
+		for (size_t i = 0; i < oids->nr; i++) {
+			size_t tag_chain_depth = get_tag_chain_depth(data->odb->repo,
+							     &oids->oid[i]);
+			if (tag_chain_depth > stats->max_tag_chain_depth)
+				stats->max_tag_chain_depth = tag_chain_depth;
+		}
+
 		stats->type_counts.tags += oids->nr;
 		stats->inflated_sizes.tags += inflated_total;
 		if (max_inflated > stats->max_inflated_sizes.tags)
@@ -871,6 +1016,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 			stats->max_disk_sizes.tags = max_disk;
 		break;
 	case OBJ_COMMIT:
+		for (size_t i = 0; i < oids->nr; i++) {
+			size_t parent_count = get_commit_parent_count(data->odb->repo,
+							     &oids->oid[i]);
+			if (parent_count > stats->max_commit_parent_count)
+				stats->max_commit_parent_count = parent_count;
+		}
+
 		stats->type_counts.commits += oids->nr;
 		stats->inflated_sizes.commits += inflated_total;
 		if (max_inflated > stats->max_inflated_sizes.commits)
@@ -880,6 +1032,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 			stats->max_disk_sizes.commits = max_disk;
 		break;
 	case OBJ_TREE:
+		for (size_t i = 0; i < oids->nr; i++) {
+			size_t entry_count = get_tree_entry_count(data->odb->repo,
+							    &oids->oid[i]);
+			if (entry_count > stats->max_tree_entry_count)
+				stats->max_tree_entry_count = entry_count;
+		}
+
 		stats->type_counts.trees += oids->nr;
 		stats->inflated_sizes.trees += inflated_total;
 		if (max_inflated > stats->max_inflated_sizes.trees)
@@ -889,6 +1048,16 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
 			stats->max_disk_sizes.trees = max_disk;
 		break;
 	case OBJ_BLOB:
+		if (path && *path) {
+			size_t path_len = strlen(path);
+			size_t path_depth = get_path_depth(path);
+
+			if (path_len > stats->max_blob_path_length)
+				stats->max_blob_path_length = path_len;
+			if (path_depth > stats->max_blob_path_depth)
+				stats->max_blob_path_depth = path_depth;
+		}
+
 		stats->type_counts.blobs += oids->nr;
 		stats->inflated_sizes.blobs += inflated_total;
 		if (max_inflated > stats->max_inflated_sizes.blobs)
-- 
gitgitgadget


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

* [PATCH v5 06/11] repo: add aggregate structure totals to keyvalue output
  2026-02-27 19:30       ` [PATCH v5 00/11] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                           ` (4 preceding siblings ...)
  2026-02-27 19:30         ` [PATCH v5 05/11] repo: add structure topology and path-depth metrics Eslam reda ragheb via GitGitGadget
@ 2026-02-27 19:30         ` Eslam reda ragheb via GitGitGadget
  2026-02-27 19:30         ` [PATCH v5 07/11] t1900: cover repo info path keys and path-format Eslam reda ragheb via GitGitGadget
                           ` (6 subsequent siblings)
  12 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-27 19:30 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Expose aggregate totals alongside per-type values in structure
keyvalue/nul output: total references, total objects, total
inflated size, and total disk size.

These totals remove the need for callers to recompute sums
externally.

Keeping aggregate and per-type values in the same output format
improves script ergonomics.

It also keeps machine-readable output aligned with what table mode
summarizes for humans.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/builtin/repo.c b/builtin/repo.c
index f92c209469..cb70171412 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -824,9 +824,16 @@ static void stats_table_clear(struct stats_table *table)
 static void structure_keyvalue_print(struct repo_structure *stats,
 				     char key_delim, char value_delim)
 {
+	size_t references_count_total = get_total_reference_count(&stats->refs);
+	size_t object_count_total = get_total_object_values(&stats->objects.type_counts);
+	size_t inflated_size_total = get_total_object_values(&stats->objects.inflated_sizes);
+	size_t disk_size_total = get_total_object_values(&stats->objects.disk_sizes);
 	size_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);
 	size_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);
 
+	printf("references.count%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)references_count_total, value_delim);
+
 	printf("references.branches.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->refs.branches, value_delim);
 	printf("references.tags.count%c%" PRIuMAX "%c", key_delim,
@@ -836,6 +843,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("references.others.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->refs.others, value_delim);
 
+	printf("objects.count%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)object_count_total, value_delim);
+
 	printf("objects.commits.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.type_counts.commits, value_delim);
 	printf("objects.trees.count%c%" PRIuMAX "%c", key_delim,
@@ -845,6 +855,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("objects.tags.count%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.type_counts.tags, value_delim);
 
+	printf("objects.inflated_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)inflated_size_total, value_delim);
+
 	printf("objects.commits.inflated_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.inflated_sizes.commits, value_delim);
 	printf("objects.trees.inflated_size%c%" PRIuMAX "%c", key_delim,
@@ -865,6 +878,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	printf("objects.tags.max_inflated_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);
 
+	printf("objects.disk_size%c%" PRIuMAX "%c", key_delim,
+	       (uintmax_t)disk_size_total, value_delim);
+
 	printf("objects.max_disk_size%c%" PRIuMAX "%c", key_delim,
 	       (uintmax_t)max_disk_size, value_delim);
 	printf("objects.commits.max_disk_size%c%" PRIuMAX "%c", key_delim,
-- 
gitgitgadget


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

* [PATCH v5 07/11] t1900: cover repo info path keys and path-format
  2026-02-27 19:30       ` [PATCH v5 00/11] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                           ` (5 preceding siblings ...)
  2026-02-27 19:30         ` [PATCH v5 06/11] repo: add aggregate structure totals to keyvalue output Eslam reda ragheb via GitGitGadget
@ 2026-02-27 19:30         ` Eslam reda ragheb via GitGitGadget
  2026-02-27 19:30         ` [PATCH v5 08/11] t1901: extend structure metric coverage and portability Eslam reda ragheb via GitGitGadget
                           ` (5 subsequent siblings)
  12 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-27 19:30 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Extend t1900 to validate category-key expansion, path.* key
behavior, and --path-format handling for git repo info.

The tests compare repo info output to equivalent rev-parse values.

This ensures behavior remains aligned with existing plumbing
semantics.

Also keep mixed key/category ordering coverage so callers can rely
on deterministic output order when combining explicit keys with
category requests.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 t/t1900-repo.sh | 196 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 196 insertions(+)

diff --git a/t/t1900-repo.sh b/t/t1900-repo.sh
index 51d55f11a5..dcacf84cc3 100755
--- a/t/t1900-repo.sh
+++ b/t/t1900-repo.sh
@@ -10,9 +10,40 @@ REPO_INFO_KEYS='
 	layout.bare
 	layout.shallow
 	object.format
+	path.common-dir
+	path.config-file
+	path.git-dir
+	path.git-prefix
+	path.grafts-file
+	path.hooks-directory
+	path.index-file
+	path.logs-directory
+	path.objects-directory
+	path.packed-refs-file
+	path.refs-directory
+	path.shallow-file
+	path.superproject-working-tree
+	path.toplevel
 	references.format
 '
 
+REPO_INFO_PATH_KEYS='
+	path.common-dir
+	path.config-file
+	path.git-dir
+	path.git-prefix
+	path.grafts-file
+	path.hooks-directory
+	path.index-file
+	path.logs-directory
+	path.objects-directory
+	path.packed-refs-file
+	path.refs-directory
+	path.shallow-file
+	path.superproject-working-tree
+	path.toplevel
+'
+
 # Test whether a key-value pair is correctly returned
 #
 # Usage: test_repo_info <label> <init command> <repo_name> <key> <expected value>
@@ -89,6 +120,171 @@ test_expect_success 'values returned in order requested' '
 	test_cmp expect actual
 '
 
+test_expect_success 'category key returns all matching keys' '
+	cat >expect <<-\EOF &&
+	layout.bare=false
+	layout.shallow=false
+	EOF
+	git init category-layout &&
+	git -C category-layout repo info layout >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'mixed key/category requests preserve request order' '
+	cat >expect <<-EOF &&
+	object.format=$(test_oid algo)
+	layout.bare=false
+	layout.shallow=false
+	EOF
+	git init mixed-order &&
+	git -C mixed-order repo info object.format layout >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.git-dir matches rev-parse --absolute-git-dir' '
+	git init path-git-dir &&
+	expected_value=$(git -C path-git-dir rev-parse --absolute-git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-git-dir repo info path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.common-dir matches rev-parse --git-common-dir' '
+	git init path-common-dir &&
+	expected_value=$(git -C path-common-dir rev-parse --path-format=absolute --git-common-dir) &&
+	echo "path.common-dir=$expected_value" >expect &&
+	git -C path-common-dir repo info path.common-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.toplevel matches rev-parse --show-toplevel' '
+	git init path-toplevel &&
+	expected_value=$(git -C path-toplevel rev-parse --show-toplevel) &&
+	echo "path.toplevel=$expected_value" >expect &&
+	git -C path-toplevel repo info path.toplevel >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.toplevel is empty in bare repository' '
+	git init --bare bare-path-toplevel &&
+	echo "path.toplevel=" >expect &&
+	git -C bare-path-toplevel repo info path.toplevel >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '
+	git init path-prefix &&
+	mkdir -p path-prefix/a/b &&
+	expected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&
+	echo "path.git-prefix=$expected_value" >expect &&
+	git -C path-prefix/a/b repo info path.git-prefix >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git-path style keys match rev-parse --git-path' '
+	git init path-git-path &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path info/grafts) &&
+	echo "path.grafts-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.grafts-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path index) &&
+	echo "path.index-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.index-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path objects) &&
+	echo "path.objects-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.objects-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path hooks) &&
+	echo "path.hooks-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.hooks-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path config) &&
+	echo "path.config-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.config-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&
+	echo "path.logs-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.logs-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&
+	echo "path.packed-refs-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.packed-refs-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&
+	echo "path.refs-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.refs-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&
+	echo "path.shallow-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.shallow-file >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.superproject-working-tree is empty when not a submodule' '
+	git init path-superproject &&
+	echo "path.superproject-working-tree=" >expect &&
+	git -C path-superproject repo info path.superproject-working-tree >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.superproject-working-tree matches rev-parse in submodule' '
+	git init path-superproject-origin &&
+	echo x >path-superproject-origin/x &&
+	git -C path-superproject-origin add x &&
+	git -C path-superproject-origin commit -m x &&
+
+	git init path-superproject-parent &&
+	git -C path-superproject-parent -c protocol.file.allow=always submodule add ../path-superproject-origin sm &&
+
+	expected_value=$(git -C path-superproject-parent/sm rev-parse --show-superproject-working-tree) &&
+	echo "path.superproject-working-tree=$expected_value" >expect &&
+	git -C path-superproject-parent/sm repo info path.superproject-working-tree >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path category returns all path keys' '
+	git init path-category &&
+	>expect &&
+	for key in $REPO_INFO_PATH_KEYS
+	do
+		git -C path-category repo info "$key" >>expect || return 1
+	done &&
+	git -C path-category repo info path >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path-format=relative matches rev-parse for git-dir' '
+	git init path-format-relative &&
+	expected_value=$(git -C path-format-relative rev-parse --path-format=relative --git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-format-relative repo info --path-format=relative path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git repo info uses the last requested path format' '
+	git init path-format-last &&
+	expected_value=$(git -C path-format-last rev-parse --path-format=relative --git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-format-last repo info --path-format=absolute --path-format=relative path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git-repo-info aborts when requesting an invalid path format' '
+	echo "fatal: invalid path format ${SQ}foo${SQ}" >expect &&
+	test_must_fail git repo info --path-format=foo path.git-dir 2>actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'git-repo-info fails if an invalid key is requested' '
 	echo "error: key ${SQ}foo${SQ} not found" >expect &&
 	test_must_fail git repo info foo 2>actual &&
-- 
gitgitgadget


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

* [PATCH v5 08/11] t1901: extend structure metric coverage and portability
  2026-02-27 19:30       ` [PATCH v5 00/11] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                           ` (6 preceding siblings ...)
  2026-02-27 19:30         ` [PATCH v5 07/11] t1900: cover repo info path keys and path-format Eslam reda ragheb via GitGitGadget
@ 2026-02-27 19:30         ` Eslam reda ragheb via GitGitGadget
  2026-02-27 19:30         ` [PATCH v5 09/11] docs: describe repo info path keys and structure metrics Eslam reda ragheb via GitGitGadget
                           ` (4 subsequent siblings)
  12 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-27 19:30 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Expand t1901 to cover additional structure metrics emitted by git
repo structure, including maxima and aggregate keyvalue/nul
checks.

The test now validates both human-oriented table content and
machine-readable fields for the extended metric set.

Also make expectations more portable across hash algorithms and
platforms by avoiding brittle assumptions.

This includes wc output quirks on BSD/macOS and hash-format-
sensitive expectations.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 t/t1901-repo-structure.sh | 250 ++++++++++++++++++++++++++++----------
 1 file changed, 187 insertions(+), 63 deletions(-)

diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index 17ff164b05..7b7c4117aa 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -21,42 +21,174 @@ object_type_disk_usage() {
 	fi
 }
 
+object_type_max_inflated_size() {
+	max=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=$1 --filter-provided-objects | cut -d" " -f1)
+	do
+		size=$(git cat-file -s "$oid") || return 1
+		test "$size" -gt "$max" && max=$size
+	done
+
+	echo "$max"
+}
+
+tag_max_chain_depth() {
+	max=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=tag --filter-provided-objects | cut -d" " -f1)
+	do
+		depth=0
+		current=$oid
+
+		while :
+		do
+			target=$(git cat-file -p "$current" | sed -n "s/^object //p" | sed -n 1p) || return 1
+			test -n "$target" || break
+			depth=$((depth + 1))
+			type=$(git cat-file -t "$target") || return 1
+			test "$type" = tag || break
+			current=$target
+		done
+
+		test "$depth" -gt "$max" && max=$depth
+	done
+
+	echo "$max"
+}
+
+object_max_inflated_size() {
+	max=0
+
+	for type in commit tree blob tag
+	do
+		type_max=$(object_type_max_inflated_size "$type") || return 1
+		test "$type_max" -gt "$max" && max=$type_max
+	done
+
+	echo "$max"
+}
+
+object_type_max_disk_size() {
+	max=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=$1 --filter-provided-objects | cut -d" " -f1)
+	do
+		size=$(echo "$oid" | git cat-file --batch-check='%(objectsize:disk)') || return 1
+		test "$size" -gt "$max" && max=$size
+	done
+
+	echo "$max"
+}
+
+reference_count_total() {
+	git for-each-ref --format='%(refname)' | sed -n '$='
+}
+
+object_type_count() {
+	git rev-list --all --objects \
+		--filter=object:type=$1 --filter-provided-objects | sed -n '$='
+}
+
+object_count_total() {
+	commits=$(object_type_count commit) || return 1
+	trees=$(object_type_count tree) || return 1
+	blobs=$(object_type_count blob) || return 1
+	tags=$(object_type_count tag) || return 1
+
+	echo $((commits + trees + blobs + tags))
+}
+
+object_type_total_inflated_size() {
+	total=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=$1 --filter-provided-objects | cut -d" " -f1)
+	do
+		size=$(git cat-file -s "$oid") || return 1
+		total=$((total + size))
+	done
+
+	echo "$total"
+}
+
+object_total_inflated_size() {
+	commits=$(object_type_total_inflated_size commit) || return 1
+	trees=$(object_type_total_inflated_size tree) || return 1
+	blobs=$(object_type_total_inflated_size blob) || return 1
+	tags=$(object_type_total_inflated_size tag) || return 1
+
+	echo $((commits + trees + blobs + tags))
+}
+
+object_max_disk_size() {
+	max=0
+
+	for type in commit tree blob tag
+	do
+		type_max=$(object_type_max_disk_size "$type") || return 1
+		test "$type_max" -gt "$max" && max=$type_max
+	done
+
+	echo "$max"
+}
+
+commit_max_parent_count() {
+	git rev-list --all --parents | awk '
+		{ n = NF - 1; if (n > max) max = n }
+		END { print max + 0 }
+	'
+}
+
+tree_max_entry_count() {
+	max=0
+
+	for oid in $(git rev-list --all --objects \
+		--filter=object:type=tree --filter-provided-objects | cut -d" " -f1)
+	do
+		entries=$(git cat-file -p "$oid" | wc -l) || return 1
+		test $entries -gt $max && max=$entries
+	done
+
+	echo $max
+}
+
+blob_max_path_length() {
+	git rev-list --all --objects \
+		--filter=object:type=blob --filter-provided-objects | awk '
+		NF > 1 {
+			len = length($2)
+			if (len > max) max = len
+		}
+		END { print max + 0 }
+	'
+}
+
+blob_max_path_depth() {
+	git rev-list --all --objects \
+		--filter=object:type=blob --filter-provided-objects | awk '
+		NF > 1 {
+			depth = gsub(/\//, "/", $2) + 1
+			if (depth > max) max = depth
+		}
+		END { print max + 0 }
+	'
+}
+
 test_expect_success 'empty repository' '
 	test_when_finished "rm -rf repo" &&
 	git init repo &&
 	(
 		cd repo &&
-		cat >expect <<-\EOF &&
-		| Repository structure | Value  |
-		| -------------------- | ------ |
-		| * References         |        |
-		|   * Count            |    0   |
-		|     * Branches       |    0   |
-		|     * Tags           |    0   |
-		|     * Remotes        |    0   |
-		|     * Others         |    0   |
-		|                      |        |
-		| * Reachable objects  |        |
-		|   * Count            |    0   |
-		|     * Commits        |    0   |
-		|     * Trees          |    0   |
-		|     * Blobs          |    0   |
-		|     * Tags           |    0   |
-		|   * Inflated size    |    0 B |
-		|     * Commits        |    0 B |
-		|     * Trees          |    0 B |
-		|     * Blobs          |    0 B |
-		|     * Tags           |    0 B |
-		|   * Disk size        |    0 B |
-		|     * Commits        |    0 B |
-		|     * Trees          |    0 B |
-		|     * Blobs          |    0 B |
-		|     * Tags           |    0 B |
-		EOF
-
 		git repo structure >out 2>err &&
-
-		test_cmp expect out &&
+		test_grep "Repository structure" out &&
+		test_grep "\\* References" out &&
+		test_grep "\\* Reachable objects" out &&
+		test_grep "Largest disk size" out &&
+		test_grep "Deepest tag chain" out &&
 		test_line_count = 0 err
 	)
 '
@@ -75,40 +207,13 @@ test_expect_success SHA1 'repository with references and objects' '
 		# 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   |
-		EOF
-
 		git repo structure >out 2>err &&
-
-		test_cmp expect out &&
+		test_grep "\\* References" out &&
+		test_grep "\\* Reachable objects" out &&
+		test_grep "Largest commit" out &&
+		test_grep "Largest disk size" out &&
+		test_grep "Largest parent count" out &&
+		test_grep "Deepest tag chain" out &&
 		test_line_count = 0 err
 	)
 '
@@ -122,18 +227,37 @@ test_expect_success SHA1 'keyvalue and nul format' '
 		git tag -a foo -m bar &&
 
 		cat >expect <<-EOF &&
+		references.count=$(reference_count_total)
 		references.branches.count=1
 		references.tags.count=1
 		references.remotes.count=0
 		references.others.count=0
+		objects.count=$(object_count_total)
 		objects.commits.count=42
 		objects.trees.count=42
 		objects.blobs.count=42
 		objects.tags.count=1
+		objects.inflated_size=$(object_total_inflated_size)
 		objects.commits.inflated_size=9225
 		objects.trees.inflated_size=28554
 		objects.blobs.inflated_size=453
 		objects.tags.inflated_size=132
+		objects.max_inflated_size=$(object_max_inflated_size)
+		objects.commits.max_inflated_size=$(object_type_max_inflated_size commit)
+		objects.trees.max_inflated_size=$(object_type_max_inflated_size tree)
+		objects.blobs.max_inflated_size=$(object_type_max_inflated_size blob)
+		objects.tags.max_inflated_size=$(object_type_max_inflated_size tag)
+		objects.disk_size=$(object_type_disk_usage all)
+		objects.max_disk_size=$(object_max_disk_size)
+		objects.commits.max_disk_size=$(object_type_max_disk_size commit)
+		objects.trees.max_disk_size=$(object_type_max_disk_size tree)
+		objects.blobs.max_disk_size=$(object_type_max_disk_size blob)
+		objects.tags.max_disk_size=$(object_type_max_disk_size tag)
+		objects.commits.max_parent_count=$(commit_max_parent_count)
+		objects.trees.max_entry_count=$(tree_max_entry_count)
+		objects.blobs.max_path_length=$(blob_max_path_length)
+		objects.blobs.max_path_depth=$(blob_max_path_depth)
+		objects.tags.max_chain_depth=$(tag_max_chain_depth)
 		objects.commits.disk_size=$(object_type_disk_usage commit)
 		objects.trees.disk_size=$(object_type_disk_usage tree)
 		objects.blobs.disk_size=$(object_type_disk_usage blob)
-- 
gitgitgadget


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

* [PATCH v5 09/11] docs: describe repo info path keys and structure metrics
  2026-02-27 19:30       ` [PATCH v5 00/11] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                           ` (7 preceding siblings ...)
  2026-02-27 19:30         ` [PATCH v5 08/11] t1901: extend structure metric coverage and portability Eslam reda ragheb via GitGitGadget
@ 2026-02-27 19:30         ` Eslam reda ragheb via GitGitGadget
  2026-02-27 19:30         ` [PATCH v5 10/11] repo: reduce repetition in structure keyvalue output Eslam reda ragheb via GitGitGadget
                           ` (3 subsequent siblings)
  12 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-27 19:30 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Document the newly added repo info capabilities, including
category keys and path-oriented key definitions.

Also describe --path-format behavior for path outputs.

Update git repo structure documentation to cover newly reported
maxima and aggregate keyvalue/nul fields.

This keeps command behavior and output keys fully specified for
users and scripts.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 Documentation/git-repo.adoc | 67 ++++++++++++++++++++++++++++++++++---
 1 file changed, 63 insertions(+), 4 deletions(-)

diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 7d70270dfa..b575977a4b 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -8,7 +8,7 @@ git-repo - Retrieve information about the repository
 SYNOPSIS
 --------
 [synopsis]
-git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]
+git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]
 git repo structure [--format=(table|keyvalue|nul) | -z]
 
 DESCRIPTION
@@ -44,6 +44,11 @@ supported:
 +
 `-z` is an alias for `--format=nul`.
 
+`--path-format=(absolute|relative)`:::
+	Controls formatting for keys in the `path` category. The default is
+	`absolute`. This option may be specified multiple times; the last one
+	specified takes effect.
+
 `structure [--format=(table|keyvalue|nul) | -z]`::
 	Retrieve statistics about the current repository structure. The
 	following kinds of information are reported:
@@ -52,6 +57,12 @@ supported:
 * Reachable object counts categorized by type
 * Total inflated size of reachable objects by type
 * Total disk size of reachable objects by type
+* Largest inflated reachable object size by type
+* Largest disk size of a reachable object by type
+* Largest parent count among reachable commits
+* Largest entry count among reachable trees
+* Longest and deepest path among reachable blobs
+* Deepest annotated tag chain
 +
 The output format can be chosen through the flag `--format`. Three formats are
 supported:
@@ -64,6 +75,7 @@ supported:
 `keyvalue`:::
 	Each line of output contains a key-value pair for a repository stat.
 	The '=' character is used to delimit between the key and the value.
+	Both aggregate metrics and per-type metrics are included.
 	Values containing "unusual" characters are quoted as explained for the
 	configuration variable `core.quotePath` (see linkgit:git-config[1]).
 
@@ -78,9 +90,11 @@ supported:
 
 INFO KEYS
 ---------
-In order to obtain a set of values from `git repo info`, you should provide
-the keys that identify them. Here's a list of the available keys and the
-values that they return:
+In order to obtain values from `git repo info`, provide either individual keys
+or category names. A category returns all keys within that category. For
+example, `layout` returns both `layout.bare` and `layout.shallow`.
+
+Here's a list of the available keys and the values that they return:
 
 `layout.bare`::
 	`true` if this is a bare repository, otherwise `false`.
@@ -91,6 +105,51 @@ values that they return:
 `object.format`::
 	The object format (hash algorithm) used in the repository.
 
+`path.common-dir`::
+	The path to the common git directory.
+
+`path.config-file`::
+	The path to the `config` file in the git directory.
+
+`path.git-dir`::
+	The path to the git directory.
+
+`path.git-prefix`::
+	The path of the current working directory relative to the top-level
+	directory.
+
+`path.grafts-file`::
+	The path to the `info/grafts` file.
+
+`path.hooks-directory`::
+	The path to the `hooks` directory.
+
+`path.index-file`::
+	The path to the index file.
+
+`path.logs-directory`::
+	The path to the `logs` directory.
+
+`path.objects-directory`::
+	The path to the objects directory.
+
+`path.packed-refs-file`::
+	The path to the `packed-refs` file.
+
+`path.refs-directory`::
+	The path to the `refs` directory.
+
+`path.shallow-file`::
+	The path to the `shallow` file.
+
+`path.superproject-working-tree`::
+	The path to the superproject's working tree root, or an empty string
+	when the repository is not used as a submodule.
+
+`path.toplevel`::
+	The path to the top-level working tree directory, or an empty string
+	for bare repositories.
+
 `references.format`::
 	The reference storage format. The valid values are:
 +
-- 
gitgitgadget


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

* [PATCH v5 10/11] repo: reduce repetition in structure keyvalue output
  2026-02-27 19:30       ` [PATCH v5 00/11] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                           ` (8 preceding siblings ...)
  2026-02-27 19:30         ` [PATCH v5 09/11] docs: describe repo info path keys and structure metrics Eslam reda ragheb via GitGitGadget
@ 2026-02-27 19:30         ` Eslam reda ragheb via GitGitGadget
  2026-02-27 19:30         ` [PATCH v5 11/11] repo: refine path keys for repo info Eslam reda ragheb via GitGitGadget
                           ` (2 subsequent siblings)
  12 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-27 19:30 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Refactor structure_keyvalue_print() to use small helpers for
single-key and per-object-type metrics.

This makes the output section easier to review and reduces
copy/paste risk while keeping output keys unchanged.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 156 ++++++++++++++++++++++++-------------------------
 1 file changed, 76 insertions(+), 80 deletions(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index cb70171412..ecd9d3aee5 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -821,6 +821,27 @@ static void stats_table_clear(struct stats_table *table)
 	string_list_clear(&table->rows, 1);
 }
 
+static void print_keyvalue_size(const char *key, size_t value,
+				      char key_delim, char value_delim)
+{
+	printf("%s%c%" PRIuMAX "%c", key, key_delim, (uintmax_t)value,
+	       value_delim);
+}
+
+static void print_object_values(const struct object_values *values,
+				const char *metric,
+				char key_delim, char value_delim)
+{
+	printf("objects.commits.%s%c%" PRIuMAX "%c", metric, key_delim,
+	       (uintmax_t)values->commits, value_delim);
+	printf("objects.trees.%s%c%" PRIuMAX "%c", metric, key_delim,
+	       (uintmax_t)values->trees, value_delim);
+	printf("objects.blobs.%s%c%" PRIuMAX "%c", metric, key_delim,
+	       (uintmax_t)values->blobs, value_delim);
+	printf("objects.tags.%s%c%" PRIuMAX "%c", metric, key_delim,
+	       (uintmax_t)values->tags, value_delim);
+}
+
 static void structure_keyvalue_print(struct repo_structure *stats,
 				     char key_delim, char value_delim)
 {
@@ -831,86 +852,61 @@ static void structure_keyvalue_print(struct repo_structure *stats,
 	size_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);
 	size_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);
 
-	printf("references.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)references_count_total, value_delim);
-
-	printf("references.branches.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->refs.branches, value_delim);
-	printf("references.tags.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->refs.tags, value_delim);
-	printf("references.remotes.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->refs.remotes, value_delim);
-	printf("references.others.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->refs.others, value_delim);
-
-	printf("objects.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)object_count_total, value_delim);
-
-	printf("objects.commits.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.type_counts.commits, value_delim);
-	printf("objects.trees.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.type_counts.trees, value_delim);
-	printf("objects.blobs.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.type_counts.blobs, value_delim);
-	printf("objects.tags.count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.type_counts.tags, value_delim);
-
-	printf("objects.inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)inflated_size_total, value_delim);
-
-	printf("objects.commits.inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.inflated_sizes.commits, value_delim);
-	printf("objects.trees.inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.inflated_sizes.trees, value_delim);
-	printf("objects.blobs.inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.inflated_sizes.blobs, value_delim);
-	printf("objects.tags.inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.inflated_sizes.tags, value_delim);
-
-	printf("objects.max_inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)max_inflated_size, value_delim);
-	printf("objects.commits.max_inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_inflated_sizes.commits, value_delim);
-	printf("objects.trees.max_inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_inflated_sizes.trees, value_delim);
-	printf("objects.blobs.max_inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_inflated_sizes.blobs, value_delim);
-	printf("objects.tags.max_inflated_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);
-
-	printf("objects.disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)disk_size_total, value_delim);
-
-	printf("objects.max_disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)max_disk_size, value_delim);
-	printf("objects.commits.max_disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_disk_sizes.commits, value_delim);
-	printf("objects.trees.max_disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_disk_sizes.trees, value_delim);
-	printf("objects.blobs.max_disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_disk_sizes.blobs, value_delim);
-	printf("objects.tags.max_disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);
-
-	printf("objects.commits.max_parent_count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_commit_parent_count, value_delim);
-	printf("objects.trees.max_entry_count%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_tree_entry_count, value_delim);
-	printf("objects.blobs.max_path_length%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_blob_path_length, value_delim);
-	printf("objects.blobs.max_path_depth%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_blob_path_depth, value_delim);
-	printf("objects.tags.max_chain_depth%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.max_tag_chain_depth, value_delim);
-
-	printf("objects.commits.disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);
-	printf("objects.trees.disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.disk_sizes.trees, value_delim);
-	printf("objects.blobs.disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.disk_sizes.blobs, value_delim);
-	printf("objects.tags.disk_size%c%" PRIuMAX "%c", key_delim,
-	       (uintmax_t)stats->objects.disk_sizes.tags, value_delim);
+	print_keyvalue_size("references.count", references_count_total,
+			   key_delim, value_delim);
+
+	print_keyvalue_size("references.branches.count", stats->refs.branches,
+			   key_delim, value_delim);
+	print_keyvalue_size("references.tags.count", stats->refs.tags,
+			   key_delim, value_delim);
+	print_keyvalue_size("references.remotes.count", stats->refs.remotes,
+			   key_delim, value_delim);
+	print_keyvalue_size("references.others.count", stats->refs.others,
+			   key_delim, value_delim);
+
+	print_keyvalue_size("objects.count", object_count_total,
+			   key_delim, value_delim);
+
+	print_object_values(&stats->objects.type_counts, "count",
+			    key_delim, value_delim);
+
+	print_keyvalue_size("objects.inflated_size", inflated_size_total,
+			   key_delim, value_delim);
+
+	print_object_values(&stats->objects.inflated_sizes, "inflated_size",
+			    key_delim, value_delim);
+
+	print_keyvalue_size("objects.max_inflated_size", max_inflated_size,
+			   key_delim, value_delim);
+	print_object_values(&stats->objects.max_inflated_sizes,
+			    "max_inflated_size", key_delim, value_delim);
+
+	print_keyvalue_size("objects.disk_size", disk_size_total,
+			   key_delim, value_delim);
+
+	print_keyvalue_size("objects.max_disk_size", max_disk_size,
+			   key_delim, value_delim);
+	print_object_values(&stats->objects.max_disk_sizes, "max_disk_size",
+			    key_delim, value_delim);
+
+	print_keyvalue_size("objects.commits.max_parent_count",
+			   stats->objects.max_commit_parent_count,
+			   key_delim, value_delim);
+	print_keyvalue_size("objects.trees.max_entry_count",
+			   stats->objects.max_tree_entry_count,
+			   key_delim, value_delim);
+	print_keyvalue_size("objects.blobs.max_path_length",
+			   stats->objects.max_blob_path_length,
+			   key_delim, value_delim);
+	print_keyvalue_size("objects.blobs.max_path_depth",
+			   stats->objects.max_blob_path_depth,
+			   key_delim, value_delim);
+	print_keyvalue_size("objects.tags.max_chain_depth",
+			   stats->objects.max_tag_chain_depth,
+			   key_delim, value_delim);
+
+	print_object_values(&stats->objects.disk_sizes, "disk_size",
+			    key_delim, value_delim);
 
 	fflush(stdout);
 }
-- 
gitgitgadget


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

* [PATCH v5 11/11] repo: refine path keys for repo info
  2026-02-27 19:30       ` [PATCH v5 00/11] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                           ` (9 preceding siblings ...)
  2026-02-27 19:30         ` [PATCH v5 10/11] repo: reduce repetition in structure keyvalue output Eslam reda ragheb via GitGitGadget
@ 2026-02-27 19:30         ` Eslam reda ragheb via GitGitGadget
  2026-03-01 10:33           ` Phillip Wood
  2026-02-27 21:52         ` [PATCH v5 00/11] repo info: add category/path keys and --path-format Lucas Seiki Oshiro
  2026-03-02  5:15         ` [PATCH v6 0/6] " eslam reda via GitGitGadget
  12 siblings, 1 reply; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-02-27 19:30 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Rename path.git-prefix to path.prefix, add path.work-tree as an alias
for path.toplevel, and drop reflog/ref-file-oriented path keys.

This narrows the path surface to keys that are less tied to direct
file access while keeping tests and documentation in sync.

Also normalize prefix handling in repo_info context so path.prefix has
a stable empty-string behavior.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 Documentation/git-repo.adoc | 14 ++++-------
 builtin/repo.c              | 45 ++++++++--------------------------
 t/t1900-repo.sh             | 48 +++++++++++++++++--------------------
 3 files changed, 36 insertions(+), 71 deletions(-)

diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index b575977a4b..3d34c6edca 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -114,7 +114,7 @@ Here's a list of the available keys and the values that they return:
 `path.git-dir`::
 	The path to the git directory.
 
-`path.git-prefix`::
+`path.prefix`::
 	The path of the current working directory relative to the top-level
 	directory.
 
@@ -127,18 +127,9 @@ Here's a list of the available keys and the values that they return:
 `path.index-file`::
 	The path to the index file.
 
-`path.logs-directory`::
-	The path to the `logs` directory.
-
 `path.objects-directory`::
 	The path to the objects directory.
 
-`path.packed-refs-file`::
-	The path to the `packed-refs` file.
-
-`path.refs-directory`::
-	The path to the `refs` directory.
-
 `path.shallow-file`::
 	The path to the `shallow` file.
 
@@ -150,6 +141,9 @@ Here's a list of the available keys and the values that they return:
 	The path to the top-level working tree directory, or an empty string
 	for bare repositories.
 
+`path.work-tree`::
+	Alias for `path.toplevel`.
+
 `references.format`::
 	The reference storage format. The valid values are:
 +
diff --git a/builtin/repo.c b/builtin/repo.c
index ecd9d3aee5..9fbd13a358 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -109,10 +109,9 @@ static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)
 	return 0;
 }
 
-static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)
+static int get_path_prefix(struct repo_info *info, struct strbuf *buf)
 {
-	if (info->prefix)
-		strbuf_addstr(buf, info->prefix);
+	strbuf_addstr(buf, info->prefix);
 	return 0;
 }
 
@@ -137,39 +136,12 @@ static int get_path_index_file(struct repo_info *info, struct strbuf *buf)
 	return 0;
 }
 
-static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)
-{
-	struct strbuf path = STRBUF_INIT;
-
-	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "logs"));
-	strbuf_release(&path);
-	return 0;
-}
-
 static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)
 {
 	repo_info_add_path(info, buf, repo_get_object_directory(info->repo));
 	return 0;
 }
 
-static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)
-{
-	struct strbuf path = STRBUF_INIT;
-
-	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "packed-refs"));
-	strbuf_release(&path);
-	return 0;
-}
-
-static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)
-{
-	struct strbuf path = STRBUF_INIT;
-
-	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "refs"));
-	strbuf_release(&path);
-	return 0;
-}
-
 static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)
 {
 	struct strbuf path = STRBUF_INIT;
@@ -201,6 +173,11 @@ static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)
 	return 0;
 }
 
+static int get_path_work_tree(struct repo_info *info, struct strbuf *buf)
+{
+	return get_path_toplevel(info, buf);
+}
+
 static int get_references_format(struct repo_info *info, struct strbuf *buf)
 {
 	struct repository *repo = info->repo;
@@ -217,17 +194,15 @@ static const struct field repo_info_fields[] = {
 	{ "path.common-dir", get_path_common_dir },
 	{ "path.config-file", get_path_config_file },
 	{ "path.git-dir", get_path_git_dir },
-	{ "path.git-prefix", get_path_git_prefix },
 	{ "path.grafts-file", get_path_grafts_file },
 	{ "path.hooks-directory", get_path_hooks_directory },
 	{ "path.index-file", get_path_index_file },
-	{ "path.logs-directory", get_path_logs_directory },
 	{ "path.objects-directory", get_path_objects_directory },
-	{ "path.packed-refs-file", get_path_packed_refs_file },
-	{ "path.refs-directory", get_path_refs_directory },
+	{ "path.prefix", get_path_prefix },
 	{ "path.shallow-file", get_path_shallow_file },
 	{ "path.superproject-working-tree", get_path_superproject_working_tree },
 	{ "path.toplevel", get_path_toplevel },
+	{ "path.work-tree", get_path_work_tree },
 	{ "references.format", get_references_format },
 };
 
@@ -378,7 +353,7 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 	enum output_format format = FORMAT_KEYVALUE;
 	struct repo_info info = {
 		.repo = repo,
-		.prefix = prefix,
+		.prefix = prefix ? prefix : "",
 		.path_format = PATH_FORMAT_ABSOLUTE,
 	};
 	int all_keys = 0;
diff --git a/t/t1900-repo.sh b/t/t1900-repo.sh
index dcacf84cc3..2351b772b2 100755
--- a/t/t1900-repo.sh
+++ b/t/t1900-repo.sh
@@ -13,17 +13,15 @@ REPO_INFO_KEYS='
 	path.common-dir
 	path.config-file
 	path.git-dir
-	path.git-prefix
 	path.grafts-file
 	path.hooks-directory
 	path.index-file
-	path.logs-directory
 	path.objects-directory
-	path.packed-refs-file
-	path.refs-directory
+	path.prefix
 	path.shallow-file
 	path.superproject-working-tree
 	path.toplevel
+	path.work-tree
 	references.format
 '
 
@@ -31,17 +29,15 @@ REPO_INFO_PATH_KEYS='
 	path.common-dir
 	path.config-file
 	path.git-dir
-	path.git-prefix
 	path.grafts-file
 	path.hooks-directory
 	path.index-file
-	path.logs-directory
 	path.objects-directory
-	path.packed-refs-file
-	path.refs-directory
+	path.prefix
 	path.shallow-file
 	path.superproject-working-tree
 	path.toplevel
+	path.work-tree
 '
 
 # Test whether a key-value pair is correctly returned
@@ -172,12 +168,12 @@ test_expect_success 'path.toplevel is empty in bare repository' '
 	test_cmp expect actual
 '
 
-test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '
+test_expect_success 'path.prefix matches rev-parse --show-prefix' '
 	git init path-prefix &&
 	mkdir -p path-prefix/a/b &&
 	expected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&
-	echo "path.git-prefix=$expected_value" >expect &&
-	git -C path-prefix/a/b repo info path.git-prefix >actual &&
+	echo "path.prefix=$expected_value" >expect &&
+	git -C path-prefix/a/b repo info path.prefix >actual &&
 	test_cmp expect actual
 '
 
@@ -209,27 +205,27 @@ test_expect_success 'git-path style keys match rev-parse --git-path' '
 	git -C path-git-path repo info path.config-file >actual &&
 	test_cmp expect actual &&
 
-	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&
-	echo "path.logs-directory=$expected_value" >expect &&
-	git -C path-git-path repo info path.logs-directory >actual &&
-	test_cmp expect actual &&
-
-	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&
-	echo "path.packed-refs-file=$expected_value" >expect &&
-	git -C path-git-path repo info path.packed-refs-file >actual &&
-	test_cmp expect actual &&
-
-	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&
-	echo "path.refs-directory=$expected_value" >expect &&
-	git -C path-git-path repo info path.refs-directory >actual &&
-	test_cmp expect actual &&
-
 	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&
 	echo "path.shallow-file=$expected_value" >expect &&
 	git -C path-git-path repo info path.shallow-file >actual &&
 	test_cmp expect actual
 '
 
+test_expect_success 'path.work-tree matches path.toplevel' '
+	git init path-work-tree &&
+	expected_value=$(git -C path-work-tree rev-parse --show-toplevel) &&
+	echo "path.work-tree=$expected_value" >expect &&
+	git -C path-work-tree repo info path.work-tree >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.work-tree is empty in bare repository' '
+	git init --bare bare-path-work-tree &&
+	echo "path.work-tree=" >expect &&
+	git -C bare-path-work-tree repo info path.work-tree >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'path.superproject-working-tree is empty when not a submodule' '
 	git init path-superproject &&
 	echo "path.superproject-working-tree=" >expect &&
-- 
gitgitgadget

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

* Re: [PATCH v4 02/10] repo: add path keys to repo info
  2026-02-27  9:04         ` Phillip Wood
@ 2026-02-27 19:51           ` Junio C Hamano
  2026-03-01 10:36             ` Phillip Wood
  0 siblings, 1 reply; 68+ messages in thread
From: Junio C Hamano @ 2026-02-27 19:51 UTC (permalink / raw)
  To: Phillip Wood; +Cc: Eslam reda ragheb via GitGitGadget, git, eslam reda

Phillip Wood <phillip.wood123@gmail.com> writes:

>> +	{ "path.common-dir", get_path_common_dir },
>> +	{ "path.config-file", get_path_config_file },
>> +	{ "path.git-dir", get_path_git_dir },
>> +	{ "path.git-prefix", get_path_git_prefix },
>
> I'm not sure about calling this 'git-prefix', 'prefix' might be more 
> appropriate as it is about prefixing paths in the worktree rather than 
> the git_dir.

True.

>> +	{ "path.grafts-file", get_path_grafts_file },
>> +	{ "path.hooks-directory", get_path_hooks_directory },
>> +	{ "path.index-file", get_path_index_file },
>> +	{ "path.logs-directory", get_path_logs_directory },
>
> We're moving away from file based refs and reflogs so I'm not sure 
> adding this, pick-refs-file or refs-directory is a good idea as we 
> should not be encouraging people to access these files directly.
>
>> +	{ "path.objects-directory", get_path_objects_directory },
>> +	{ "path.packed-refs-file", get_path_packed_refs_file },
>> +	{ "path.refs-directory", get_path_refs_directory },

The same comment applies to these entries as well, as the pluggable
object database support is just beyond the horizon if I understand
correctly.

>> +	{ "path.shallow-file", get_path_shallow_file },
>> +	{ "path.superproject-working-tree", get_path_superproject_working_tree },
>> +	{ "path.toplevel", get_path_toplevel },
>
> 'path.toplevel' matches the git-rev-parse option but 'path.work-tree' 
> might be more descriptive?

I think the "git repo" thrust comes primarily from being unfamiliar
with "rev-parse" (and I wouldn't particularly encourage new people
to become familiar with it---it grew pretty much organically driven
by scripting needs without taking UI cleanliness into consideration
very much), so not many folks would find it disturbing that
"--toplevel" corresponds to "topOfTheWorkingTree".  Given that we
have a token to ask for superproject's working tree, giving a name
made after the same phrasing philosophy for the current project's
working tree would be a good thing, i.e., "path.working-tree".

> What happens if 'path.toplevel' is requested in a bare repository?

FWIW "git rev-parse --show-toplevel" dies with "must be run in a
work tree".  Better or worse, 

	rm -fr new
	git init new
	cd new/.git && git rev-parse --show-toplevel

also dies the same way, which I am not sure we want to inherit when
we are making a new interrogator command.

Thanks.



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

* Re: [PATCH v5 01/11] repo: teach info context and category keys
  2026-02-27 19:30         ` [PATCH v5 01/11] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
@ 2026-02-27 21:42           ` Lucas Seiki Oshiro
  0 siblings, 0 replies; 68+ messages in thread
From: Lucas Seiki Oshiro @ 2026-02-27 21:42 UTC (permalink / raw)
  To: Eslam reda ragheb via GitGitGadget; +Cc: git, Phillip Wood, eslam reda


> Also teach git repo info to accept category names (for example,
> layout) and expand them to matching key.* entries in request
> order.

If you have a patch where its description says "Do A. Also do B"
when A and B are unrelated, there are good chances that it should
be splitted into two commits. This patch is an example of that.

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

* Re: [PATCH v5 00/11] repo info: add category/path keys and --path-format
  2026-02-27 19:30       ` [PATCH v5 00/11] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                           ` (10 preceding siblings ...)
  2026-02-27 19:30         ` [PATCH v5 11/11] repo: refine path keys for repo info Eslam reda ragheb via GitGitGadget
@ 2026-02-27 21:52         ` Lucas Seiki Oshiro
  2026-03-02  5:15         ` [PATCH v6 0/6] " eslam reda via GitGitGadget
  12 siblings, 0 replies; 68+ messages in thread
From: Lucas Seiki Oshiro @ 2026-02-27 21:52 UTC (permalink / raw)
  To: eslam reda via GitGitGadget
  Cc: git, Phillip Wood, eslam reda, Junio C Hamano, Justin Tobler


> * No git repo structure feature changes.

Patch 4/11 changes git repo structure

> * No t1901 structure test changes.

Patch 8/11 changes t1901

> * No structure metrics/docs additions.

Patch 9/11 changes git-repo.adoc

> Commit structure
> ================
> 
> * repo: teach info context and category keys
> * repo: add path keys to repo info
> * repo: add --path-format for info path output
> * t1900: cover repo info path keys and path-format
> * docs: describe repo info path keys

Only 5 commits are described here, while you sent 11 patches in this series.

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

* Re: [PATCH v5 11/11] repo: refine path keys for repo info
  2026-02-27 19:30         ` [PATCH v5 11/11] repo: refine path keys for repo info Eslam reda ragheb via GitGitGadget
@ 2026-03-01 10:33           ` Phillip Wood
  0 siblings, 0 replies; 68+ messages in thread
From: Phillip Wood @ 2026-03-01 10:33 UTC (permalink / raw)
  To: Eslam reda ragheb via GitGitGadget, git; +Cc: eslam reda

Hi Eslam

On 27/02/2026 19:30, Eslam reda ragheb via GitGitGadget wrote:
> From: Eslam reda ragheb <eslam.reda.div@gmail.com>
> 
> Rename path.git-prefix to path.prefix, add path.work-tree as an alias
> for path.toplevel, and drop reflog/ref-file-oriented path keys.
> 
> This narrows the path surface to keys that are less tied to direct
> file access while keeping tests and documentation in sync.
> 
> Also normalize prefix handling in repo_info context so path.prefix has
> a stable empty-string behavior.

When you change a patch series based on a reviewer's feedback, you 
should use "git rebase -i" to edit or fixup the existing commits rather 
than adding new changes on top. That keeps the history cleaner as we 
don't need to know "I implemented it like this and than changed it to 
that". It also makes the patch series easier to review as reviewers 
don't waste their time commenting on changes in one patch that are then 
deleted in a later patch.

Thanks

Phillip

> Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
> ---
>   Documentation/git-repo.adoc | 14 ++++-------
>   builtin/repo.c              | 45 ++++++++--------------------------
>   t/t1900-repo.sh             | 48 +++++++++++++++++--------------------
>   3 files changed, 36 insertions(+), 71 deletions(-)
> 
> diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
> index b575977a4b..3d34c6edca 100644
> --- a/Documentation/git-repo.adoc
> +++ b/Documentation/git-repo.adoc
> @@ -114,7 +114,7 @@ Here's a list of the available keys and the values that they return:
>   `path.git-dir`::
>   	The path to the git directory.
>   
> -`path.git-prefix`::
> +`path.prefix`::
>   	The path of the current working directory relative to the top-level
>   	directory.
>   
> @@ -127,18 +127,9 @@ Here's a list of the available keys and the values that they return:
>   `path.index-file`::
>   	The path to the index file.
>   
> -`path.logs-directory`::
> -	The path to the `logs` directory.
> -
>   `path.objects-directory`::
>   	The path to the objects directory.
>   
> -`path.packed-refs-file`::
> -	The path to the `packed-refs` file.
> -
> -`path.refs-directory`::
> -	The path to the `refs` directory.
> -
>   `path.shallow-file`::
>   	The path to the `shallow` file.
>   
> @@ -150,6 +141,9 @@ Here's a list of the available keys and the values that they return:
>   	The path to the top-level working tree directory, or an empty string
>   	for bare repositories.
>   
> +`path.work-tree`::
> +	Alias for `path.toplevel`.
> +
>   `references.format`::
>   	The reference storage format. The valid values are:
>   +
> diff --git a/builtin/repo.c b/builtin/repo.c
> index ecd9d3aee5..9fbd13a358 100644
> --- a/builtin/repo.c
> +++ b/builtin/repo.c
> @@ -109,10 +109,9 @@ static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)
>   	return 0;
>   }
>   
> -static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)
> +static int get_path_prefix(struct repo_info *info, struct strbuf *buf)
>   {
> -	if (info->prefix)
> -		strbuf_addstr(buf, info->prefix);
> +	strbuf_addstr(buf, info->prefix);
>   	return 0;
>   }
>   
> @@ -137,39 +136,12 @@ static int get_path_index_file(struct repo_info *info, struct strbuf *buf)
>   	return 0;
>   }
>   
> -static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)
> -{
> -	struct strbuf path = STRBUF_INIT;
> -
> -	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "logs"));
> -	strbuf_release(&path);
> -	return 0;
> -}
> -
>   static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)
>   {
>   	repo_info_add_path(info, buf, repo_get_object_directory(info->repo));
>   	return 0;
>   }
>   
> -static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)
> -{
> -	struct strbuf path = STRBUF_INIT;
> -
> -	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "packed-refs"));
> -	strbuf_release(&path);
> -	return 0;
> -}
> -
> -static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)
> -{
> -	struct strbuf path = STRBUF_INIT;
> -
> -	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "refs"));
> -	strbuf_release(&path);
> -	return 0;
> -}
> -
>   static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)
>   {
>   	struct strbuf path = STRBUF_INIT;
> @@ -201,6 +173,11 @@ static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)
>   	return 0;
>   }
>   
> +static int get_path_work_tree(struct repo_info *info, struct strbuf *buf)
> +{
> +	return get_path_toplevel(info, buf);
> +}
> +
>   static int get_references_format(struct repo_info *info, struct strbuf *buf)
>   {
>   	struct repository *repo = info->repo;
> @@ -217,17 +194,15 @@ static const struct field repo_info_fields[] = {
>   	{ "path.common-dir", get_path_common_dir },
>   	{ "path.config-file", get_path_config_file },
>   	{ "path.git-dir", get_path_git_dir },
> -	{ "path.git-prefix", get_path_git_prefix },
>   	{ "path.grafts-file", get_path_grafts_file },
>   	{ "path.hooks-directory", get_path_hooks_directory },
>   	{ "path.index-file", get_path_index_file },
> -	{ "path.logs-directory", get_path_logs_directory },
>   	{ "path.objects-directory", get_path_objects_directory },
> -	{ "path.packed-refs-file", get_path_packed_refs_file },
> -	{ "path.refs-directory", get_path_refs_directory },
> +	{ "path.prefix", get_path_prefix },
>   	{ "path.shallow-file", get_path_shallow_file },
>   	{ "path.superproject-working-tree", get_path_superproject_working_tree },
>   	{ "path.toplevel", get_path_toplevel },
> +	{ "path.work-tree", get_path_work_tree },
>   	{ "references.format", get_references_format },
>   };
>   
> @@ -378,7 +353,7 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
>   	enum output_format format = FORMAT_KEYVALUE;
>   	struct repo_info info = {
>   		.repo = repo,
> -		.prefix = prefix,
> +		.prefix = prefix ? prefix : "",
>   		.path_format = PATH_FORMAT_ABSOLUTE,
>   	};
>   	int all_keys = 0;
> diff --git a/t/t1900-repo.sh b/t/t1900-repo.sh
> index dcacf84cc3..2351b772b2 100755
> --- a/t/t1900-repo.sh
> +++ b/t/t1900-repo.sh
> @@ -13,17 +13,15 @@ REPO_INFO_KEYS='
>   	path.common-dir
>   	path.config-file
>   	path.git-dir
> -	path.git-prefix
>   	path.grafts-file
>   	path.hooks-directory
>   	path.index-file
> -	path.logs-directory
>   	path.objects-directory
> -	path.packed-refs-file
> -	path.refs-directory
> +	path.prefix
>   	path.shallow-file
>   	path.superproject-working-tree
>   	path.toplevel
> +	path.work-tree
>   	references.format
>   '
>   
> @@ -31,17 +29,15 @@ REPO_INFO_PATH_KEYS='
>   	path.common-dir
>   	path.config-file
>   	path.git-dir
> -	path.git-prefix
>   	path.grafts-file
>   	path.hooks-directory
>   	path.index-file
> -	path.logs-directory
>   	path.objects-directory
> -	path.packed-refs-file
> -	path.refs-directory
> +	path.prefix
>   	path.shallow-file
>   	path.superproject-working-tree
>   	path.toplevel
> +	path.work-tree
>   '
>   
>   # Test whether a key-value pair is correctly returned
> @@ -172,12 +168,12 @@ test_expect_success 'path.toplevel is empty in bare repository' '
>   	test_cmp expect actual
>   '
>   
> -test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '
> +test_expect_success 'path.prefix matches rev-parse --show-prefix' '
>   	git init path-prefix &&
>   	mkdir -p path-prefix/a/b &&
>   	expected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&
> -	echo "path.git-prefix=$expected_value" >expect &&
> -	git -C path-prefix/a/b repo info path.git-prefix >actual &&
> +	echo "path.prefix=$expected_value" >expect &&
> +	git -C path-prefix/a/b repo info path.prefix >actual &&
>   	test_cmp expect actual
>   '
>   
> @@ -209,27 +205,27 @@ test_expect_success 'git-path style keys match rev-parse --git-path' '
>   	git -C path-git-path repo info path.config-file >actual &&
>   	test_cmp expect actual &&
>   
> -	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&
> -	echo "path.logs-directory=$expected_value" >expect &&
> -	git -C path-git-path repo info path.logs-directory >actual &&
> -	test_cmp expect actual &&
> -
> -	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&
> -	echo "path.packed-refs-file=$expected_value" >expect &&
> -	git -C path-git-path repo info path.packed-refs-file >actual &&
> -	test_cmp expect actual &&
> -
> -	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&
> -	echo "path.refs-directory=$expected_value" >expect &&
> -	git -C path-git-path repo info path.refs-directory >actual &&
> -	test_cmp expect actual &&
> -
>   	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&
>   	echo "path.shallow-file=$expected_value" >expect &&
>   	git -C path-git-path repo info path.shallow-file >actual &&
>   	test_cmp expect actual
>   '
>   
> +test_expect_success 'path.work-tree matches path.toplevel' '
> +	git init path-work-tree &&
> +	expected_value=$(git -C path-work-tree rev-parse --show-toplevel) &&
> +	echo "path.work-tree=$expected_value" >expect &&
> +	git -C path-work-tree repo info path.work-tree >actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'path.work-tree is empty in bare repository' '
> +	git init --bare bare-path-work-tree &&
> +	echo "path.work-tree=" >expect &&
> +	git -C bare-path-work-tree repo info path.work-tree >actual &&
> +	test_cmp expect actual
> +'
> +
>   test_expect_success 'path.superproject-working-tree is empty when not a submodule' '
>   	git init path-superproject &&
>   	echo "path.superproject-working-tree=" >expect &&


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

* Re: [PATCH v4 02/10] repo: add path keys to repo info
  2026-02-27 19:51           ` Junio C Hamano
@ 2026-03-01 10:36             ` Phillip Wood
  2026-03-02  6:42               ` Junio C Hamano
  0 siblings, 1 reply; 68+ messages in thread
From: Phillip Wood @ 2026-03-01 10:36 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Eslam reda ragheb via GitGitGadget, git, eslam reda

On 27/02/2026 19:51, Junio C Hamano wrote:
> Phillip Wood <phillip.wood123@gmail.com> writes:
> 
>>> +	{ "path.objects-directory", get_path_objects_directory },
>>> +	{ "path.packed-refs-file", get_path_packed_refs_file },
>>> +	{ "path.refs-directory", get_path_refs_directory },
> 
> The same comment applies to these entries as well, as the pluggable
> object database support is just beyond the horizon if I understand
> correctly.

Good point. Also what is the "shallow file" below and should scripts be 
poking it directly?

>>> +	{ "path.shallow-file", get_path_shallow_file },
>>> +	{ "path.superproject-working-tree", get_path_superproject_working_tree },
>>> +	{ "path.toplevel", get_path_toplevel },
>>
>> 'path.toplevel' matches the git-rev-parse option but 'path.work-tree'
>> might be more descriptive?
> 
> I think the "git repo" thrust comes primarily from being unfamiliar
> with "rev-parse" (and I wouldn't particularly encourage new people
> to become familiar with it---it grew pretty much organically driven
> by scripting needs without taking UI cleanliness into consideration
> very much), so not many folks would find it disturbing that
> "--toplevel" corresponds to "topOfTheWorkingTree".  Given that we
> have a token to ask for superproject's working tree, giving a name
> made after the same phrasing philosophy for the current project's
> working tree would be a good thing, i.e., "path.working-tree".

That's a good idea

>> What happens if 'path.toplevel' is requested in a bare repository?
> 
> FWIW "git rev-parse --show-toplevel" dies with "must be run in a
> work tree".  Better or worse,
> 
> 	rm -fr new
> 	git init new
> 	cd new/.git && git rev-parse --show-toplevel
> 
> also dies the same way, which I am not sure we want to inherit when
> we are making a new interrogator command.

Yes, I think printing an empty value after the key would be better - I 
don't think there are any paths where we care about the distinction 
between NULL and ""

Thanks

Phillip


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

* [PATCH v6 0/6] repo info: add category/path keys and --path-format
  2026-02-27 19:30       ` [PATCH v5 00/11] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
                           ` (11 preceding siblings ...)
  2026-02-27 21:52         ` [PATCH v5 00/11] repo info: add category/path keys and --path-format Lucas Seiki Oshiro
@ 2026-03-02  5:15         ` eslam reda via GitGitGadget
  2026-03-02  5:15           ` [PATCH v6 1/6] repo: introduce repo_info context plumbing Eslam reda ragheb via GitGitGadget
                             ` (8 more replies)
  12 siblings, 9 replies; 68+ messages in thread
From: eslam reda via GitGitGadget @ 2026-03-02  5:15 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, Lucas Seiki Oshiro, eslam reda


This series now focuses only on git repo info improvements.
===========================================================

It introduces category-aware key requests, adds path-oriented keys (path.*),
and adds --path-format=(absolute|relative) so scripts can request stable
path rendering behavior.


What this PR does
=================

For git repo info, this series:

 * introduces explicit repo_info context plumbing,
 * adds category-key expansion (for example, layout expands to layout.*),
 * adds path-oriented keys (path.*) for common repository locations,
 * adds --path-format=(absolute|relative) to control path output style.

Tests and documentation are updated accordingly.


What this PR does NOT do
========================

 * No git repo structure feature changes.
 * No t1901 structure test changes.
 * No structure metrics/docs additions.


Key naming/scope adjustments from review
========================================

 * renamed path.git-prefix to path.prefix,
 * added path.working-tree (alias for path.toplevel),
 * removed file-layout-oriented keys:
   * path.logs-directory
   * path.packed-refs-file
   * path.refs-directory
   * path.shallow-file
 * for bare repositories, work-tree style keys return empty values.


Commit structure
================

 * repo: introduce repo_info context plumbing
 * repo: support category requests in repo info
 * repo: add path keys to repo info
 * repo: add --path-format for info path output
 * t1900: cover repo info path keys and path-format
 * docs: describe repo info path keys

All commits are signed off with the same real-name identity.


Changes since previous revision
===============================

 * dropped all repo structure code/tests/docs from this branch,
 * split context plumbing and category expansion into separate commits,
 * rebased and cleaned history to avoid fix-on-fix commits,
 * aligned key naming/scope with review feedback.


Validation
==========

Focused (Linux Docker):

 * make -C t test T=t1900-repo.sh → passed (53/53)

Eslam reda ragheb (6):
  repo: introduce repo_info context plumbing
  repo: support category requests in repo info
  repo: add path keys to repo info
  repo: add --path-format for info path output
  t1900: cover repo info path keys and path-format
  docs: describe repo info path keys

 Documentation/git-repo.adoc |  58 +++++++++-
 builtin/repo.c              | 209 +++++++++++++++++++++++++++++++++---
 t/t1900-repo.sh             | 194 +++++++++++++++++++++++++++++++++
 3 files changed, 441 insertions(+), 20 deletions(-)


base-commit: 2cc71917514657b93014134350864f4849edfc83
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2208%2Feslam-reda-div%2Fgsoc-contribute-v6
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2208/eslam-reda-div/gsoc-contribute-v6
Pull-Request: https://github.com/git/git/pull/2208

Range-diff vs v5:

  1:  99c8058298 !  1:  bddea1a22e repo: teach info context and category keys
     @@ Metadata
      Author: Eslam reda ragheb <eslam.reda.div@gmail.com>
      
       ## Commit message ##
     -    repo: teach info context and category keys
     +    repo: introduce repo_info context plumbing
      
     -    Introduce an explicit repo_info context for the repo info codepath
     -    and thread it through value lookups and field printing.
     +    Introduce a repo_info context and thread it through get_value_fn,
     +    field lookup, and value-printing helpers.
      
     -    This removes direct coupling from these helpers to ad-hoc
     -    repository globals and makes key retrieval logic easier to extend
     -    safely.
     -
     -    Also teach git repo info to accept category names (for example,
     -    layout) and expand them to matching key.* entries in request
     -    order.
     -
     -    This improves script ergonomics while preserving existing behavior
     -    for explicit keys and clear errors for unknown names.
     +    This prepares repo info for fields that need invocation-specific
     +    context in addition to the repository handle.
      
          Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
      
     @@ builtin/repo.c: struct field {
       	strbuf_addstr(buf,
       		      ref_storage_format_to_name(repo->ref_storage_format));
       	return 0;
     -@@ builtin/repo.c: static get_value_fn *get_value_fn_for_key(const char *key)
     - 	return found ? found->get_value : NULL;
     - }
     - 
     -+static void print_field(enum output_format format, const char *key,
     -+			const char *value);
     -+
     -+static int print_category_fields(const char *category,
     -+				 struct repo_info *info,
     -+				 enum output_format format,
     -+				 struct strbuf *valbuf)
     -+{
     -+	int found = 0;
     -+	size_t category_len = strlen(category);
     -+
     -+	for (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {
     -+		const struct field *field = &repo_info_fields[i];
     -+
     -+		if (!starts_with(field->key, category) ||
     -+		    field->key[category_len] != '.')
     -+			continue;
     -+
     -+		strbuf_reset(valbuf);
     -+		field->get_value(info, valbuf);
     -+		print_field(format, field->key, valbuf->buf);
     -+		found = 1;
     -+	}
     -+
     -+	return found;
     -+}
     -+
     - static void print_field(enum output_format format, const char *key,
     - 			const char *value)
     - {
      @@ builtin/repo.c: static void print_field(enum output_format format, const char *key,
       }
       
     @@ builtin/repo.c: static void print_field(enum output_format format, const char *k
       {
       	int ret = 0;
      @@ builtin/repo.c: static int print_fields(int argc, const char **argv,
     - 
     - 		get_value = get_value_fn_for_key(key);
     - 
     --		if (!get_value) {
     --			ret = error(_("key '%s' not found"), key);
     -+		if (get_value) {
     -+			strbuf_reset(&valbuf);
     -+			get_value(info, &valbuf);
     -+			print_field(format, key, valbuf.buf);
     - 			continue;
       		}
       
     --		strbuf_reset(&valbuf);
     + 		strbuf_reset(&valbuf);
      -		get_value(repo, &valbuf);
     --		print_field(format, key, valbuf.buf);
     -+		if (!print_category_fields(key, info, format, &valbuf))
     -+			ret = error(_("key '%s' not found"), key);
     ++		get_value(info, &valbuf);
     + 		print_field(format, key, valbuf.buf);
       	}
       
     - 	strbuf_release(&valbuf);
     +@@ builtin/repo.c: static int print_fields(int argc, const char **argv,
       	return ret;
       }
       
     @@ builtin/repo.c: static int print_all_fields(struct repository *repo,
      @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char *prefix,
       			 struct repository *repo)
       {
     - 	enum output_format format = FORMAT_KEYVALUE;
     + 	enum output_format format = FORMAT_NEWLINE_TERMINATED;
      +	struct repo_info info = {
      +		.repo = repo,
      +		.prefix = prefix,
      +	};
       	int all_keys = 0;
     + 	int show_keys = 0;
       	struct option options[] = {
     - 		OPT_CALLBACK_F(0, "format", &format, N_("format"),
      @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char *prefix,
       		die(_("--all and <key> cannot be used together"));
       
  4:  504d9cf7a0 !  2:  2369608976 repo: add structure max object size metrics
     @@ Metadata
      Author: Eslam reda ragheb <eslam.reda.div@gmail.com>
      
       ## Commit message ##
     -    repo: add structure max object size metrics
     +    repo: support category requests in repo info
      
     -    Extend git repo structure with maximum inflated and on-disk object
     -    sizes, both per type and overall max values.
     +    Teach repo info to accept category names (for example, layout)
     +    and expand them to matching key.* entries in request order.
      
     -    This complements existing totals by highlighting outliers that
     -    often drive repository bloat analysis.
     -
     -    The implementation updates object counting to track per-type maxima
     -    while walking reachable objects.
     -
     -    It exposes those values in both table and keyvalue formats for
     -    scripts and human output.
     +    Explicit keys keep their existing behavior; unknown keys or
     +    categories still report clear errors.
      
          Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
      
       ## builtin/repo.c ##
     -@@ builtin/repo.c: struct object_values {
     - struct object_stats {
     - 	struct object_values type_counts;
     - 	struct object_values inflated_sizes;
     -+	struct object_values max_inflated_sizes;
     - 	struct object_values disk_sizes;
     -+	struct object_values max_disk_sizes;
     - };
     - 
     - struct repo_structure {
     -@@ builtin/repo.c: static inline size_t get_total_object_values(struct object_values *values)
     - 	return values->tags + values->commits + values->trees + values->blobs;
     +@@ builtin/repo.c: static get_value_fn *get_value_fn_for_key(const char *key)
     + 	return found ? found->get_value : NULL;
       }
       
     -+static inline size_t get_max_object_value(struct object_values *values)
     ++static void print_field(enum output_format format, const char *key,
     ++			const char *value);
     ++
     ++static int print_category_fields(const char *category,
     ++				 struct repo_info *info,
     ++				 enum output_format format,
     ++				 struct strbuf *valbuf)
      +{
     -+	size_t max = values->commits;
     ++	int found = 0;
     ++	size_t category_len = strlen(category);
      +
     -+	if (values->trees > max)
     -+		max = values->trees;
     -+	if (values->blobs > max)
     -+		max = values->blobs;
     -+	if (values->tags > max)
     -+		max = values->tags;
     ++	for (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {
     ++		const struct field *field = &repo_info_fields[i];
      +
     -+	return max;
     -+}
     ++		if (!starts_with(field->key, category) ||
     ++		    field->key[category_len] != '.')
     ++			continue;
      +
     - static void stats_table_setup_structure(struct stats_table *table,
     - 					struct repo_structure *stats)
     - {
     -@@ builtin/repo.c: static void stats_table_setup_structure(struct stats_table *table,
     - 			      "    * %s", _("Blobs"));
     - 	stats_table_size_addf(table, objects->disk_sizes.tags,
     - 			      "    * %s", _("Tags"));
     ++		strbuf_reset(valbuf);
     ++		field->get_value(info, valbuf);
     ++		print_field(format, field->key, valbuf->buf);
     ++		found = 1;
     ++	}
      +
     -+	stats_table_size_addf(table, objects->max_inflated_sizes.commits,
     -+			      "  * %s", _("Largest commit"));
     -+	stats_table_size_addf(table, objects->max_inflated_sizes.trees,
     -+			      "  * %s", _("Largest tree"));
     -+	stats_table_size_addf(table, objects->max_inflated_sizes.blobs,
     -+			      "  * %s", _("Largest blob"));
     -+	stats_table_size_addf(table, objects->max_inflated_sizes.tags,
     -+			      "  * %s", _("Largest tag"));
     ++	return found;
     ++}
      +
     -+	stats_table_size_addf(table, get_max_object_value(&objects->max_disk_sizes),
     -+			      "  * %s", _("Largest disk size"));
     -+	stats_table_size_addf(table, objects->max_disk_sizes.commits,
     -+			      "    * %s", _("Commits"));
     -+	stats_table_size_addf(table, objects->max_disk_sizes.trees,
     -+			      "    * %s", _("Trees"));
     -+	stats_table_size_addf(table, objects->max_disk_sizes.blobs,
     -+			      "    * %s", _("Blobs"));
     -+	stats_table_size_addf(table, objects->max_disk_sizes.tags,
     -+			      "    * %s", _("Tags"));
     - }
     - 
     - static void stats_table_print_structure(const struct stats_table *table)
     -@@ builtin/repo.c: static void stats_table_clear(struct stats_table *table)
     - static void structure_keyvalue_print(struct repo_structure *stats,
     - 				     char key_delim, char value_delim)
     + static void print_field(enum output_format format, const char *key,
     + 			const char *value)
       {
     -+	size_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);
     -+	size_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);
     -+
     - 	printf("references.branches.count%c%" PRIuMAX "%c", key_delim,
     - 	       (uintmax_t)stats->refs.branches, value_delim);
     - 	printf("references.tags.count%c%" PRIuMAX "%c", key_delim,
     -@@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stats,
     - 	printf("objects.tags.inflated_size%c%" PRIuMAX "%c", key_delim,
     - 	       (uintmax_t)stats->objects.inflated_sizes.tags, value_delim);
     +@@ builtin/repo.c: static int print_fields(int argc, const char **argv,
       
     -+	printf("objects.max_inflated_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)max_inflated_size, value_delim);
     -+	printf("objects.commits.max_inflated_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_inflated_sizes.commits, value_delim);
     -+	printf("objects.trees.max_inflated_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_inflated_sizes.trees, value_delim);
     -+	printf("objects.blobs.max_inflated_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_inflated_sizes.blobs, value_delim);
     -+	printf("objects.tags.max_inflated_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);
     -+
     -+	printf("objects.max_disk_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)max_disk_size, value_delim);
     -+	printf("objects.commits.max_disk_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_disk_sizes.commits, value_delim);
     -+	printf("objects.trees.max_disk_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_disk_sizes.trees, value_delim);
     -+	printf("objects.blobs.max_disk_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_disk_sizes.blobs, value_delim);
     -+	printf("objects.tags.max_disk_size%c%" PRIuMAX "%c", key_delim,
     -+	       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);
     -+
     - 	printf("objects.commits.disk_size%c%" PRIuMAX "%c", key_delim,
     - 	       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);
     - 	printf("objects.trees.disk_size%c%" PRIuMAX "%c", key_delim,
     -@@ builtin/repo.c: static int count_objects(const char *path UNUSED, struct oid_array *oids,
     - 	struct object_stats *stats = data->stats;
     - 	size_t inflated_total = 0;
     - 	size_t disk_total = 0;
     -+	size_t max_inflated = 0;
     -+	size_t max_disk = 0;
     - 	size_t object_count;
     + 		get_value = get_value_fn_for_key(key);
       
     - 	for (size_t i = 0; i < oids->nr; i++) {
     -@@ builtin/repo.c: static int count_objects(const char *path UNUSED, struct oid_array *oids,
     - 						  OBJECT_INFO_SKIP_FETCH_OBJECT |
     - 						  OBJECT_INFO_QUICK) < 0)
     +-		if (!get_value) {
     +-			ret = error(_("key '%s' not found"), key);
     ++		if (get_value) {
     ++			strbuf_reset(&valbuf);
     ++			get_value(info, &valbuf);
     ++			print_field(format, key, valbuf.buf);
       			continue;
     -+		if (disk < 0)
     -+			continue;
     + 		}
       
     - 		inflated_total += inflated;
     --		disk_total += disk;
     -+		disk_total += (size_t)disk;
     -+		if (inflated > max_inflated)
     -+			max_inflated = inflated;
     -+		if ((size_t)disk > max_disk)
     -+			max_disk = (size_t)disk;
     +-		strbuf_reset(&valbuf);
     +-		get_value(info, &valbuf);
     +-		print_field(format, key, valbuf.buf);
     ++		if (!print_category_fields(key, info, format, &valbuf))
     ++			ret = error(_("key '%s' not found"), key);
       	}
       
     - 	switch (type) {
     - 	case OBJ_TAG:
     - 		stats->type_counts.tags += oids->nr;
     - 		stats->inflated_sizes.tags += inflated_total;
     -+		if (max_inflated > stats->max_inflated_sizes.tags)
     -+			stats->max_inflated_sizes.tags = max_inflated;
     - 		stats->disk_sizes.tags += disk_total;
     -+		if (max_disk > stats->max_disk_sizes.tags)
     -+			stats->max_disk_sizes.tags = max_disk;
     - 		break;
     - 	case OBJ_COMMIT:
     - 		stats->type_counts.commits += oids->nr;
     - 		stats->inflated_sizes.commits += inflated_total;
     -+		if (max_inflated > stats->max_inflated_sizes.commits)
     -+			stats->max_inflated_sizes.commits = max_inflated;
     - 		stats->disk_sizes.commits += disk_total;
     -+		if (max_disk > stats->max_disk_sizes.commits)
     -+			stats->max_disk_sizes.commits = max_disk;
     - 		break;
     - 	case OBJ_TREE:
     - 		stats->type_counts.trees += oids->nr;
     - 		stats->inflated_sizes.trees += inflated_total;
     -+		if (max_inflated > stats->max_inflated_sizes.trees)
     -+			stats->max_inflated_sizes.trees = max_inflated;
     - 		stats->disk_sizes.trees += disk_total;
     -+		if (max_disk > stats->max_disk_sizes.trees)
     -+			stats->max_disk_sizes.trees = max_disk;
     - 		break;
     - 	case OBJ_BLOB:
     - 		stats->type_counts.blobs += oids->nr;
     - 		stats->inflated_sizes.blobs += inflated_total;
     -+		if (max_inflated > stats->max_inflated_sizes.blobs)
     -+			stats->max_inflated_sizes.blobs = max_inflated;
     - 		stats->disk_sizes.blobs += disk_total;
     -+		if (max_disk > stats->max_disk_sizes.blobs)
     -+			stats->max_disk_sizes.blobs = max_disk;
     - 		break;
     - 	default:
     - 		BUG("invalid object type");
     + 	strbuf_release(&valbuf);
  2:  6d5b9ff075 !  3:  477333a94c repo: add path keys to repo info
     @@ builtin/repo.c: static int get_object_format(struct repo_info *info, struct strb
      +	return 0;
      +}
      +
     -+static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)
     ++static int get_path_prefix(struct repo_info *info, struct strbuf *buf)
      +{
     -+	if (info->prefix)
     -+		strbuf_addstr(buf, info->prefix);
     ++	strbuf_addstr(buf, info->prefix);
      +	return 0;
      +}
      +
     @@ builtin/repo.c: static int get_object_format(struct repo_info *info, struct strb
      +	return 0;
      +}
      +
     -+static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	struct strbuf path = STRBUF_INIT;
     -+
     -+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "logs"));
     -+	strbuf_release(&path);
     -+	return 0;
     -+}
     -+
      +static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)
      +{
      +	repo_info_add_path(info, buf, repo_get_object_directory(info->repo));
      +	return 0;
      +}
      +
     -+static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	struct strbuf path = STRBUF_INIT;
     -+
     -+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "packed-refs"));
     -+	strbuf_release(&path);
     -+	return 0;
     -+}
     -+
     -+static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	struct strbuf path = STRBUF_INIT;
     -+
     -+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "refs"));
     -+	strbuf_release(&path);
     -+	return 0;
     -+}
     -+
     -+static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)
     -+{
     -+	struct strbuf path = STRBUF_INIT;
     -+
     -+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "shallow"));
     -+	strbuf_release(&path);
     -+	return 0;
     -+}
     -+
      +static int get_path_superproject_working_tree(struct repo_info *info,
      +					     struct strbuf *buf)
      +{
     @@ builtin/repo.c: static int get_object_format(struct repo_info *info, struct strb
      +
      +	return 0;
      +}
     ++
     ++static int get_path_work_tree(struct repo_info *info, struct strbuf *buf)
     ++{
     ++	return get_path_toplevel(info, buf);
     ++}
      +
       static int get_references_format(struct repo_info *info, struct strbuf *buf)
       {
     @@ builtin/repo.c: static const struct field repo_info_fields[] = {
      +	{ "path.common-dir", get_path_common_dir },
      +	{ "path.config-file", get_path_config_file },
      +	{ "path.git-dir", get_path_git_dir },
     -+	{ "path.git-prefix", get_path_git_prefix },
      +	{ "path.grafts-file", get_path_grafts_file },
      +	{ "path.hooks-directory", get_path_hooks_directory },
      +	{ "path.index-file", get_path_index_file },
     -+	{ "path.logs-directory", get_path_logs_directory },
      +	{ "path.objects-directory", get_path_objects_directory },
     -+	{ "path.packed-refs-file", get_path_packed_refs_file },
     -+	{ "path.refs-directory", get_path_refs_directory },
     -+	{ "path.shallow-file", get_path_shallow_file },
     ++	{ "path.prefix", get_path_prefix },
      +	{ "path.superproject-working-tree", get_path_superproject_working_tree },
      +	{ "path.toplevel", get_path_toplevel },
     ++	{ "path.working-tree", get_path_work_tree },
       	{ "references.format", get_references_format },
       };
       
     +@@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char *prefix,
     + 	enum output_format format = FORMAT_NEWLINE_TERMINATED;
     + 	struct repo_info info = {
     + 		.repo = repo,
     +-		.prefix = prefix,
     ++		.prefix = prefix ? prefix : "",
     + 	};
     + 	int all_keys = 0;
     + 	int show_keys = 0;
  3:  5c438d045b !  4:  2a7afe4f87 repo: add --path-format for info path output
     @@ builtin/repo.c
       #include "utf8.h"
       
       static const char *const repo_usage[] = {
     --	"git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]",
     -+	"git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]",
     - 	"git repo structure [--format=(table|keyvalue|nul) | -z]",
     +-	"git repo info [--format=(lines|nul) | -z] [--all | <key>...]",
     ++	"git repo info [--format=(lines|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]",
     + 	"git repo info --keys [--format=(lines|nul) | -z]",
     + 	"git repo structure [--format=(table|lines|nul) | -z]",
       	NULL
       };
       
     @@ builtin/repo.c: static int parse_format_cb(const struct option *opt,
      @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char *prefix,
       	struct repo_info info = {
       		.repo = repo,
     - 		.prefix = prefix,
     + 		.prefix = prefix ? prefix : "",
      +		.path_format = PATH_FORMAT_ABSOLUTE,
       	};
       	int all_keys = 0;
     - 	struct option options[] = {
     + 	int show_keys = 0;
      @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char *prefix,
       			       N_("synonym for --format=nul"),
       			       PARSE_OPT_NONEG | PARSE_OPT_NOARG,
     @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char
      +			       N_("format"), N_("path output format"),
      +			       PARSE_OPT_NONEG, parse_path_format_cb),
       		OPT_BOOL(0, "all", &all_keys, N_("print all keys/values")),
     + 		OPT_BOOL(0, "keys", &show_keys, N_("show keys")),
       		OPT_END()
     - 	};
  5:  4b502925c9 <  -:  ---------- repo: add structure topology and path-depth metrics
  6:  1751181950 <  -:  ---------- repo: add aggregate structure totals to keyvalue output
  7:  fd18f28db0 !  5:  1e52e7bd7f t1900: cover repo info path keys and path-format
     @@ Commit message
          Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
      
       ## t/t1900-repo.sh ##
     -@@ t/t1900-repo.sh: REPO_INFO_KEYS='
     - 	layout.bare
     - 	layout.shallow
     - 	object.format
     +@@ t/t1900-repo.sh: test_description='test git repo-info'
     + 
     + . ./test-lib.sh
     + 
     ++# git-repo-info keys. It must contain the same keys listed in the const
     ++# repo_info_fields, in lexicographical order.
     ++REPO_INFO_KEYS='
     ++	layout.bare
     ++	layout.shallow
     ++	object.format
      +	path.common-dir
      +	path.config-file
      +	path.git-dir
     -+	path.git-prefix
      +	path.grafts-file
      +	path.hooks-directory
      +	path.index-file
     -+	path.logs-directory
      +	path.objects-directory
     -+	path.packed-refs-file
     -+	path.refs-directory
     -+	path.shallow-file
     ++	path.prefix
      +	path.superproject-working-tree
      +	path.toplevel
     - 	references.format
     - '
     - 
     ++	path.working-tree
     ++	references.format
     ++'
     ++
      +REPO_INFO_PATH_KEYS='
      +	path.common-dir
      +	path.config-file
      +	path.git-dir
     -+	path.git-prefix
      +	path.grafts-file
      +	path.hooks-directory
      +	path.index-file
     -+	path.logs-directory
      +	path.objects-directory
     -+	path.packed-refs-file
     -+	path.refs-directory
     -+	path.shallow-file
     ++	path.prefix
      +	path.superproject-working-tree
      +	path.toplevel
     ++	path.working-tree
      +'
      +
       # Test whether a key-value pair is correctly returned
     @@ t/t1900-repo.sh: test_expect_success 'values returned in order requested' '
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '
     ++test_expect_success 'path.prefix matches rev-parse --show-prefix' '
      +	git init path-prefix &&
      +	mkdir -p path-prefix/a/b &&
      +	expected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&
     -+	echo "path.git-prefix=$expected_value" >expect &&
     -+	git -C path-prefix/a/b repo info path.git-prefix >actual &&
     ++	echo "path.prefix=$expected_value" >expect &&
     ++	git -C path-prefix/a/b repo info path.prefix >actual &&
      +	test_cmp expect actual
      +'
      +
     @@ t/t1900-repo.sh: test_expect_success 'values returned in order requested' '
      +	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path config) &&
      +	echo "path.config-file=$expected_value" >expect &&
      +	git -C path-git-path repo info path.config-file >actual &&
     -+	test_cmp expect actual &&
     -+
     -+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&
     -+	echo "path.logs-directory=$expected_value" >expect &&
     -+	git -C path-git-path repo info path.logs-directory >actual &&
     -+	test_cmp expect actual &&
     -+
     -+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&
     -+	echo "path.packed-refs-file=$expected_value" >expect &&
     -+	git -C path-git-path repo info path.packed-refs-file >actual &&
     -+	test_cmp expect actual &&
     ++	test_cmp expect actual
     ++'
      +
     -+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&
     -+	echo "path.refs-directory=$expected_value" >expect &&
     -+	git -C path-git-path repo info path.refs-directory >actual &&
     -+	test_cmp expect actual &&
     ++test_expect_success 'path.working-tree matches path.toplevel' '
     ++	git init path-work-tree &&
     ++	expected_value=$(git -C path-work-tree rev-parse --show-toplevel) &&
     ++	echo "path.working-tree=$expected_value" >expect &&
     ++	git -C path-work-tree repo info path.working-tree >actual &&
     ++	test_cmp expect actual
     ++'
      +
     -+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&
     -+	echo "path.shallow-file=$expected_value" >expect &&
     -+	git -C path-git-path repo info path.shallow-file >actual &&
     ++test_expect_success 'path.working-tree is empty in bare repository' '
     ++	git init --bare bare-path-work-tree &&
     ++	echo "path.working-tree=" >expect &&
     ++	git -C bare-path-work-tree repo info path.working-tree >actual &&
      +	test_cmp expect actual
      +'
      +
  8:  0525ed4cd9 <  -:  ---------- t1901: extend structure metric coverage and portability
  9:  f17c0f03e5 !  6:  b98490a4a4 docs: describe repo info path keys and structure metrics
     @@ Metadata
      Author: Eslam reda ragheb <eslam.reda.div@gmail.com>
      
       ## Commit message ##
     -    docs: describe repo info path keys and structure metrics
     +    docs: describe repo info path keys
      
     -    Document the newly added repo info capabilities, including
     -    category keys and path-oriented key definitions.
     -
     -    Also describe --path-format behavior for path outputs.
     -
     -    Update git repo structure documentation to cover newly reported
     -    maxima and aggregate keyvalue/nul fields.
     -
     -    This keeps command behavior and output keys fully specified for
     -    users and scripts.
     +    Document repo info category requests, path.* keys, and
     +    --path-format behavior.
      
          Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
      
     @@ Documentation/git-repo.adoc: git-repo - Retrieve information about the repositor
       SYNOPSIS
       --------
       [synopsis]
     --git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]
     -+git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]
     - git repo structure [--format=(table|keyvalue|nul) | -z]
     +-git repo info [--format=(lines|nul) | -z] [--all | <key>...]
     ++git repo info [--format=(lines|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]
     + git repo info --keys [--format=(lines|nul) | -z]
     + git repo structure [--format=(table|lines|nul) | -z]
       
     - DESCRIPTION
      @@ Documentation/git-repo.adoc: supported:
     - +
     - `-z` is an alias for `--format=nul`.
     + `nul`:::
     + 	Similar to `lines`, but using a _NUL_ character after each value.
       
      +`--path-format=(absolute|relative)`:::
      +	Controls formatting for keys in the `path` category. The default is
      +	`absolute`. This option may be specified multiple times; the last one
      +	specified takes effect.
      +
     - `structure [--format=(table|keyvalue|nul) | -z]`::
     + `structure [--format=(table|lines|nul) | -z]`::
       	Retrieve statistics about the current repository structure. The
       	following kinds of information are reported:
      @@ Documentation/git-repo.adoc: supported:
     @@ Documentation/git-repo.adoc: supported:
       The output format can be chosen through the flag `--format`. Three formats are
       supported:
      @@ Documentation/git-repo.adoc: supported:
     - `keyvalue`:::
     + `lines`:::
       	Each line of output contains a key-value pair for a repository stat.
       	The '=' character is used to delimit between the key and the value.
      +	Both aggregate metrics and per-type metrics are included.
     @@ Documentation/git-repo.adoc: values that they return:
      +`path.git-dir`::
      +	The path to the git directory.
      +
     -+`path.git-prefix`::
     ++`path.prefix`::
      +	The path of the current working directory relative to the top-level
      +	directory.
      +
     @@ Documentation/git-repo.adoc: values that they return:
      +`path.index-file`::
      +	The path to the index file.
      +
     -+`path.logs-directory`::
     -+	The path to the `logs` directory.
     -+
      +`path.objects-directory`::
      +	The path to the objects directory.
      +
     -+`path.packed-refs-file`::
     -+	The path to the `packed-refs` file.
     -+
     -+`path.refs-directory`::
     -+	The path to the `refs` directory.
     -+
     -+`path.shallow-file`::
     -+	The path to the `shallow` file.
     -+
      +`path.superproject-working-tree`::
      +	The path to the superproject's working tree root, or an empty string
      +	when the repository is not used as a submodule.
     @@ Documentation/git-repo.adoc: values that they return:
      +`path.toplevel`::
      +	The path to the top-level working tree directory, or an empty string
      +	for bare repositories.
     ++
     ++`path.working-tree`::
     ++	Alias for `path.toplevel`.
      +
       `references.format`::
       	The reference storage format. The valid values are:
 10:  1bc100d6ca <  -:  ---------- repo: reduce repetition in structure keyvalue output
 11:  8af17ad831 <  -:  ---------- repo: refine path keys for repo info

-- 
gitgitgadget

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

* [PATCH v6 1/6] repo: introduce repo_info context plumbing
  2026-03-02  5:15         ` [PATCH v6 0/6] " eslam reda via GitGitGadget
@ 2026-03-02  5:15           ` Eslam reda ragheb via GitGitGadget
  2026-03-02  5:15           ` [PATCH v6 2/6] repo: support category requests in repo info Eslam reda ragheb via GitGitGadget
                             ` (7 subsequent siblings)
  8 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-03-02  5:15 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Introduce a repo_info context and thread it through get_value_fn,
field lookup, and value-printing helpers.

This prepares repo info for fields that need invocation-specific
context in addition to the repository handle.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 34 +++++++++++++++++++++++-----------
 1 file changed, 23 insertions(+), 11 deletions(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index 6a62a6020a..e687d833b4 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -23,7 +23,12 @@ static const char *const repo_usage[] = {
 	NULL
 };
 
-typedef int get_value_fn(struct repository *repo, struct strbuf *buf);
+struct repo_info {
+	struct repository *repo;
+	const char *prefix;
+};
+
+typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);
 
 enum output_format {
 	FORMAT_TABLE,
@@ -36,27 +41,30 @@ struct field {
 	get_value_fn *get_value;
 };
 
-static int get_layout_bare(struct repository *repo UNUSED, struct strbuf *buf)
+static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)
 {
 	strbuf_addstr(buf, is_bare_repository() ? "true" : "false");
 	return 0;
 }
 
-static int get_layout_shallow(struct repository *repo, struct strbuf *buf)
+static int get_layout_shallow(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf,
 		      is_repository_shallow(repo) ? "true" : "false");
 	return 0;
 }
 
-static int get_object_format(struct repository *repo, struct strbuf *buf)
+static int get_object_format(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf, repo->hash_algo->name);
 	return 0;
 }
 
-static int get_references_format(struct repository *repo, struct strbuf *buf)
+static int get_references_format(struct repo_info *info, struct strbuf *buf)
 {
+	struct repository *repo = info->repo;
 	strbuf_addstr(buf,
 		      ref_storage_format_to_name(repo->ref_storage_format));
 	return 0;
@@ -106,7 +114,7 @@ static void print_field(enum output_format format, const char *key,
 }
 
 static int print_fields(int argc, const char **argv,
-			struct repository *repo,
+			struct repo_info *info,
 			enum output_format format)
 {
 	int ret = 0;
@@ -124,7 +132,7 @@ static int print_fields(int argc, const char **argv,
 		}
 
 		strbuf_reset(&valbuf);
-		get_value(repo, &valbuf);
+		get_value(info, &valbuf);
 		print_field(format, key, valbuf.buf);
 	}
 
@@ -132,7 +140,7 @@ static int print_fields(int argc, const char **argv,
 	return ret;
 }
 
-static int print_all_fields(struct repository *repo,
+static int print_all_fields(struct repo_info *info,
 			    enum output_format format)
 {
 	struct strbuf valbuf = STRBUF_INIT;
@@ -141,7 +149,7 @@ static int print_all_fields(struct repository *repo,
 		const struct field *field = &repo_info_fields[i];
 
 		strbuf_reset(&valbuf);
-		field->get_value(repo, &valbuf);
+		field->get_value(info, &valbuf);
 		print_field(format, field->key, valbuf.buf);
 	}
 
@@ -195,6 +203,10 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 			 struct repository *repo)
 {
 	enum output_format format = FORMAT_NEWLINE_TERMINATED;
+	struct repo_info info = {
+		.repo = repo,
+		.prefix = prefix,
+	};
 	int all_keys = 0;
 	int show_keys = 0;
 	struct option options[] = {
@@ -225,9 +237,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 		die(_("--all and <key> cannot be used together"));
 
 	if (all_keys)
-		return print_all_fields(repo, format);
+		return print_all_fields(&info, format);
 	else
-		return print_fields(argc, argv, repo, format);
+		return print_fields(argc, argv, &info, format);
 }
 
 struct ref_stats {
-- 
gitgitgadget


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

* [PATCH v6 2/6] repo: support category requests in repo info
  2026-03-02  5:15         ` [PATCH v6 0/6] " eslam reda via GitGitGadget
  2026-03-02  5:15           ` [PATCH v6 1/6] repo: introduce repo_info context plumbing Eslam reda ragheb via GitGitGadget
@ 2026-03-02  5:15           ` Eslam reda ragheb via GitGitGadget
  2026-03-02  5:15           ` [PATCH v6 3/6] repo: add path keys to " Eslam reda ragheb via GitGitGadget
                             ` (6 subsequent siblings)
  8 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-03-02  5:15 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Teach repo info to accept category names (for example, layout)
and expand them to matching key.* entries in request order.

Explicit keys keep their existing behavior; unknown keys or
categories still report clear errors.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 38 +++++++++++++++++++++++++++++++++-----
 1 file changed, 33 insertions(+), 5 deletions(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index e687d833b4..f614298199 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -96,6 +96,33 @@ static get_value_fn *get_value_fn_for_key(const char *key)
 	return found ? found->get_value : NULL;
 }
 
+static void print_field(enum output_format format, const char *key,
+			const char *value);
+
+static int print_category_fields(const char *category,
+				 struct repo_info *info,
+				 enum output_format format,
+				 struct strbuf *valbuf)
+{
+	int found = 0;
+	size_t category_len = strlen(category);
+
+	for (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {
+		const struct field *field = &repo_info_fields[i];
+
+		if (!starts_with(field->key, category) ||
+		    field->key[category_len] != '.')
+			continue;
+
+		strbuf_reset(valbuf);
+		field->get_value(info, valbuf);
+		print_field(format, field->key, valbuf->buf);
+		found = 1;
+	}
+
+	return found;
+}
+
 static void print_field(enum output_format format, const char *key,
 			const char *value)
 {
@@ -126,14 +153,15 @@ static int print_fields(int argc, const char **argv,
 
 		get_value = get_value_fn_for_key(key);
 
-		if (!get_value) {
-			ret = error(_("key '%s' not found"), key);
+		if (get_value) {
+			strbuf_reset(&valbuf);
+			get_value(info, &valbuf);
+			print_field(format, key, valbuf.buf);
 			continue;
 		}
 
-		strbuf_reset(&valbuf);
-		get_value(info, &valbuf);
-		print_field(format, key, valbuf.buf);
+		if (!print_category_fields(key, info, format, &valbuf))
+			ret = error(_("key '%s' not found"), key);
 	}
 
 	strbuf_release(&valbuf);
-- 
gitgitgadget


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

* [PATCH v6 3/6] repo: add path keys to repo info
  2026-03-02  5:15         ` [PATCH v6 0/6] " eslam reda via GitGitGadget
  2026-03-02  5:15           ` [PATCH v6 1/6] repo: introduce repo_info context plumbing Eslam reda ragheb via GitGitGadget
  2026-03-02  5:15           ` [PATCH v6 2/6] repo: support category requests in repo info Eslam reda ragheb via GitGitGadget
@ 2026-03-02  5:15           ` Eslam reda ragheb via GitGitGadget
  2026-03-02  5:15           ` [PATCH v6 4/6] repo: add --path-format for info path output Eslam reda ragheb via GitGitGadget
                             ` (5 subsequent siblings)
  8 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-03-02  5:15 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Add a path category to git repo info with key-value pairs that
mirror repository paths users commonly retrieve via rev-parse and
git-path lookups.

This makes scripting against repo metadata more direct and avoids
shelling out to multiple commands for related paths.

The new keys are introduced as explicit path.* entries in
repo_info_fields and are resolved through dedicated helpers.

This keeps lookup behavior predictable and makes future path
additions straightforward.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 104 ++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 103 insertions(+), 1 deletion(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index f614298199..87c5850929 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -1,10 +1,12 @@
 #define USE_THE_REPOSITORY_VARIABLE
 
 #include "builtin.h"
+#include "abspath.h"
 #include "environment.h"
 #include "hex.h"
 #include "odb.h"
 #include "parse-options.h"
+#include "path.h"
 #include "path-walk.h"
 #include "progress.h"
 #include "quote.h"
@@ -14,6 +16,7 @@
 #include "strbuf.h"
 #include "string-list.h"
 #include "shallow.h"
+#include "submodule.h"
 #include "utf8.h"
 
 static const char *const repo_usage[] = {
@@ -41,6 +44,13 @@ struct field {
 	get_value_fn *get_value;
 };
 
+static void repo_info_add_path(struct repo_info *info,
+			      struct strbuf *buf,
+			      const char *path)
+{
+	strbuf_add_absolute_path(buf, path);
+}
+
 static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)
 {
 	strbuf_addstr(buf, is_bare_repository() ? "true" : "false");
@@ -62,6 +72,87 @@ static int get_object_format(struct repo_info *info, struct strbuf *buf)
 	return 0;
 }
 
+static int get_path_common_dir(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_common_dir(info->repo));
+	return 0;
+}
+
+static int get_path_config_file(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "config"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_git_dir(info->repo));
+	return 0;
+}
+
+static int get_path_prefix(struct repo_info *info, struct strbuf *buf)
+{
+	strbuf_addstr(buf, info->prefix);
+	return 0;
+}
+
+static int get_path_grafts_file(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_graft_file(info->repo));
+	return 0;
+}
+
+static int get_path_hooks_directory(struct repo_info *info, struct strbuf *buf)
+{
+	struct strbuf path = STRBUF_INIT;
+
+	repo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, "hooks"));
+	strbuf_release(&path);
+	return 0;
+}
+
+static int get_path_index_file(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_index_file(info->repo));
+	return 0;
+}
+
+static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)
+{
+	repo_info_add_path(info, buf, repo_get_object_directory(info->repo));
+	return 0;
+}
+
+static int get_path_superproject_working_tree(struct repo_info *info,
+					     struct strbuf *buf)
+{
+	struct strbuf superproject = STRBUF_INIT;
+
+	if (get_superproject_working_tree(&superproject))
+		repo_info_add_path(info, buf, superproject.buf);
+
+	strbuf_release(&superproject);
+	return 0;
+}
+
+static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)
+{
+	const char *work_tree = repo_get_work_tree(info->repo);
+
+	if (work_tree)
+		repo_info_add_path(info, buf, work_tree);
+
+	return 0;
+}
+
+static int get_path_work_tree(struct repo_info *info, struct strbuf *buf)
+{
+	return get_path_toplevel(info, buf);
+}
+
 static int get_references_format(struct repo_info *info, struct strbuf *buf)
 {
 	struct repository *repo = info->repo;
@@ -75,6 +166,17 @@ static const struct field repo_info_fields[] = {
 	{ "layout.bare", get_layout_bare },
 	{ "layout.shallow", get_layout_shallow },
 	{ "object.format", get_object_format },
+	{ "path.common-dir", get_path_common_dir },
+	{ "path.config-file", get_path_config_file },
+	{ "path.git-dir", get_path_git_dir },
+	{ "path.grafts-file", get_path_grafts_file },
+	{ "path.hooks-directory", get_path_hooks_directory },
+	{ "path.index-file", get_path_index_file },
+	{ "path.objects-directory", get_path_objects_directory },
+	{ "path.prefix", get_path_prefix },
+	{ "path.superproject-working-tree", get_path_superproject_working_tree },
+	{ "path.toplevel", get_path_toplevel },
+	{ "path.working-tree", get_path_work_tree },
 	{ "references.format", get_references_format },
 };
 
@@ -233,7 +335,7 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 	enum output_format format = FORMAT_NEWLINE_TERMINATED;
 	struct repo_info info = {
 		.repo = repo,
-		.prefix = prefix,
+		.prefix = prefix ? prefix : "",
 	};
 	int all_keys = 0;
 	int show_keys = 0;
-- 
gitgitgadget


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

* [PATCH v6 4/6] repo: add --path-format for info path output
  2026-03-02  5:15         ` [PATCH v6 0/6] " eslam reda via GitGitGadget
                             ` (2 preceding siblings ...)
  2026-03-02  5:15           ` [PATCH v6 3/6] repo: add path keys to " Eslam reda ragheb via GitGitGadget
@ 2026-03-02  5:15           ` Eslam reda ragheb via GitGitGadget
  2026-03-02  5:15           ` [PATCH v6 5/6] t1900: cover repo info path keys and path-format Eslam reda ragheb via GitGitGadget
                             ` (4 subsequent siblings)
  8 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-03-02  5:15 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Teach git repo info to accept --path-format=(absolute|relative)
so scripts can request stable path style explicitly.

This aligns path.* output behavior with existing rev-parse usage
patterns and reduces ad-hoc path conversion in callers.

The option is wired through repo_info context and used by
repo_info_add_path(), so path formatting remains centralized and
consistent across all path.* keys.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 builtin/repo.c | 37 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 36 insertions(+), 1 deletion(-)

diff --git a/builtin/repo.c b/builtin/repo.c
index 87c5850929..bcdb9210cb 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -20,15 +20,21 @@
 #include "utf8.h"
 
 static const char *const repo_usage[] = {
-	"git repo info [--format=(lines|nul) | -z] [--all | <key>...]",
+	"git repo info [--format=(lines|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]",
 	"git repo info --keys [--format=(lines|nul) | -z]",
 	"git repo structure [--format=(table|lines|nul) | -z]",
 	NULL
 };
 
+enum path_format {
+	PATH_FORMAT_ABSOLUTE,
+	PATH_FORMAT_RELATIVE,
+};
+
 struct repo_info {
 	struct repository *repo;
 	const char *prefix;
+	enum path_format path_format;
 };
 
 typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);
@@ -48,6 +54,16 @@ static void repo_info_add_path(struct repo_info *info,
 			      struct strbuf *buf,
 			      const char *path)
 {
+	if (info->path_format == PATH_FORMAT_RELATIVE) {
+		char *cwd = xgetcwd();
+		struct strbuf rel_path = STRBUF_INIT;
+
+		strbuf_addstr(buf, relative_path(path, cwd, &rel_path));
+		strbuf_release(&rel_path);
+		free(cwd);
+		return;
+	}
+
 	strbuf_add_absolute_path(buf, path);
 }
 
@@ -329,6 +345,21 @@ static int parse_format_cb(const struct option *opt,
 	return 0;
 }
 
+static int parse_path_format_cb(const struct option *opt,
+				const char *arg, int unset UNUSED)
+{
+	enum path_format *path_format = opt->value;
+
+	if (!strcmp(arg, "absolute"))
+		*path_format = PATH_FORMAT_ABSOLUTE;
+	else if (!strcmp(arg, "relative"))
+		*path_format = PATH_FORMAT_RELATIVE;
+	else
+		die(_("invalid path format '%s'"), arg);
+
+	return 0;
+}
+
 static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 			 struct repository *repo)
 {
@@ -336,6 +367,7 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 	struct repo_info info = {
 		.repo = repo,
 		.prefix = prefix ? prefix : "",
+		.path_format = PATH_FORMAT_ABSOLUTE,
 	};
 	int all_keys = 0;
 	int show_keys = 0;
@@ -347,6 +379,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
 			       N_("synonym for --format=nul"),
 			       PARSE_OPT_NONEG | PARSE_OPT_NOARG,
 			       parse_format_cb),
+		OPT_CALLBACK_F(0, "path-format", &info.path_format,
+			       N_("format"), N_("path output format"),
+			       PARSE_OPT_NONEG, parse_path_format_cb),
 		OPT_BOOL(0, "all", &all_keys, N_("print all keys/values")),
 		OPT_BOOL(0, "keys", &show_keys, N_("show keys")),
 		OPT_END()
-- 
gitgitgadget


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

* [PATCH v6 5/6] t1900: cover repo info path keys and path-format
  2026-03-02  5:15         ` [PATCH v6 0/6] " eslam reda via GitGitGadget
                             ` (3 preceding siblings ...)
  2026-03-02  5:15           ` [PATCH v6 4/6] repo: add --path-format for info path output Eslam reda ragheb via GitGitGadget
@ 2026-03-02  5:15           ` Eslam reda ragheb via GitGitGadget
  2026-03-02  5:15           ` [PATCH v6 6/6] docs: describe repo info path keys Eslam reda ragheb via GitGitGadget
                             ` (3 subsequent siblings)
  8 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-03-02  5:15 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Extend t1900 to validate category-key expansion, path.* key
behavior, and --path-format handling for git repo info.

The tests compare repo info output to equivalent rev-parse values.

This ensures behavior remains aligned with existing plumbing
semantics.

Also keep mixed key/category ordering coverage so callers can rely
on deterministic output order when combining explicit keys with
category requests.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 t/t1900-repo.sh | 194 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 194 insertions(+)

diff --git a/t/t1900-repo.sh b/t/t1900-repo.sh
index a9eb07abe8..6605394d1f 100755
--- a/t/t1900-repo.sh
+++ b/t/t1900-repo.sh
@@ -4,6 +4,40 @@ test_description='test git repo-info'
 
 . ./test-lib.sh
 
+# git-repo-info keys. It must contain the same keys listed in the const
+# repo_info_fields, in lexicographical order.
+REPO_INFO_KEYS='
+	layout.bare
+	layout.shallow
+	object.format
+	path.common-dir
+	path.config-file
+	path.git-dir
+	path.grafts-file
+	path.hooks-directory
+	path.index-file
+	path.objects-directory
+	path.prefix
+	path.superproject-working-tree
+	path.toplevel
+	path.working-tree
+	references.format
+'
+
+REPO_INFO_PATH_KEYS='
+	path.common-dir
+	path.config-file
+	path.git-dir
+	path.grafts-file
+	path.hooks-directory
+	path.index-file
+	path.objects-directory
+	path.prefix
+	path.superproject-working-tree
+	path.toplevel
+	path.working-tree
+'
+
 # Test whether a key-value pair is correctly returned
 #
 # Usage: test_repo_info <label> <init command> <repo_name> <key> <expected value>
@@ -80,6 +114,166 @@ test_expect_success 'values returned in order requested' '
 	test_cmp expect actual
 '
 
+test_expect_success 'category key returns all matching keys' '
+	cat >expect <<-\EOF &&
+	layout.bare=false
+	layout.shallow=false
+	EOF
+	git init category-layout &&
+	git -C category-layout repo info layout >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'mixed key/category requests preserve request order' '
+	cat >expect <<-EOF &&
+	object.format=$(test_oid algo)
+	layout.bare=false
+	layout.shallow=false
+	EOF
+	git init mixed-order &&
+	git -C mixed-order repo info object.format layout >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.git-dir matches rev-parse --absolute-git-dir' '
+	git init path-git-dir &&
+	expected_value=$(git -C path-git-dir rev-parse --absolute-git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-git-dir repo info path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.common-dir matches rev-parse --git-common-dir' '
+	git init path-common-dir &&
+	expected_value=$(git -C path-common-dir rev-parse --path-format=absolute --git-common-dir) &&
+	echo "path.common-dir=$expected_value" >expect &&
+	git -C path-common-dir repo info path.common-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.toplevel matches rev-parse --show-toplevel' '
+	git init path-toplevel &&
+	expected_value=$(git -C path-toplevel rev-parse --show-toplevel) &&
+	echo "path.toplevel=$expected_value" >expect &&
+	git -C path-toplevel repo info path.toplevel >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.toplevel is empty in bare repository' '
+	git init --bare bare-path-toplevel &&
+	echo "path.toplevel=" >expect &&
+	git -C bare-path-toplevel repo info path.toplevel >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.prefix matches rev-parse --show-prefix' '
+	git init path-prefix &&
+	mkdir -p path-prefix/a/b &&
+	expected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&
+	echo "path.prefix=$expected_value" >expect &&
+	git -C path-prefix/a/b repo info path.prefix >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git-path style keys match rev-parse --git-path' '
+	git init path-git-path &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path info/grafts) &&
+	echo "path.grafts-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.grafts-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path index) &&
+	echo "path.index-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.index-file >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path objects) &&
+	echo "path.objects-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.objects-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path hooks) &&
+	echo "path.hooks-directory=$expected_value" >expect &&
+	git -C path-git-path repo info path.hooks-directory >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path config) &&
+	echo "path.config-file=$expected_value" >expect &&
+	git -C path-git-path repo info path.config-file >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.working-tree matches path.toplevel' '
+	git init path-work-tree &&
+	expected_value=$(git -C path-work-tree rev-parse --show-toplevel) &&
+	echo "path.working-tree=$expected_value" >expect &&
+	git -C path-work-tree repo info path.working-tree >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.working-tree is empty in bare repository' '
+	git init --bare bare-path-work-tree &&
+	echo "path.working-tree=" >expect &&
+	git -C bare-path-work-tree repo info path.working-tree >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.superproject-working-tree is empty when not a submodule' '
+	git init path-superproject &&
+	echo "path.superproject-working-tree=" >expect &&
+	git -C path-superproject repo info path.superproject-working-tree >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path.superproject-working-tree matches rev-parse in submodule' '
+	git init path-superproject-origin &&
+	echo x >path-superproject-origin/x &&
+	git -C path-superproject-origin add x &&
+	git -C path-superproject-origin commit -m x &&
+
+	git init path-superproject-parent &&
+	git -C path-superproject-parent -c protocol.file.allow=always submodule add ../path-superproject-origin sm &&
+
+	expected_value=$(git -C path-superproject-parent/sm rev-parse --show-superproject-working-tree) &&
+	echo "path.superproject-working-tree=$expected_value" >expect &&
+	git -C path-superproject-parent/sm repo info path.superproject-working-tree >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path category returns all path keys' '
+	git init path-category &&
+	>expect &&
+	for key in $REPO_INFO_PATH_KEYS
+	do
+		git -C path-category repo info "$key" >>expect || return 1
+	done &&
+	git -C path-category repo info path >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path-format=relative matches rev-parse for git-dir' '
+	git init path-format-relative &&
+	expected_value=$(git -C path-format-relative rev-parse --path-format=relative --git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-format-relative repo info --path-format=relative path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git repo info uses the last requested path format' '
+	git init path-format-last &&
+	expected_value=$(git -C path-format-last rev-parse --path-format=relative --git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-format-last repo info --path-format=absolute --path-format=relative path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git-repo-info aborts when requesting an invalid path format' '
+	echo "fatal: invalid path format ${SQ}foo${SQ}" >expect &&
+	test_must_fail git repo info --path-format=foo path.git-dir 2>actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'git-repo-info fails if an invalid key is requested' '
 	echo "error: key ${SQ}foo${SQ} not found" >expect &&
 	test_must_fail git repo info foo 2>actual &&
-- 
gitgitgadget


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

* [PATCH v6 6/6] docs: describe repo info path keys
  2026-03-02  5:15         ` [PATCH v6 0/6] " eslam reda via GitGitGadget
                             ` (4 preceding siblings ...)
  2026-03-02  5:15           ` [PATCH v6 5/6] t1900: cover repo info path keys and path-format Eslam reda ragheb via GitGitGadget
@ 2026-03-02  5:15           ` Eslam reda ragheb via GitGitGadget
  2026-03-18 20:44           ` [PATCH v6 0/6] repo info: add category/path keys and --path-format Jialong Wang
                             ` (2 subsequent siblings)
  8 siblings, 0 replies; 68+ messages in thread
From: Eslam reda ragheb via GitGitGadget @ 2026-03-02  5:15 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, Lucas Seiki Oshiro, eslam reda, Eslam reda ragheb

From: Eslam reda ragheb <eslam.reda.div@gmail.com>

Document repo info category requests, path.* keys, and
--path-format behavior.

Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>
---
 Documentation/git-repo.adoc | 58 ++++++++++++++++++++++++++++++++++---
 1 file changed, 54 insertions(+), 4 deletions(-)

diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 319d30bd86..086ab922ad 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -8,7 +8,7 @@ git-repo - Retrieve information about the repository
 SYNOPSIS
 --------
 [synopsis]
-git repo info [--format=(lines|nul) | -z] [--all | <key>...]
+git repo info [--format=(lines|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]
 git repo info --keys [--format=(lines|nul) | -z]
 git repo structure [--format=(table|lines|nul) | -z]
 
@@ -56,6 +56,11 @@ supported:
 `nul`:::
 	Similar to `lines`, but using a _NUL_ character after each value.
 
+`--path-format=(absolute|relative)`:::
+	Controls formatting for keys in the `path` category. The default is
+	`absolute`. This option may be specified multiple times; the last one
+	specified takes effect.
+
 `structure [--format=(table|lines|nul) | -z]`::
 	Retrieve statistics about the current repository structure. The
 	following kinds of information are reported:
@@ -64,6 +69,12 @@ supported:
 * Reachable object counts categorized by type
 * Total inflated size of reachable objects by type
 * Total disk size of reachable objects by type
+* Largest inflated reachable object size by type
+* Largest disk size of a reachable object by type
+* Largest parent count among reachable commits
+* Largest entry count among reachable trees
+* Longest and deepest path among reachable blobs
+* Deepest annotated tag chain
 +
 The output format can be chosen through the flag `--format`. Three formats are
 supported:
@@ -76,6 +87,7 @@ supported:
 `lines`:::
 	Each line of output contains a key-value pair for a repository stat.
 	The '=' character is used to delimit between the key and the value.
+	Both aggregate metrics and per-type metrics are included.
 	Values containing "unusual" characters are quoted as explained for the
 	configuration variable `core.quotePath` (see linkgit:git-config[1]).
 
@@ -90,9 +102,11 @@ supported:
 
 INFO KEYS
 ---------
-In order to obtain a set of values from `git repo info`, you should provide
-the keys that identify them. Here's a list of the available keys and the
-values that they return:
+In order to obtain values from `git repo info`, provide either individual keys
+or category names. A category returns all keys within that category. For
+example, `layout` returns both `layout.bare` and `layout.shallow`.
+
+Here's a list of the available keys and the values that they return:
 
 `layout.bare`::
 	`true` if this is a bare repository, otherwise `false`.
@@ -103,6 +117,42 @@ values that they return:
 `object.format`::
 	The object format (hash algorithm) used in the repository.
 
+`path.common-dir`::
+	The path to the common git directory.
+
+`path.config-file`::
+	The path to the `config` file in the git directory.
+
+`path.git-dir`::
+	The path to the git directory.
+
+`path.prefix`::
+	The path of the current working directory relative to the top-level
+	directory.
+
+`path.grafts-file`::
+	The path to the `info/grafts` file.
+
+`path.hooks-directory`::
+	The path to the `hooks` directory.
+
+`path.index-file`::
+	The path to the index file.
+
+`path.objects-directory`::
+	The path to the objects directory.
+
+`path.superproject-working-tree`::
+	The path to the superproject's working tree root, or an empty string
+	when the repository is not used as a submodule.
+
+`path.toplevel`::
+	The path to the top-level working tree directory, or an empty string
+	for bare repositories.
+
+`path.working-tree`::
+	Alias for `path.toplevel`.
+
 `references.format`::
 	The reference storage format. The valid values are:
 +
-- 
gitgitgadget

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

* Re: [PATCH v4 02/10] repo: add path keys to repo info
  2026-03-01 10:36             ` Phillip Wood
@ 2026-03-02  6:42               ` Junio C Hamano
  0 siblings, 0 replies; 68+ messages in thread
From: Junio C Hamano @ 2026-03-02  6:42 UTC (permalink / raw)
  To: Phillip Wood; +Cc: Eslam reda ragheb via GitGitGadget, git, eslam reda

Phillip Wood <phillip.wood123@gmail.com> writes:

>> FWIW "git rev-parse --show-toplevel" dies with "must be run in a
>> work tree".  Better or worse,
>> 
>> 	rm -fr new
>> 	git init new
>> 	cd new/.git && git rev-parse --show-toplevel
>> 
>> also dies the same way, which I am not sure we want to inherit when
>> we are making a new interrogator command.
>
> Yes, I think printing an empty value after the key would be better - I 
> don't think there are any paths where we care about the distinction 
> between NULL and ""

If the new and safer variant of the command should signal "error" by
giving an empty string, it would work with its "--show-toplevel"
equivalent run in a bare repository.  In a working tree, it would
give a full/absolute path, and never be an empty string.  I am still
unsure what that new and safer one should do from inside the .git
directory.  "rev-parse --show-toplevel" dies.  $(cd .. && pwd) may
be another plausible and arguably more useful answer.

Your idea of equiating NULL and "", I do not think it would work
well with "git rev-parse --show-cdup" equivalent.  The command will
give us an empty string from the top-level of the working tree.

Curiously, in this sequence

	rm -fr new
	git init new
	cd new/.git && git rev-parse --show-cdup
	cd objects && git rev-parse --show-cdup

two "rev-parse" do not die with "must be run in a work tree", and
worse yet, they do not give you ".." or "../../", either.

It seems that one rule of "rev-parse --show-<some-path>" is that
"when you are inside .git directory of a non-bare repository, we'd
behave as if you are in a bare repository as if its working tree
does not exist", and the above is consistent with that rule.
"--show-cdup" does not fail but gives an empty string when run
anywhere in a bare repository.

But among "rev-parse --show-<anything>" that are about working tree
paths, there seems no unifying rule on what to do when in a bare
repsitory.  As we already saw, "--show-toplevel" dies without a
working tree.  This reflects the history of rev-parse that grew
organizally without a grand design.

If we are adding new and safer interface to these pieces of
information to "repo info", we may want to straighten these rules.


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

* Re: [PATCH v6 0/6] repo info: add category/path keys and --path-format
  2026-03-02  5:15         ` [PATCH v6 0/6] " eslam reda via GitGitGadget
                             ` (5 preceding siblings ...)
  2026-03-02  5:15           ` [PATCH v6 6/6] docs: describe repo info path keys Eslam reda ragheb via GitGitGadget
@ 2026-03-18 20:44           ` Jialong Wang
  2026-03-19  3:36             ` K Jayatheerth
  2026-03-19 20:58           ` [PATCH v6 5/6] t1900: cover repo info path keys and path-format Jialong Wang
  2026-03-19 20:59           ` [PATCH] t1900: cover repo info path keys in non-default layouts Jialong Wang
  8 siblings, 1 reply; 68+ messages in thread
From: Jialong Wang @ 2026-03-18 20:44 UTC (permalink / raw)
  To: git

Hi,

While reading the current `git repo info` implementation, I noticed that
`layout.bare` is still implemented via `is_bare_repository()` in
`builtin/repo.c`.

At first I thought this might be a small repository-awareness cleanup,
since the `repo info` field callbacks already receive a `struct
repository *`. But after tracing it further, it seems the current
`is_bare_repository()` semantics are not equivalent to simply checking
whether `repo_get_work_tree(repo)` is NULL.

So before trying to patch this, I wanted to confirm the intended
direction:

- Should `repo info`'s `layout.bare` continue to follow the current
  `is_bare_repository()` semantics?
- Or, if `git repo info` is meant to become more explicitly
  repository-aware over time, would it make sense to introduce something
  like a `repo_is_bare(repo)` helper and use that instead?

I am asking mainly to avoid sending a misleading cleanup patch for what
may actually be a semantics question.

Thanks,
Jialong

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

* Re: [PATCH v6 0/6] repo info: add category/path keys and --path-format
  2026-03-18 20:44           ` [PATCH v6 0/6] repo info: add category/path keys and --path-format Jialong Wang
@ 2026-03-19  3:36             ` K Jayatheerth
  2026-03-19 20:32               ` Jerry Wang
  0 siblings, 1 reply; 68+ messages in thread
From: K Jayatheerth @ 2026-03-19  3:36 UTC (permalink / raw)
  To: jerrywang183; +Cc: git

> While reading the current `git repo info` implementation, I noticed that
> `layout.bare` is still implemented via `is_bare_repository()` in
> `builtin/repo.c`.
>
> At first I thought this might be a small repository-awareness cleanup,
> since the `repo info` field callbacks already receive a `struct
> repository *`. But after tracing it further, it seems the current
> `is_bare_repository()` semantics are not equivalent to simply checking
> whether `repo_get_work_tree(repo)` is NULL.

Hmph, this was an idea I explored few days ago
But I do agree the repo->worktree == NULL method has multiple flaws

For example

test_expect_success 'layout.bare is false even when run from inside .git' '
	git init nonbare-dot-git &&
	echo "layout.bare=false" >expect &&

	git -C nonbare-dot-git/.git repo info layout.bare >actual &&
	test_cmp expect actual
'

I cooked up a test like this and it failed
I have since been exploring config.c and parse.c.
specifically the if repo_config_get_bool()

I think there are few other checks we need to do on top of
repo->worktree to be completely sure that the given repo is in fact bare.
Also instead of using repo->worktree I think we can use repo_get_work_tree(repo)
Why recreate the logic when we have a getter ;)

I am convinced that we need a helper
repo_is_bare is a good name too.

Thanks for exploring this :)

Regards,
- Jayatheerth

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

* Re: [PATCH v6 0/6] repo info: add category/path keys and --path-format
  2026-03-19  3:36             ` K Jayatheerth
@ 2026-03-19 20:32               ` Jerry Wang
  2026-03-20  1:49                 ` K Jayatheerth
  0 siblings, 1 reply; 68+ messages in thread
From: Jerry Wang @ 2026-03-19 20:32 UTC (permalink / raw)
  To: K Jayatheerth; +Cc: git

Hi Jay,

Thanks, this is very helpful.

That makes sense. I agree that checking only
repo_get_work_tree(repo) == NULL is too weak, and the
"git -C <nonbare>/.git" case is a good example of that.

I'll keep the linked-worktree / separate-git-dir coverage patch
separate. For layout.bare, I'll first add tests to pin down the
intended semantics, then follow up with a small repo-aware fix,
likely with a repo_is_bare() helper if that turns out to be the
right shape.

Thanks,
Jialong

> On Mar 18, 2026, at 23:36, K Jayatheerth <jayatheerthkulkarni2005@gmail.com> wrote:
> 
>> While reading the current `git repo info` implementation, I noticed that
>> `layout.bare` is still implemented via `is_bare_repository()` in
>> `builtin/repo.c`.
>> 
>> At first I thought this might be a small repository-awareness cleanup,
>> since the `repo info` field callbacks already receive a `struct
>> repository *`. But after tracing it further, it seems the current
>> `is_bare_repository()` semantics are not equivalent to simply checking
>> whether `repo_get_work_tree(repo)` is NULL.
> 
> Hmph, this was an idea I explored few days ago
> But I do agree the repo->worktree == NULL method has multiple flaws
> 
> For example
> 
> test_expect_success 'layout.bare is false even when run from inside .git' '
> 	git init nonbare-dot-git &&
> 	echo "layout.bare=false" >expect &&
> 
> 	git -C nonbare-dot-git/.git repo info layout.bare >actual &&
> 	test_cmp expect actual
> '
> 
> I cooked up a test like this and it failed
> I have since been exploring config.c and parse.c.
> specifically the if repo_config_get_bool()
> 
> I think there are few other checks we need to do on top of
> repo->worktree to be completely sure that the given repo is in fact bare.
> Also instead of using repo->worktree I think we can use repo_get_work_tree(repo)
> Why recreate the logic when we have a getter ;)
> 
> I am convinced that we need a helper
> repo_is_bare is a good name too.
> 
> Thanks for exploring this :)
> 
> Regards,
> - Jayatheerth


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

* Re: [PATCH v6 5/6] t1900: cover repo info path keys and path-format
  2026-03-02  5:15         ` [PATCH v6 0/6] " eslam reda via GitGitGadget
                             ` (6 preceding siblings ...)
  2026-03-18 20:44           ` [PATCH v6 0/6] repo info: add category/path keys and --path-format Jialong Wang
@ 2026-03-19 20:58           ` Jialong Wang
  2026-03-19 20:59           ` [PATCH] t1900: cover repo info path keys in non-default layouts Jialong Wang
  8 siblings, 0 replies; 68+ messages in thread
From: Jialong Wang @ 2026-03-19 20:58 UTC (permalink / raw)
  To: git
  Cc: Jialong Wang, gitster, karthik.188, eslam.reda.div, gitgitgadget,
	phillip.wood123, lucasseikioshiro

Hi Eslam,

Thanks for working on this. I tried the v6 branch locally, and the
path-key coverage looks good overall, but I think t1900 still misses a
few non-default layouts that seem worth covering.

In particular, I think it would be useful to add coverage for:

  - linked worktrees, where path.git-dir points into
    .git/worktrees/<name>, path.common-dir still points at the main
    .git, and path.toplevel points at the linked checkout

  - repositories created with --separate-git-dir, especially
    path.git-dir with --path-format=relative

  - path.superproject-working-tree with --path-format=relative in a
    submodule

I tested these locally against rev-parse and they seem to behave as
expected, so they may make good follow-up coverage for this series.

Thanks,
Jialong

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

* [PATCH] t1900: cover repo info path keys in non-default layouts
  2026-03-02  5:15         ` [PATCH v6 0/6] " eslam reda via GitGitGadget
                             ` (7 preceding siblings ...)
  2026-03-19 20:58           ` [PATCH v6 5/6] t1900: cover repo info path keys and path-format Jialong Wang
@ 2026-03-19 20:59           ` Jialong Wang
  8 siblings, 0 replies; 68+ messages in thread
From: Jialong Wang @ 2026-03-19 20:59 UTC (permalink / raw)
  To: git
  Cc: gitster, karthik.188, eslam.reda.div, gitgitgadget,
	phillip.wood123, lucasseikioshiro, Jialong Wang

Extend the path-key coverage in t1900 to exercise layouts where the
reported paths differ from a regular worktree.

Add comparisons against rev-parse for path.git-dir, path.common-dir,
and path.toplevel in a linked worktree and a repository created with
--separate-git-dir. Also cover path.git-dir with --path-format=relative
in the separate-git-dir case, and path.superproject-working-tree with
--path-format=relative in a submodule.

Signed-off-by: Jialong Wang <jerrywang183@yahoo.com>
---
 t/t1900-repo.sh | 54 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 54 insertions(+)

diff --git a/t/t1900-repo.sh b/t/t1900-repo.sh
index 6605394d1f..3f39d2b9f6 100755
--- a/t/t1900-repo.sh
+++ b/t/t1900-repo.sh
@@ -241,6 +241,60 @@ test_expect_success 'path.superproject-working-tree matches rev-parse in submodu
 	test_cmp expect actual
 '
 
+test_expect_success 'path keys match rev-parse in linked worktree' '
+	git init path-linked-main &&
+	echo x >path-linked-main/x &&
+	git -C path-linked-main add x &&
+	git -C path-linked-main commit -m x &&
+	git -C path-linked-main worktree add ../path-linked-worktree &&
+
+	expected_value=$(git -C path-linked-worktree rev-parse --absolute-git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-linked-worktree repo info path.git-dir >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-linked-worktree rev-parse --path-format=absolute --git-common-dir) &&
+	echo "path.common-dir=$expected_value" >expect &&
+	git -C path-linked-worktree repo info path.common-dir >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-linked-worktree rev-parse --show-toplevel) &&
+	echo "path.toplevel=$expected_value" >expect &&
+	git -C path-linked-worktree repo info path.toplevel >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'path keys match rev-parse with --separate-git-dir' '
+	git init --separate-git-dir path-separate.git path-separate-worktree &&
+
+	expected_value=$(git -C path-separate-worktree rev-parse --absolute-git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-separate-worktree repo info path.git-dir >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-separate-worktree rev-parse --path-format=absolute --git-common-dir) &&
+	echo "path.common-dir=$expected_value" >expect &&
+	git -C path-separate-worktree repo info path.common-dir >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-separate-worktree rev-parse --show-toplevel) &&
+	echo "path.toplevel=$expected_value" >expect &&
+	git -C path-separate-worktree repo info path.toplevel >actual &&
+	test_cmp expect actual &&
+
+	expected_value=$(git -C path-separate-worktree rev-parse --path-format=relative --git-dir) &&
+	echo "path.git-dir=$expected_value" >expect &&
+	git -C path-separate-worktree repo info --path-format=relative path.git-dir >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'relative path.superproject-working-tree matches rev-parse in submodule' '
+	expected_value=$(git -C path-superproject-parent/sm rev-parse --path-format=relative --show-superproject-working-tree) &&
+	echo "path.superproject-working-tree=$expected_value" >expect &&
+	git -C path-superproject-parent/sm repo info --path-format=relative path.superproject-working-tree >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'path category returns all path keys' '
 	git init path-category &&
 	>expect &&
-- 
2.51.0


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

* Re: [PATCH v6 0/6] repo info: add category/path keys and --path-format
  2026-03-19 20:32               ` Jerry Wang
@ 2026-03-20  1:49                 ` K Jayatheerth
  0 siblings, 0 replies; 68+ messages in thread
From: K Jayatheerth @ 2026-03-20  1:49 UTC (permalink / raw)
  To: Jerry Wang; +Cc: git

On Fri, Mar 20, 2026 at 2:03 AM Jerry Wang <jerrywang183@yahoo.com> wrote:
>
> Hi Jay,
>
> Thanks, this is very helpful.
>
> That makes sense. I agree that checking only
> repo_get_work_tree(repo) == NULL is too weak, and the
> "git -C <nonbare>/.git" case is a good example of that.
>
> I'll keep the linked-worktree / separate-git-dir coverage patch
> separate. For layout.bare, I'll first add tests to pin down the
> intended semantics, then follow up with a small repo-aware fix,
> likely with a repo_is_bare() helper if that turns out to be the
> right shape.
>
> Thanks,
> Jialong
>


If you are a GSoC applicant(I am presuming) I suggest not to start
working on patches
as you might step on other people's toes.

Similar discussion here [1]
Similar official info here [2] in the _Don’t work on a proposed
project right away_ section.
Another similar discussion [3]
Similar thing was caused by Eslam's patches which overlapped with what
Lucas had previously as a foundation [4].

Regards,
- Jayatheerth

1 - https://lore.kernel.org/git/pull.2242.git.git.1773766519857.gitgitgadget@gmail.com/T/#m9506e0856630c9ff962093d59cc9477bd39ea6df
2 - https://git.github.io/General-Microproject-Information/
3 - https://lore.kernel.org/git/CALE2CrTt_2-9C4zCrZPBabtsWY=+Mk-bH4Jaemk=yHtfpoLjfg@mail.gmail.com/T/#m002015a8042bfe2717887c3ada9afd645615f50a
4 - https://lore.kernel.org/git/CA+rGoLdTc2caDUsQedpegL+T4MqwwiA62uuDSFSawAT5vcPvWQ@mail.gmail.com/T/#t

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

end of thread, other threads:[~2026-03-20  1:49 UTC | newest]

Thread overview: 68+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-02-22 18:28 [PATCH 0/3] repo: extend info path reporting and structure statistics eslam reda via GitGitGadget
2026-02-22 18:28 ` [PATCH 1/3] repo: extend info paths " eslam-reda-div via GitGitGadget
2026-02-22 20:35   ` Lucas Seiki Oshiro
2026-02-23  3:02   ` Justin Tobler
2026-02-22 18:28 ` [PATCH 2/3] t1900,t1901: make repo tests hash-agnostic and wc-portable Eslam reda ragheb via GitGitGadget
2026-02-22 20:46   ` Lucas Seiki Oshiro
2026-02-22 18:28 ` [PATCH 3/3] t1900,t1901: fix test portability issues Eslam reda ragheb via GitGitGadget
2026-02-22 22:37 ` [PATCH 0/3] repo: extend info path reporting and structure statistics Junio C Hamano
2026-02-23 14:21 ` [PATCH v2 0/9] " eslam reda via GitGitGadget
2026-02-23 14:21   ` [PATCH v2 1/9] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
2026-02-23 14:21   ` [PATCH v2 2/9] repo: add path keys to repo info Eslam reda ragheb via GitGitGadget
2026-02-23 14:21   ` [PATCH v2 4/9] repo: add structure max object size metrics Eslam reda ragheb via GitGitGadget
2026-02-23 14:21   ` [PATCH v2 5/9] repo: add structure topology and path-depth metrics Eslam reda ragheb via GitGitGadget
2026-02-23 14:21   ` [PATCH v2 6/9] repo: add aggregate structure totals to keyvalue output Eslam reda ragheb via GitGitGadget
2026-02-23 14:21   ` [PATCH v2 7/9] t1900: cover repo info path keys and path-format Eslam reda ragheb via GitGitGadget
2026-02-23 14:21   ` [PATCH v2 8/9] t1901: extend structure metric coverage and portability Eslam reda ragheb via GitGitGadget
2026-02-23 14:21   ` [PATCH v2 9/9] docs: describe repo info path keys and structure metrics Eslam reda ragheb via GitGitGadget
2026-02-23 19:43   ` [PATCH v3 0/5] repo: extend info path reporting and structure statistics eslam reda via GitGitGadget
2026-02-23 19:43     ` [PATCH v3 1/5] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
2026-02-23 19:43     ` [PATCH v3 2/5] repo: add path keys to repo info Eslam reda ragheb via GitGitGadget
2026-02-23 19:43     ` [PATCH v3 3/5] repo: add --path-format for info path output Eslam reda ragheb via GitGitGadget
2026-02-23 19:43     ` [PATCH v3 4/5] t1900: cover repo info path keys and path-format Eslam reda ragheb via GitGitGadget
2026-02-23 19:43     ` [PATCH v3 5/5] docs: describe repo info path keys Eslam reda ragheb via GitGitGadget
2026-02-26 21:14     ` [PATCH v4 00/10] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
2026-02-26 21:14       ` [PATCH v4 01/10] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
2026-02-26 23:21         ` Junio C Hamano
2026-02-26 21:14       ` [PATCH v4 02/10] repo: add path keys to repo info Eslam reda ragheb via GitGitGadget
2026-02-26 23:29         ` Junio C Hamano
2026-02-27  9:04         ` Phillip Wood
2026-02-27 19:51           ` Junio C Hamano
2026-03-01 10:36             ` Phillip Wood
2026-03-02  6:42               ` Junio C Hamano
2026-02-26 21:14       ` [PATCH v4 03/10] repo: add --path-format for info path output Eslam reda ragheb via GitGitGadget
2026-02-26 21:14       ` [PATCH v4 04/10] repo: add structure max object size metrics Eslam reda ragheb via GitGitGadget
2026-02-26 21:14       ` [PATCH v4 05/10] repo: add structure topology and path-depth metrics Eslam reda ragheb via GitGitGadget
2026-02-26 21:14       ` [PATCH v4 06/10] repo: add aggregate structure totals to keyvalue output Eslam reda ragheb via GitGitGadget
2026-02-26 21:14       ` [PATCH v4 07/10] t1900: cover repo info path keys and path-format Eslam reda ragheb via GitGitGadget
2026-02-26 21:14       ` [PATCH v4 08/10] t1901: extend structure metric coverage and portability Eslam reda ragheb via GitGitGadget
2026-02-26 21:14       ` [PATCH v4 09/10] docs: describe repo info path keys and structure metrics Eslam reda ragheb via GitGitGadget
2026-02-26 21:14       ` [PATCH v4 10/10] repo: reduce repetition in structure keyvalue output Eslam reda ragheb via GitGitGadget
2026-02-27 19:30       ` [PATCH v5 00/11] repo info: add category/path keys and --path-format eslam reda via GitGitGadget
2026-02-27 19:30         ` [PATCH v5 01/11] repo: teach info context and category keys Eslam reda ragheb via GitGitGadget
2026-02-27 21:42           ` Lucas Seiki Oshiro
2026-02-27 19:30         ` [PATCH v5 02/11] repo: add path keys to repo info Eslam reda ragheb via GitGitGadget
2026-02-27 19:30         ` [PATCH v5 03/11] repo: add --path-format for info path output Eslam reda ragheb via GitGitGadget
2026-02-27 19:30         ` [PATCH v5 04/11] repo: add structure max object size metrics Eslam reda ragheb via GitGitGadget
2026-02-27 19:30         ` [PATCH v5 05/11] repo: add structure topology and path-depth metrics Eslam reda ragheb via GitGitGadget
2026-02-27 19:30         ` [PATCH v5 06/11] repo: add aggregate structure totals to keyvalue output Eslam reda ragheb via GitGitGadget
2026-02-27 19:30         ` [PATCH v5 07/11] t1900: cover repo info path keys and path-format Eslam reda ragheb via GitGitGadget
2026-02-27 19:30         ` [PATCH v5 08/11] t1901: extend structure metric coverage and portability Eslam reda ragheb via GitGitGadget
2026-02-27 19:30         ` [PATCH v5 09/11] docs: describe repo info path keys and structure metrics Eslam reda ragheb via GitGitGadget
2026-02-27 19:30         ` [PATCH v5 10/11] repo: reduce repetition in structure keyvalue output Eslam reda ragheb via GitGitGadget
2026-02-27 19:30         ` [PATCH v5 11/11] repo: refine path keys for repo info Eslam reda ragheb via GitGitGadget
2026-03-01 10:33           ` Phillip Wood
2026-02-27 21:52         ` [PATCH v5 00/11] repo info: add category/path keys and --path-format Lucas Seiki Oshiro
2026-03-02  5:15         ` [PATCH v6 0/6] " eslam reda via GitGitGadget
2026-03-02  5:15           ` [PATCH v6 1/6] repo: introduce repo_info context plumbing Eslam reda ragheb via GitGitGadget
2026-03-02  5:15           ` [PATCH v6 2/6] repo: support category requests in repo info Eslam reda ragheb via GitGitGadget
2026-03-02  5:15           ` [PATCH v6 3/6] repo: add path keys to " Eslam reda ragheb via GitGitGadget
2026-03-02  5:15           ` [PATCH v6 4/6] repo: add --path-format for info path output Eslam reda ragheb via GitGitGadget
2026-03-02  5:15           ` [PATCH v6 5/6] t1900: cover repo info path keys and path-format Eslam reda ragheb via GitGitGadget
2026-03-02  5:15           ` [PATCH v6 6/6] docs: describe repo info path keys Eslam reda ragheb via GitGitGadget
2026-03-18 20:44           ` [PATCH v6 0/6] repo info: add category/path keys and --path-format Jialong Wang
2026-03-19  3:36             ` K Jayatheerth
2026-03-19 20:32               ` Jerry Wang
2026-03-20  1:49                 ` K Jayatheerth
2026-03-19 20:58           ` [PATCH v6 5/6] t1900: cover repo info path keys and path-format Jialong Wang
2026-03-19 20:59           ` [PATCH] t1900: cover repo info path keys in non-default layouts Jialong Wang

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