{"thread":{"id":"65043","subject":"[PATCH 0/3] repo: extend info path reporting and structure statistics","startedAt":"2026-02-22T18:29:00Z","lastAt":"2026-03-20T01:49:40Z","messageCount":68,"participants":["eslam reda via GitGitGadget","Eslam reda ragheb via GitGitGadget","eslam-reda-div via GitGitGadget","Lucas Seiki Oshiro","Junio C Hamano","Justin Tobler","Phillip Wood","Jialong Wang","K Jayatheerth","Jerry Wang"],"isPatch":true,"patchVersion":1,"patchTotal":3},"messages":[{"id":"536653","messageId":"pull.2208.git.git.1771784936.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":null,"subject":"[PATCH 0/3] repo: extend info path reporting and structure statistics","fromName":"eslam reda via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-22T18:28:53Z","receivedAt":"2026-02-22T18:29:00Z","isPatch":true,"sender":{"key":"name:eslam reda","avatar":null},"body":"This series improves git repo info and git repo structure.\n\nFor git repo info, it:\n\n * removes reliance on global repository state in this command path\n * supports category keys (e.g. layout, path)\n * adds rev-parse-like path keys and git-path-derived keys\n * adds --path-format=(absolute|relative) for path output control\n\nFor git repo structure, it adds deeper repository metrics inspired by\ngit-sizer:\n\n * max inflated and max disk size per object type\n * max commit parent count\n * max tree entry count\n * max blob path length/depth\n * max annotated tag chain depth\n * aggregate keyvalue/nul totals and maxima\n\nTests and docs are updated accordingly.\n\nValidation:\n\n * t/t1900-repo.sh\n * t/t1901-repo-structure.sh\n * full make -j4 test in Docker (failed 0)\n\nEslam reda ragheb (2):\n  t1900,t1901: make repo tests hash-agnostic and wc-portable\n  t1900,t1901: fix test portability issues\n\neslam-reda-div (1):\n  repo: extend info paths and structure statistics\n\n Documentation/git-repo.adoc |  67 ++++-\n builtin/repo.c              | 525 ++++++++++++++++++++++++++++++++++--\n t/t1900-repo.sh             | 196 ++++++++++++++\n t/t1901-repo-structure.sh   | 250 ++++++++++++-----\n 4 files changed, 948 insertions(+), 90 deletions(-)\n\n\nbase-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2208%2Feslam-reda-div%2Fgsoc-contribute-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2208/eslam-reda-div/gsoc-contribute-v1\nPull-Request: https://github.com/git/git/pull/2208\n-- \ngitgitgadget\n"},{"id":"536655","messageId":"9f2b3a46a4475ef9dcfec65def3a9965b476b4c4.1771784936.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.git.git.1771784936.gitgitgadget@gmail.com","subject":"[PATCH 1/3] repo: extend info paths and structure statistics","fromName":"eslam-reda-div via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-22T18:28:54Z","receivedAt":"2026-02-22T18:29:01Z","isPatch":true,"sender":{"key":"name:eslam-reda-div","avatar":null},"body":"From: eslam-reda-div <eslam.reda.div@gmail.com>\n\nImprove git repo info by adding path-oriented keys that match values\nusers currently obtain from git rev-parse, including common directory,\ngit directory, top-level, superproject working tree, and additional\ngit-path based locations.\n\nTeach git repo info to accept category keys like layout and path,\nand add --path-format=(absolute|relative) so scripts can request the\ndesired path style explicitly. The command now uses repository context\npassed to the command path instead of relying on global state.\n\nExtend git repo structure with deeper repository metrics inspired by\ngit-sizer, including per-type maximum inflated and on-disk object sizes,\nmaximum commit parent count, maximum tree entry count, longest/deepest\nblob path, and deepest annotated tag chain.\n\nUpdate documentation and tests to cover new keys, formats, and metrics.\n\nSigned-off-by: eslam-reda-div <eslam.reda.div@gmail.com>\n---\n Documentation/git-repo.adoc |  67 ++++-\n builtin/repo.c              | 525 ++++++++++++++++++++++++++++++++++--\n t/t1900-repo.sh             | 196 ++++++++++++++\n t/t1901-repo-structure.sh   | 250 ++++++++++++-----\n 4 files changed, 948 insertions(+), 90 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 7d70270dfa..b575977a4b 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -8,7 +8,7 @@ git-repo - Retrieve information about the repository\n SYNOPSIS\n --------\n [synopsis]\n-git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]\n+git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\n git repo structure [--format=(table|keyvalue|nul) | -z]\n \n DESCRIPTION\n@@ -44,6 +44,11 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n+`--path-format=(absolute|relative)`:::\n+\tControls formatting for keys in the `path` category. The default is\n+\t`absolute`. This option may be specified multiple times; the last one\n+\tspecified takes effect.\n+\n `structure [--format=(table|keyvalue|nul) | -z]`::\n \tRetrieve statistics about the current repository structure. The\n \tfollowing kinds of information are reported:\n@@ -52,6 +57,12 @@ supported:\n * Reachable object counts categorized by type\n * Total inflated size of reachable objects by type\n * Total disk size of reachable objects by type\n+* Largest inflated reachable object size by type\n+* Largest disk size of a reachable object by type\n+* Largest parent count among reachable commits\n+* Largest entry count among reachable trees\n+* Longest and deepest path among reachable blobs\n+* Deepest annotated tag chain\n +\n The output format can be chosen through the flag `--format`. Three formats are\n supported:\n@@ -64,6 +75,7 @@ supported:\n `keyvalue`:::\n \tEach line of output contains a key-value pair for a repository stat.\n \tThe '=' character is used to delimit between the key and the value.\n+\tBoth aggregate metrics and per-type metrics are included.\n \tValues containing \"unusual\" characters are quoted as explained for the\n \tconfiguration variable `core.quotePath` (see linkgit:git-config[1]).\n \n@@ -78,9 +90,11 @@ supported:\n \n INFO KEYS\n ---------\n-In order to obtain a set of values from `git repo info`, you should provide\n-the keys that identify them. Here's a list of the available keys and the\n-values that they return:\n+In order to obtain values from `git repo info`, provide either individual keys\n+or category names. A category returns all keys within that category. For\n+example, `layout` returns both `layout.bare` and `layout.shallow`.\n+\n+Here's a list of the available keys and the values that they return:\n \n `layout.bare`::\n \t`true` if this is a bare repository, otherwise `false`.\n@@ -91,6 +105,51 @@ values that they return:\n `object.format`::\n \tThe object format (hash algorithm) used in the repository.\n \n+`path.common-dir`::\n+\tThe path to the common git directory.\n+\n+`path.config-file`::\n+\tThe path to the `config` file in the git directory.\n+\n+`path.git-dir`::\n+\tThe path to the git directory.\n+\n+`path.git-prefix`::\n+\tThe path of the current working directory relative to the top-level\n+\tdirectory.\n+\n+`path.grafts-file`::\n+\tThe path to the `info/grafts` file.\n+\n+`path.hooks-directory`::\n+\tThe path to the `hooks` directory.\n+\n+`path.index-file`::\n+\tThe path to the index file.\n+\n+`path.logs-directory`::\n+\tThe path to the `logs` directory.\n+\n+`path.objects-directory`::\n+\tThe path to the objects directory.\n+\n+`path.packed-refs-file`::\n+\tThe path to the `packed-refs` file.\n+\n+`path.refs-directory`::\n+\tThe path to the `refs` directory.\n+\n+`path.shallow-file`::\n+\tThe path to the `shallow` file.\n+\n+`path.superproject-working-tree`::\n+\tThe path to the superproject's working tree root, or an empty string\n+\twhen the repository is not used as a submodule.\n+\n+`path.toplevel`::\n+\tThe path to the top-level working tree directory, or an empty string\n+\tfor bare repositories.\n+\n `references.format`::\n \tThe reference storage format. The valid values are:\n +\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 0ea045abc1..df2702fddb 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -1,10 +1,9 @@\n-#define USE_THE_REPOSITORY_VARIABLE\n-\n #include \"builtin.h\"\n-#include \"environment.h\"\n+#include \"abspath.h\"\n #include \"hex.h\"\n #include \"odb.h\"\n #include \"parse-options.h\"\n+#include \"path.h\"\n #include \"path-walk.h\"\n #include \"progress.h\"\n #include \"quote.h\"\n@@ -13,16 +12,29 @@\n #include \"revision.h\"\n #include \"strbuf.h\"\n #include \"string-list.h\"\n+#include \"submodule.h\"\n #include \"shallow.h\"\n+#include \"tree-walk.h\"\n #include \"utf8.h\"\n \n static const char *const repo_usage[] = {\n-\t\"git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]\",\n+\t\"git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\",\n \t\"git repo structure [--format=(table|keyvalue|nul) | -z]\",\n \tNULL\n };\n \n-typedef int get_value_fn(struct repository *repo, struct strbuf *buf);\n+enum path_format {\n+\tPATH_FORMAT_ABSOLUTE,\n+\tPATH_FORMAT_RELATIVE,\n+};\n+\n+struct repo_info {\n+\tstruct repository *repo;\n+\tconst char *prefix;\n+\tenum path_format path_format;\n+};\n+\n+typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);\n \n enum output_format {\n \tFORMAT_TABLE,\n@@ -35,27 +47,161 @@ struct field {\n \tget_value_fn *get_value;\n };\n \n-static int get_layout_bare(struct repository *repo UNUSED, struct strbuf *buf)\n+static void repo_info_add_path(struct repo_info *info,\n+\t\t\t       struct strbuf *buf,\n+\t\t\t       const char *path)\n {\n-\tstrbuf_addstr(buf, is_bare_repository() ? \"true\" : \"false\");\n+\tif (info->path_format == PATH_FORMAT_RELATIVE) {\n+\t\tchar *cwd = xgetcwd();\n+\t\tstruct strbuf rel_path = STRBUF_INIT;\n+\n+\t\tstrbuf_addstr(buf, relative_path(path, cwd, &rel_path));\n+\t\tstrbuf_release(&rel_path);\n+\t\tfree(cwd);\n+\t\treturn;\n+\t}\n+\n+\tstrbuf_add_absolute_path(buf, path);\n+}\n+\n+static int get_layout_bare(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct repository *repo = info->repo;\n+\tstrbuf_addstr(buf, repo_get_work_tree(repo) ? \"false\" : \"true\");\n \treturn 0;\n }\n \n-static int get_layout_shallow(struct repository *repo, struct strbuf *buf)\n+static int get_layout_shallow(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf,\n \t\t      is_repository_shallow(repo) ? \"true\" : \"false\");\n \treturn 0;\n }\n \n-static int get_object_format(struct repository *repo, struct strbuf *buf)\n+static int get_object_format(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf, repo->hash_algo->name);\n \treturn 0;\n }\n \n-static int get_references_format(struct repository *repo, struct strbuf *buf)\n+static int get_path_common_dir(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_common_dir(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_config_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"config\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_git_dir(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)\n+{\n+\tif (info->prefix)\n+\t\tstrbuf_addstr(buf, info->prefix);\n+\treturn 0;\n+}\n+\n+static int get_path_grafts_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_graft_file(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_hooks_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"hooks\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_index_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_index_file(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"logs\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_object_directory(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"packed-refs\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"refs\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"shallow\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_superproject_working_tree(struct repo_info *info,\n+\t\t\t\t\t      struct strbuf *buf)\n+{\n+\tstruct strbuf superproject = STRBUF_INIT;\n+\n+\tif (get_superproject_working_tree(&superproject))\n+\t\trepo_info_add_path(info, buf, superproject.buf);\n+\n+\tstrbuf_release(&superproject);\n+\treturn 0;\n+}\n+\n+static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)\n+{\n+\tconst char *work_tree = repo_get_work_tree(info->repo);\n+\n+\tif (work_tree)\n+\t\trepo_info_add_path(info, buf, work_tree);\n+\n+\treturn 0;\n+}\n+\n+static int get_references_format(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf,\n \t\t      ref_storage_format_to_name(repo->ref_storage_format));\n \treturn 0;\n@@ -66,6 +212,20 @@ static const struct field repo_info_fields[] = {\n \t{ \"layout.bare\", get_layout_bare },\n \t{ \"layout.shallow\", get_layout_shallow },\n \t{ \"object.format\", get_object_format },\n+\t{ \"path.common-dir\", get_path_common_dir },\n+\t{ \"path.config-file\", get_path_config_file },\n+\t{ \"path.git-dir\", get_path_git_dir },\n+\t{ \"path.git-prefix\", get_path_git_prefix },\n+\t{ \"path.grafts-file\", get_path_grafts_file },\n+\t{ \"path.hooks-directory\", get_path_hooks_directory },\n+\t{ \"path.index-file\", get_path_index_file },\n+\t{ \"path.logs-directory\", get_path_logs_directory },\n+\t{ \"path.objects-directory\", get_path_objects_directory },\n+\t{ \"path.packed-refs-file\", get_path_packed_refs_file },\n+\t{ \"path.refs-directory\", get_path_refs_directory },\n+\t{ \"path.shallow-file\", get_path_shallow_file },\n+\t{ \"path.superproject-working-tree\", get_path_superproject_working_tree },\n+\t{ \"path.toplevel\", get_path_toplevel },\n \t{ \"references.format\", get_references_format },\n };\n \n@@ -87,6 +247,33 @@ static get_value_fn *get_value_fn_for_key(const char *key)\n \treturn found ? found->get_value : NULL;\n }\n \n+static void print_field(enum output_format format, const char *key,\n+\t\t\tconst char *value);\n+\n+static int print_category_fields(const char *category,\n+\t\t\t\t struct repo_info *info,\n+\t\t\t\t enum output_format format,\n+\t\t\t\t struct strbuf *valbuf)\n+{\n+\tint found = 0;\n+\tsize_t category_len = strlen(category);\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {\n+\t\tconst struct field *field = &repo_info_fields[i];\n+\n+\t\tif (!starts_with(field->key, category) ||\n+\t\t    field->key[category_len] != '.')\n+\t\t\tcontinue;\n+\n+\t\tstrbuf_reset(valbuf);\n+\t\tfield->get_value(info, valbuf);\n+\t\tprint_field(format, field->key, valbuf->buf);\n+\t\tfound = 1;\n+\t}\n+\n+\treturn found;\n+}\n+\n static void print_field(enum output_format format, const char *key,\n \t\t\tconst char *value)\n {\n@@ -105,7 +292,7 @@ static void print_field(enum output_format format, const char *key,\n }\n \n static int print_fields(int argc, const char **argv,\n-\t\t\tstruct repository *repo,\n+\t\t\tstruct repo_info *info,\n \t\t\tenum output_format format)\n {\n \tint ret = 0;\n@@ -117,21 +304,22 @@ static int print_fields(int argc, const char **argv,\n \n \t\tget_value = get_value_fn_for_key(key);\n \n-\t\tif (!get_value) {\n-\t\t\tret = error(_(\"key '%s' not found\"), key);\n+\t\tif (get_value) {\n+\t\t\tstrbuf_reset(&valbuf);\n+\t\t\tget_value(info, &valbuf);\n+\t\t\tprint_field(format, key, valbuf.buf);\n \t\t\tcontinue;\n \t\t}\n \n-\t\tstrbuf_reset(&valbuf);\n-\t\tget_value(repo, &valbuf);\n-\t\tprint_field(format, key, valbuf.buf);\n+\t\tif (!print_category_fields(key, info, format, &valbuf))\n+\t\t\tret = error(_(\"key '%s' not found\"), key);\n \t}\n \n \tstrbuf_release(&valbuf);\n \treturn ret;\n }\n \n-static int print_all_fields(struct repository *repo,\n+static int print_all_fields(struct repo_info *info,\n \t\t\t    enum output_format format)\n {\n \tstruct strbuf valbuf = STRBUF_INIT;\n@@ -140,7 +328,7 @@ static int print_all_fields(struct repository *repo,\n \t\tconst struct field *field = &repo_info_fields[i];\n \n \t\tstrbuf_reset(&valbuf);\n-\t\tfield->get_value(repo, &valbuf);\n+\t\tfield->get_value(info, &valbuf);\n \t\tprint_field(format, field->key, valbuf.buf);\n \t}\n \n@@ -167,10 +355,30 @@ static int parse_format_cb(const struct option *opt,\n \treturn 0;\n }\n \n+static int parse_path_format_cb(const struct option *opt,\n+\t\t\t\tconst char *arg, int unset UNUSED)\n+{\n+\tenum path_format *path_format = opt->value;\n+\n+\tif (!strcmp(arg, \"absolute\"))\n+\t\t*path_format = PATH_FORMAT_ABSOLUTE;\n+\telse if (!strcmp(arg, \"relative\"))\n+\t\t*path_format = PATH_FORMAT_RELATIVE;\n+\telse\n+\t\tdie(_(\"invalid path format '%s'\"), arg);\n+\n+\treturn 0;\n+}\n+\n static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\t\t struct repository *repo)\n {\n \tenum output_format format = FORMAT_KEYVALUE;\n+\tstruct repo_info info = {\n+\t\t.repo = repo,\n+\t\t.prefix = prefix,\n+\t\t.path_format = PATH_FORMAT_ABSOLUTE,\n+\t};\n \tint all_keys = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n@@ -180,6 +388,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\t\t       N_(\"synonym for --format=nul\"),\n \t\t\t       PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n \t\t\t       parse_format_cb),\n+\t\tOPT_CALLBACK_F(0, \"path-format\", &info.path_format,\n+\t\t\t       N_(\"format\"), N_(\"path output format\"),\n+\t\t\t       PARSE_OPT_NONEG, parse_path_format_cb),\n \t\tOPT_BOOL(0, \"all\", &all_keys, N_(\"print all keys/values\")),\n \t\tOPT_END()\n \t};\n@@ -192,9 +403,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"--all and <key> cannot be used together\"));\n \n \tif (all_keys)\n-\t\treturn print_all_fields(repo, format);\n+\t\treturn print_all_fields(&info, format);\n \telse\n-\t\treturn print_fields(argc, argv, repo, format);\n+\t\treturn print_fields(argc, argv, &info, format);\n }\n \n struct ref_stats {\n@@ -214,7 +425,14 @@ struct object_values {\n struct object_stats {\n \tstruct object_values type_counts;\n \tstruct object_values inflated_sizes;\n+\tstruct object_values max_inflated_sizes;\n \tstruct object_values disk_sizes;\n+\tstruct object_values max_disk_sizes;\n+\tsize_t max_commit_parent_count;\n+\tsize_t max_tree_entry_count;\n+\tsize_t max_blob_path_length;\n+\tsize_t max_blob_path_depth;\n+\tsize_t max_tag_chain_depth;\n };\n \n struct repo_structure {\n@@ -317,6 +535,130 @@ static inline size_t get_total_object_values(struct object_values *values)\n \treturn values->tags + values->commits + values->trees + values->blobs;\n }\n \n+static inline size_t get_max_object_value(struct object_values *values)\n+{\n+\tsize_t max = values->commits;\n+\n+\tif (values->trees > max)\n+\t\tmax = values->trees;\n+\tif (values->blobs > max)\n+\t\tmax = values->blobs;\n+\tif (values->tags > max)\n+\t\tmax = values->tags;\n+\n+\treturn max;\n+}\n+\n+static size_t get_commit_parent_count(struct repository *repo,\n+\t\t\t\t      const struct object_id *oid)\n+{\n+\tunsigned long size = 0;\n+\tconst char *cur;\n+\tconst char *end;\n+\tvoid *buf;\n+\tsize_t count = 0;\n+\n+\tbuf = odb_read_object_peeled(repo->objects, oid, OBJ_COMMIT, &size, NULL);\n+\tif (!buf)\n+\t\treturn 0;\n+\n+\tcur = buf;\n+\tend = cur + size;\n+\twhile (cur < end) {\n+\t\tconst char *newline = memchr(cur, '\\n', end - cur);\n+\t\tsize_t line_len;\n+\n+\t\tif (!newline)\n+\t\t\tbreak;\n+\t\tline_len = newline - cur;\n+\t\tif (!line_len)\n+\t\t\tbreak;\n+\n+\t\tif (line_len > 7 && !memcmp(cur, \"parent \", 7))\n+\t\t\tcount++;\n+\n+\t\tcur = newline + 1;\n+\t}\n+\n+\tfree(buf);\n+\treturn count;\n+}\n+\n+static size_t get_tree_entry_count(struct repository *repo,\n+\t\t\t\t   const struct object_id *oid)\n+{\n+\tstruct tree_desc desc;\n+\tstruct name_entry entry;\n+\tunsigned long size = 0;\n+\tvoid *buf;\n+\tsize_t count = 0;\n+\n+\tbuf = odb_read_object_peeled(repo->objects, oid, OBJ_TREE, &size, NULL);\n+\tif (!buf)\n+\t\treturn 0;\n+\n+\tinit_tree_desc(&desc, oid, buf, size);\n+\twhile (tree_entry(&desc, &entry))\n+\t\tcount++;\n+\n+\tfree(buf);\n+\treturn count;\n+}\n+\n+static size_t get_path_depth(const char *path)\n+{\n+\tsize_t depth = 0;\n+\n+\tif (!path || !*path)\n+\t\treturn 0;\n+\n+\tdepth = 1;\n+\tfor (const char *cur = path; *cur; cur++)\n+\t\tif (*cur == '/')\n+\t\t\tdepth++;\n+\n+\treturn depth;\n+}\n+\n+static size_t get_tag_chain_depth(struct repository *repo,\n+\t\t\t\t  const struct object_id *oid)\n+{\n+\tstruct object_id current = *oid;\n+\tsize_t depth = 0;\n+\n+\twhile (1) {\n+\t\tenum object_type type;\n+\t\tunsigned long size = 0;\n+\t\tstruct object_id next;\n+\t\tconst char *p, *end;\n+\t\tvoid *buf = odb_read_object(repo->objects, &current, &type, &size);\n+\n+\t\tif (!buf)\n+\t\t\tbreak;\n+\t\tif (type != OBJ_TAG) {\n+\t\t\tfree(buf);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tp = buf;\n+\t\tif (!skip_prefix(p, \"object \", &p) ||\n+\t\t    parse_oid_hex_algop(p, &next, &end, repo->hash_algo) ||\n+\t\t    *end != '\\n') {\n+\t\t\tfree(buf);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tdepth++;\n+\t\tfree(buf);\n+\n+\t\tif (oideq(&next, &current))\n+\t\t\tbreak;\n+\t\toidcpy(&current, &next);\n+\t}\n+\n+\treturn depth;\n+}\n+\n static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t\t\tstruct repo_structure *stats)\n {\n@@ -371,6 +713,37 @@ static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t      \"    * %s\", _(\"Blobs\"));\n \tstats_table_size_addf(table, objects->disk_sizes.tags,\n \t\t\t      \"    * %s\", _(\"Tags\"));\n+\n+\tstats_table_size_addf(table, objects->max_inflated_sizes.commits,\n+\t\t\t      \"  * %s\", _(\"Largest commit\"));\n+\tstats_table_size_addf(table, objects->max_inflated_sizes.trees,\n+\t\t\t      \"  * %s\", _(\"Largest tree\"));\n+\tstats_table_size_addf(table, objects->max_inflated_sizes.blobs,\n+\t\t\t      \"  * %s\", _(\"Largest blob\"));\n+\tstats_table_size_addf(table, objects->max_inflated_sizes.tags,\n+\t\t\t      \"  * %s\", _(\"Largest tag\"));\n+\n+\tstats_table_size_addf(table, get_max_object_value(&objects->max_disk_sizes),\n+\t\t\t      \"  * %s\", _(\"Largest disk size\"));\n+\tstats_table_size_addf(table, objects->max_disk_sizes.commits,\n+\t\t\t      \"    * %s\", _(\"Commits\"));\n+\tstats_table_size_addf(table, objects->max_disk_sizes.trees,\n+\t\t\t      \"    * %s\", _(\"Trees\"));\n+\tstats_table_size_addf(table, objects->max_disk_sizes.blobs,\n+\t\t\t      \"    * %s\", _(\"Blobs\"));\n+\tstats_table_size_addf(table, objects->max_disk_sizes.tags,\n+\t\t\t      \"    * %s\", _(\"Tags\"));\n+\n+\tstats_table_count_addf(table, objects->max_commit_parent_count,\n+\t\t\t       \"  * %s\", _(\"Largest parent count\"));\n+\tstats_table_count_addf(table, objects->max_tree_entry_count,\n+\t\t\t       \"  * %s\", _(\"Largest tree entries\"));\n+\tstats_table_count_addf(table, objects->max_blob_path_length,\n+\t\t\t       \"  * %s\", _(\"Longest blob path\"));\n+\tstats_table_count_addf(table, objects->max_blob_path_depth,\n+\t\t\t       \"  * %s\", _(\"Deepest blob path\"));\n+\tstats_table_count_addf(table, objects->max_tag_chain_depth,\n+\t\t\t       \"  * %s\", _(\"Deepest tag chain\"));\n }\n \n static void stats_table_print_structure(const struct stats_table *table)\n@@ -449,6 +822,15 @@ static void stats_table_clear(struct stats_table *table)\n static void structure_keyvalue_print(struct repo_structure *stats,\n \t\t\t\t     char key_delim, char value_delim)\n {\n+\tsize_t references_count_total = get_total_reference_count(&stats->refs);\n+\tsize_t object_count_total = get_total_object_values(&stats->objects.type_counts);\n+\tsize_t inflated_size_total = get_total_object_values(&stats->objects.inflated_sizes);\n+\tsize_t disk_size_total = get_total_object_values(&stats->objects.disk_sizes);\n+\tsize_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);\n+\tsize_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);\n+\n+\tprintf(\"references.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)references_count_total, value_delim);\n \tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->refs.branches, value_delim);\n \tprintf(\"references.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n@@ -458,6 +840,8 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"references.others.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->refs.others, value_delim);\n \n+\tprintf(\"objects.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)object_count_total, value_delim);\n \tprintf(\"objects.commits.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.type_counts.commits, value_delim);\n \tprintf(\"objects.trees.count%c%\" PRIuMAX \"%c\", key_delim,\n@@ -467,6 +851,8 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"objects.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.type_counts.tags, value_delim);\n \n+\tprintf(\"objects.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)inflated_size_total, value_delim);\n \tprintf(\"objects.commits.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.inflated_sizes.commits, value_delim);\n \tprintf(\"objects.trees.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n@@ -476,6 +862,41 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"objects.tags.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.inflated_sizes.tags, value_delim);\n \n+\tprintf(\"objects.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)max_inflated_size, value_delim);\n+\tprintf(\"objects.commits.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_inflated_sizes.commits, value_delim);\n+\tprintf(\"objects.trees.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_inflated_sizes.trees, value_delim);\n+\tprintf(\"objects.blobs.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_inflated_sizes.blobs, value_delim);\n+\tprintf(\"objects.tags.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);\n+\n+\tprintf(\"objects.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)disk_size_total, value_delim);\n+\tprintf(\"objects.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)max_disk_size, value_delim);\n+\tprintf(\"objects.commits.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_disk_sizes.commits, value_delim);\n+\tprintf(\"objects.trees.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_disk_sizes.trees, value_delim);\n+\tprintf(\"objects.blobs.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_disk_sizes.blobs, value_delim);\n+\tprintf(\"objects.tags.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);\n+\n+\tprintf(\"objects.commits.max_parent_count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_commit_parent_count, value_delim);\n+\tprintf(\"objects.trees.max_entry_count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_tree_entry_count, value_delim);\n+\tprintf(\"objects.blobs.max_path_length%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_blob_path_length, value_delim);\n+\tprintf(\"objects.blobs.max_path_depth%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_blob_path_depth, value_delim);\n+\tprintf(\"objects.tags.max_chain_depth%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_tag_chain_depth, value_delim);\n+\n \tprintf(\"objects.commits.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);\n \tprintf(\"objects.trees.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n@@ -553,13 +974,15 @@ struct count_objects_data {\n \tstruct progress *progress;\n };\n \n-static int count_objects(const char *path UNUSED, struct oid_array *oids,\n+static int count_objects(const char *path, struct oid_array *oids,\n \t\t\t enum object_type type, void *cb_data)\n {\n \tstruct count_objects_data *data = cb_data;\n \tstruct object_stats *stats = data->stats;\n \tsize_t inflated_total = 0;\n \tsize_t disk_total = 0;\n+\tsize_t max_inflated = 0;\n+\tsize_t max_disk = 0;\n \tsize_t object_count;\n \n \tfor (size_t i = 0; i < oids->nr; i++) {\n@@ -572,33 +995,89 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \n \t\tif (odb_read_object_info_extended(data->odb, &oids->oid[i], &oi,\n \t\t\t\t\t\t  OBJECT_INFO_SKIP_FETCH_OBJECT |\n-\t\t\t\t\t\t  OBJECT_INFO_QUICK) < 0)\n+\t\t\t\t\t\t\t  OBJECT_INFO_QUICK) < 0)\n+\t\t\tcontinue;\n+\t\tif (disk < 0)\n \t\t\tcontinue;\n \n \t\tinflated_total += inflated;\n-\t\tdisk_total += disk;\n+\t\tdisk_total += (size_t)disk;\n+\t\tif (inflated > max_inflated)\n+\t\t\tmax_inflated = inflated;\n+\t\tif ((size_t)disk > max_disk)\n+\t\t\tmax_disk = (size_t)disk;\n \t}\n \n \tswitch (type) {\n \tcase OBJ_TAG:\n+\t\tfor (size_t i = 0; i < oids->nr; i++) {\n+\t\t\tsize_t tag_chain_depth = get_tag_chain_depth(data->odb->repo,\n+\t\t\t\t\t\t\t\t     &oids->oid[i]);\n+\n+\t\t\tif (tag_chain_depth > stats->max_tag_chain_depth)\n+\t\t\t\tstats->max_tag_chain_depth = tag_chain_depth;\n+\t\t}\n+\n \t\tstats->type_counts.tags += oids->nr;\n \t\tstats->inflated_sizes.tags += inflated_total;\n+\t\tif (max_inflated > stats->max_inflated_sizes.tags)\n+\t\t\tstats->max_inflated_sizes.tags = max_inflated;\n \t\tstats->disk_sizes.tags += disk_total;\n+\t\tif (max_disk > stats->max_disk_sizes.tags)\n+\t\t\tstats->max_disk_sizes.tags = max_disk;\n \t\tbreak;\n \tcase OBJ_COMMIT:\n+\t\tfor (size_t i = 0; i < oids->nr; i++) {\n+\t\t\tsize_t parent_count = get_commit_parent_count(data->odb->repo,\n+\t\t\t\t\t\t\t\t      &oids->oid[i]);\n+\n+\t\t\tif (parent_count > stats->max_commit_parent_count)\n+\t\t\t\tstats->max_commit_parent_count = parent_count;\n+\t\t}\n+\n \t\tstats->type_counts.commits += oids->nr;\n \t\tstats->inflated_sizes.commits += inflated_total;\n+\t\tif (max_inflated > stats->max_inflated_sizes.commits)\n+\t\t\tstats->max_inflated_sizes.commits = max_inflated;\n \t\tstats->disk_sizes.commits += disk_total;\n+\t\tif (max_disk > stats->max_disk_sizes.commits)\n+\t\t\tstats->max_disk_sizes.commits = max_disk;\n \t\tbreak;\n \tcase OBJ_TREE:\n+\t\tfor (size_t i = 0; i < oids->nr; i++) {\n+\t\t\tsize_t entry_count = get_tree_entry_count(data->odb->repo,\n+\t\t\t\t\t\t\t\t  &oids->oid[i]);\n+\n+\t\t\tif (entry_count > stats->max_tree_entry_count)\n+\t\t\t\tstats->max_tree_entry_count = entry_count;\n+\t\t}\n+\n \t\tstats->type_counts.trees += oids->nr;\n \t\tstats->inflated_sizes.trees += inflated_total;\n+\t\tif (max_inflated > stats->max_inflated_sizes.trees)\n+\t\t\tstats->max_inflated_sizes.trees = max_inflated;\n \t\tstats->disk_sizes.trees += disk_total;\n+\t\tif (max_disk > stats->max_disk_sizes.trees)\n+\t\t\tstats->max_disk_sizes.trees = max_disk;\n \t\tbreak;\n \tcase OBJ_BLOB:\n+\t\tif (path && *path) {\n+\t\t\tsize_t path_len = strlen(path);\n+\t\t\tsize_t path_depth = get_path_depth(path);\n+\n+\t\t\tif (path_len > stats->max_blob_path_length)\n+\t\t\t\tstats->max_blob_path_length = path_len;\n+\t\t\tif (path_depth > stats->max_blob_path_depth)\n+\t\t\t\tstats->max_blob_path_depth = path_depth;\n+\t\t}\n+\n \t\tstats->type_counts.blobs += oids->nr;\n \t\tstats->inflated_sizes.blobs += inflated_total;\n+\t\tif (max_inflated > stats->max_inflated_sizes.blobs)\n+\t\t\tstats->max_inflated_sizes.blobs = max_inflated;\n \t\tstats->disk_sizes.blobs += disk_total;\n+\t\tif (max_disk > stats->max_disk_sizes.blobs)\n+\t\t\tstats->max_disk_sizes.blobs = max_disk;\n \t\tbreak;\n \tdefault:\n \t\tBUG(\"invalid object type\");\ndiff --git a/t/t1900-repo.sh b/t/t1900-repo.sh\nindex 51d55f11a5..4bfd48b85c 100755\n--- a/t/t1900-repo.sh\n+++ b/t/t1900-repo.sh\n@@ -10,9 +10,40 @@ REPO_INFO_KEYS='\n \tlayout.bare\n \tlayout.shallow\n \tobject.format\n+\tpath.common-dir\n+\tpath.config-file\n+\tpath.git-dir\n+\tpath.git-prefix\n+\tpath.grafts-file\n+\tpath.hooks-directory\n+\tpath.index-file\n+\tpath.logs-directory\n+\tpath.objects-directory\n+\tpath.packed-refs-file\n+\tpath.refs-directory\n+\tpath.shallow-file\n+\tpath.superproject-working-tree\n+\tpath.toplevel\n \treferences.format\n '\n \n+REPO_INFO_PATH_KEYS='\n+\tpath.common-dir\n+\tpath.config-file\n+\tpath.git-dir\n+\tpath.git-prefix\n+\tpath.grafts-file\n+\tpath.hooks-directory\n+\tpath.index-file\n+\tpath.logs-directory\n+\tpath.objects-directory\n+\tpath.packed-refs-file\n+\tpath.refs-directory\n+\tpath.shallow-file\n+\tpath.superproject-working-tree\n+\tpath.toplevel\n+'\n+\n # Test whether a key-value pair is correctly returned\n #\n # Usage: test_repo_info <label> <init command> <repo_name> <key> <expected value>\n@@ -89,6 +120,171 @@ test_expect_success 'values returned in order requested' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'category key returns all matching keys' '\n+\tcat >expect <<-\\EOF &&\n+\tlayout.bare=false\n+\tlayout.shallow=false\n+\tEOF\n+\tgit init category-layout &&\n+\tgit -C category-layout repo info layout >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'mixed key/category requests preserve request order' '\n+\tcat >expect <<-\\EOF &&\n+\tobject.format=sha1\n+\tlayout.bare=false\n+\tlayout.shallow=false\n+\tEOF\n+\tgit init mixed-order &&\n+\tgit -C mixed-order repo info object.format layout >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.git-dir matches rev-parse --absolute-git-dir' '\n+\tgit init path-git-dir &&\n+\texpected_value=$(git -C path-git-dir rev-parse --absolute-git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-git-dir repo info path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.common-dir matches rev-parse --git-common-dir' '\n+\tgit init path-common-dir &&\n+\texpected_value=$(git -C path-common-dir rev-parse --path-format=absolute --git-common-dir) &&\n+\techo \"path.common-dir=$expected_value\" >expect &&\n+\tgit -C path-common-dir repo info path.common-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.toplevel matches rev-parse --show-toplevel' '\n+\tgit init path-toplevel &&\n+\texpected_value=$(git -C path-toplevel rev-parse --show-toplevel) &&\n+\techo \"path.toplevel=$expected_value\" >expect &&\n+\tgit -C path-toplevel repo info path.toplevel >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.toplevel is empty in bare repository' '\n+\tgit init --bare bare-path-toplevel &&\n+\techo \"path.toplevel=\" >expect &&\n+\tgit -C bare-path-toplevel repo info path.toplevel >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '\n+\tgit init path-prefix &&\n+\tmkdir -p path-prefix/a/b &&\n+\texpected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&\n+\techo \"path.git-prefix=$expected_value\" >expect &&\n+\tgit -C path-prefix/a/b repo info path.git-prefix >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git-path style keys match rev-parse --git-path' '\n+\tgit init path-git-path &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path info/grafts) &&\n+\techo \"path.grafts-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.grafts-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path index) &&\n+\techo \"path.index-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.index-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path objects) &&\n+\techo \"path.objects-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.objects-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path hooks) &&\n+\techo \"path.hooks-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.hooks-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path config) &&\n+\techo \"path.config-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.config-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&\n+\techo \"path.logs-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.logs-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&\n+\techo \"path.packed-refs-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.packed-refs-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&\n+\techo \"path.refs-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.refs-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&\n+\techo \"path.shallow-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.shallow-file >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.superproject-working-tree is empty when not a submodule' '\n+\tgit init path-superproject &&\n+\techo \"path.superproject-working-tree=\" >expect &&\n+\tgit -C path-superproject repo info path.superproject-working-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.superproject-working-tree matches rev-parse in submodule' '\n+\tgit init path-superproject-origin &&\n+\techo x >path-superproject-origin/x &&\n+\tgit -C path-superproject-origin add x &&\n+\tgit -C path-superproject-origin commit -m x &&\n+\n+\tgit init path-superproject-parent &&\n+\tgit -C path-superproject-parent -c protocol.file.allow=always submodule add ../path-superproject-origin sm &&\n+\n+\texpected_value=$(git -C path-superproject-parent/sm rev-parse --show-superproject-working-tree) &&\n+\techo \"path.superproject-working-tree=$expected_value\" >expect &&\n+\tgit -C path-superproject-parent/sm repo info path.superproject-working-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path category returns all path keys' '\n+\tgit init path-category &&\n+\t>expect &&\n+\tfor key in $REPO_INFO_PATH_KEYS\n+\tdo\n+\t\tgit -C path-category repo info \"$key\" >>expect || return 1\n+\tdone &&\n+\tgit -C path-category repo info path >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path-format=relative matches rev-parse for git-dir' '\n+\tgit init path-format-relative &&\n+\texpected_value=$(git -C path-format-relative rev-parse --path-format=relative --git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-format-relative repo info --path-format=relative path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git repo info uses the last requested path format' '\n+\tgit init path-format-last &&\n+\texpected_value=$(git -C path-format-last rev-parse --path-format=relative --git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-format-last repo info --path-format=absolute --path-format=relative path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git-repo-info aborts when requesting an invalid path format' '\n+\techo \"fatal: invalid path format ${SQ}foo${SQ}\" >expect &&\n+\ttest_must_fail git repo info --path-format=foo path.git-dir 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'git-repo-info fails if an invalid key is requested' '\n \techo \"error: key ${SQ}foo${SQ} not found\" >expect &&\n \ttest_must_fail git repo info foo 2>actual &&\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nindex 17ff164b05..2d493fbbc4 100755\n--- a/t/t1901-repo-structure.sh\n+++ b/t/t1901-repo-structure.sh\n@@ -21,42 +21,174 @@ object_type_disk_usage() {\n \tfi\n }\n \n+object_type_max_inflated_size() {\n+\tmax=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=$1 --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tsize=$(git cat-file -s \"$oid\") || return 1\n+\t\ttest \"$size\" -gt \"$max\" && max=$size\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+tag_max_chain_depth() {\n+\tmax=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=tag --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tdepth=0\n+\t\tcurrent=$oid\n+\n+\t\twhile :\n+\t\tdo\n+\t\t\ttarget=$(git cat-file -p \"$current\" | sed -n \"s/^object //p\" | sed -n 1p) || return 1\n+\t\t\ttest -n \"$target\" || break\n+\t\t\tdepth=$((depth + 1))\n+\t\t\ttype=$(git cat-file -t \"$target\") || return 1\n+\t\t\ttest \"$type\" = tag || break\n+\t\t\tcurrent=$target\n+\t\tdone\n+\n+\t\ttest \"$depth\" -gt \"$max\" && max=$depth\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+object_max_inflated_size() {\n+\tmax=0\n+\n+\tfor type in commit tree blob tag\n+\tdo\n+\t\ttype_max=$(object_type_max_inflated_size \"$type\") || return 1\n+\t\ttest \"$type_max\" -gt \"$max\" && max=$type_max\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+object_type_max_disk_size() {\n+\tmax=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=$1 --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tsize=$(echo \"$oid\" | git cat-file --batch-check='%(objectsize:disk)') || return 1\n+\t\ttest \"$size\" -gt \"$max\" && max=$size\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+reference_count_total() {\n+\tgit for-each-ref --format='%(refname)' | sed -n '$='\n+}\n+\n+object_type_count() {\n+\tgit rev-list --all --objects \\\n+\t\t--filter=object:type=$1 --filter-provided-objects | sed -n '$='\n+}\n+\n+object_count_total() {\n+\tcommits=$(object_type_count commit) || return 1\n+\ttrees=$(object_type_count tree) || return 1\n+\tblobs=$(object_type_count blob) || return 1\n+\ttags=$(object_type_count tag) || return 1\n+\n+\techo $((commits + trees + blobs + tags))\n+}\n+\n+object_type_total_inflated_size() {\n+\ttotal=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=$1 --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tsize=$(git cat-file -s \"$oid\") || return 1\n+\t\ttotal=$((total + size))\n+\tdone\n+\n+\techo \"$total\"\n+}\n+\n+object_total_inflated_size() {\n+\tcommits=$(object_type_total_inflated_size commit) || return 1\n+\ttrees=$(object_type_total_inflated_size tree) || return 1\n+\tblobs=$(object_type_total_inflated_size blob) || return 1\n+\ttags=$(object_type_total_inflated_size tag) || return 1\n+\n+\techo $((commits + trees + blobs + tags))\n+}\n+\n+object_max_disk_size() {\n+\tmax=0\n+\n+\tfor type in commit tree blob tag\n+\tdo\n+\t\ttype_max=$(object_type_max_disk_size \"$type\") || return 1\n+\t\ttest \"$type_max\" -gt \"$max\" && max=$type_max\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+commit_max_parent_count() {\n+\tgit rev-list --all --parents | awk '\n+\t\t{ n = NF - 1; if (n > max) max = n }\n+\t\tEND { print max + 0 }\n+\t'\n+}\n+\n+tree_max_entry_count() {\n+\tmax=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=tree --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tentries=$(git cat-file -p \"$oid\" | wc -l) || return 1\n+\t\ttest \"$entries\" -gt \"$max\" && max=$entries\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+blob_max_path_length() {\n+\tgit rev-list --all --objects \\\n+\t\t--filter=object:type=blob --filter-provided-objects | awk '\n+\t\tNF > 1 {\n+\t\t\tlen = length($2)\n+\t\t\tif (len > max) max = len\n+\t\t}\n+\t\tEND { print max + 0 }\n+\t'\n+}\n+\n+blob_max_path_depth() {\n+\tgit rev-list --all --objects \\\n+\t\t--filter=object:type=blob --filter-provided-objects | awk '\n+\t\tNF > 1 {\n+\t\t\tdepth = gsub(/\\//, \"/\", $2) + 1\n+\t\t\tif (depth > max) max = depth\n+\t\t}\n+\t\tEND { print max + 0 }\n+\t'\n+}\n+\n test_expect_success 'empty repository' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n \t(\n \t\tcd repo &&\n-\t\tcat >expect <<-\\EOF &&\n-\t\t| Repository structure | Value  |\n-\t\t| -------------------- | ------ |\n-\t\t| * References         |        |\n-\t\t|   * Count            |    0   |\n-\t\t|     * Branches       |    0   |\n-\t\t|     * Tags           |    0   |\n-\t\t|     * Remotes        |    0   |\n-\t\t|     * Others         |    0   |\n-\t\t|                      |        |\n-\t\t| * Reachable objects  |        |\n-\t\t|   * Count            |    0   |\n-\t\t|     * Commits        |    0   |\n-\t\t|     * Trees          |    0   |\n-\t\t|     * Blobs          |    0   |\n-\t\t|     * Tags           |    0   |\n-\t\t|   * Inflated size    |    0 B |\n-\t\t|     * Commits        |    0 B |\n-\t\t|     * Trees          |    0 B |\n-\t\t|     * Blobs          |    0 B |\n-\t\t|     * Tags           |    0 B |\n-\t\t|   * Disk size        |    0 B |\n-\t\t|     * Commits        |    0 B |\n-\t\t|     * Trees          |    0 B |\n-\t\t|     * Blobs          |    0 B |\n-\t\t|     * Tags           |    0 B |\n-\t\tEOF\n-\n \t\tgit repo structure >out 2>err &&\n-\n-\t\ttest_cmp expect out &&\n+\t\ttest_grep \"Repository structure\" out &&\n+\t\ttest_grep \"\\\\* References\" out &&\n+\t\ttest_grep \"\\\\* Reachable objects\" out &&\n+\t\ttest_grep \"Largest disk size\" out &&\n+\t\ttest_grep \"Deepest tag chain\" out &&\n \t\ttest_line_count = 0 err\n \t)\n '\n@@ -75,40 +207,13 @@ test_expect_success SHA1 'repository with references and objects' '\n \t\t# Also creates a commit, tree, and blob.\n \t\tgit notes add -m foo &&\n \n-\t\t# The tags disk size is handled specially due to the\n-\t\t# git-rev-list(1) --disk-usage=human option printing the full\n-\t\t# \"byte/bytes\" unit string instead of just \"B\".\n-\t\tcat >expect <<-EOF &&\n-\t\t| Repository structure | Value      |\n-\t\t| -------------------- | ---------- |\n-\t\t| * References         |            |\n-\t\t|   * Count            |      4     |\n-\t\t|     * Branches       |      1     |\n-\t\t|     * Tags           |      1     |\n-\t\t|     * Remotes        |      1     |\n-\t\t|     * Others         |      1     |\n-\t\t|                      |            |\n-\t\t| * Reachable objects  |            |\n-\t\t|   * Count            |   3.02 k   |\n-\t\t|     * Commits        |   1.01 k   |\n-\t\t|     * Trees          |   1.01 k   |\n-\t\t|     * Blobs          |   1.01 k   |\n-\t\t|     * Tags           |      1     |\n-\t\t|   * Inflated size    |  16.03 MiB |\n-\t\t|     * Commits        | 217.92 KiB |\n-\t\t|     * Trees          |  15.81 MiB |\n-\t\t|     * Blobs          |  11.68 KiB |\n-\t\t|     * Tags           |    132 B   |\n-\t\t|   * Disk size        | $(object_type_disk_usage all true) |\n-\t\t|     * Commits        | $(object_type_disk_usage commit true) |\n-\t\t|     * Trees          | $(object_type_disk_usage tree true) |\n-\t\t|     * Blobs          |  $(object_type_disk_usage blob true) |\n-\t\t|     * Tags           |    $(object_type_disk_usage tag) B   |\n-\t\tEOF\n-\n \t\tgit repo structure >out 2>err &&\n-\n-\t\ttest_cmp expect out &&\n+\t\ttest_grep \"\\\\* References\" out &&\n+\t\ttest_grep \"\\\\* Reachable objects\" out &&\n+\t\ttest_grep \"Largest commit\" out &&\n+\t\ttest_grep \"Largest disk size\" out &&\n+\t\ttest_grep \"Largest parent count\" out &&\n+\t\ttest_grep \"Deepest tag chain\" out &&\n \t\ttest_line_count = 0 err\n \t)\n '\n@@ -122,18 +227,37 @@ test_expect_success SHA1 'keyvalue and nul format' '\n \t\tgit tag -a foo -m bar &&\n \n \t\tcat >expect <<-EOF &&\n+\t\treferences.count=$(reference_count_total)\n \t\treferences.branches.count=1\n \t\treferences.tags.count=1\n \t\treferences.remotes.count=0\n \t\treferences.others.count=0\n+\t\tobjects.count=$(object_count_total)\n \t\tobjects.commits.count=42\n \t\tobjects.trees.count=42\n \t\tobjects.blobs.count=42\n \t\tobjects.tags.count=1\n+\t\tobjects.inflated_size=$(object_total_inflated_size)\n \t\tobjects.commits.inflated_size=9225\n \t\tobjects.trees.inflated_size=28554\n \t\tobjects.blobs.inflated_size=453\n \t\tobjects.tags.inflated_size=132\n+\t\tobjects.max_inflated_size=$(object_max_inflated_size)\n+\t\tobjects.commits.max_inflated_size=$(object_type_max_inflated_size commit)\n+\t\tobjects.trees.max_inflated_size=$(object_type_max_inflated_size tree)\n+\t\tobjects.blobs.max_inflated_size=$(object_type_max_inflated_size blob)\n+\t\tobjects.tags.max_inflated_size=$(object_type_max_inflated_size tag)\n+\t\tobjects.disk_size=$(object_type_disk_usage all)\n+\t\tobjects.max_disk_size=$(object_max_disk_size)\n+\t\tobjects.commits.max_disk_size=$(object_type_max_disk_size commit)\n+\t\tobjects.trees.max_disk_size=$(object_type_max_disk_size tree)\n+\t\tobjects.blobs.max_disk_size=$(object_type_max_disk_size blob)\n+\t\tobjects.tags.max_disk_size=$(object_type_max_disk_size tag)\n+\t\tobjects.commits.max_parent_count=$(commit_max_parent_count)\n+\t\tobjects.trees.max_entry_count=$(tree_max_entry_count)\n+\t\tobjects.blobs.max_path_length=$(blob_max_path_length)\n+\t\tobjects.blobs.max_path_depth=$(blob_max_path_depth)\n+\t\tobjects.tags.max_chain_depth=$(tag_max_chain_depth)\n \t\tobjects.commits.disk_size=$(object_type_disk_usage commit)\n \t\tobjects.trees.disk_size=$(object_type_disk_usage tree)\n \t\tobjects.blobs.disk_size=$(object_type_disk_usage blob)\n-- \ngitgitgadget\n\n"},{"id":"536654","messageId":"cb85ee3b48371048397a22564c3b5c112d7df57a.1771784936.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.git.git.1771784936.gitgitgadget@gmail.com","subject":"[PATCH 2/3] t1900,t1901: make repo tests hash-agnostic and wc-portable","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-22T18:28:55Z","receivedAt":"2026-02-22T18:29:02Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n t/t1900-repo.sh           |  4 ++--\n t/t1901-repo-structure.sh | 26 +++++++++++++-------------\n 2 files changed, 15 insertions(+), 15 deletions(-)\n\ndiff --git a/t/t1900-repo.sh b/t/t1900-repo.sh\nindex 4bfd48b85c..dcacf84cc3 100755\n--- a/t/t1900-repo.sh\n+++ b/t/t1900-repo.sh\n@@ -131,8 +131,8 @@ test_expect_success 'category key returns all matching keys' '\n '\n \n test_expect_success 'mixed key/category requests preserve request order' '\n-\tcat >expect <<-\\EOF &&\n-\tobject.format=sha1\n+\tcat >expect <<-EOF &&\n+\tobject.format=$(test_oid algo)\n \tlayout.bare=false\n \tlayout.shallow=false\n \tEOF\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nindex 2d493fbbc4..d9e2842307 100755\n--- a/t/t1901-repo-structure.sh\n+++ b/t/t1901-repo-structure.sh\n@@ -150,10 +150,10 @@ tree_max_entry_count() {\n \t\t--filter=object:type=tree --filter-provided-objects | cut -d\" \" -f1)\n \tdo\n \t\tentries=$(git cat-file -p \"$oid\" | wc -l) || return 1\n-\t\ttest \"$entries\" -gt \"$max\" && max=$entries\n+\t\ttest $entries -gt $max && max=$entries\n \tdone\n \n-\techo \"$max\"\n+\techo $max\n }\n \n blob_max_path_length() {\n@@ -228,20 +228,20 @@ test_expect_success SHA1 'keyvalue and nul format' '\n \n \t\tcat >expect <<-EOF &&\n \t\treferences.count=$(reference_count_total)\n-\t\treferences.branches.count=1\n-\t\treferences.tags.count=1\n-\t\treferences.remotes.count=0\n+\t\treferences.branches.count=$(git for-each-ref --format=\"%(refname)\" refs/heads | sed -n \"\\$=\")\n+\t\treferences.tags.count=$(git for-each-ref --format=\"%(refname)\" refs/tags | sed -n \"\\$=\")\n+\t\treferences.remotes.count=$(git for-each-ref --format=\"%(refname)\" refs/remotes | sed -n \"\\$=\")\n \t\treferences.others.count=0\n \t\tobjects.count=$(object_count_total)\n-\t\tobjects.commits.count=42\n-\t\tobjects.trees.count=42\n-\t\tobjects.blobs.count=42\n-\t\tobjects.tags.count=1\n+\t\tobjects.commits.count=$(object_type_count commit)\n+\t\tobjects.trees.count=$(object_type_count tree)\n+\t\tobjects.blobs.count=$(object_type_count blob)\n+\t\tobjects.tags.count=$(object_type_count tag)\n \t\tobjects.inflated_size=$(object_total_inflated_size)\n-\t\tobjects.commits.inflated_size=9225\n-\t\tobjects.trees.inflated_size=28554\n-\t\tobjects.blobs.inflated_size=453\n-\t\tobjects.tags.inflated_size=132\n+\t\tobjects.commits.inflated_size=$(object_type_total_inflated_size commit)\n+\t\tobjects.trees.inflated_size=$(object_type_total_inflated_size tree)\n+\t\tobjects.blobs.inflated_size=$(object_type_total_inflated_size blob)\n+\t\tobjects.tags.inflated_size=$(object_type_total_inflated_size tag)\n \t\tobjects.max_inflated_size=$(object_max_inflated_size)\n \t\tobjects.commits.max_inflated_size=$(object_type_max_inflated_size commit)\n \t\tobjects.trees.max_inflated_size=$(object_type_max_inflated_size tree)\n-- \ngitgitgadget\n\n"},{"id":"536656","messageId":"3c656bf152a9d18ab87c60b0581dad5d67f55104.1771784936.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.git.git.1771784936.gitgitgadget@gmail.com","subject":"[PATCH 3/3] t1900,t1901: fix test portability issues","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-22T18:28:56Z","receivedAt":"2026-02-22T18:29:04Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n t/t1901-repo-structure.sh | 22 +++++++++++-----------\n 1 file changed, 11 insertions(+), 11 deletions(-)\n\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nindex d9e2842307..7b7c4117aa 100755\n--- a/t/t1901-repo-structure.sh\n+++ b/t/t1901-repo-structure.sh\n@@ -228,20 +228,20 @@ test_expect_success SHA1 'keyvalue and nul format' '\n \n \t\tcat >expect <<-EOF &&\n \t\treferences.count=$(reference_count_total)\n-\t\treferences.branches.count=$(git for-each-ref --format=\"%(refname)\" refs/heads | sed -n \"\\$=\")\n-\t\treferences.tags.count=$(git for-each-ref --format=\"%(refname)\" refs/tags | sed -n \"\\$=\")\n-\t\treferences.remotes.count=$(git for-each-ref --format=\"%(refname)\" refs/remotes | sed -n \"\\$=\")\n+\t\treferences.branches.count=1\n+\t\treferences.tags.count=1\n+\t\treferences.remotes.count=0\n \t\treferences.others.count=0\n \t\tobjects.count=$(object_count_total)\n-\t\tobjects.commits.count=$(object_type_count commit)\n-\t\tobjects.trees.count=$(object_type_count tree)\n-\t\tobjects.blobs.count=$(object_type_count blob)\n-\t\tobjects.tags.count=$(object_type_count tag)\n+\t\tobjects.commits.count=42\n+\t\tobjects.trees.count=42\n+\t\tobjects.blobs.count=42\n+\t\tobjects.tags.count=1\n \t\tobjects.inflated_size=$(object_total_inflated_size)\n-\t\tobjects.commits.inflated_size=$(object_type_total_inflated_size commit)\n-\t\tobjects.trees.inflated_size=$(object_type_total_inflated_size tree)\n-\t\tobjects.blobs.inflated_size=$(object_type_total_inflated_size blob)\n-\t\tobjects.tags.inflated_size=$(object_type_total_inflated_size tag)\n+\t\tobjects.commits.inflated_size=9225\n+\t\tobjects.trees.inflated_size=28554\n+\t\tobjects.blobs.inflated_size=453\n+\t\tobjects.tags.inflated_size=132\n \t\tobjects.max_inflated_size=$(object_max_inflated_size)\n \t\tobjects.commits.max_inflated_size=$(object_type_max_inflated_size commit)\n \t\tobjects.trees.max_inflated_size=$(object_type_max_inflated_size tree)\n-- \ngitgitgadget\n"},{"id":"536664","messageId":"F484D1DC-5446-4FB1-9A77-6645F7F24C7B@gmail.com","threadId":"65043","inReplyTo":"9f2b3a46a4475ef9dcfec65def3a9965b476b4c4.1771784936.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 1/3] repo: extend info paths and structure statistics","fromName":"Lucas Seiki Oshiro","fromEmail":"lucasseikioshiro@gmail.com","sentAt":"2026-02-22T20:35:45Z","receivedAt":"2026-02-22T20:36:00Z","isPatch":true,"sender":{"key":"lucasseikioshiro@gmail.com","avatar":"https://avatars.githubusercontent.com/u/12701580?v=4"},"body":"> and additional git-path based locations.\n\nThis statement is too vague. Each one of the covered key-value pairs\nneeds to be mentioned and discussed about why they are being added.\n\n> Extend git repo structure with deeper repository metrics inspired by\n> git-sizer, including per-type maximum inflated and on-disk object sizes,\n> maximum commit parent count, maximum tree entry count, longest/deepest\n> blob path, and deepest annotated tag chain.\n\nThis patch is doing too much. It needs to be broken, at least, into 20\nsmaller patches, one per git-repo-info and git-repo-structure key.\n\n"},{"id":"536666","messageId":"D1CDCFD8-F4F1-46F2-80F5-C2F55ADCF8DC@gmail.com","threadId":"65043","inReplyTo":"cb85ee3b48371048397a22564c3b5c112d7df57a.1771784936.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 2/3] t1900,t1901: make repo tests hash-agnostic and wc-portable","fromName":"Lucas Seiki Oshiro","fromEmail":"lucasseikioshiro@gmail.com","sentAt":"2026-02-22T20:46:09Z","receivedAt":"2026-02-22T20:46:24Z","isPatch":true,"sender":{"key":"lucasseikioshiro@gmail.com","avatar":"https://avatars.githubusercontent.com/u/12701580?v=4"},"body":"\n> From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n> \n> Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nMissing patch message.\n\n> test_expect_success 'mixed key/category requests preserve request order' '\n> - cat >expect <<-\\EOF &&\n> - object.format=sha1\n> + cat >expect <<-EOF &&\n> + object.format=$(test_oid algo)\n\nDon't send patches fixing the code you have just added. Instead, rewrite\nthe history applying the fix to the original patch.\n\n"},{"id":"536672","messageId":"xmqqzf50pfpv.fsf@gitster.g","threadId":"65043","inReplyTo":"pull.2208.git.git.1771784936.gitgitgadget@gmail.com","subject":"Re: [PATCH 0/3] repo: extend info path reporting and structure statistics","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-22T22:37:32Z","receivedAt":"2026-02-22T22:37:36Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"eslam reda via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> Eslam reda ragheb (2):\n>   t1900,t1901: make repo tests hash-agnostic and wc-portable\n>   t1900,t1901: fix test portability issues\n>\n> eslam-reda-div (1):\n>   repo: extend info paths and structure statistics\n\nI do not think you meant to send a patch seriees with these two same\nbut different looking folks claiming authorship.\n\nI'll let the main change 1/3 reviewed and commented on those who are\nmore invested into \"git repo\", but both [2/3] and [3/3] need to sell\nthemselves with a more meaningful proposed log messages.\n\nI think the test updates in [2/3] are all good things to do---they\nmake the test more robust against changes in the hash algorithm\nused, what the history left by the previous test steps exactly have,\netc., by avoiding hardcoded constants in the expectation.  And in\nlight of that, I do not think [3/3] is a good change, especially\nwithout explanation of why we may want to use hardcoded numbres.\n\nThanks.\n\n\n"},{"id":"536684","messageId":"aZvBLAvM0rfA8VUr@denethor","threadId":"65043","inReplyTo":"9f2b3a46a4475ef9dcfec65def3a9965b476b4c4.1771784936.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 1/3] repo: extend info paths and structure statistics","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-02-23T03:02:09Z","receivedAt":"2026-02-23T03:02:11Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/02/22 06:28PM, eslam-reda-div via GitGitGadget wrote:\n> From: eslam-reda-div <eslam.reda.div@gmail.com>\n> \n> Improve git repo info by adding path-oriented keys that match values\n> users currently obtain from git rev-parse, including common directory,\n> git directory, top-level, superproject working tree, and additional\n> git-path based locations.\n> \n> Teach git repo info to accept category keys like layout and path,\n> and add --path-format=(absolute|relative) so scripts can request the\n> desired path style explicitly. The command now uses repository context\n> passed to the command path instead of relying on global state.\n> \n> Extend git repo structure with deeper repository metrics inspired by\n> git-sizer, including per-type maximum inflated and on-disk object sizes,\n> maximum commit parent count, maximum tree entry count, longest/deepest\n> blob path, and deepest annotated tag chain.\n\nHello,\n\nJust FYI, there is already a series on the list I'm currently working on\nthat is extending the \"structure\" output for git-repo [1] similar to\nwhat is being done here. This series collects largest inflated object\ninfo, max commit parents, and max tree entries. It does look like this\nseries is collecting a couple of other data points too, but maybe we\ncould do so it a separate followup series to the one I'm working on? :)\n\nThanks,\n-Justin\n\n[1]: https://lore.kernel.org/git/20260203221758.1164434-1-jltobler@gmail.com/\n"},{"id":"536801","messageId":"pull.2208.v2.git.git.1771856469.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.git.git.1771784936.gitgitgadget@gmail.com","subject":"[PATCH v2 0/9] repo: extend info path reporting and structure statistics","fromName":"eslam reda via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-23T14:21:00Z","receivedAt":"2026-02-23T14:21:12Z","isPatch":true,"sender":{"key":"name:eslam reda","avatar":null},"body":"\nThis series improves git repo info and git repo structure with a\n================================================================\n\ncleanly split commit history and explicit documentation/tests.\n\nFor git repo info, this series:\n\n * introduces explicit command context plumbing (instead of ad-hoc global\n   reliance in this codepath),\n * adds category-key expansion (for example, requesting layout expands to\n   layout.* keys),\n * adds path-oriented keys (path.*) that expose repository locations,\n * adds --path-format=(absolute|relative) to control path rendering.\n\nFor git repo structure, this series adds richer metrics:\n\n * maximum inflated object size (overall + per type),\n * maximum on-disk object size (overall + per type),\n * maximum commit parent count,\n * maximum tree entry count,\n * maximum blob path length and path depth,\n * maximum annotated tag chain depth,\n * aggregate keyvalue/nul totals (references.count, objects.count,\n   objects.inflated_size, objects.disk_size).\n\nTests and documentation are updated accordingly.\n\n----------------------------------------------------------------------------\n\n\nWhy this change\n===============\n\nThe intent is to make git repo more script-friendly and more useful for\nrepository diagnostics:\n\n * repo info becomes easier to query programmatically (category keys,\n   explicit path formatting).\n * repo structure becomes more actionable by exposing outlier-focused\n   metrics (maxima), not only totals.\n * keyvalue/nul output now includes aggregate totals so scripts do not need\n   to recompute them externally.\n\n----------------------------------------------------------------------------\n\n\nCommit structure (v2 rewrite)\n=============================\n\nThis iteration rewrites history into smaller logical steps (no\n“fix-on-fix”):\n\n 1. repo: teach info context and category keys\n 2. repo: add path keys to repo info\n 3. repo: add --path-format for info path output\n 4. repo: add structure max object size metrics\n 5. repo: add structure topology and path-depth metrics\n 6. repo: add aggregate structure totals to keyvalue output\n 7. t1900: cover repo info path keys and path-format\n 8. t1901: extend structure metric coverage and portability\n 9. docs: describe repo info path keys and structure metrics\n\nAll commits are signed off using real-name identity.\n\n----------------------------------------------------------------------------\n\n\nChanges since v1\n================\n\n * Rewrote series into smaller logical commits.\n * Folded fixes into proper history (no trailing fix-up patches).\n * Unified author/sign-off identity to real name.\n * Addressed portability concerns raised in review:\n   * hash-algorithm-sensitive expectations are handled robustly,\n   * BSD/macOS wc whitespace behavior is handled in tests.\n * Improved test robustness around keyvalue/nul expectations.\n * Expanded docs to explicitly describe new keys/metrics and behavior.\n * Revalidated in Docker with focused and full test runs.\n\n----------------------------------------------------------------------------\n\n\nValidation\n==========\n\nFocused:\n\n * t1900-repo.sh\n * GIT_TEST_DEFAULT_REF_FORMAT=reftable t/t1901-repo-structure.sh\n\nFull:\n\n * make -C t -j4 test in clean Docker environment\n * Result: failed 0\n\n----------------------------------------------------------------------------\n\nEslam reda ragheb (9):\n  repo: teach info context and category keys\n  repo: add path keys to repo info\n  repo: add --path-format for info path output\n  repo: add structure max object size metrics\n  repo: add structure topology and path-depth metrics\n  repo: add aggregate structure totals to keyvalue output\n  t1900: cover repo info path keys and path-format\n  t1901: extend structure metric coverage and portability\n  docs: describe repo info path keys and structure metrics\n\n Documentation/git-repo.adoc |  67 ++++-\n builtin/repo.c              | 518 ++++++++++++++++++++++++++++++++++--\n t/t1900-repo.sh             | 196 ++++++++++++++\n t/t1901-repo-structure.sh   | 250 ++++++++++++-----\n 4 files changed, 946 insertions(+), 85 deletions(-)\n\n\nbase-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2208%2Feslam-reda-div%2Fgsoc-contribute-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2208/eslam-reda-div/gsoc-contribute-v2\nPull-Request: https://github.com/git/git/pull/2208\n\nRange-diff vs v1:\n\n  -:  ---------- >  1:  99c8058298 repo: teach info context and category keys\n  -:  ---------- >  2:  6d5b9ff075 repo: add path keys to repo info\n  -:  ---------- >  3:  5c438d045b repo: add --path-format for info path output\n  -:  ---------- >  4:  504d9cf7a0 repo: add structure max object size metrics\n  -:  ---------- >  5:  4b502925c9 repo: add structure topology and path-depth metrics\n  -:  ---------- >  6:  1751181950 repo: add aggregate structure totals to keyvalue output\n  -:  ---------- >  7:  fd18f28db0 t1900: cover repo info path keys and path-format\n  1:  9f2b3a46a4 !  8:  0525ed4cd9 repo: extend info paths and structure statistics\n     @@\n       ## Metadata ##\n     -Author: eslam-reda-div <eslam.reda.div@gmail.com>\n     +Author: Eslam reda ragheb <eslam.reda.div@gmail.com>\n      \n       ## Commit message ##\n     -    repo: extend info paths and structure statistics\n     +    t1901: extend structure metric coverage and portability\n      \n     -    Improve git repo info by adding path-oriented keys that match values\n     -    users currently obtain from git rev-parse, including common directory,\n     -    git directory, top-level, superproject working tree, and additional\n     -    git-path based locations.\n     +    Expand t1901 to cover additional structure metrics emitted by git\n     +    repo structure, including maxima and aggregate keyvalue/nul\n     +    checks.\n      \n     -    Teach git repo info to accept category keys like layout and path,\n     -    and add --path-format=(absolute|relative) so scripts can request the\n     -    desired path style explicitly. The command now uses repository context\n     -    passed to the command path instead of relying on global state.\n     +    The test now validates both human-oriented table content and\n     +    machine-readable fields for the extended metric set.\n      \n     -    Extend git repo structure with deeper repository metrics inspired by\n     -    git-sizer, including per-type maximum inflated and on-disk object sizes,\n     -    maximum commit parent count, maximum tree entry count, longest/deepest\n     -    blob path, and deepest annotated tag chain.\n     +    Also make expectations more portable across hash algorithms and\n     +    platforms by avoiding brittle assumptions.\n      \n     -    Update documentation and tests to cover new keys, formats, and metrics.\n     +    This includes wc output quirks on BSD/macOS and hash-format-\n     +    sensitive expectations.\n      \n     -    Signed-off-by: eslam-reda-div <eslam.reda.div@gmail.com>\n     -\n     - ## Documentation/git-repo.adoc ##\n     -@@ Documentation/git-repo.adoc: git-repo - Retrieve information about the repository\n     - SYNOPSIS\n     - --------\n     - [synopsis]\n     --git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]\n     -+git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\n     - git repo structure [--format=(table|keyvalue|nul) | -z]\n     - \n     - DESCRIPTION\n     -@@ Documentation/git-repo.adoc: supported:\n     - +\n     - `-z` is an alias for `--format=nul`.\n     - \n     -+`--path-format=(absolute|relative)`:::\n     -+\tControls formatting for keys in the `path` category. The default is\n     -+\t`absolute`. This option may be specified multiple times; the last one\n     -+\tspecified takes effect.\n     -+\n     - `structure [--format=(table|keyvalue|nul) | -z]`::\n     - \tRetrieve statistics about the current repository structure. The\n     - \tfollowing kinds of information are reported:\n     -@@ Documentation/git-repo.adoc: supported:\n     - * Reachable object counts categorized by type\n     - * Total inflated size of reachable objects by type\n     - * Total disk size of reachable objects by type\n     -+* Largest inflated reachable object size by type\n     -+* Largest disk size of a reachable object by type\n     -+* Largest parent count among reachable commits\n     -+* Largest entry count among reachable trees\n     -+* Longest and deepest path among reachable blobs\n     -+* Deepest annotated tag chain\n     - +\n     - The output format can be chosen through the flag `--format`. Three formats are\n     - supported:\n     -@@ Documentation/git-repo.adoc: supported:\n     - `keyvalue`:::\n     - \tEach line of output contains a key-value pair for a repository stat.\n     - \tThe '=' character is used to delimit between the key and the value.\n     -+\tBoth aggregate metrics and per-type metrics are included.\n     - \tValues containing \"unusual\" characters are quoted as explained for the\n     - \tconfiguration variable `core.quotePath` (see linkgit:git-config[1]).\n     - \n     -@@ Documentation/git-repo.adoc: supported:\n     - \n     - INFO KEYS\n     - ---------\n     --In order to obtain a set of values from `git repo info`, you should provide\n     --the keys that identify them. Here's a list of the available keys and the\n     --values that they return:\n     -+In order to obtain values from `git repo info`, provide either individual keys\n     -+or category names. A category returns all keys within that category. For\n     -+example, `layout` returns both `layout.bare` and `layout.shallow`.\n     -+\n     -+Here's a list of the available keys and the values that they return:\n     - \n     - `layout.bare`::\n     - \t`true` if this is a bare repository, otherwise `false`.\n     -@@ Documentation/git-repo.adoc: values that they return:\n     - `object.format`::\n     - \tThe object format (hash algorithm) used in the repository.\n     - \n     -+`path.common-dir`::\n     -+\tThe path to the common git directory.\n     -+\n     -+`path.config-file`::\n     -+\tThe path to the `config` file in the git directory.\n     -+\n     -+`path.git-dir`::\n     -+\tThe path to the git directory.\n     -+\n     -+`path.git-prefix`::\n     -+\tThe path of the current working directory relative to the top-level\n     -+\tdirectory.\n     -+\n     -+`path.grafts-file`::\n     -+\tThe path to the `info/grafts` file.\n     -+\n     -+`path.hooks-directory`::\n     -+\tThe path to the `hooks` directory.\n     -+\n     -+`path.index-file`::\n     -+\tThe path to the index file.\n     -+\n     -+`path.logs-directory`::\n     -+\tThe path to the `logs` directory.\n     -+\n     -+`path.objects-directory`::\n     -+\tThe path to the objects directory.\n     -+\n     -+`path.packed-refs-file`::\n     -+\tThe path to the `packed-refs` file.\n     -+\n     -+`path.refs-directory`::\n     -+\tThe path to the `refs` directory.\n     -+\n     -+`path.shallow-file`::\n     -+\tThe path to the `shallow` file.\n     -+\n     -+`path.superproject-working-tree`::\n     -+\tThe path to the superproject's working tree root, or an empty string\n     -+\twhen the repository is not used as a submodule.\n     -+\n     -+`path.toplevel`::\n     -+\tThe path to the top-level working tree directory, or an empty string\n     -+\tfor bare repositories.\n     -+\n     - `references.format`::\n     - \tThe reference storage format. The valid values are:\n     - +\n     -\n     - ## builtin/repo.c ##\n     -@@\n     --#define USE_THE_REPOSITORY_VARIABLE\n     --\n     - #include \"builtin.h\"\n     --#include \"environment.h\"\n     -+#include \"abspath.h\"\n     - #include \"hex.h\"\n     - #include \"odb.h\"\n     - #include \"parse-options.h\"\n     -+#include \"path.h\"\n     - #include \"path-walk.h\"\n     - #include \"progress.h\"\n     - #include \"quote.h\"\n     -@@\n     - #include \"revision.h\"\n     - #include \"strbuf.h\"\n     - #include \"string-list.h\"\n     -+#include \"submodule.h\"\n     - #include \"shallow.h\"\n     -+#include \"tree-walk.h\"\n     - #include \"utf8.h\"\n     - \n     - static const char *const repo_usage[] = {\n     --\t\"git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]\",\n     -+\t\"git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\",\n     - \t\"git repo structure [--format=(table|keyvalue|nul) | -z]\",\n     - \tNULL\n     - };\n     - \n     --typedef int get_value_fn(struct repository *repo, struct strbuf *buf);\n     -+enum path_format {\n     -+\tPATH_FORMAT_ABSOLUTE,\n     -+\tPATH_FORMAT_RELATIVE,\n     -+};\n     -+\n     -+struct repo_info {\n     -+\tstruct repository *repo;\n     -+\tconst char *prefix;\n     -+\tenum path_format path_format;\n     -+};\n     -+\n     -+typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);\n     - \n     - enum output_format {\n     - \tFORMAT_TABLE,\n     -@@ builtin/repo.c: struct field {\n     - \tget_value_fn *get_value;\n     - };\n     - \n     --static int get_layout_bare(struct repository *repo UNUSED, struct strbuf *buf)\n     -+static void repo_info_add_path(struct repo_info *info,\n     -+\t\t\t       struct strbuf *buf,\n     -+\t\t\t       const char *path)\n     - {\n     --\tstrbuf_addstr(buf, is_bare_repository() ? \"true\" : \"false\");\n     -+\tif (info->path_format == PATH_FORMAT_RELATIVE) {\n     -+\t\tchar *cwd = xgetcwd();\n     -+\t\tstruct strbuf rel_path = STRBUF_INIT;\n     -+\n     -+\t\tstrbuf_addstr(buf, relative_path(path, cwd, &rel_path));\n     -+\t\tstrbuf_release(&rel_path);\n     -+\t\tfree(cwd);\n     -+\t\treturn;\n     -+\t}\n     -+\n     -+\tstrbuf_add_absolute_path(buf, path);\n     -+}\n     -+\n     -+static int get_layout_bare(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\tstruct repository *repo = info->repo;\n     -+\tstrbuf_addstr(buf, repo_get_work_tree(repo) ? \"false\" : \"true\");\n     - \treturn 0;\n     - }\n     - \n     --static int get_layout_shallow(struct repository *repo, struct strbuf *buf)\n     -+static int get_layout_shallow(struct repo_info *info, struct strbuf *buf)\n     - {\n     -+\tstruct repository *repo = info->repo;\n     - \tstrbuf_addstr(buf,\n     - \t\t      is_repository_shallow(repo) ? \"true\" : \"false\");\n     - \treturn 0;\n     - }\n     - \n     --static int get_object_format(struct repository *repo, struct strbuf *buf)\n     -+static int get_object_format(struct repo_info *info, struct strbuf *buf)\n     - {\n     -+\tstruct repository *repo = info->repo;\n     - \tstrbuf_addstr(buf, repo->hash_algo->name);\n     - \treturn 0;\n     - }\n     - \n     --static int get_references_format(struct repository *repo, struct strbuf *buf)\n     -+static int get_path_common_dir(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\trepo_info_add_path(info, buf, repo_get_common_dir(info->repo));\n     -+\treturn 0;\n     -+}\n     -+\n     -+static int get_path_config_file(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\tstruct strbuf path = STRBUF_INIT;\n     -+\n     -+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"config\"));\n     -+\tstrbuf_release(&path);\n     -+\treturn 0;\n     -+}\n     -+\n     -+static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\trepo_info_add_path(info, buf, repo_get_git_dir(info->repo));\n     -+\treturn 0;\n     -+}\n     -+\n     -+static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\tif (info->prefix)\n     -+\t\tstrbuf_addstr(buf, info->prefix);\n     -+\treturn 0;\n     -+}\n     -+\n     -+static int get_path_grafts_file(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\trepo_info_add_path(info, buf, repo_get_graft_file(info->repo));\n     -+\treturn 0;\n     -+}\n     -+\n     -+static int get_path_hooks_directory(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\tstruct strbuf path = STRBUF_INIT;\n     -+\n     -+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"hooks\"));\n     -+\tstrbuf_release(&path);\n     -+\treturn 0;\n     -+}\n     -+\n     -+static int get_path_index_file(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\trepo_info_add_path(info, buf, repo_get_index_file(info->repo));\n     -+\treturn 0;\n     -+}\n     -+\n     -+static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\tstruct strbuf path = STRBUF_INIT;\n     -+\n     -+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"logs\"));\n     -+\tstrbuf_release(&path);\n     -+\treturn 0;\n     -+}\n     -+\n     -+static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\trepo_info_add_path(info, buf, repo_get_object_directory(info->repo));\n     -+\treturn 0;\n     -+}\n     -+\n     -+static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\tstruct strbuf path = STRBUF_INIT;\n     -+\n     -+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"packed-refs\"));\n     -+\tstrbuf_release(&path);\n     -+\treturn 0;\n     -+}\n     -+\n     -+static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\tstruct strbuf path = STRBUF_INIT;\n     -+\n     -+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"refs\"));\n     -+\tstrbuf_release(&path);\n     -+\treturn 0;\n     -+}\n     -+\n     -+static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\tstruct strbuf path = STRBUF_INIT;\n     -+\n     -+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"shallow\"));\n     -+\tstrbuf_release(&path);\n     -+\treturn 0;\n     -+}\n     -+\n     -+static int get_path_superproject_working_tree(struct repo_info *info,\n     -+\t\t\t\t\t      struct strbuf *buf)\n     -+{\n     -+\tstruct strbuf superproject = STRBUF_INIT;\n     -+\n     -+\tif (get_superproject_working_tree(&superproject))\n     -+\t\trepo_info_add_path(info, buf, superproject.buf);\n     -+\n     -+\tstrbuf_release(&superproject);\n     -+\treturn 0;\n     -+}\n     -+\n     -+static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\tconst char *work_tree = repo_get_work_tree(info->repo);\n     -+\n     -+\tif (work_tree)\n     -+\t\trepo_info_add_path(info, buf, work_tree);\n     -+\n     -+\treturn 0;\n     -+}\n     -+\n     -+static int get_references_format(struct repo_info *info, struct strbuf *buf)\n     - {\n     -+\tstruct repository *repo = info->repo;\n     - \tstrbuf_addstr(buf,\n     - \t\t      ref_storage_format_to_name(repo->ref_storage_format));\n     - \treturn 0;\n     -@@ builtin/repo.c: static const struct field repo_info_fields[] = {\n     - \t{ \"layout.bare\", get_layout_bare },\n     - \t{ \"layout.shallow\", get_layout_shallow },\n     - \t{ \"object.format\", get_object_format },\n     -+\t{ \"path.common-dir\", get_path_common_dir },\n     -+\t{ \"path.config-file\", get_path_config_file },\n     -+\t{ \"path.git-dir\", get_path_git_dir },\n     -+\t{ \"path.git-prefix\", get_path_git_prefix },\n     -+\t{ \"path.grafts-file\", get_path_grafts_file },\n     -+\t{ \"path.hooks-directory\", get_path_hooks_directory },\n     -+\t{ \"path.index-file\", get_path_index_file },\n     -+\t{ \"path.logs-directory\", get_path_logs_directory },\n     -+\t{ \"path.objects-directory\", get_path_objects_directory },\n     -+\t{ \"path.packed-refs-file\", get_path_packed_refs_file },\n     -+\t{ \"path.refs-directory\", get_path_refs_directory },\n     -+\t{ \"path.shallow-file\", get_path_shallow_file },\n     -+\t{ \"path.superproject-working-tree\", get_path_superproject_working_tree },\n     -+\t{ \"path.toplevel\", get_path_toplevel },\n     - \t{ \"references.format\", get_references_format },\n     - };\n     - \n     -@@ builtin/repo.c: static get_value_fn *get_value_fn_for_key(const char *key)\n     - \treturn found ? found->get_value : NULL;\n     - }\n     - \n     -+static void print_field(enum output_format format, const char *key,\n     -+\t\t\tconst char *value);\n     -+\n     -+static int print_category_fields(const char *category,\n     -+\t\t\t\t struct repo_info *info,\n     -+\t\t\t\t enum output_format format,\n     -+\t\t\t\t struct strbuf *valbuf)\n     -+{\n     -+\tint found = 0;\n     -+\tsize_t category_len = strlen(category);\n     -+\n     -+\tfor (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {\n     -+\t\tconst struct field *field = &repo_info_fields[i];\n     -+\n     -+\t\tif (!starts_with(field->key, category) ||\n     -+\t\t    field->key[category_len] != '.')\n     -+\t\t\tcontinue;\n     -+\n     -+\t\tstrbuf_reset(valbuf);\n     -+\t\tfield->get_value(info, valbuf);\n     -+\t\tprint_field(format, field->key, valbuf->buf);\n     -+\t\tfound = 1;\n     -+\t}\n     -+\n     -+\treturn found;\n     -+}\n     -+\n     - static void print_field(enum output_format format, const char *key,\n     - \t\t\tconst char *value)\n     - {\n     -@@ builtin/repo.c: static void print_field(enum output_format format, const char *key,\n     - }\n     - \n     - static int print_fields(int argc, const char **argv,\n     --\t\t\tstruct repository *repo,\n     -+\t\t\tstruct repo_info *info,\n     - \t\t\tenum output_format format)\n     - {\n     - \tint ret = 0;\n     -@@ builtin/repo.c: static int print_fields(int argc, const char **argv,\n     - \n     - \t\tget_value = get_value_fn_for_key(key);\n     - \n     --\t\tif (!get_value) {\n     --\t\t\tret = error(_(\"key '%s' not found\"), key);\n     -+\t\tif (get_value) {\n     -+\t\t\tstrbuf_reset(&valbuf);\n     -+\t\t\tget_value(info, &valbuf);\n     -+\t\t\tprint_field(format, key, valbuf.buf);\n     - \t\t\tcontinue;\n     - \t\t}\n     - \n     --\t\tstrbuf_reset(&valbuf);\n     --\t\tget_value(repo, &valbuf);\n     --\t\tprint_field(format, key, valbuf.buf);\n     -+\t\tif (!print_category_fields(key, info, format, &valbuf))\n     -+\t\t\tret = error(_(\"key '%s' not found\"), key);\n     - \t}\n     - \n     - \tstrbuf_release(&valbuf);\n     - \treturn ret;\n     - }\n     - \n     --static int print_all_fields(struct repository *repo,\n     -+static int print_all_fields(struct repo_info *info,\n     - \t\t\t    enum output_format format)\n     - {\n     - \tstruct strbuf valbuf = STRBUF_INIT;\n     -@@ builtin/repo.c: static int print_all_fields(struct repository *repo,\n     - \t\tconst struct field *field = &repo_info_fields[i];\n     - \n     - \t\tstrbuf_reset(&valbuf);\n     --\t\tfield->get_value(repo, &valbuf);\n     -+\t\tfield->get_value(info, &valbuf);\n     - \t\tprint_field(format, field->key, valbuf.buf);\n     - \t}\n     - \n     -@@ builtin/repo.c: static int parse_format_cb(const struct option *opt,\n     - \treturn 0;\n     - }\n     - \n     -+static int parse_path_format_cb(const struct option *opt,\n     -+\t\t\t\tconst char *arg, int unset UNUSED)\n     -+{\n     -+\tenum path_format *path_format = opt->value;\n     -+\n     -+\tif (!strcmp(arg, \"absolute\"))\n     -+\t\t*path_format = PATH_FORMAT_ABSOLUTE;\n     -+\telse if (!strcmp(arg, \"relative\"))\n     -+\t\t*path_format = PATH_FORMAT_RELATIVE;\n     -+\telse\n     -+\t\tdie(_(\"invalid path format '%s'\"), arg);\n     -+\n     -+\treturn 0;\n     -+}\n     -+\n     - static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n     - \t\t\t struct repository *repo)\n     - {\n     - \tenum output_format format = FORMAT_KEYVALUE;\n     -+\tstruct repo_info info = {\n     -+\t\t.repo = repo,\n     -+\t\t.prefix = prefix,\n     -+\t\t.path_format = PATH_FORMAT_ABSOLUTE,\n     -+\t};\n     - \tint all_keys = 0;\n     - \tstruct option options[] = {\n     - \t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n     -@@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n     - \t\t\t       N_(\"synonym for --format=nul\"),\n     - \t\t\t       PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n     - \t\t\t       parse_format_cb),\n     -+\t\tOPT_CALLBACK_F(0, \"path-format\", &info.path_format,\n     -+\t\t\t       N_(\"format\"), N_(\"path output format\"),\n     -+\t\t\t       PARSE_OPT_NONEG, parse_path_format_cb),\n     - \t\tOPT_BOOL(0, \"all\", &all_keys, N_(\"print all keys/values\")),\n     - \t\tOPT_END()\n     - \t};\n     -@@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n     - \t\tdie(_(\"--all and <key> cannot be used together\"));\n     - \n     - \tif (all_keys)\n     --\t\treturn print_all_fields(repo, format);\n     -+\t\treturn print_all_fields(&info, format);\n     - \telse\n     --\t\treturn print_fields(argc, argv, repo, format);\n     -+\t\treturn print_fields(argc, argv, &info, format);\n     - }\n     - \n     - struct ref_stats {\n     -@@ builtin/repo.c: struct object_values {\n     - struct object_stats {\n     - \tstruct object_values type_counts;\n     - \tstruct object_values inflated_sizes;\n     -+\tstruct object_values max_inflated_sizes;\n     - \tstruct object_values disk_sizes;\n     -+\tstruct object_values max_disk_sizes;\n     -+\tsize_t max_commit_parent_count;\n     -+\tsize_t max_tree_entry_count;\n     -+\tsize_t max_blob_path_length;\n     -+\tsize_t max_blob_path_depth;\n     -+\tsize_t max_tag_chain_depth;\n     - };\n     - \n     - struct repo_structure {\n     -@@ builtin/repo.c: static inline size_t get_total_object_values(struct object_values *values)\n     - \treturn values->tags + values->commits + values->trees + values->blobs;\n     - }\n     - \n     -+static inline size_t get_max_object_value(struct object_values *values)\n     -+{\n     -+\tsize_t max = values->commits;\n     -+\n     -+\tif (values->trees > max)\n     -+\t\tmax = values->trees;\n     -+\tif (values->blobs > max)\n     -+\t\tmax = values->blobs;\n     -+\tif (values->tags > max)\n     -+\t\tmax = values->tags;\n     -+\n     -+\treturn max;\n     -+}\n     -+\n     -+static size_t get_commit_parent_count(struct repository *repo,\n     -+\t\t\t\t      const struct object_id *oid)\n     -+{\n     -+\tunsigned long size = 0;\n     -+\tconst char *cur;\n     -+\tconst char *end;\n     -+\tvoid *buf;\n     -+\tsize_t count = 0;\n     -+\n     -+\tbuf = odb_read_object_peeled(repo->objects, oid, OBJ_COMMIT, &size, NULL);\n     -+\tif (!buf)\n     -+\t\treturn 0;\n     -+\n     -+\tcur = buf;\n     -+\tend = cur + size;\n     -+\twhile (cur < end) {\n     -+\t\tconst char *newline = memchr(cur, '\\n', end - cur);\n     -+\t\tsize_t line_len;\n     -+\n     -+\t\tif (!newline)\n     -+\t\t\tbreak;\n     -+\t\tline_len = newline - cur;\n     -+\t\tif (!line_len)\n     -+\t\t\tbreak;\n     -+\n     -+\t\tif (line_len > 7 && !memcmp(cur, \"parent \", 7))\n     -+\t\t\tcount++;\n     -+\n     -+\t\tcur = newline + 1;\n     -+\t}\n     -+\n     -+\tfree(buf);\n     -+\treturn count;\n     -+}\n     -+\n     -+static size_t get_tree_entry_count(struct repository *repo,\n     -+\t\t\t\t   const struct object_id *oid)\n     -+{\n     -+\tstruct tree_desc desc;\n     -+\tstruct name_entry entry;\n     -+\tunsigned long size = 0;\n     -+\tvoid *buf;\n     -+\tsize_t count = 0;\n     -+\n     -+\tbuf = odb_read_object_peeled(repo->objects, oid, OBJ_TREE, &size, NULL);\n     -+\tif (!buf)\n     -+\t\treturn 0;\n     -+\n     -+\tinit_tree_desc(&desc, oid, buf, size);\n     -+\twhile (tree_entry(&desc, &entry))\n     -+\t\tcount++;\n     -+\n     -+\tfree(buf);\n     -+\treturn count;\n     -+}\n     -+\n     -+static size_t get_path_depth(const char *path)\n     -+{\n     -+\tsize_t depth = 0;\n     -+\n     -+\tif (!path || !*path)\n     -+\t\treturn 0;\n     -+\n     -+\tdepth = 1;\n     -+\tfor (const char *cur = path; *cur; cur++)\n     -+\t\tif (*cur == '/')\n     -+\t\t\tdepth++;\n     -+\n     -+\treturn depth;\n     -+}\n     -+\n     -+static size_t get_tag_chain_depth(struct repository *repo,\n     -+\t\t\t\t  const struct object_id *oid)\n     -+{\n     -+\tstruct object_id current = *oid;\n     -+\tsize_t depth = 0;\n     -+\n     -+\twhile (1) {\n     -+\t\tenum object_type type;\n     -+\t\tunsigned long size = 0;\n     -+\t\tstruct object_id next;\n     -+\t\tconst char *p, *end;\n     -+\t\tvoid *buf = odb_read_object(repo->objects, &current, &type, &size);\n     -+\n     -+\t\tif (!buf)\n     -+\t\t\tbreak;\n     -+\t\tif (type != OBJ_TAG) {\n     -+\t\t\tfree(buf);\n     -+\t\t\tbreak;\n     -+\t\t}\n     -+\n     -+\t\tp = buf;\n     -+\t\tif (!skip_prefix(p, \"object \", &p) ||\n     -+\t\t    parse_oid_hex_algop(p, &next, &end, repo->hash_algo) ||\n     -+\t\t    *end != '\\n') {\n     -+\t\t\tfree(buf);\n     -+\t\t\tbreak;\n     -+\t\t}\n     -+\n     -+\t\tdepth++;\n     -+\t\tfree(buf);\n     -+\n     -+\t\tif (oideq(&next, &current))\n     -+\t\t\tbreak;\n     -+\t\toidcpy(&current, &next);\n     -+\t}\n     -+\n     -+\treturn depth;\n     -+}\n     -+\n     - static void stats_table_setup_structure(struct stats_table *table,\n     - \t\t\t\t\tstruct repo_structure *stats)\n     - {\n     -@@ builtin/repo.c: static void stats_table_setup_structure(struct stats_table *table,\n     - \t\t\t      \"    * %s\", _(\"Blobs\"));\n     - \tstats_table_size_addf(table, objects->disk_sizes.tags,\n     - \t\t\t      \"    * %s\", _(\"Tags\"));\n     -+\n     -+\tstats_table_size_addf(table, objects->max_inflated_sizes.commits,\n     -+\t\t\t      \"  * %s\", _(\"Largest commit\"));\n     -+\tstats_table_size_addf(table, objects->max_inflated_sizes.trees,\n     -+\t\t\t      \"  * %s\", _(\"Largest tree\"));\n     -+\tstats_table_size_addf(table, objects->max_inflated_sizes.blobs,\n     -+\t\t\t      \"  * %s\", _(\"Largest blob\"));\n     -+\tstats_table_size_addf(table, objects->max_inflated_sizes.tags,\n     -+\t\t\t      \"  * %s\", _(\"Largest tag\"));\n     -+\n     -+\tstats_table_size_addf(table, get_max_object_value(&objects->max_disk_sizes),\n     -+\t\t\t      \"  * %s\", _(\"Largest disk size\"));\n     -+\tstats_table_size_addf(table, objects->max_disk_sizes.commits,\n     -+\t\t\t      \"    * %s\", _(\"Commits\"));\n     -+\tstats_table_size_addf(table, objects->max_disk_sizes.trees,\n     -+\t\t\t      \"    * %s\", _(\"Trees\"));\n     -+\tstats_table_size_addf(table, objects->max_disk_sizes.blobs,\n     -+\t\t\t      \"    * %s\", _(\"Blobs\"));\n     -+\tstats_table_size_addf(table, objects->max_disk_sizes.tags,\n     -+\t\t\t      \"    * %s\", _(\"Tags\"));\n     -+\n     -+\tstats_table_count_addf(table, objects->max_commit_parent_count,\n     -+\t\t\t       \"  * %s\", _(\"Largest parent count\"));\n     -+\tstats_table_count_addf(table, objects->max_tree_entry_count,\n     -+\t\t\t       \"  * %s\", _(\"Largest tree entries\"));\n     -+\tstats_table_count_addf(table, objects->max_blob_path_length,\n     -+\t\t\t       \"  * %s\", _(\"Longest blob path\"));\n     -+\tstats_table_count_addf(table, objects->max_blob_path_depth,\n     -+\t\t\t       \"  * %s\", _(\"Deepest blob path\"));\n     -+\tstats_table_count_addf(table, objects->max_tag_chain_depth,\n     -+\t\t\t       \"  * %s\", _(\"Deepest tag chain\"));\n     - }\n     - \n     - static void stats_table_print_structure(const struct stats_table *table)\n     -@@ builtin/repo.c: static void stats_table_clear(struct stats_table *table)\n     - static void structure_keyvalue_print(struct repo_structure *stats,\n     - \t\t\t\t     char key_delim, char value_delim)\n     - {\n     -+\tsize_t references_count_total = get_total_reference_count(&stats->refs);\n     -+\tsize_t object_count_total = get_total_object_values(&stats->objects.type_counts);\n     -+\tsize_t inflated_size_total = get_total_object_values(&stats->objects.inflated_sizes);\n     -+\tsize_t disk_size_total = get_total_object_values(&stats->objects.disk_sizes);\n     -+\tsize_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);\n     -+\tsize_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);\n     -+\n     -+\tprintf(\"references.count%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)references_count_total, value_delim);\n     - \tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n     - \t       (uintmax_t)stats->refs.branches, value_delim);\n     - \tprintf(\"references.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n     -@@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stats,\n     - \tprintf(\"references.others.count%c%\" PRIuMAX \"%c\", key_delim,\n     - \t       (uintmax_t)stats->refs.others, value_delim);\n     - \n     -+\tprintf(\"objects.count%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)object_count_total, value_delim);\n     - \tprintf(\"objects.commits.count%c%\" PRIuMAX \"%c\", key_delim,\n     - \t       (uintmax_t)stats->objects.type_counts.commits, value_delim);\n     - \tprintf(\"objects.trees.count%c%\" PRIuMAX \"%c\", key_delim,\n     -@@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stats,\n     - \tprintf(\"objects.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n     - \t       (uintmax_t)stats->objects.type_counts.tags, value_delim);\n     - \n     -+\tprintf(\"objects.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)inflated_size_total, value_delim);\n     - \tprintf(\"objects.commits.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n     - \t       (uintmax_t)stats->objects.inflated_sizes.commits, value_delim);\n     - \tprintf(\"objects.trees.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n     -@@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stats,\n     - \tprintf(\"objects.tags.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n     - \t       (uintmax_t)stats->objects.inflated_sizes.tags, value_delim);\n     - \n     -+\tprintf(\"objects.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)max_inflated_size, value_delim);\n     -+\tprintf(\"objects.commits.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_inflated_sizes.commits, value_delim);\n     -+\tprintf(\"objects.trees.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_inflated_sizes.trees, value_delim);\n     -+\tprintf(\"objects.blobs.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_inflated_sizes.blobs, value_delim);\n     -+\tprintf(\"objects.tags.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);\n     -+\n     -+\tprintf(\"objects.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)disk_size_total, value_delim);\n     -+\tprintf(\"objects.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)max_disk_size, value_delim);\n     -+\tprintf(\"objects.commits.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_disk_sizes.commits, value_delim);\n     -+\tprintf(\"objects.trees.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_disk_sizes.trees, value_delim);\n     -+\tprintf(\"objects.blobs.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_disk_sizes.blobs, value_delim);\n     -+\tprintf(\"objects.tags.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);\n     -+\n     -+\tprintf(\"objects.commits.max_parent_count%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_commit_parent_count, value_delim);\n     -+\tprintf(\"objects.trees.max_entry_count%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_tree_entry_count, value_delim);\n     -+\tprintf(\"objects.blobs.max_path_length%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_blob_path_length, value_delim);\n     -+\tprintf(\"objects.blobs.max_path_depth%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_blob_path_depth, value_delim);\n     -+\tprintf(\"objects.tags.max_chain_depth%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_tag_chain_depth, value_delim);\n     -+\n     - \tprintf(\"objects.commits.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n     - \t       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);\n     - \tprintf(\"objects.trees.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n     -@@ builtin/repo.c: struct count_objects_data {\n     - \tstruct progress *progress;\n     - };\n     - \n     --static int count_objects(const char *path UNUSED, struct oid_array *oids,\n     -+static int count_objects(const char *path, struct oid_array *oids,\n     - \t\t\t enum object_type type, void *cb_data)\n     - {\n     - \tstruct count_objects_data *data = cb_data;\n     - \tstruct object_stats *stats = data->stats;\n     - \tsize_t inflated_total = 0;\n     - \tsize_t disk_total = 0;\n     -+\tsize_t max_inflated = 0;\n     -+\tsize_t max_disk = 0;\n     - \tsize_t object_count;\n     - \n     - \tfor (size_t i = 0; i < oids->nr; i++) {\n     -@@ builtin/repo.c: static int count_objects(const char *path UNUSED, struct oid_array *oids,\n     - \n     - \t\tif (odb_read_object_info_extended(data->odb, &oids->oid[i], &oi,\n     - \t\t\t\t\t\t  OBJECT_INFO_SKIP_FETCH_OBJECT |\n     --\t\t\t\t\t\t  OBJECT_INFO_QUICK) < 0)\n     -+\t\t\t\t\t\t\t  OBJECT_INFO_QUICK) < 0)\n     -+\t\t\tcontinue;\n     -+\t\tif (disk < 0)\n     - \t\t\tcontinue;\n     - \n     - \t\tinflated_total += inflated;\n     --\t\tdisk_total += disk;\n     -+\t\tdisk_total += (size_t)disk;\n     -+\t\tif (inflated > max_inflated)\n     -+\t\t\tmax_inflated = inflated;\n     -+\t\tif ((size_t)disk > max_disk)\n     -+\t\t\tmax_disk = (size_t)disk;\n     - \t}\n     - \n     - \tswitch (type) {\n     - \tcase OBJ_TAG:\n     -+\t\tfor (size_t i = 0; i < oids->nr; i++) {\n     -+\t\t\tsize_t tag_chain_depth = get_tag_chain_depth(data->odb->repo,\n     -+\t\t\t\t\t\t\t\t     &oids->oid[i]);\n     -+\n     -+\t\t\tif (tag_chain_depth > stats->max_tag_chain_depth)\n     -+\t\t\t\tstats->max_tag_chain_depth = tag_chain_depth;\n     -+\t\t}\n     -+\n     - \t\tstats->type_counts.tags += oids->nr;\n     - \t\tstats->inflated_sizes.tags += inflated_total;\n     -+\t\tif (max_inflated > stats->max_inflated_sizes.tags)\n     -+\t\t\tstats->max_inflated_sizes.tags = max_inflated;\n     - \t\tstats->disk_sizes.tags += disk_total;\n     -+\t\tif (max_disk > stats->max_disk_sizes.tags)\n     -+\t\t\tstats->max_disk_sizes.tags = max_disk;\n     - \t\tbreak;\n     - \tcase OBJ_COMMIT:\n     -+\t\tfor (size_t i = 0; i < oids->nr; i++) {\n     -+\t\t\tsize_t parent_count = get_commit_parent_count(data->odb->repo,\n     -+\t\t\t\t\t\t\t\t      &oids->oid[i]);\n     -+\n     -+\t\t\tif (parent_count > stats->max_commit_parent_count)\n     -+\t\t\t\tstats->max_commit_parent_count = parent_count;\n     -+\t\t}\n     -+\n     - \t\tstats->type_counts.commits += oids->nr;\n     - \t\tstats->inflated_sizes.commits += inflated_total;\n     -+\t\tif (max_inflated > stats->max_inflated_sizes.commits)\n     -+\t\t\tstats->max_inflated_sizes.commits = max_inflated;\n     - \t\tstats->disk_sizes.commits += disk_total;\n     -+\t\tif (max_disk > stats->max_disk_sizes.commits)\n     -+\t\t\tstats->max_disk_sizes.commits = max_disk;\n     - \t\tbreak;\n     - \tcase OBJ_TREE:\n     -+\t\tfor (size_t i = 0; i < oids->nr; i++) {\n     -+\t\t\tsize_t entry_count = get_tree_entry_count(data->odb->repo,\n     -+\t\t\t\t\t\t\t\t  &oids->oid[i]);\n     -+\n     -+\t\t\tif (entry_count > stats->max_tree_entry_count)\n     -+\t\t\t\tstats->max_tree_entry_count = entry_count;\n     -+\t\t}\n     -+\n     - \t\tstats->type_counts.trees += oids->nr;\n     - \t\tstats->inflated_sizes.trees += inflated_total;\n     -+\t\tif (max_inflated > stats->max_inflated_sizes.trees)\n     -+\t\t\tstats->max_inflated_sizes.trees = max_inflated;\n     - \t\tstats->disk_sizes.trees += disk_total;\n     -+\t\tif (max_disk > stats->max_disk_sizes.trees)\n     -+\t\t\tstats->max_disk_sizes.trees = max_disk;\n     - \t\tbreak;\n     - \tcase OBJ_BLOB:\n     -+\t\tif (path && *path) {\n     -+\t\t\tsize_t path_len = strlen(path);\n     -+\t\t\tsize_t path_depth = get_path_depth(path);\n     -+\n     -+\t\t\tif (path_len > stats->max_blob_path_length)\n     -+\t\t\t\tstats->max_blob_path_length = path_len;\n     -+\t\t\tif (path_depth > stats->max_blob_path_depth)\n     -+\t\t\t\tstats->max_blob_path_depth = path_depth;\n     -+\t\t}\n     -+\n     - \t\tstats->type_counts.blobs += oids->nr;\n     - \t\tstats->inflated_sizes.blobs += inflated_total;\n     -+\t\tif (max_inflated > stats->max_inflated_sizes.blobs)\n     -+\t\t\tstats->max_inflated_sizes.blobs = max_inflated;\n     - \t\tstats->disk_sizes.blobs += disk_total;\n     -+\t\tif (max_disk > stats->max_disk_sizes.blobs)\n     -+\t\t\tstats->max_disk_sizes.blobs = max_disk;\n     - \t\tbreak;\n     - \tdefault:\n     - \t\tBUG(\"invalid object type\");\n     -\n     - ## t/t1900-repo.sh ##\n     -@@ t/t1900-repo.sh: REPO_INFO_KEYS='\n     - \tlayout.bare\n     - \tlayout.shallow\n     - \tobject.format\n     -+\tpath.common-dir\n     -+\tpath.config-file\n     -+\tpath.git-dir\n     -+\tpath.git-prefix\n     -+\tpath.grafts-file\n     -+\tpath.hooks-directory\n     -+\tpath.index-file\n     -+\tpath.logs-directory\n     -+\tpath.objects-directory\n     -+\tpath.packed-refs-file\n     -+\tpath.refs-directory\n     -+\tpath.shallow-file\n     -+\tpath.superproject-working-tree\n     -+\tpath.toplevel\n     - \treferences.format\n     - '\n     - \n     -+REPO_INFO_PATH_KEYS='\n     -+\tpath.common-dir\n     -+\tpath.config-file\n     -+\tpath.git-dir\n     -+\tpath.git-prefix\n     -+\tpath.grafts-file\n     -+\tpath.hooks-directory\n     -+\tpath.index-file\n     -+\tpath.logs-directory\n     -+\tpath.objects-directory\n     -+\tpath.packed-refs-file\n     -+\tpath.refs-directory\n     -+\tpath.shallow-file\n     -+\tpath.superproject-working-tree\n     -+\tpath.toplevel\n     -+'\n     -+\n     - # Test whether a key-value pair is correctly returned\n     - #\n     - # Usage: test_repo_info <label> <init command> <repo_name> <key> <expected value>\n     -@@ t/t1900-repo.sh: test_expect_success 'values returned in order requested' '\n     - \ttest_cmp expect actual\n     - '\n     - \n     -+test_expect_success 'category key returns all matching keys' '\n     -+\tcat >expect <<-\\EOF &&\n     -+\tlayout.bare=false\n     -+\tlayout.shallow=false\n     -+\tEOF\n     -+\tgit init category-layout &&\n     -+\tgit -C category-layout repo info layout >actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'mixed key/category requests preserve request order' '\n     -+\tcat >expect <<-\\EOF &&\n     -+\tobject.format=sha1\n     -+\tlayout.bare=false\n     -+\tlayout.shallow=false\n     -+\tEOF\n     -+\tgit init mixed-order &&\n     -+\tgit -C mixed-order repo info object.format layout >actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'path.git-dir matches rev-parse --absolute-git-dir' '\n     -+\tgit init path-git-dir &&\n     -+\texpected_value=$(git -C path-git-dir rev-parse --absolute-git-dir) &&\n     -+\techo \"path.git-dir=$expected_value\" >expect &&\n     -+\tgit -C path-git-dir repo info path.git-dir >actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'path.common-dir matches rev-parse --git-common-dir' '\n     -+\tgit init path-common-dir &&\n     -+\texpected_value=$(git -C path-common-dir rev-parse --path-format=absolute --git-common-dir) &&\n     -+\techo \"path.common-dir=$expected_value\" >expect &&\n     -+\tgit -C path-common-dir repo info path.common-dir >actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'path.toplevel matches rev-parse --show-toplevel' '\n     -+\tgit init path-toplevel &&\n     -+\texpected_value=$(git -C path-toplevel rev-parse --show-toplevel) &&\n     -+\techo \"path.toplevel=$expected_value\" >expect &&\n     -+\tgit -C path-toplevel repo info path.toplevel >actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'path.toplevel is empty in bare repository' '\n     -+\tgit init --bare bare-path-toplevel &&\n     -+\techo \"path.toplevel=\" >expect &&\n     -+\tgit -C bare-path-toplevel repo info path.toplevel >actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '\n     -+\tgit init path-prefix &&\n     -+\tmkdir -p path-prefix/a/b &&\n     -+\texpected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&\n     -+\techo \"path.git-prefix=$expected_value\" >expect &&\n     -+\tgit -C path-prefix/a/b repo info path.git-prefix >actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'git-path style keys match rev-parse --git-path' '\n     -+\tgit init path-git-path &&\n     -+\n     -+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path info/grafts) &&\n     -+\techo \"path.grafts-file=$expected_value\" >expect &&\n     -+\tgit -C path-git-path repo info path.grafts-file >actual &&\n     -+\ttest_cmp expect actual &&\n     -+\n     -+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path index) &&\n     -+\techo \"path.index-file=$expected_value\" >expect &&\n     -+\tgit -C path-git-path repo info path.index-file >actual &&\n     -+\ttest_cmp expect actual &&\n     -+\n     -+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path objects) &&\n     -+\techo \"path.objects-directory=$expected_value\" >expect &&\n     -+\tgit -C path-git-path repo info path.objects-directory >actual &&\n     -+\ttest_cmp expect actual &&\n     -+\n     -+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path hooks) &&\n     -+\techo \"path.hooks-directory=$expected_value\" >expect &&\n     -+\tgit -C path-git-path repo info path.hooks-directory >actual &&\n     -+\ttest_cmp expect actual &&\n     -+\n     -+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path config) &&\n     -+\techo \"path.config-file=$expected_value\" >expect &&\n     -+\tgit -C path-git-path repo info path.config-file >actual &&\n     -+\ttest_cmp expect actual &&\n     -+\n     -+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&\n     -+\techo \"path.logs-directory=$expected_value\" >expect &&\n     -+\tgit -C path-git-path repo info path.logs-directory >actual &&\n     -+\ttest_cmp expect actual &&\n     -+\n     -+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&\n     -+\techo \"path.packed-refs-file=$expected_value\" >expect &&\n     -+\tgit -C path-git-path repo info path.packed-refs-file >actual &&\n     -+\ttest_cmp expect actual &&\n     -+\n     -+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&\n     -+\techo \"path.refs-directory=$expected_value\" >expect &&\n     -+\tgit -C path-git-path repo info path.refs-directory >actual &&\n     -+\ttest_cmp expect actual &&\n     -+\n     -+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&\n     -+\techo \"path.shallow-file=$expected_value\" >expect &&\n     -+\tgit -C path-git-path repo info path.shallow-file >actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'path.superproject-working-tree is empty when not a submodule' '\n     -+\tgit init path-superproject &&\n     -+\techo \"path.superproject-working-tree=\" >expect &&\n     -+\tgit -C path-superproject repo info path.superproject-working-tree >actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'path.superproject-working-tree matches rev-parse in submodule' '\n     -+\tgit init path-superproject-origin &&\n     -+\techo x >path-superproject-origin/x &&\n     -+\tgit -C path-superproject-origin add x &&\n     -+\tgit -C path-superproject-origin commit -m x &&\n     -+\n     -+\tgit init path-superproject-parent &&\n     -+\tgit -C path-superproject-parent -c protocol.file.allow=always submodule add ../path-superproject-origin sm &&\n     -+\n     -+\texpected_value=$(git -C path-superproject-parent/sm rev-parse --show-superproject-working-tree) &&\n     -+\techo \"path.superproject-working-tree=$expected_value\" >expect &&\n     -+\tgit -C path-superproject-parent/sm repo info path.superproject-working-tree >actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'path category returns all path keys' '\n     -+\tgit init path-category &&\n     -+\t>expect &&\n     -+\tfor key in $REPO_INFO_PATH_KEYS\n     -+\tdo\n     -+\t\tgit -C path-category repo info \"$key\" >>expect || return 1\n     -+\tdone &&\n     -+\tgit -C path-category repo info path >actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'path-format=relative matches rev-parse for git-dir' '\n     -+\tgit init path-format-relative &&\n     -+\texpected_value=$(git -C path-format-relative rev-parse --path-format=relative --git-dir) &&\n     -+\techo \"path.git-dir=$expected_value\" >expect &&\n     -+\tgit -C path-format-relative repo info --path-format=relative path.git-dir >actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'git repo info uses the last requested path format' '\n     -+\tgit init path-format-last &&\n     -+\texpected_value=$(git -C path-format-last rev-parse --path-format=relative --git-dir) &&\n     -+\techo \"path.git-dir=$expected_value\" >expect &&\n     -+\tgit -C path-format-last repo info --path-format=absolute --path-format=relative path.git-dir >actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'git-repo-info aborts when requesting an invalid path format' '\n     -+\techo \"fatal: invalid path format ${SQ}foo${SQ}\" >expect &&\n     -+\ttest_must_fail git repo info --path-format=foo path.git-dir 2>actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     - test_expect_success 'git-repo-info fails if an invalid key is requested' '\n     - \techo \"error: key ${SQ}foo${SQ} not found\" >expect &&\n     - \ttest_must_fail git repo info foo 2>actual &&\n     +    Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n      \n       ## t/t1901-repo-structure.sh ##\n      @@ t/t1901-repo-structure.sh: object_type_disk_usage() {\n     @@ t/t1901-repo-structure.sh: object_type_disk_usage() {\n      +\t\t--filter=object:type=tree --filter-provided-objects | cut -d\" \" -f1)\n      +\tdo\n      +\t\tentries=$(git cat-file -p \"$oid\" | wc -l) || return 1\n     -+\t\ttest \"$entries\" -gt \"$max\" && max=$entries\n     ++\t\ttest $entries -gt $max && max=$entries\n      +\tdone\n      +\n     -+\techo \"$max\"\n     ++\techo $max\n      +}\n      +\n      +blob_max_path_length() {\n  2:  cb85ee3b48 <  -:  ---------- t1900,t1901: make repo tests hash-agnostic and wc-portable\n  3:  3c656bf152 <  -:  ---------- t1900,t1901: fix test portability issues\n  -:  ---------- >  9:  f17c0f03e5 docs: describe repo info path keys and structure metrics\n\n-- \ngitgitgadget\n"},{"id":"536800","messageId":"99c8058298d403508ab2a23ed06c36b8707989d0.1771856469.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v2.git.git.1771856469.gitgitgadget@gmail.com","subject":"[PATCH v2 1/9] repo: teach info context and category keys","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-23T14:21:01Z","receivedAt":"2026-02-23T14:21:13Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nIntroduce an explicit repo_info context for the repo info codepath\nand thread it through value lookups and field printing.\n\nThis removes direct coupling from these helpers to ad-hoc\nrepository globals and makes key retrieval logic easier to extend\nsafely.\n\nAlso teach git repo info to accept category names (for example,\nlayout) and expand them to matching key.* entries in request\norder.\n\nThis improves script ergonomics while preserving existing behavior\nfor explicit keys and clear errors for unknown names.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 70 +++++++++++++++++++++++++++++++++++++++-----------\n 1 file changed, 55 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 0ea045abc1..e34914a9a7 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -22,7 +22,12 @@ static const char *const repo_usage[] = {\n \tNULL\n };\n \n-typedef int get_value_fn(struct repository *repo, struct strbuf *buf);\n+struct repo_info {\n+\tstruct repository *repo;\n+\tconst char *prefix;\n+};\n+\n+typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);\n \n enum output_format {\n \tFORMAT_TABLE,\n@@ -35,27 +40,30 @@ struct field {\n \tget_value_fn *get_value;\n };\n \n-static int get_layout_bare(struct repository *repo UNUSED, struct strbuf *buf)\n+static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)\n {\n \tstrbuf_addstr(buf, is_bare_repository() ? \"true\" : \"false\");\n \treturn 0;\n }\n \n-static int get_layout_shallow(struct repository *repo, struct strbuf *buf)\n+static int get_layout_shallow(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf,\n \t\t      is_repository_shallow(repo) ? \"true\" : \"false\");\n \treturn 0;\n }\n \n-static int get_object_format(struct repository *repo, struct strbuf *buf)\n+static int get_object_format(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf, repo->hash_algo->name);\n \treturn 0;\n }\n \n-static int get_references_format(struct repository *repo, struct strbuf *buf)\n+static int get_references_format(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf,\n \t\t      ref_storage_format_to_name(repo->ref_storage_format));\n \treturn 0;\n@@ -87,6 +95,33 @@ static get_value_fn *get_value_fn_for_key(const char *key)\n \treturn found ? found->get_value : NULL;\n }\n \n+static void print_field(enum output_format format, const char *key,\n+\t\t\tconst char *value);\n+\n+static int print_category_fields(const char *category,\n+\t\t\t\t struct repo_info *info,\n+\t\t\t\t enum output_format format,\n+\t\t\t\t struct strbuf *valbuf)\n+{\n+\tint found = 0;\n+\tsize_t category_len = strlen(category);\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {\n+\t\tconst struct field *field = &repo_info_fields[i];\n+\n+\t\tif (!starts_with(field->key, category) ||\n+\t\t    field->key[category_len] != '.')\n+\t\t\tcontinue;\n+\n+\t\tstrbuf_reset(valbuf);\n+\t\tfield->get_value(info, valbuf);\n+\t\tprint_field(format, field->key, valbuf->buf);\n+\t\tfound = 1;\n+\t}\n+\n+\treturn found;\n+}\n+\n static void print_field(enum output_format format, const char *key,\n \t\t\tconst char *value)\n {\n@@ -105,7 +140,7 @@ static void print_field(enum output_format format, const char *key,\n }\n \n static int print_fields(int argc, const char **argv,\n-\t\t\tstruct repository *repo,\n+\t\t\tstruct repo_info *info,\n \t\t\tenum output_format format)\n {\n \tint ret = 0;\n@@ -117,21 +152,22 @@ static int print_fields(int argc, const char **argv,\n \n \t\tget_value = get_value_fn_for_key(key);\n \n-\t\tif (!get_value) {\n-\t\t\tret = error(_(\"key '%s' not found\"), key);\n+\t\tif (get_value) {\n+\t\t\tstrbuf_reset(&valbuf);\n+\t\t\tget_value(info, &valbuf);\n+\t\t\tprint_field(format, key, valbuf.buf);\n \t\t\tcontinue;\n \t\t}\n \n-\t\tstrbuf_reset(&valbuf);\n-\t\tget_value(repo, &valbuf);\n-\t\tprint_field(format, key, valbuf.buf);\n+\t\tif (!print_category_fields(key, info, format, &valbuf))\n+\t\t\tret = error(_(\"key '%s' not found\"), key);\n \t}\n \n \tstrbuf_release(&valbuf);\n \treturn ret;\n }\n \n-static int print_all_fields(struct repository *repo,\n+static int print_all_fields(struct repo_info *info,\n \t\t\t    enum output_format format)\n {\n \tstruct strbuf valbuf = STRBUF_INIT;\n@@ -140,7 +176,7 @@ static int print_all_fields(struct repository *repo,\n \t\tconst struct field *field = &repo_info_fields[i];\n \n \t\tstrbuf_reset(&valbuf);\n-\t\tfield->get_value(repo, &valbuf);\n+\t\tfield->get_value(info, &valbuf);\n \t\tprint_field(format, field->key, valbuf.buf);\n \t}\n \n@@ -171,6 +207,10 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\t\t struct repository *repo)\n {\n \tenum output_format format = FORMAT_KEYVALUE;\n+\tstruct repo_info info = {\n+\t\t.repo = repo,\n+\t\t.prefix = prefix,\n+\t};\n \tint all_keys = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n@@ -192,9 +232,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"--all and <key> cannot be used together\"));\n \n \tif (all_keys)\n-\t\treturn print_all_fields(repo, format);\n+\t\treturn print_all_fields(&info, format);\n \telse\n-\t\treturn print_fields(argc, argv, repo, format);\n+\t\treturn print_fields(argc, argv, &info, format);\n }\n \n struct ref_stats {\n-- \ngitgitgadget\n\n"},{"id":"536802","messageId":"6d5b9ff07566e1cc28a672cf1f47988e9c8c45da.1771856469.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v2.git.git.1771856469.gitgitgadget@gmail.com","subject":"[PATCH v2 2/9] repo: add path keys to repo info","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-23T14:21:02Z","receivedAt":"2026-02-23T14:21:14Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nAdd a path category to git repo info with key-value pairs that\nmirror repository paths users commonly retrieve via rev-parse and\ngit-path lookups.\n\nThis makes scripting against repo metadata more direct and avoids\nshelling out to multiple commands for related paths.\n\nThe new keys are introduced as explicit path.* entries in\nrepo_info_fields and are resolved through dedicated helpers.\n\nThis keeps lookup behavior predictable and makes future path\nadditions straightforward.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 137 +++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 137 insertions(+)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex e34914a9a7..35e1eaf7d7 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -1,10 +1,12 @@\n #define USE_THE_REPOSITORY_VARIABLE\n \n #include \"builtin.h\"\n+#include \"abspath.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n #include \"odb.h\"\n #include \"parse-options.h\"\n+#include \"path.h\"\n #include \"path-walk.h\"\n #include \"progress.h\"\n #include \"quote.h\"\n@@ -14,6 +16,7 @@\n #include \"strbuf.h\"\n #include \"string-list.h\"\n #include \"shallow.h\"\n+#include \"submodule.h\"\n #include \"utf8.h\"\n \n static const char *const repo_usage[] = {\n@@ -40,6 +43,13 @@ struct field {\n \tget_value_fn *get_value;\n };\n \n+static void repo_info_add_path(struct repo_info *info,\n+\t\t\t      struct strbuf *buf,\n+\t\t\t      const char *path)\n+{\n+\tstrbuf_add_absolute_path(buf, path);\n+}\n+\n static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)\n {\n \tstrbuf_addstr(buf, is_bare_repository() ? \"true\" : \"false\");\n@@ -61,6 +71,119 @@ static int get_object_format(struct repo_info *info, struct strbuf *buf)\n \treturn 0;\n }\n \n+static int get_path_common_dir(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_common_dir(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_config_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"config\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_git_dir(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)\n+{\n+\tif (info->prefix)\n+\t\tstrbuf_addstr(buf, info->prefix);\n+\treturn 0;\n+}\n+\n+static int get_path_grafts_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_graft_file(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_hooks_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"hooks\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_index_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_index_file(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"logs\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_object_directory(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"packed-refs\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"refs\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"shallow\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_superproject_working_tree(struct repo_info *info,\n+\t\t\t\t\t     struct strbuf *buf)\n+{\n+\tstruct strbuf superproject = STRBUF_INIT;\n+\n+\tif (get_superproject_working_tree(&superproject))\n+\t\trepo_info_add_path(info, buf, superproject.buf);\n+\n+\tstrbuf_release(&superproject);\n+\treturn 0;\n+}\n+\n+static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)\n+{\n+\tconst char *work_tree = repo_get_work_tree(info->repo);\n+\n+\tif (work_tree)\n+\t\trepo_info_add_path(info, buf, work_tree);\n+\n+\treturn 0;\n+}\n+\n static int get_references_format(struct repo_info *info, struct strbuf *buf)\n {\n \tstruct repository *repo = info->repo;\n@@ -74,6 +197,20 @@ static const struct field repo_info_fields[] = {\n \t{ \"layout.bare\", get_layout_bare },\n \t{ \"layout.shallow\", get_layout_shallow },\n \t{ \"object.format\", get_object_format },\n+\t{ \"path.common-dir\", get_path_common_dir },\n+\t{ \"path.config-file\", get_path_config_file },\n+\t{ \"path.git-dir\", get_path_git_dir },\n+\t{ \"path.git-prefix\", get_path_git_prefix },\n+\t{ \"path.grafts-file\", get_path_grafts_file },\n+\t{ \"path.hooks-directory\", get_path_hooks_directory },\n+\t{ \"path.index-file\", get_path_index_file },\n+\t{ \"path.logs-directory\", get_path_logs_directory },\n+\t{ \"path.objects-directory\", get_path_objects_directory },\n+\t{ \"path.packed-refs-file\", get_path_packed_refs_file },\n+\t{ \"path.refs-directory\", get_path_refs_directory },\n+\t{ \"path.shallow-file\", get_path_shallow_file },\n+\t{ \"path.superproject-working-tree\", get_path_superproject_working_tree },\n+\t{ \"path.toplevel\", get_path_toplevel },\n \t{ \"references.format\", get_references_format },\n };\n \n-- \ngitgitgadget\n\n"},{"id":"536803","messageId":"504d9cf7a0dbd663ea88c75217e1564504a60937.1771856469.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v2.git.git.1771856469.gitgitgadget@gmail.com","subject":"[PATCH v2 4/9] repo: add structure max object size metrics","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-23T14:21:04Z","receivedAt":"2026-02-23T14:21:16Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nExtend git repo structure with maximum inflated and on-disk object\nsizes, both per type and overall max values.\n\nThis complements existing totals by highlighting outliers that\noften drive repository bloat analysis.\n\nThe implementation updates object counting to track per-type maxima\nwhile walking reachable objects.\n\nIt exposes those values in both table and keyvalue formats for\nscripts and human output.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 87 +++++++++++++++++++++++++++++++++++++++++++++++++-\n 1 file changed, 86 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex e5078e5459..a2fc3fd8cc 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -426,7 +426,9 @@ struct object_values {\n struct object_stats {\n \tstruct object_values type_counts;\n \tstruct object_values inflated_sizes;\n+\tstruct object_values max_inflated_sizes;\n \tstruct object_values disk_sizes;\n+\tstruct object_values max_disk_sizes;\n };\n \n struct repo_structure {\n@@ -529,6 +531,20 @@ static inline size_t get_total_object_values(struct object_values *values)\n \treturn values->tags + values->commits + values->trees + values->blobs;\n }\n \n+static inline size_t get_max_object_value(struct object_values *values)\n+{\n+\tsize_t max = values->commits;\n+\n+\tif (values->trees > max)\n+\t\tmax = values->trees;\n+\tif (values->blobs > max)\n+\t\tmax = values->blobs;\n+\tif (values->tags > max)\n+\t\tmax = values->tags;\n+\n+\treturn max;\n+}\n+\n static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t\t\tstruct repo_structure *stats)\n {\n@@ -583,6 +599,26 @@ static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t      \"    * %s\", _(\"Blobs\"));\n \tstats_table_size_addf(table, objects->disk_sizes.tags,\n \t\t\t      \"    * %s\", _(\"Tags\"));\n+\n+\tstats_table_size_addf(table, objects->max_inflated_sizes.commits,\n+\t\t\t      \"  * %s\", _(\"Largest commit\"));\n+\tstats_table_size_addf(table, objects->max_inflated_sizes.trees,\n+\t\t\t      \"  * %s\", _(\"Largest tree\"));\n+\tstats_table_size_addf(table, objects->max_inflated_sizes.blobs,\n+\t\t\t      \"  * %s\", _(\"Largest blob\"));\n+\tstats_table_size_addf(table, objects->max_inflated_sizes.tags,\n+\t\t\t      \"  * %s\", _(\"Largest tag\"));\n+\n+\tstats_table_size_addf(table, get_max_object_value(&objects->max_disk_sizes),\n+\t\t\t      \"  * %s\", _(\"Largest disk size\"));\n+\tstats_table_size_addf(table, objects->max_disk_sizes.commits,\n+\t\t\t      \"    * %s\", _(\"Commits\"));\n+\tstats_table_size_addf(table, objects->max_disk_sizes.trees,\n+\t\t\t      \"    * %s\", _(\"Trees\"));\n+\tstats_table_size_addf(table, objects->max_disk_sizes.blobs,\n+\t\t\t      \"    * %s\", _(\"Blobs\"));\n+\tstats_table_size_addf(table, objects->max_disk_sizes.tags,\n+\t\t\t      \"    * %s\", _(\"Tags\"));\n }\n \n static void stats_table_print_structure(const struct stats_table *table)\n@@ -661,6 +697,9 @@ static void stats_table_clear(struct stats_table *table)\n static void structure_keyvalue_print(struct repo_structure *stats,\n \t\t\t\t     char key_delim, char value_delim)\n {\n+\tsize_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);\n+\tsize_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);\n+\n \tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->refs.branches, value_delim);\n \tprintf(\"references.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n@@ -688,6 +727,28 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"objects.tags.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.inflated_sizes.tags, value_delim);\n \n+\tprintf(\"objects.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)max_inflated_size, value_delim);\n+\tprintf(\"objects.commits.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_inflated_sizes.commits, value_delim);\n+\tprintf(\"objects.trees.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_inflated_sizes.trees, value_delim);\n+\tprintf(\"objects.blobs.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_inflated_sizes.blobs, value_delim);\n+\tprintf(\"objects.tags.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);\n+\n+\tprintf(\"objects.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)max_disk_size, value_delim);\n+\tprintf(\"objects.commits.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_disk_sizes.commits, value_delim);\n+\tprintf(\"objects.trees.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_disk_sizes.trees, value_delim);\n+\tprintf(\"objects.blobs.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_disk_sizes.blobs, value_delim);\n+\tprintf(\"objects.tags.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);\n+\n \tprintf(\"objects.commits.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);\n \tprintf(\"objects.trees.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n@@ -772,6 +833,8 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \tstruct object_stats *stats = data->stats;\n \tsize_t inflated_total = 0;\n \tsize_t disk_total = 0;\n+\tsize_t max_inflated = 0;\n+\tsize_t max_disk = 0;\n \tsize_t object_count;\n \n \tfor (size_t i = 0; i < oids->nr; i++) {\n@@ -786,31 +849,53 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\t\t\t\t  OBJECT_INFO_SKIP_FETCH_OBJECT |\n \t\t\t\t\t\t  OBJECT_INFO_QUICK) < 0)\n \t\t\tcontinue;\n+\t\tif (disk < 0)\n+\t\t\tcontinue;\n \n \t\tinflated_total += inflated;\n-\t\tdisk_total += disk;\n+\t\tdisk_total += (size_t)disk;\n+\t\tif (inflated > max_inflated)\n+\t\t\tmax_inflated = inflated;\n+\t\tif ((size_t)disk > max_disk)\n+\t\t\tmax_disk = (size_t)disk;\n \t}\n \n \tswitch (type) {\n \tcase OBJ_TAG:\n \t\tstats->type_counts.tags += oids->nr;\n \t\tstats->inflated_sizes.tags += inflated_total;\n+\t\tif (max_inflated > stats->max_inflated_sizes.tags)\n+\t\t\tstats->max_inflated_sizes.tags = max_inflated;\n \t\tstats->disk_sizes.tags += disk_total;\n+\t\tif (max_disk > stats->max_disk_sizes.tags)\n+\t\t\tstats->max_disk_sizes.tags = max_disk;\n \t\tbreak;\n \tcase OBJ_COMMIT:\n \t\tstats->type_counts.commits += oids->nr;\n \t\tstats->inflated_sizes.commits += inflated_total;\n+\t\tif (max_inflated > stats->max_inflated_sizes.commits)\n+\t\t\tstats->max_inflated_sizes.commits = max_inflated;\n \t\tstats->disk_sizes.commits += disk_total;\n+\t\tif (max_disk > stats->max_disk_sizes.commits)\n+\t\t\tstats->max_disk_sizes.commits = max_disk;\n \t\tbreak;\n \tcase OBJ_TREE:\n \t\tstats->type_counts.trees += oids->nr;\n \t\tstats->inflated_sizes.trees += inflated_total;\n+\t\tif (max_inflated > stats->max_inflated_sizes.trees)\n+\t\t\tstats->max_inflated_sizes.trees = max_inflated;\n \t\tstats->disk_sizes.trees += disk_total;\n+\t\tif (max_disk > stats->max_disk_sizes.trees)\n+\t\t\tstats->max_disk_sizes.trees = max_disk;\n \t\tbreak;\n \tcase OBJ_BLOB:\n \t\tstats->type_counts.blobs += oids->nr;\n \t\tstats->inflated_sizes.blobs += inflated_total;\n+\t\tif (max_inflated > stats->max_inflated_sizes.blobs)\n+\t\t\tstats->max_inflated_sizes.blobs = max_inflated;\n \t\tstats->disk_sizes.blobs += disk_total;\n+\t\tif (max_disk > stats->max_disk_sizes.blobs)\n+\t\t\tstats->max_disk_sizes.blobs = max_disk;\n \t\tbreak;\n \tdefault:\n \t\tBUG(\"invalid object type\");\n-- \ngitgitgadget\n\n"},{"id":"536804","messageId":"4b502925c9d70d37e0752fadfe061f1cdf692488.1771856469.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v2.git.git.1771856469.gitgitgadget@gmail.com","subject":"[PATCH v2 5/9] repo: add structure topology and path-depth metrics","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-23T14:21:05Z","receivedAt":"2026-02-23T14:21:18Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nTrack additional structure-oriented maxima that are useful when\ndiagnosing unusually complex histories.\n\nThese include commit parent fanout, tree entry count, blob path\nlength/depth, and annotated tag chain depth.\n\nThe counters are gathered while traversing reachable objects and\nare reported in both table and keyvalue output.\n\nThis lets both humans and scripts consume the same topology\nsignals.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 171 ++++++++++++++++++++++++++++++++++++++++++++++++-\n 1 file changed, 170 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex a2fc3fd8cc..f92c209469 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -17,6 +17,7 @@\n #include \"string-list.h\"\n #include \"shallow.h\"\n #include \"submodule.h\"\n+#include \"tree-walk.h\"\n #include \"utf8.h\"\n \n static const char *const repo_usage[] = {\n@@ -429,6 +430,11 @@ struct object_stats {\n \tstruct object_values max_inflated_sizes;\n \tstruct object_values disk_sizes;\n \tstruct object_values max_disk_sizes;\n+\tsize_t max_commit_parent_count;\n+\tsize_t max_tree_entry_count;\n+\tsize_t max_blob_path_length;\n+\tsize_t max_blob_path_depth;\n+\tsize_t max_tag_chain_depth;\n };\n \n struct repo_structure {\n@@ -545,6 +551,116 @@ static inline size_t get_max_object_value(struct object_values *values)\n \treturn max;\n }\n \n+static size_t get_commit_parent_count(struct repository *repo,\n+\t\t\t\t      const struct object_id *oid)\n+{\n+\tunsigned long size = 0;\n+\tconst char *cur;\n+\tconst char *end;\n+\tvoid *buf;\n+\tsize_t count = 0;\n+\n+\tbuf = odb_read_object_peeled(repo->objects, oid, OBJ_COMMIT, &size, NULL);\n+\tif (!buf)\n+\t\treturn 0;\n+\n+\tcur = buf;\n+\tend = cur + size;\n+\twhile (cur < end) {\n+\t\tconst char *newline = memchr(cur, '\\n', end - cur);\n+\t\tsize_t line_len;\n+\n+\t\tif (!newline)\n+\t\t\tbreak;\n+\t\tline_len = newline - cur;\n+\t\tif (!line_len)\n+\t\t\tbreak;\n+\n+\t\tif (line_len > 7 && !memcmp(cur, \"parent \", 7))\n+\t\t\tcount++;\n+\n+\t\tcur = newline + 1;\n+\t}\n+\n+\tfree(buf);\n+\treturn count;\n+}\n+\n+static size_t get_tree_entry_count(struct repository *repo,\n+\t\t\t\t   const struct object_id *oid)\n+{\n+\tstruct tree_desc desc;\n+\tstruct name_entry entry;\n+\tunsigned long size = 0;\n+\tvoid *buf;\n+\tsize_t count = 0;\n+\n+\tbuf = odb_read_object_peeled(repo->objects, oid, OBJ_TREE, &size, NULL);\n+\tif (!buf)\n+\t\treturn 0;\n+\n+\tinit_tree_desc(&desc, oid, buf, size);\n+\twhile (tree_entry(&desc, &entry))\n+\t\tcount++;\n+\n+\tfree(buf);\n+\treturn count;\n+}\n+\n+static size_t get_path_depth(const char *path)\n+{\n+\tsize_t depth = 0;\n+\n+\tif (!path || !*path)\n+\t\treturn 0;\n+\n+\tdepth = 1;\n+\tfor (const char *cur = path; *cur; cur++)\n+\t\tif (*cur == '/')\n+\t\t\tdepth++;\n+\n+\treturn depth;\n+}\n+\n+static size_t get_tag_chain_depth(struct repository *repo,\n+\t\t\t\t  const struct object_id *oid)\n+{\n+\tstruct object_id current = *oid;\n+\tsize_t depth = 0;\n+\n+\twhile (1) {\n+\t\tenum object_type type;\n+\t\tunsigned long size = 0;\n+\t\tstruct object_id next;\n+\t\tconst char *p, *end;\n+\t\tvoid *buf = odb_read_object(repo->objects, &current, &type, &size);\n+\n+\t\tif (!buf)\n+\t\t\tbreak;\n+\t\tif (type != OBJ_TAG) {\n+\t\t\tfree(buf);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tp = buf;\n+\t\tif (!skip_prefix(p, \"object \", &p) ||\n+\t\t    parse_oid_hex_algop(p, &next, &end, repo->hash_algo) ||\n+\t\t    *end != '\\n') {\n+\t\t\tfree(buf);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tdepth++;\n+\t\tfree(buf);\n+\n+\t\tif (oideq(&next, &current))\n+\t\t\tbreak;\n+\t\toidcpy(&current, &next);\n+\t}\n+\n+\treturn depth;\n+}\n+\n static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t\t\tstruct repo_structure *stats)\n {\n@@ -619,6 +735,17 @@ static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t      \"    * %s\", _(\"Blobs\"));\n \tstats_table_size_addf(table, objects->max_disk_sizes.tags,\n \t\t\t      \"    * %s\", _(\"Tags\"));\n+\n+\tstats_table_count_addf(table, objects->max_commit_parent_count,\n+\t\t\t       \"  * %s\", _(\"Largest parent count\"));\n+\tstats_table_count_addf(table, objects->max_tree_entry_count,\n+\t\t\t       \"  * %s\", _(\"Largest tree entries\"));\n+\tstats_table_count_addf(table, objects->max_blob_path_length,\n+\t\t\t       \"  * %s\", _(\"Longest blob path\"));\n+\tstats_table_count_addf(table, objects->max_blob_path_depth,\n+\t\t\t       \"  * %s\", _(\"Deepest blob path\"));\n+\tstats_table_count_addf(table, objects->max_tag_chain_depth,\n+\t\t\t       \"  * %s\", _(\"Deepest tag chain\"));\n }\n \n static void stats_table_print_structure(const struct stats_table *table)\n@@ -749,6 +876,17 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"objects.tags.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);\n \n+\tprintf(\"objects.commits.max_parent_count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_commit_parent_count, value_delim);\n+\tprintf(\"objects.trees.max_entry_count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_tree_entry_count, value_delim);\n+\tprintf(\"objects.blobs.max_path_length%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_blob_path_length, value_delim);\n+\tprintf(\"objects.blobs.max_path_depth%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_blob_path_depth, value_delim);\n+\tprintf(\"objects.tags.max_chain_depth%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_tag_chain_depth, value_delim);\n+\n \tprintf(\"objects.commits.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);\n \tprintf(\"objects.trees.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n@@ -826,7 +964,7 @@ struct count_objects_data {\n \tstruct progress *progress;\n };\n \n-static int count_objects(const char *path UNUSED, struct oid_array *oids,\n+static int count_objects(const char *path, struct oid_array *oids,\n \t\t\t enum object_type type, void *cb_data)\n {\n \tstruct count_objects_data *data = cb_data;\n@@ -862,6 +1000,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \n \tswitch (type) {\n \tcase OBJ_TAG:\n+\t\tfor (size_t i = 0; i < oids->nr; i++) {\n+\t\t\tsize_t tag_chain_depth = get_tag_chain_depth(data->odb->repo,\n+\t\t\t\t\t\t\t     &oids->oid[i]);\n+\t\t\tif (tag_chain_depth > stats->max_tag_chain_depth)\n+\t\t\t\tstats->max_tag_chain_depth = tag_chain_depth;\n+\t\t}\n+\n \t\tstats->type_counts.tags += oids->nr;\n \t\tstats->inflated_sizes.tags += inflated_total;\n \t\tif (max_inflated > stats->max_inflated_sizes.tags)\n@@ -871,6 +1016,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\tstats->max_disk_sizes.tags = max_disk;\n \t\tbreak;\n \tcase OBJ_COMMIT:\n+\t\tfor (size_t i = 0; i < oids->nr; i++) {\n+\t\t\tsize_t parent_count = get_commit_parent_count(data->odb->repo,\n+\t\t\t\t\t\t\t     &oids->oid[i]);\n+\t\t\tif (parent_count > stats->max_commit_parent_count)\n+\t\t\t\tstats->max_commit_parent_count = parent_count;\n+\t\t}\n+\n \t\tstats->type_counts.commits += oids->nr;\n \t\tstats->inflated_sizes.commits += inflated_total;\n \t\tif (max_inflated > stats->max_inflated_sizes.commits)\n@@ -880,6 +1032,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\tstats->max_disk_sizes.commits = max_disk;\n \t\tbreak;\n \tcase OBJ_TREE:\n+\t\tfor (size_t i = 0; i < oids->nr; i++) {\n+\t\t\tsize_t entry_count = get_tree_entry_count(data->odb->repo,\n+\t\t\t\t\t\t\t    &oids->oid[i]);\n+\t\t\tif (entry_count > stats->max_tree_entry_count)\n+\t\t\t\tstats->max_tree_entry_count = entry_count;\n+\t\t}\n+\n \t\tstats->type_counts.trees += oids->nr;\n \t\tstats->inflated_sizes.trees += inflated_total;\n \t\tif (max_inflated > stats->max_inflated_sizes.trees)\n@@ -889,6 +1048,16 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\tstats->max_disk_sizes.trees = max_disk;\n \t\tbreak;\n \tcase OBJ_BLOB:\n+\t\tif (path && *path) {\n+\t\t\tsize_t path_len = strlen(path);\n+\t\t\tsize_t path_depth = get_path_depth(path);\n+\n+\t\t\tif (path_len > stats->max_blob_path_length)\n+\t\t\t\tstats->max_blob_path_length = path_len;\n+\t\t\tif (path_depth > stats->max_blob_path_depth)\n+\t\t\t\tstats->max_blob_path_depth = path_depth;\n+\t\t}\n+\n \t\tstats->type_counts.blobs += oids->nr;\n \t\tstats->inflated_sizes.blobs += inflated_total;\n \t\tif (max_inflated > stats->max_inflated_sizes.blobs)\n-- \ngitgitgadget\n\n"},{"id":"536805","messageId":"1751181950734d2571ef85a419ef62000458bd39.1771856469.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v2.git.git.1771856469.gitgitgadget@gmail.com","subject":"[PATCH v2 6/9] repo: add aggregate structure totals to keyvalue output","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-23T14:21:06Z","receivedAt":"2026-02-23T14:21:23Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nExpose aggregate totals alongside per-type values in structure\nkeyvalue/nul output: total references, total objects, total\ninflated size, and total disk size.\n\nThese totals remove the need for callers to recompute sums\nexternally.\n\nKeeping aggregate and per-type values in the same output format\nimproves script ergonomics.\n\nIt also keeps machine-readable output aligned with what table mode\nsummarizes for humans.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 16 ++++++++++++++++\n 1 file changed, 16 insertions(+)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex f92c209469..cb70171412 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -824,9 +824,16 @@ static void stats_table_clear(struct stats_table *table)\n static void structure_keyvalue_print(struct repo_structure *stats,\n \t\t\t\t     char key_delim, char value_delim)\n {\n+\tsize_t references_count_total = get_total_reference_count(&stats->refs);\n+\tsize_t object_count_total = get_total_object_values(&stats->objects.type_counts);\n+\tsize_t inflated_size_total = get_total_object_values(&stats->objects.inflated_sizes);\n+\tsize_t disk_size_total = get_total_object_values(&stats->objects.disk_sizes);\n \tsize_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);\n \tsize_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);\n \n+\tprintf(\"references.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)references_count_total, value_delim);\n+\n \tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->refs.branches, value_delim);\n \tprintf(\"references.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n@@ -836,6 +843,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"references.others.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->refs.others, value_delim);\n \n+\tprintf(\"objects.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)object_count_total, value_delim);\n+\n \tprintf(\"objects.commits.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.type_counts.commits, value_delim);\n \tprintf(\"objects.trees.count%c%\" PRIuMAX \"%c\", key_delim,\n@@ -845,6 +855,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"objects.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.type_counts.tags, value_delim);\n \n+\tprintf(\"objects.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)inflated_size_total, value_delim);\n+\n \tprintf(\"objects.commits.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.inflated_sizes.commits, value_delim);\n \tprintf(\"objects.trees.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n@@ -865,6 +878,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"objects.tags.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);\n \n+\tprintf(\"objects.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)disk_size_total, value_delim);\n+\n \tprintf(\"objects.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)max_disk_size, value_delim);\n \tprintf(\"objects.commits.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-- \ngitgitgadget\n\n"},{"id":"536806","messageId":"fd18f28db0bac01317c892f14bd9bf16038e07f0.1771856469.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v2.git.git.1771856469.gitgitgadget@gmail.com","subject":"[PATCH v2 7/9] t1900: cover repo info path keys and path-format","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-23T14:21:07Z","receivedAt":"2026-02-23T14:21:24Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nExtend t1900 to validate category-key expansion, path.* key\nbehavior, and --path-format handling for git repo info.\n\nThe tests compare repo info output to equivalent rev-parse values.\n\nThis ensures behavior remains aligned with existing plumbing\nsemantics.\n\nAlso keep mixed key/category ordering coverage so callers can rely\non deterministic output order when combining explicit keys with\ncategory requests.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n t/t1900-repo.sh | 196 ++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 196 insertions(+)\n\ndiff --git a/t/t1900-repo.sh b/t/t1900-repo.sh\nindex 51d55f11a5..dcacf84cc3 100755\n--- a/t/t1900-repo.sh\n+++ b/t/t1900-repo.sh\n@@ -10,9 +10,40 @@ REPO_INFO_KEYS='\n \tlayout.bare\n \tlayout.shallow\n \tobject.format\n+\tpath.common-dir\n+\tpath.config-file\n+\tpath.git-dir\n+\tpath.git-prefix\n+\tpath.grafts-file\n+\tpath.hooks-directory\n+\tpath.index-file\n+\tpath.logs-directory\n+\tpath.objects-directory\n+\tpath.packed-refs-file\n+\tpath.refs-directory\n+\tpath.shallow-file\n+\tpath.superproject-working-tree\n+\tpath.toplevel\n \treferences.format\n '\n \n+REPO_INFO_PATH_KEYS='\n+\tpath.common-dir\n+\tpath.config-file\n+\tpath.git-dir\n+\tpath.git-prefix\n+\tpath.grafts-file\n+\tpath.hooks-directory\n+\tpath.index-file\n+\tpath.logs-directory\n+\tpath.objects-directory\n+\tpath.packed-refs-file\n+\tpath.refs-directory\n+\tpath.shallow-file\n+\tpath.superproject-working-tree\n+\tpath.toplevel\n+'\n+\n # Test whether a key-value pair is correctly returned\n #\n # Usage: test_repo_info <label> <init command> <repo_name> <key> <expected value>\n@@ -89,6 +120,171 @@ test_expect_success 'values returned in order requested' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'category key returns all matching keys' '\n+\tcat >expect <<-\\EOF &&\n+\tlayout.bare=false\n+\tlayout.shallow=false\n+\tEOF\n+\tgit init category-layout &&\n+\tgit -C category-layout repo info layout >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'mixed key/category requests preserve request order' '\n+\tcat >expect <<-EOF &&\n+\tobject.format=$(test_oid algo)\n+\tlayout.bare=false\n+\tlayout.shallow=false\n+\tEOF\n+\tgit init mixed-order &&\n+\tgit -C mixed-order repo info object.format layout >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.git-dir matches rev-parse --absolute-git-dir' '\n+\tgit init path-git-dir &&\n+\texpected_value=$(git -C path-git-dir rev-parse --absolute-git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-git-dir repo info path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.common-dir matches rev-parse --git-common-dir' '\n+\tgit init path-common-dir &&\n+\texpected_value=$(git -C path-common-dir rev-parse --path-format=absolute --git-common-dir) &&\n+\techo \"path.common-dir=$expected_value\" >expect &&\n+\tgit -C path-common-dir repo info path.common-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.toplevel matches rev-parse --show-toplevel' '\n+\tgit init path-toplevel &&\n+\texpected_value=$(git -C path-toplevel rev-parse --show-toplevel) &&\n+\techo \"path.toplevel=$expected_value\" >expect &&\n+\tgit -C path-toplevel repo info path.toplevel >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.toplevel is empty in bare repository' '\n+\tgit init --bare bare-path-toplevel &&\n+\techo \"path.toplevel=\" >expect &&\n+\tgit -C bare-path-toplevel repo info path.toplevel >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '\n+\tgit init path-prefix &&\n+\tmkdir -p path-prefix/a/b &&\n+\texpected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&\n+\techo \"path.git-prefix=$expected_value\" >expect &&\n+\tgit -C path-prefix/a/b repo info path.git-prefix >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git-path style keys match rev-parse --git-path' '\n+\tgit init path-git-path &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path info/grafts) &&\n+\techo \"path.grafts-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.grafts-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path index) &&\n+\techo \"path.index-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.index-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path objects) &&\n+\techo \"path.objects-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.objects-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path hooks) &&\n+\techo \"path.hooks-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.hooks-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path config) &&\n+\techo \"path.config-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.config-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&\n+\techo \"path.logs-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.logs-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&\n+\techo \"path.packed-refs-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.packed-refs-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&\n+\techo \"path.refs-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.refs-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&\n+\techo \"path.shallow-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.shallow-file >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.superproject-working-tree is empty when not a submodule' '\n+\tgit init path-superproject &&\n+\techo \"path.superproject-working-tree=\" >expect &&\n+\tgit -C path-superproject repo info path.superproject-working-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.superproject-working-tree matches rev-parse in submodule' '\n+\tgit init path-superproject-origin &&\n+\techo x >path-superproject-origin/x &&\n+\tgit -C path-superproject-origin add x &&\n+\tgit -C path-superproject-origin commit -m x &&\n+\n+\tgit init path-superproject-parent &&\n+\tgit -C path-superproject-parent -c protocol.file.allow=always submodule add ../path-superproject-origin sm &&\n+\n+\texpected_value=$(git -C path-superproject-parent/sm rev-parse --show-superproject-working-tree) &&\n+\techo \"path.superproject-working-tree=$expected_value\" >expect &&\n+\tgit -C path-superproject-parent/sm repo info path.superproject-working-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path category returns all path keys' '\n+\tgit init path-category &&\n+\t>expect &&\n+\tfor key in $REPO_INFO_PATH_KEYS\n+\tdo\n+\t\tgit -C path-category repo info \"$key\" >>expect || return 1\n+\tdone &&\n+\tgit -C path-category repo info path >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path-format=relative matches rev-parse for git-dir' '\n+\tgit init path-format-relative &&\n+\texpected_value=$(git -C path-format-relative rev-parse --path-format=relative --git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-format-relative repo info --path-format=relative path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git repo info uses the last requested path format' '\n+\tgit init path-format-last &&\n+\texpected_value=$(git -C path-format-last rev-parse --path-format=relative --git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-format-last repo info --path-format=absolute --path-format=relative path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git-repo-info aborts when requesting an invalid path format' '\n+\techo \"fatal: invalid path format ${SQ}foo${SQ}\" >expect &&\n+\ttest_must_fail git repo info --path-format=foo path.git-dir 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'git-repo-info fails if an invalid key is requested' '\n \techo \"error: key ${SQ}foo${SQ} not found\" >expect &&\n \ttest_must_fail git repo info foo 2>actual &&\n-- \ngitgitgadget\n\n"},{"id":"536807","messageId":"0525ed4cd99f312b35cd7c005f16679c682352c0.1771856469.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v2.git.git.1771856469.gitgitgadget@gmail.com","subject":"[PATCH v2 8/9] t1901: extend structure metric coverage and portability","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-23T14:21:08Z","receivedAt":"2026-02-23T14:21:25Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nExpand t1901 to cover additional structure metrics emitted by git\nrepo structure, including maxima and aggregate keyvalue/nul\nchecks.\n\nThe test now validates both human-oriented table content and\nmachine-readable fields for the extended metric set.\n\nAlso make expectations more portable across hash algorithms and\nplatforms by avoiding brittle assumptions.\n\nThis includes wc output quirks on BSD/macOS and hash-format-\nsensitive expectations.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n t/t1901-repo-structure.sh | 250 ++++++++++++++++++++++++++++----------\n 1 file changed, 187 insertions(+), 63 deletions(-)\n\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nindex 17ff164b05..7b7c4117aa 100755\n--- a/t/t1901-repo-structure.sh\n+++ b/t/t1901-repo-structure.sh\n@@ -21,42 +21,174 @@ object_type_disk_usage() {\n \tfi\n }\n \n+object_type_max_inflated_size() {\n+\tmax=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=$1 --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tsize=$(git cat-file -s \"$oid\") || return 1\n+\t\ttest \"$size\" -gt \"$max\" && max=$size\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+tag_max_chain_depth() {\n+\tmax=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=tag --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tdepth=0\n+\t\tcurrent=$oid\n+\n+\t\twhile :\n+\t\tdo\n+\t\t\ttarget=$(git cat-file -p \"$current\" | sed -n \"s/^object //p\" | sed -n 1p) || return 1\n+\t\t\ttest -n \"$target\" || break\n+\t\t\tdepth=$((depth + 1))\n+\t\t\ttype=$(git cat-file -t \"$target\") || return 1\n+\t\t\ttest \"$type\" = tag || break\n+\t\t\tcurrent=$target\n+\t\tdone\n+\n+\t\ttest \"$depth\" -gt \"$max\" && max=$depth\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+object_max_inflated_size() {\n+\tmax=0\n+\n+\tfor type in commit tree blob tag\n+\tdo\n+\t\ttype_max=$(object_type_max_inflated_size \"$type\") || return 1\n+\t\ttest \"$type_max\" -gt \"$max\" && max=$type_max\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+object_type_max_disk_size() {\n+\tmax=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=$1 --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tsize=$(echo \"$oid\" | git cat-file --batch-check='%(objectsize:disk)') || return 1\n+\t\ttest \"$size\" -gt \"$max\" && max=$size\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+reference_count_total() {\n+\tgit for-each-ref --format='%(refname)' | sed -n '$='\n+}\n+\n+object_type_count() {\n+\tgit rev-list --all --objects \\\n+\t\t--filter=object:type=$1 --filter-provided-objects | sed -n '$='\n+}\n+\n+object_count_total() {\n+\tcommits=$(object_type_count commit) || return 1\n+\ttrees=$(object_type_count tree) || return 1\n+\tblobs=$(object_type_count blob) || return 1\n+\ttags=$(object_type_count tag) || return 1\n+\n+\techo $((commits + trees + blobs + tags))\n+}\n+\n+object_type_total_inflated_size() {\n+\ttotal=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=$1 --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tsize=$(git cat-file -s \"$oid\") || return 1\n+\t\ttotal=$((total + size))\n+\tdone\n+\n+\techo \"$total\"\n+}\n+\n+object_total_inflated_size() {\n+\tcommits=$(object_type_total_inflated_size commit) || return 1\n+\ttrees=$(object_type_total_inflated_size tree) || return 1\n+\tblobs=$(object_type_total_inflated_size blob) || return 1\n+\ttags=$(object_type_total_inflated_size tag) || return 1\n+\n+\techo $((commits + trees + blobs + tags))\n+}\n+\n+object_max_disk_size() {\n+\tmax=0\n+\n+\tfor type in commit tree blob tag\n+\tdo\n+\t\ttype_max=$(object_type_max_disk_size \"$type\") || return 1\n+\t\ttest \"$type_max\" -gt \"$max\" && max=$type_max\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+commit_max_parent_count() {\n+\tgit rev-list --all --parents | awk '\n+\t\t{ n = NF - 1; if (n > max) max = n }\n+\t\tEND { print max + 0 }\n+\t'\n+}\n+\n+tree_max_entry_count() {\n+\tmax=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=tree --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tentries=$(git cat-file -p \"$oid\" | wc -l) || return 1\n+\t\ttest $entries -gt $max && max=$entries\n+\tdone\n+\n+\techo $max\n+}\n+\n+blob_max_path_length() {\n+\tgit rev-list --all --objects \\\n+\t\t--filter=object:type=blob --filter-provided-objects | awk '\n+\t\tNF > 1 {\n+\t\t\tlen = length($2)\n+\t\t\tif (len > max) max = len\n+\t\t}\n+\t\tEND { print max + 0 }\n+\t'\n+}\n+\n+blob_max_path_depth() {\n+\tgit rev-list --all --objects \\\n+\t\t--filter=object:type=blob --filter-provided-objects | awk '\n+\t\tNF > 1 {\n+\t\t\tdepth = gsub(/\\//, \"/\", $2) + 1\n+\t\t\tif (depth > max) max = depth\n+\t\t}\n+\t\tEND { print max + 0 }\n+\t'\n+}\n+\n test_expect_success 'empty repository' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n \t(\n \t\tcd repo &&\n-\t\tcat >expect <<-\\EOF &&\n-\t\t| Repository structure | Value  |\n-\t\t| -------------------- | ------ |\n-\t\t| * References         |        |\n-\t\t|   * Count            |    0   |\n-\t\t|     * Branches       |    0   |\n-\t\t|     * Tags           |    0   |\n-\t\t|     * Remotes        |    0   |\n-\t\t|     * Others         |    0   |\n-\t\t|                      |        |\n-\t\t| * Reachable objects  |        |\n-\t\t|   * Count            |    0   |\n-\t\t|     * Commits        |    0   |\n-\t\t|     * Trees          |    0   |\n-\t\t|     * Blobs          |    0   |\n-\t\t|     * Tags           |    0   |\n-\t\t|   * Inflated size    |    0 B |\n-\t\t|     * Commits        |    0 B |\n-\t\t|     * Trees          |    0 B |\n-\t\t|     * Blobs          |    0 B |\n-\t\t|     * Tags           |    0 B |\n-\t\t|   * Disk size        |    0 B |\n-\t\t|     * Commits        |    0 B |\n-\t\t|     * Trees          |    0 B |\n-\t\t|     * Blobs          |    0 B |\n-\t\t|     * Tags           |    0 B |\n-\t\tEOF\n-\n \t\tgit repo structure >out 2>err &&\n-\n-\t\ttest_cmp expect out &&\n+\t\ttest_grep \"Repository structure\" out &&\n+\t\ttest_grep \"\\\\* References\" out &&\n+\t\ttest_grep \"\\\\* Reachable objects\" out &&\n+\t\ttest_grep \"Largest disk size\" out &&\n+\t\ttest_grep \"Deepest tag chain\" out &&\n \t\ttest_line_count = 0 err\n \t)\n '\n@@ -75,40 +207,13 @@ test_expect_success SHA1 'repository with references and objects' '\n \t\t# Also creates a commit, tree, and blob.\n \t\tgit notes add -m foo &&\n \n-\t\t# The tags disk size is handled specially due to the\n-\t\t# git-rev-list(1) --disk-usage=human option printing the full\n-\t\t# \"byte/bytes\" unit string instead of just \"B\".\n-\t\tcat >expect <<-EOF &&\n-\t\t| Repository structure | Value      |\n-\t\t| -------------------- | ---------- |\n-\t\t| * References         |            |\n-\t\t|   * Count            |      4     |\n-\t\t|     * Branches       |      1     |\n-\t\t|     * Tags           |      1     |\n-\t\t|     * Remotes        |      1     |\n-\t\t|     * Others         |      1     |\n-\t\t|                      |            |\n-\t\t| * Reachable objects  |            |\n-\t\t|   * Count            |   3.02 k   |\n-\t\t|     * Commits        |   1.01 k   |\n-\t\t|     * Trees          |   1.01 k   |\n-\t\t|     * Blobs          |   1.01 k   |\n-\t\t|     * Tags           |      1     |\n-\t\t|   * Inflated size    |  16.03 MiB |\n-\t\t|     * Commits        | 217.92 KiB |\n-\t\t|     * Trees          |  15.81 MiB |\n-\t\t|     * Blobs          |  11.68 KiB |\n-\t\t|     * Tags           |    132 B   |\n-\t\t|   * Disk size        | $(object_type_disk_usage all true) |\n-\t\t|     * Commits        | $(object_type_disk_usage commit true) |\n-\t\t|     * Trees          | $(object_type_disk_usage tree true) |\n-\t\t|     * Blobs          |  $(object_type_disk_usage blob true) |\n-\t\t|     * Tags           |    $(object_type_disk_usage tag) B   |\n-\t\tEOF\n-\n \t\tgit repo structure >out 2>err &&\n-\n-\t\ttest_cmp expect out &&\n+\t\ttest_grep \"\\\\* References\" out &&\n+\t\ttest_grep \"\\\\* Reachable objects\" out &&\n+\t\ttest_grep \"Largest commit\" out &&\n+\t\ttest_grep \"Largest disk size\" out &&\n+\t\ttest_grep \"Largest parent count\" out &&\n+\t\ttest_grep \"Deepest tag chain\" out &&\n \t\ttest_line_count = 0 err\n \t)\n '\n@@ -122,18 +227,37 @@ test_expect_success SHA1 'keyvalue and nul format' '\n \t\tgit tag -a foo -m bar &&\n \n \t\tcat >expect <<-EOF &&\n+\t\treferences.count=$(reference_count_total)\n \t\treferences.branches.count=1\n \t\treferences.tags.count=1\n \t\treferences.remotes.count=0\n \t\treferences.others.count=0\n+\t\tobjects.count=$(object_count_total)\n \t\tobjects.commits.count=42\n \t\tobjects.trees.count=42\n \t\tobjects.blobs.count=42\n \t\tobjects.tags.count=1\n+\t\tobjects.inflated_size=$(object_total_inflated_size)\n \t\tobjects.commits.inflated_size=9225\n \t\tobjects.trees.inflated_size=28554\n \t\tobjects.blobs.inflated_size=453\n \t\tobjects.tags.inflated_size=132\n+\t\tobjects.max_inflated_size=$(object_max_inflated_size)\n+\t\tobjects.commits.max_inflated_size=$(object_type_max_inflated_size commit)\n+\t\tobjects.trees.max_inflated_size=$(object_type_max_inflated_size tree)\n+\t\tobjects.blobs.max_inflated_size=$(object_type_max_inflated_size blob)\n+\t\tobjects.tags.max_inflated_size=$(object_type_max_inflated_size tag)\n+\t\tobjects.disk_size=$(object_type_disk_usage all)\n+\t\tobjects.max_disk_size=$(object_max_disk_size)\n+\t\tobjects.commits.max_disk_size=$(object_type_max_disk_size commit)\n+\t\tobjects.trees.max_disk_size=$(object_type_max_disk_size tree)\n+\t\tobjects.blobs.max_disk_size=$(object_type_max_disk_size blob)\n+\t\tobjects.tags.max_disk_size=$(object_type_max_disk_size tag)\n+\t\tobjects.commits.max_parent_count=$(commit_max_parent_count)\n+\t\tobjects.trees.max_entry_count=$(tree_max_entry_count)\n+\t\tobjects.blobs.max_path_length=$(blob_max_path_length)\n+\t\tobjects.blobs.max_path_depth=$(blob_max_path_depth)\n+\t\tobjects.tags.max_chain_depth=$(tag_max_chain_depth)\n \t\tobjects.commits.disk_size=$(object_type_disk_usage commit)\n \t\tobjects.trees.disk_size=$(object_type_disk_usage tree)\n \t\tobjects.blobs.disk_size=$(object_type_disk_usage blob)\n-- \ngitgitgadget\n\n"},{"id":"536808","messageId":"f17c0f03e5daf6b905cbdba88285ee9d8e371f3f.1771856469.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v2.git.git.1771856469.gitgitgadget@gmail.com","subject":"[PATCH v2 9/9] docs: describe repo info path keys and structure metrics","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-23T14:21:09Z","receivedAt":"2026-02-23T14:21:27Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nDocument the newly added repo info capabilities, including\ncategory keys and path-oriented key definitions.\n\nAlso describe --path-format behavior for path outputs.\n\nUpdate git repo structure documentation to cover newly reported\nmaxima and aggregate keyvalue/nul fields.\n\nThis keeps command behavior and output keys fully specified for\nusers and scripts.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n Documentation/git-repo.adoc | 67 ++++++++++++++++++++++++++++++++++---\n 1 file changed, 63 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 7d70270dfa..b575977a4b 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -8,7 +8,7 @@ git-repo - Retrieve information about the repository\n SYNOPSIS\n --------\n [synopsis]\n-git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]\n+git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\n git repo structure [--format=(table|keyvalue|nul) | -z]\n \n DESCRIPTION\n@@ -44,6 +44,11 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n+`--path-format=(absolute|relative)`:::\n+\tControls formatting for keys in the `path` category. The default is\n+\t`absolute`. This option may be specified multiple times; the last one\n+\tspecified takes effect.\n+\n `structure [--format=(table|keyvalue|nul) | -z]`::\n \tRetrieve statistics about the current repository structure. The\n \tfollowing kinds of information are reported:\n@@ -52,6 +57,12 @@ supported:\n * Reachable object counts categorized by type\n * Total inflated size of reachable objects by type\n * Total disk size of reachable objects by type\n+* Largest inflated reachable object size by type\n+* Largest disk size of a reachable object by type\n+* Largest parent count among reachable commits\n+* Largest entry count among reachable trees\n+* Longest and deepest path among reachable blobs\n+* Deepest annotated tag chain\n +\n The output format can be chosen through the flag `--format`. Three formats are\n supported:\n@@ -64,6 +75,7 @@ supported:\n `keyvalue`:::\n \tEach line of output contains a key-value pair for a repository stat.\n \tThe '=' character is used to delimit between the key and the value.\n+\tBoth aggregate metrics and per-type metrics are included.\n \tValues containing \"unusual\" characters are quoted as explained for the\n \tconfiguration variable `core.quotePath` (see linkgit:git-config[1]).\n \n@@ -78,9 +90,11 @@ supported:\n \n INFO KEYS\n ---------\n-In order to obtain a set of values from `git repo info`, you should provide\n-the keys that identify them. Here's a list of the available keys and the\n-values that they return:\n+In order to obtain values from `git repo info`, provide either individual keys\n+or category names. A category returns all keys within that category. For\n+example, `layout` returns both `layout.bare` and `layout.shallow`.\n+\n+Here's a list of the available keys and the values that they return:\n \n `layout.bare`::\n \t`true` if this is a bare repository, otherwise `false`.\n@@ -91,6 +105,51 @@ values that they return:\n `object.format`::\n \tThe object format (hash algorithm) used in the repository.\n \n+`path.common-dir`::\n+\tThe path to the common git directory.\n+\n+`path.config-file`::\n+\tThe path to the `config` file in the git directory.\n+\n+`path.git-dir`::\n+\tThe path to the git directory.\n+\n+`path.git-prefix`::\n+\tThe path of the current working directory relative to the top-level\n+\tdirectory.\n+\n+`path.grafts-file`::\n+\tThe path to the `info/grafts` file.\n+\n+`path.hooks-directory`::\n+\tThe path to the `hooks` directory.\n+\n+`path.index-file`::\n+\tThe path to the index file.\n+\n+`path.logs-directory`::\n+\tThe path to the `logs` directory.\n+\n+`path.objects-directory`::\n+\tThe path to the objects directory.\n+\n+`path.packed-refs-file`::\n+\tThe path to the `packed-refs` file.\n+\n+`path.refs-directory`::\n+\tThe path to the `refs` directory.\n+\n+`path.shallow-file`::\n+\tThe path to the `shallow` file.\n+\n+`path.superproject-working-tree`::\n+\tThe path to the superproject's working tree root, or an empty string\n+\twhen the repository is not used as a submodule.\n+\n+`path.toplevel`::\n+\tThe path to the top-level working tree directory, or an empty string\n+\tfor bare repositories.\n+\n `references.format`::\n \tThe reference storage format. The valid values are:\n +\n-- \ngitgitgadget\n"},{"id":"536874","messageId":"pull.2208.v3.git.git.1771875812.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v2.git.git.1771856469.gitgitgadget@gmail.com","subject":"[PATCH v3 0/5] repo: extend info path reporting and structure statistics","fromName":"eslam reda via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-23T19:43:27Z","receivedAt":"2026-02-23T19:43:36Z","isPatch":true,"sender":{"key":"name:eslam reda","avatar":null},"body":"\nThis series improves git repo info and git repo structure with a\n================================================================\n\ncleanly split commit history and explicit documentation/tests.\n\nFor git repo info, this series:\n\n * introduces explicit command context plumbing (instead of ad-hoc global\n   reliance in this codepath),\n * adds category-key expansion (for example, requesting layout expands to\n   layout.* keys),\n * adds path-oriented keys (path.*) that expose repository locations,\n * adds --path-format=(absolute|relative) to control path rendering.\n\nFor git repo structure, this series adds richer metrics:\n\n * maximum inflated object size (overall + per type),\n * maximum on-disk object size (overall + per type),\n * maximum commit parent count,\n * maximum tree entry count,\n * maximum blob path length and path depth,\n * maximum annotated tag chain depth,\n * aggregate keyvalue/nul totals (references.count, objects.count,\n   objects.inflated_size, objects.disk_size).\n\nTests and documentation are updated accordingly.\n\n----------------------------------------------------------------------------\n\n\nWhy this change\n===============\n\nThe intent is to make git repo more script-friendly and more useful for\nrepository diagnostics:\n\n * repo info becomes easier to query programmatically (category keys,\n   explicit path formatting).\n * repo structure becomes more actionable by exposing outlier-focused\n   metrics (maxima), not only totals.\n * keyvalue/nul output now includes aggregate totals so scripts do not need\n   to recompute them externally.\n\n----------------------------------------------------------------------------\n\n\nCommit structure (v2 rewrite)\n=============================\n\nThis iteration rewrites history into smaller logical steps (no\n“fix-on-fix”):\n\n 1. repo: teach info context and category keys\n 2. repo: add path keys to repo info\n 3. repo: add --path-format for info path output\n 4. repo: add structure max object size metrics\n 5. repo: add structure topology and path-depth metrics\n 6. repo: add aggregate structure totals to keyvalue output\n 7. t1900: cover repo info path keys and path-format\n 8. t1901: extend structure metric coverage and portability\n 9. docs: describe repo info path keys and structure metrics\n\nAll commits are signed off using real-name identity.\n\n----------------------------------------------------------------------------\n\n\nChanges since v1\n================\n\n * Rewrote series into smaller logical commits.\n * Folded fixes into proper history (no trailing fix-up patches).\n * Unified author/sign-off identity to real name.\n * Addressed portability concerns raised in review:\n   * hash-algorithm-sensitive expectations are handled robustly,\n   * BSD/macOS wc whitespace behavior is handled in tests.\n * Improved test robustness around keyvalue/nul expectations.\n * Expanded docs to explicitly describe new keys/metrics and behavior.\n * Revalidated in Docker with focused and full test runs.\n\n----------------------------------------------------------------------------\n\n\nValidation\n==========\n\nFocused:\n\n * t1900-repo.sh\n * GIT_TEST_DEFAULT_REF_FORMAT=reftable t/t1901-repo-structure.sh\n\nFull:\n\n * make -C t -j4 test in clean Docker environment\n * Result: failed 0\n\n----------------------------------------------------------------------------\n\nEslam reda ragheb (5):\n  repo: teach info context and category keys\n  repo: add path keys to repo info\n  repo: add --path-format for info path output\n  t1900: cover repo info path keys and path-format\n  docs: describe repo info path keys\n\n Documentation/git-repo.adoc |  60 ++++++++-\n builtin/repo.c              | 244 +++++++++++++++++++++++++++++++++---\n t/t1900-repo.sh             | 196 +++++++++++++++++++++++++++++\n 3 files changed, 480 insertions(+), 20 deletions(-)\n\n\nbase-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2208%2Feslam-reda-div%2Fgsoc-contribute-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2208/eslam-reda-div/gsoc-contribute-v3\nPull-Request: https://github.com/git/git/pull/2208\n\nRange-diff vs v2:\n\n  1:  99c8058298 =  1:  e9ea572e52 repo: teach info context and category keys\n  2:  6d5b9ff075 =  2:  348e361fe7 repo: add path keys to repo info\n  3:  5c438d045b =  3:  93585ad079 repo: add --path-format for info path output\n  4:  504d9cf7a0 <  -:  ---------- repo: add structure max object size metrics\n  5:  4b502925c9 <  -:  ---------- repo: add structure topology and path-depth metrics\n  6:  1751181950 <  -:  ---------- repo: add aggregate structure totals to keyvalue output\n  7:  fd18f28db0 =  4:  6ea263fee9 t1900: cover repo info path keys and path-format\n  8:  0525ed4cd9 <  -:  ---------- t1901: extend structure metric coverage and portability\n  9:  f17c0f03e5 !  5:  a2a6768042 docs: describe repo info path keys and structure metrics\n     @@ Metadata\n      Author: Eslam reda ragheb <eslam.reda.div@gmail.com>\n      \n       ## Commit message ##\n     -    docs: describe repo info path keys and structure metrics\n     +    docs: describe repo info path keys\n      \n     -    Document the newly added repo info capabilities, including\n     -    category keys and path-oriented key definitions.\n     -\n     -    Also describe --path-format behavior for path outputs.\n     -\n     -    Update git repo structure documentation to cover newly reported\n     -    maxima and aggregate keyvalue/nul fields.\n     -\n     -    This keeps command behavior and output keys fully specified for\n     -    users and scripts.\n     +    Document repo info category keys, path.* keys, and\n     +    --path-format behavior.\n      \n          Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n      \n     @@ Documentation/git-repo.adoc: supported:\n       `structure [--format=(table|keyvalue|nul) | -z]`::\n       \tRetrieve statistics about the current repository structure. The\n       \tfollowing kinds of information are reported:\n     -@@ Documentation/git-repo.adoc: supported:\n     - * Reachable object counts categorized by type\n     - * Total inflated size of reachable objects by type\n     - * Total disk size of reachable objects by type\n     -+* Largest inflated reachable object size by type\n     -+* Largest disk size of a reachable object by type\n     -+* Largest parent count among reachable commits\n     -+* Largest entry count among reachable trees\n     -+* Longest and deepest path among reachable blobs\n     -+* Deepest annotated tag chain\n     - +\n     - The output format can be chosen through the flag `--format`. Three formats are\n     - supported:\n     -@@ Documentation/git-repo.adoc: supported:\n     - `keyvalue`:::\n     - \tEach line of output contains a key-value pair for a repository stat.\n     - \tThe '=' character is used to delimit between the key and the value.\n     -+\tBoth aggregate metrics and per-type metrics are included.\n     - \tValues containing \"unusual\" characters are quoted as explained for the\n     - \tconfiguration variable `core.quotePath` (see linkgit:git-config[1]).\n     - \n      @@ Documentation/git-repo.adoc: supported:\n       \n       INFO KEYS\n\n-- \ngitgitgadget\n"},{"id":"536875","messageId":"e9ea572e52efe8e406c56b8690bb798aeef956e1.1771875812.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v3.git.git.1771875812.gitgitgadget@gmail.com","subject":"[PATCH v3 1/5] repo: teach info context and category keys","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-23T19:43:28Z","receivedAt":"2026-02-23T19:43:37Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nIntroduce an explicit repo_info context for the repo info codepath\nand thread it through value lookups and field printing.\n\nThis removes direct coupling from these helpers to ad-hoc\nrepository globals and makes key retrieval logic easier to extend\nsafely.\n\nAlso teach git repo info to accept category names (for example,\nlayout) and expand them to matching key.* entries in request\norder.\n\nThis improves script ergonomics while preserving existing behavior\nfor explicit keys and clear errors for unknown names.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 70 +++++++++++++++++++++++++++++++++++++++-----------\n 1 file changed, 55 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 0ea045abc1..e34914a9a7 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -22,7 +22,12 @@ static const char *const repo_usage[] = {\n \tNULL\n };\n \n-typedef int get_value_fn(struct repository *repo, struct strbuf *buf);\n+struct repo_info {\n+\tstruct repository *repo;\n+\tconst char *prefix;\n+};\n+\n+typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);\n \n enum output_format {\n \tFORMAT_TABLE,\n@@ -35,27 +40,30 @@ struct field {\n \tget_value_fn *get_value;\n };\n \n-static int get_layout_bare(struct repository *repo UNUSED, struct strbuf *buf)\n+static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)\n {\n \tstrbuf_addstr(buf, is_bare_repository() ? \"true\" : \"false\");\n \treturn 0;\n }\n \n-static int get_layout_shallow(struct repository *repo, struct strbuf *buf)\n+static int get_layout_shallow(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf,\n \t\t      is_repository_shallow(repo) ? \"true\" : \"false\");\n \treturn 0;\n }\n \n-static int get_object_format(struct repository *repo, struct strbuf *buf)\n+static int get_object_format(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf, repo->hash_algo->name);\n \treturn 0;\n }\n \n-static int get_references_format(struct repository *repo, struct strbuf *buf)\n+static int get_references_format(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf,\n \t\t      ref_storage_format_to_name(repo->ref_storage_format));\n \treturn 0;\n@@ -87,6 +95,33 @@ static get_value_fn *get_value_fn_for_key(const char *key)\n \treturn found ? found->get_value : NULL;\n }\n \n+static void print_field(enum output_format format, const char *key,\n+\t\t\tconst char *value);\n+\n+static int print_category_fields(const char *category,\n+\t\t\t\t struct repo_info *info,\n+\t\t\t\t enum output_format format,\n+\t\t\t\t struct strbuf *valbuf)\n+{\n+\tint found = 0;\n+\tsize_t category_len = strlen(category);\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {\n+\t\tconst struct field *field = &repo_info_fields[i];\n+\n+\t\tif (!starts_with(field->key, category) ||\n+\t\t    field->key[category_len] != '.')\n+\t\t\tcontinue;\n+\n+\t\tstrbuf_reset(valbuf);\n+\t\tfield->get_value(info, valbuf);\n+\t\tprint_field(format, field->key, valbuf->buf);\n+\t\tfound = 1;\n+\t}\n+\n+\treturn found;\n+}\n+\n static void print_field(enum output_format format, const char *key,\n \t\t\tconst char *value)\n {\n@@ -105,7 +140,7 @@ static void print_field(enum output_format format, const char *key,\n }\n \n static int print_fields(int argc, const char **argv,\n-\t\t\tstruct repository *repo,\n+\t\t\tstruct repo_info *info,\n \t\t\tenum output_format format)\n {\n \tint ret = 0;\n@@ -117,21 +152,22 @@ static int print_fields(int argc, const char **argv,\n \n \t\tget_value = get_value_fn_for_key(key);\n \n-\t\tif (!get_value) {\n-\t\t\tret = error(_(\"key '%s' not found\"), key);\n+\t\tif (get_value) {\n+\t\t\tstrbuf_reset(&valbuf);\n+\t\t\tget_value(info, &valbuf);\n+\t\t\tprint_field(format, key, valbuf.buf);\n \t\t\tcontinue;\n \t\t}\n \n-\t\tstrbuf_reset(&valbuf);\n-\t\tget_value(repo, &valbuf);\n-\t\tprint_field(format, key, valbuf.buf);\n+\t\tif (!print_category_fields(key, info, format, &valbuf))\n+\t\t\tret = error(_(\"key '%s' not found\"), key);\n \t}\n \n \tstrbuf_release(&valbuf);\n \treturn ret;\n }\n \n-static int print_all_fields(struct repository *repo,\n+static int print_all_fields(struct repo_info *info,\n \t\t\t    enum output_format format)\n {\n \tstruct strbuf valbuf = STRBUF_INIT;\n@@ -140,7 +176,7 @@ static int print_all_fields(struct repository *repo,\n \t\tconst struct field *field = &repo_info_fields[i];\n \n \t\tstrbuf_reset(&valbuf);\n-\t\tfield->get_value(repo, &valbuf);\n+\t\tfield->get_value(info, &valbuf);\n \t\tprint_field(format, field->key, valbuf.buf);\n \t}\n \n@@ -171,6 +207,10 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\t\t struct repository *repo)\n {\n \tenum output_format format = FORMAT_KEYVALUE;\n+\tstruct repo_info info = {\n+\t\t.repo = repo,\n+\t\t.prefix = prefix,\n+\t};\n \tint all_keys = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n@@ -192,9 +232,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"--all and <key> cannot be used together\"));\n \n \tif (all_keys)\n-\t\treturn print_all_fields(repo, format);\n+\t\treturn print_all_fields(&info, format);\n \telse\n-\t\treturn print_fields(argc, argv, repo, format);\n+\t\treturn print_fields(argc, argv, &info, format);\n }\n \n struct ref_stats {\n-- \ngitgitgadget\n\n"},{"id":"536876","messageId":"348e361fe77946b9fd3b751591d9475a95499c48.1771875812.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v3.git.git.1771875812.gitgitgadget@gmail.com","subject":"[PATCH v3 2/5] repo: add path keys to repo info","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-23T19:43:29Z","receivedAt":"2026-02-23T19:43:39Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nAdd a path category to git repo info with key-value pairs that\nmirror repository paths users commonly retrieve via rev-parse and\ngit-path lookups.\n\nThis makes scripting against repo metadata more direct and avoids\nshelling out to multiple commands for related paths.\n\nThe new keys are introduced as explicit path.* entries in\nrepo_info_fields and are resolved through dedicated helpers.\n\nThis keeps lookup behavior predictable and makes future path\nadditions straightforward.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 137 +++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 137 insertions(+)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex e34914a9a7..35e1eaf7d7 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -1,10 +1,12 @@\n #define USE_THE_REPOSITORY_VARIABLE\n \n #include \"builtin.h\"\n+#include \"abspath.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n #include \"odb.h\"\n #include \"parse-options.h\"\n+#include \"path.h\"\n #include \"path-walk.h\"\n #include \"progress.h\"\n #include \"quote.h\"\n@@ -14,6 +16,7 @@\n #include \"strbuf.h\"\n #include \"string-list.h\"\n #include \"shallow.h\"\n+#include \"submodule.h\"\n #include \"utf8.h\"\n \n static const char *const repo_usage[] = {\n@@ -40,6 +43,13 @@ struct field {\n \tget_value_fn *get_value;\n };\n \n+static void repo_info_add_path(struct repo_info *info,\n+\t\t\t      struct strbuf *buf,\n+\t\t\t      const char *path)\n+{\n+\tstrbuf_add_absolute_path(buf, path);\n+}\n+\n static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)\n {\n \tstrbuf_addstr(buf, is_bare_repository() ? \"true\" : \"false\");\n@@ -61,6 +71,119 @@ static int get_object_format(struct repo_info *info, struct strbuf *buf)\n \treturn 0;\n }\n \n+static int get_path_common_dir(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_common_dir(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_config_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"config\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_git_dir(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)\n+{\n+\tif (info->prefix)\n+\t\tstrbuf_addstr(buf, info->prefix);\n+\treturn 0;\n+}\n+\n+static int get_path_grafts_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_graft_file(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_hooks_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"hooks\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_index_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_index_file(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"logs\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_object_directory(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"packed-refs\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"refs\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"shallow\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_superproject_working_tree(struct repo_info *info,\n+\t\t\t\t\t     struct strbuf *buf)\n+{\n+\tstruct strbuf superproject = STRBUF_INIT;\n+\n+\tif (get_superproject_working_tree(&superproject))\n+\t\trepo_info_add_path(info, buf, superproject.buf);\n+\n+\tstrbuf_release(&superproject);\n+\treturn 0;\n+}\n+\n+static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)\n+{\n+\tconst char *work_tree = repo_get_work_tree(info->repo);\n+\n+\tif (work_tree)\n+\t\trepo_info_add_path(info, buf, work_tree);\n+\n+\treturn 0;\n+}\n+\n static int get_references_format(struct repo_info *info, struct strbuf *buf)\n {\n \tstruct repository *repo = info->repo;\n@@ -74,6 +197,20 @@ static const struct field repo_info_fields[] = {\n \t{ \"layout.bare\", get_layout_bare },\n \t{ \"layout.shallow\", get_layout_shallow },\n \t{ \"object.format\", get_object_format },\n+\t{ \"path.common-dir\", get_path_common_dir },\n+\t{ \"path.config-file\", get_path_config_file },\n+\t{ \"path.git-dir\", get_path_git_dir },\n+\t{ \"path.git-prefix\", get_path_git_prefix },\n+\t{ \"path.grafts-file\", get_path_grafts_file },\n+\t{ \"path.hooks-directory\", get_path_hooks_directory },\n+\t{ \"path.index-file\", get_path_index_file },\n+\t{ \"path.logs-directory\", get_path_logs_directory },\n+\t{ \"path.objects-directory\", get_path_objects_directory },\n+\t{ \"path.packed-refs-file\", get_path_packed_refs_file },\n+\t{ \"path.refs-directory\", get_path_refs_directory },\n+\t{ \"path.shallow-file\", get_path_shallow_file },\n+\t{ \"path.superproject-working-tree\", get_path_superproject_working_tree },\n+\t{ \"path.toplevel\", get_path_toplevel },\n \t{ \"references.format\", get_references_format },\n };\n \n-- \ngitgitgadget\n\n"},{"id":"536877","messageId":"93585ad0796245e57a3305598d13fdb3c72d7fdb.1771875812.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v3.git.git.1771875812.gitgitgadget@gmail.com","subject":"[PATCH v3 3/5] repo: add --path-format for info path output","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-23T19:43:30Z","receivedAt":"2026-02-23T19:43:40Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nTeach git repo info to accept --path-format=(absolute|relative)\nso scripts can request stable path style explicitly.\n\nThis aligns path.* output behavior with existing rev-parse usage\npatterns and reduces ad-hoc path conversion in callers.\n\nThe option is wired through repo_info context and used by\nrepo_info_add_path(), so path formatting remains centralized and\nconsistent across all path.* keys.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 37 ++++++++++++++++++++++++++++++++++++-\n 1 file changed, 36 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 35e1eaf7d7..e5078e5459 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -20,14 +20,20 @@\n #include \"utf8.h\"\n \n static const char *const repo_usage[] = {\n-\t\"git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]\",\n+\t\"git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\",\n \t\"git repo structure [--format=(table|keyvalue|nul) | -z]\",\n \tNULL\n };\n \n+enum path_format {\n+\tPATH_FORMAT_ABSOLUTE,\n+\tPATH_FORMAT_RELATIVE,\n+};\n+\n struct repo_info {\n \tstruct repository *repo;\n \tconst char *prefix;\n+\tenum path_format path_format;\n };\n \n typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);\n@@ -47,6 +53,16 @@ static void repo_info_add_path(struct repo_info *info,\n \t\t\t      struct strbuf *buf,\n \t\t\t      const char *path)\n {\n+\tif (info->path_format == PATH_FORMAT_RELATIVE) {\n+\t\tchar *cwd = xgetcwd();\n+\t\tstruct strbuf rel_path = STRBUF_INIT;\n+\n+\t\tstrbuf_addstr(buf, relative_path(path, cwd, &rel_path));\n+\t\tstrbuf_release(&rel_path);\n+\t\tfree(cwd);\n+\t\treturn;\n+\t}\n+\n \tstrbuf_add_absolute_path(buf, path);\n }\n \n@@ -340,6 +356,21 @@ static int parse_format_cb(const struct option *opt,\n \treturn 0;\n }\n \n+static int parse_path_format_cb(const struct option *opt,\n+\t\t\t\tconst char *arg, int unset UNUSED)\n+{\n+\tenum path_format *path_format = opt->value;\n+\n+\tif (!strcmp(arg, \"absolute\"))\n+\t\t*path_format = PATH_FORMAT_ABSOLUTE;\n+\telse if (!strcmp(arg, \"relative\"))\n+\t\t*path_format = PATH_FORMAT_RELATIVE;\n+\telse\n+\t\tdie(_(\"invalid path format '%s'\"), arg);\n+\n+\treturn 0;\n+}\n+\n static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\t\t struct repository *repo)\n {\n@@ -347,6 +378,7 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \tstruct repo_info info = {\n \t\t.repo = repo,\n \t\t.prefix = prefix,\n+\t\t.path_format = PATH_FORMAT_ABSOLUTE,\n \t};\n \tint all_keys = 0;\n \tstruct option options[] = {\n@@ -357,6 +389,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\t\t       N_(\"synonym for --format=nul\"),\n \t\t\t       PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n \t\t\t       parse_format_cb),\n+\t\tOPT_CALLBACK_F(0, \"path-format\", &info.path_format,\n+\t\t\t       N_(\"format\"), N_(\"path output format\"),\n+\t\t\t       PARSE_OPT_NONEG, parse_path_format_cb),\n \t\tOPT_BOOL(0, \"all\", &all_keys, N_(\"print all keys/values\")),\n \t\tOPT_END()\n \t};\n-- \ngitgitgadget\n\n"},{"id":"536878","messageId":"6ea263fee9fcf670ca2f9f26fa86a76ce0701c9e.1771875812.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v3.git.git.1771875812.gitgitgadget@gmail.com","subject":"[PATCH v3 4/5] t1900: cover repo info path keys and path-format","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-23T19:43:31Z","receivedAt":"2026-02-23T19:43:41Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nExtend t1900 to validate category-key expansion, path.* key\nbehavior, and --path-format handling for git repo info.\n\nThe tests compare repo info output to equivalent rev-parse values.\n\nThis ensures behavior remains aligned with existing plumbing\nsemantics.\n\nAlso keep mixed key/category ordering coverage so callers can rely\non deterministic output order when combining explicit keys with\ncategory requests.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n t/t1900-repo.sh | 196 ++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 196 insertions(+)\n\ndiff --git a/t/t1900-repo.sh b/t/t1900-repo.sh\nindex 51d55f11a5..dcacf84cc3 100755\n--- a/t/t1900-repo.sh\n+++ b/t/t1900-repo.sh\n@@ -10,9 +10,40 @@ REPO_INFO_KEYS='\n \tlayout.bare\n \tlayout.shallow\n \tobject.format\n+\tpath.common-dir\n+\tpath.config-file\n+\tpath.git-dir\n+\tpath.git-prefix\n+\tpath.grafts-file\n+\tpath.hooks-directory\n+\tpath.index-file\n+\tpath.logs-directory\n+\tpath.objects-directory\n+\tpath.packed-refs-file\n+\tpath.refs-directory\n+\tpath.shallow-file\n+\tpath.superproject-working-tree\n+\tpath.toplevel\n \treferences.format\n '\n \n+REPO_INFO_PATH_KEYS='\n+\tpath.common-dir\n+\tpath.config-file\n+\tpath.git-dir\n+\tpath.git-prefix\n+\tpath.grafts-file\n+\tpath.hooks-directory\n+\tpath.index-file\n+\tpath.logs-directory\n+\tpath.objects-directory\n+\tpath.packed-refs-file\n+\tpath.refs-directory\n+\tpath.shallow-file\n+\tpath.superproject-working-tree\n+\tpath.toplevel\n+'\n+\n # Test whether a key-value pair is correctly returned\n #\n # Usage: test_repo_info <label> <init command> <repo_name> <key> <expected value>\n@@ -89,6 +120,171 @@ test_expect_success 'values returned in order requested' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'category key returns all matching keys' '\n+\tcat >expect <<-\\EOF &&\n+\tlayout.bare=false\n+\tlayout.shallow=false\n+\tEOF\n+\tgit init category-layout &&\n+\tgit -C category-layout repo info layout >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'mixed key/category requests preserve request order' '\n+\tcat >expect <<-EOF &&\n+\tobject.format=$(test_oid algo)\n+\tlayout.bare=false\n+\tlayout.shallow=false\n+\tEOF\n+\tgit init mixed-order &&\n+\tgit -C mixed-order repo info object.format layout >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.git-dir matches rev-parse --absolute-git-dir' '\n+\tgit init path-git-dir &&\n+\texpected_value=$(git -C path-git-dir rev-parse --absolute-git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-git-dir repo info path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.common-dir matches rev-parse --git-common-dir' '\n+\tgit init path-common-dir &&\n+\texpected_value=$(git -C path-common-dir rev-parse --path-format=absolute --git-common-dir) &&\n+\techo \"path.common-dir=$expected_value\" >expect &&\n+\tgit -C path-common-dir repo info path.common-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.toplevel matches rev-parse --show-toplevel' '\n+\tgit init path-toplevel &&\n+\texpected_value=$(git -C path-toplevel rev-parse --show-toplevel) &&\n+\techo \"path.toplevel=$expected_value\" >expect &&\n+\tgit -C path-toplevel repo info path.toplevel >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.toplevel is empty in bare repository' '\n+\tgit init --bare bare-path-toplevel &&\n+\techo \"path.toplevel=\" >expect &&\n+\tgit -C bare-path-toplevel repo info path.toplevel >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '\n+\tgit init path-prefix &&\n+\tmkdir -p path-prefix/a/b &&\n+\texpected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&\n+\techo \"path.git-prefix=$expected_value\" >expect &&\n+\tgit -C path-prefix/a/b repo info path.git-prefix >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git-path style keys match rev-parse --git-path' '\n+\tgit init path-git-path &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path info/grafts) &&\n+\techo \"path.grafts-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.grafts-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path index) &&\n+\techo \"path.index-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.index-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path objects) &&\n+\techo \"path.objects-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.objects-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path hooks) &&\n+\techo \"path.hooks-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.hooks-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path config) &&\n+\techo \"path.config-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.config-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&\n+\techo \"path.logs-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.logs-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&\n+\techo \"path.packed-refs-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.packed-refs-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&\n+\techo \"path.refs-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.refs-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&\n+\techo \"path.shallow-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.shallow-file >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.superproject-working-tree is empty when not a submodule' '\n+\tgit init path-superproject &&\n+\techo \"path.superproject-working-tree=\" >expect &&\n+\tgit -C path-superproject repo info path.superproject-working-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.superproject-working-tree matches rev-parse in submodule' '\n+\tgit init path-superproject-origin &&\n+\techo x >path-superproject-origin/x &&\n+\tgit -C path-superproject-origin add x &&\n+\tgit -C path-superproject-origin commit -m x &&\n+\n+\tgit init path-superproject-parent &&\n+\tgit -C path-superproject-parent -c protocol.file.allow=always submodule add ../path-superproject-origin sm &&\n+\n+\texpected_value=$(git -C path-superproject-parent/sm rev-parse --show-superproject-working-tree) &&\n+\techo \"path.superproject-working-tree=$expected_value\" >expect &&\n+\tgit -C path-superproject-parent/sm repo info path.superproject-working-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path category returns all path keys' '\n+\tgit init path-category &&\n+\t>expect &&\n+\tfor key in $REPO_INFO_PATH_KEYS\n+\tdo\n+\t\tgit -C path-category repo info \"$key\" >>expect || return 1\n+\tdone &&\n+\tgit -C path-category repo info path >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path-format=relative matches rev-parse for git-dir' '\n+\tgit init path-format-relative &&\n+\texpected_value=$(git -C path-format-relative rev-parse --path-format=relative --git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-format-relative repo info --path-format=relative path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git repo info uses the last requested path format' '\n+\tgit init path-format-last &&\n+\texpected_value=$(git -C path-format-last rev-parse --path-format=relative --git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-format-last repo info --path-format=absolute --path-format=relative path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git-repo-info aborts when requesting an invalid path format' '\n+\techo \"fatal: invalid path format ${SQ}foo${SQ}\" >expect &&\n+\ttest_must_fail git repo info --path-format=foo path.git-dir 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'git-repo-info fails if an invalid key is requested' '\n \techo \"error: key ${SQ}foo${SQ} not found\" >expect &&\n \ttest_must_fail git repo info foo 2>actual &&\n-- \ngitgitgadget\n\n"},{"id":"536879","messageId":"a2a6768042239b62faff71b0bcf1cc3144f6bc7f.1771875812.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v3.git.git.1771875812.gitgitgadget@gmail.com","subject":"[PATCH v3 5/5] docs: describe repo info path keys","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-23T19:43:32Z","receivedAt":"2026-02-23T19:43:43Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nDocument repo info category keys, path.* keys, and\n--path-format behavior.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n Documentation/git-repo.adoc | 60 ++++++++++++++++++++++++++++++++++---\n 1 file changed, 56 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 7d70270dfa..6e99951343 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -8,7 +8,7 @@ git-repo - Retrieve information about the repository\n SYNOPSIS\n --------\n [synopsis]\n-git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]\n+git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\n git repo structure [--format=(table|keyvalue|nul) | -z]\n \n DESCRIPTION\n@@ -44,6 +44,11 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n+`--path-format=(absolute|relative)`:::\n+\tControls formatting for keys in the `path` category. The default is\n+\t`absolute`. This option may be specified multiple times; the last one\n+\tspecified takes effect.\n+\n `structure [--format=(table|keyvalue|nul) | -z]`::\n \tRetrieve statistics about the current repository structure. The\n \tfollowing kinds of information are reported:\n@@ -78,9 +83,11 @@ supported:\n \n INFO KEYS\n ---------\n-In order to obtain a set of values from `git repo info`, you should provide\n-the keys that identify them. Here's a list of the available keys and the\n-values that they return:\n+In order to obtain values from `git repo info`, provide either individual keys\n+or category names. A category returns all keys within that category. For\n+example, `layout` returns both `layout.bare` and `layout.shallow`.\n+\n+Here's a list of the available keys and the values that they return:\n \n `layout.bare`::\n \t`true` if this is a bare repository, otherwise `false`.\n@@ -91,6 +98,51 @@ values that they return:\n `object.format`::\n \tThe object format (hash algorithm) used in the repository.\n \n+`path.common-dir`::\n+\tThe path to the common git directory.\n+\n+`path.config-file`::\n+\tThe path to the `config` file in the git directory.\n+\n+`path.git-dir`::\n+\tThe path to the git directory.\n+\n+`path.git-prefix`::\n+\tThe path of the current working directory relative to the top-level\n+\tdirectory.\n+\n+`path.grafts-file`::\n+\tThe path to the `info/grafts` file.\n+\n+`path.hooks-directory`::\n+\tThe path to the `hooks` directory.\n+\n+`path.index-file`::\n+\tThe path to the index file.\n+\n+`path.logs-directory`::\n+\tThe path to the `logs` directory.\n+\n+`path.objects-directory`::\n+\tThe path to the objects directory.\n+\n+`path.packed-refs-file`::\n+\tThe path to the `packed-refs` file.\n+\n+`path.refs-directory`::\n+\tThe path to the `refs` directory.\n+\n+`path.shallow-file`::\n+\tThe path to the `shallow` file.\n+\n+`path.superproject-working-tree`::\n+\tThe path to the superproject's working tree root, or an empty string\n+\twhen the repository is not used as a submodule.\n+\n+`path.toplevel`::\n+\tThe path to the top-level working tree directory, or an empty string\n+\tfor bare repositories.\n+\n `references.format`::\n \tThe reference storage format. The valid values are:\n +\n-- \ngitgitgadget\n"},{"id":"537236","messageId":"pull.2208.v4.git.git.1772140487.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v3.git.git.1771875812.gitgitgadget@gmail.com","subject":"[PATCH v4 00/10] repo info: add category/path keys and --path-format","fromName":"eslam reda via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T21:14:37Z","receivedAt":"2026-02-26T21:14:50Z","isPatch":true,"sender":{"key":"name:eslam reda","avatar":null},"body":"\nThis series now focuses only on git repo info improvements.\n===========================================================\n\nIt introduces category-aware key requests, adds path-oriented keys (path.*),\nand adds --path-format=(absolute|relative) so scripts can request stable\npath rendering behavior.\n\n\nWhat this PR does\n=================\n\nFor git repo info, this series:\n\n * introduces explicit info-context plumbing in the codepath,\n * adds category-key expansion (for example, layout expands to layout.*),\n * adds path-oriented keys (path.*) for common repository locations,\n * adds --path-format=(absolute|relative) to control path output style.\n\nTests and documentation are updated accordingly.\n\n\nWhat this PR does NOT do\n========================\n\n * No git repo structure feature changes.\n * No t1901 structure test changes.\n * No structure metrics/docs additions.\n\n\nWhy this change\n===============\n\n * Makes git repo info more script-friendly by reducing the need for\n   multiple plumbing calls.\n * Improves output ergonomics through category requests and explicit path\n   formatting.\n * Keeps this series narrowly scoped and non-overlapping with in-flight repo\n   structure work.\n\n\nCommit structure\n================\n\n * repo: teach info context and category keys\n * repo: add path keys to repo info\n * repo: add --path-format for info path output\n * t1900: cover repo info path keys and path-format\n * docs: describe repo info path keys\n\nAll commits are signed off with the same real-name identity.\n\n\nChanges since previous revision\n===============================\n\n * Dropped all repo structure code, tests, and docs from this PR.\n * Kept only the repo info subset and matching t1900/documentation updates.\n * Preserved split, review-friendly commit structure.\n\n\nValidation\n==========\n\nFocused:\n\n * make -C t test T=t1900-repo.sh (Linux container): passed.\n\nFull:\n\n * make test in Linux Docker environment: failed 0 (with expected\n   prereq-based broken/skipped categories).\n\nEslam reda ragheb (10):\n  repo: teach info context and category keys\n  repo: add path keys to repo info\n  repo: add --path-format for info path output\n  repo: add structure max object size metrics\n  repo: add structure topology and path-depth metrics\n  repo: add aggregate structure totals to keyvalue output\n  t1900: cover repo info path keys and path-format\n  t1901: extend structure metric coverage and portability\n  docs: describe repo info path keys and structure metrics\n  repo: reduce repetition in structure keyvalue output\n\n Documentation/git-repo.adoc |  67 ++++-\n builtin/repo.c              | 584 ++++++++++++++++++++++++++++++++----\n t/t1900-repo.sh             | 196 ++++++++++++\n t/t1901-repo-structure.sh   | 250 +++++++++++----\n 4 files changed, 977 insertions(+), 120 deletions(-)\n\n\nbase-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2208%2Feslam-reda-div%2Fgsoc-contribute-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2208/eslam-reda-div/gsoc-contribute-v4\nPull-Request: https://github.com/git/git/pull/2208\n\nRange-diff vs v3:\n\n  1:  e9ea572e52 =  1:  99c8058298 repo: teach info context and category keys\n  2:  348e361fe7 =  2:  6d5b9ff075 repo: add path keys to repo info\n  3:  93585ad079 =  3:  5c438d045b repo: add --path-format for info path output\n  -:  ---------- >  4:  504d9cf7a0 repo: add structure max object size metrics\n  -:  ---------- >  5:  4b502925c9 repo: add structure topology and path-depth metrics\n  -:  ---------- >  6:  1751181950 repo: add aggregate structure totals to keyvalue output\n  4:  6ea263fee9 =  7:  fd18f28db0 t1900: cover repo info path keys and path-format\n  -:  ---------- >  8:  0525ed4cd9 t1901: extend structure metric coverage and portability\n  5:  a2a6768042 !  9:  f17c0f03e5 docs: describe repo info path keys\n     @@ Metadata\n      Author: Eslam reda ragheb <eslam.reda.div@gmail.com>\n      \n       ## Commit message ##\n     -    docs: describe repo info path keys\n     +    docs: describe repo info path keys and structure metrics\n      \n     -    Document repo info category keys, path.* keys, and\n     -    --path-format behavior.\n     +    Document the newly added repo info capabilities, including\n     +    category keys and path-oriented key definitions.\n     +\n     +    Also describe --path-format behavior for path outputs.\n     +\n     +    Update git repo structure documentation to cover newly reported\n     +    maxima and aggregate keyvalue/nul fields.\n     +\n     +    This keeps command behavior and output keys fully specified for\n     +    users and scripts.\n      \n          Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n      \n     @@ Documentation/git-repo.adoc: supported:\n       `structure [--format=(table|keyvalue|nul) | -z]`::\n       \tRetrieve statistics about the current repository structure. The\n       \tfollowing kinds of information are reported:\n     +@@ Documentation/git-repo.adoc: supported:\n     + * Reachable object counts categorized by type\n     + * Total inflated size of reachable objects by type\n     + * Total disk size of reachable objects by type\n     ++* Largest inflated reachable object size by type\n     ++* Largest disk size of a reachable object by type\n     ++* Largest parent count among reachable commits\n     ++* Largest entry count among reachable trees\n     ++* Longest and deepest path among reachable blobs\n     ++* Deepest annotated tag chain\n     + +\n     + The output format can be chosen through the flag `--format`. Three formats are\n     + supported:\n     +@@ Documentation/git-repo.adoc: supported:\n     + `keyvalue`:::\n     + \tEach line of output contains a key-value pair for a repository stat.\n     + \tThe '=' character is used to delimit between the key and the value.\n     ++\tBoth aggregate metrics and per-type metrics are included.\n     + \tValues containing \"unusual\" characters are quoted as explained for the\n     + \tconfiguration variable `core.quotePath` (see linkgit:git-config[1]).\n     + \n      @@ Documentation/git-repo.adoc: supported:\n       \n       INFO KEYS\n  -:  ---------- > 10:  1bc100d6ca repo: reduce repetition in structure keyvalue output\n\n-- \ngitgitgadget\n"},{"id":"537237","messageId":"99c8058298d403508ab2a23ed06c36b8707989d0.1772140487.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v4.git.git.1772140487.gitgitgadget@gmail.com","subject":"[PATCH v4 01/10] repo: teach info context and category keys","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T21:14:38Z","receivedAt":"2026-02-26T21:14:51Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nIntroduce an explicit repo_info context for the repo info codepath\nand thread it through value lookups and field printing.\n\nThis removes direct coupling from these helpers to ad-hoc\nrepository globals and makes key retrieval logic easier to extend\nsafely.\n\nAlso teach git repo info to accept category names (for example,\nlayout) and expand them to matching key.* entries in request\norder.\n\nThis improves script ergonomics while preserving existing behavior\nfor explicit keys and clear errors for unknown names.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 70 +++++++++++++++++++++++++++++++++++++++-----------\n 1 file changed, 55 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 0ea045abc1..e34914a9a7 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -22,7 +22,12 @@ static const char *const repo_usage[] = {\n \tNULL\n };\n \n-typedef int get_value_fn(struct repository *repo, struct strbuf *buf);\n+struct repo_info {\n+\tstruct repository *repo;\n+\tconst char *prefix;\n+};\n+\n+typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);\n \n enum output_format {\n \tFORMAT_TABLE,\n@@ -35,27 +40,30 @@ struct field {\n \tget_value_fn *get_value;\n };\n \n-static int get_layout_bare(struct repository *repo UNUSED, struct strbuf *buf)\n+static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)\n {\n \tstrbuf_addstr(buf, is_bare_repository() ? \"true\" : \"false\");\n \treturn 0;\n }\n \n-static int get_layout_shallow(struct repository *repo, struct strbuf *buf)\n+static int get_layout_shallow(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf,\n \t\t      is_repository_shallow(repo) ? \"true\" : \"false\");\n \treturn 0;\n }\n \n-static int get_object_format(struct repository *repo, struct strbuf *buf)\n+static int get_object_format(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf, repo->hash_algo->name);\n \treturn 0;\n }\n \n-static int get_references_format(struct repository *repo, struct strbuf *buf)\n+static int get_references_format(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf,\n \t\t      ref_storage_format_to_name(repo->ref_storage_format));\n \treturn 0;\n@@ -87,6 +95,33 @@ static get_value_fn *get_value_fn_for_key(const char *key)\n \treturn found ? found->get_value : NULL;\n }\n \n+static void print_field(enum output_format format, const char *key,\n+\t\t\tconst char *value);\n+\n+static int print_category_fields(const char *category,\n+\t\t\t\t struct repo_info *info,\n+\t\t\t\t enum output_format format,\n+\t\t\t\t struct strbuf *valbuf)\n+{\n+\tint found = 0;\n+\tsize_t category_len = strlen(category);\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {\n+\t\tconst struct field *field = &repo_info_fields[i];\n+\n+\t\tif (!starts_with(field->key, category) ||\n+\t\t    field->key[category_len] != '.')\n+\t\t\tcontinue;\n+\n+\t\tstrbuf_reset(valbuf);\n+\t\tfield->get_value(info, valbuf);\n+\t\tprint_field(format, field->key, valbuf->buf);\n+\t\tfound = 1;\n+\t}\n+\n+\treturn found;\n+}\n+\n static void print_field(enum output_format format, const char *key,\n \t\t\tconst char *value)\n {\n@@ -105,7 +140,7 @@ static void print_field(enum output_format format, const char *key,\n }\n \n static int print_fields(int argc, const char **argv,\n-\t\t\tstruct repository *repo,\n+\t\t\tstruct repo_info *info,\n \t\t\tenum output_format format)\n {\n \tint ret = 0;\n@@ -117,21 +152,22 @@ static int print_fields(int argc, const char **argv,\n \n \t\tget_value = get_value_fn_for_key(key);\n \n-\t\tif (!get_value) {\n-\t\t\tret = error(_(\"key '%s' not found\"), key);\n+\t\tif (get_value) {\n+\t\t\tstrbuf_reset(&valbuf);\n+\t\t\tget_value(info, &valbuf);\n+\t\t\tprint_field(format, key, valbuf.buf);\n \t\t\tcontinue;\n \t\t}\n \n-\t\tstrbuf_reset(&valbuf);\n-\t\tget_value(repo, &valbuf);\n-\t\tprint_field(format, key, valbuf.buf);\n+\t\tif (!print_category_fields(key, info, format, &valbuf))\n+\t\t\tret = error(_(\"key '%s' not found\"), key);\n \t}\n \n \tstrbuf_release(&valbuf);\n \treturn ret;\n }\n \n-static int print_all_fields(struct repository *repo,\n+static int print_all_fields(struct repo_info *info,\n \t\t\t    enum output_format format)\n {\n \tstruct strbuf valbuf = STRBUF_INIT;\n@@ -140,7 +176,7 @@ static int print_all_fields(struct repository *repo,\n \t\tconst struct field *field = &repo_info_fields[i];\n \n \t\tstrbuf_reset(&valbuf);\n-\t\tfield->get_value(repo, &valbuf);\n+\t\tfield->get_value(info, &valbuf);\n \t\tprint_field(format, field->key, valbuf.buf);\n \t}\n \n@@ -171,6 +207,10 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\t\t struct repository *repo)\n {\n \tenum output_format format = FORMAT_KEYVALUE;\n+\tstruct repo_info info = {\n+\t\t.repo = repo,\n+\t\t.prefix = prefix,\n+\t};\n \tint all_keys = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n@@ -192,9 +232,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"--all and <key> cannot be used together\"));\n \n \tif (all_keys)\n-\t\treturn print_all_fields(repo, format);\n+\t\treturn print_all_fields(&info, format);\n \telse\n-\t\treturn print_fields(argc, argv, repo, format);\n+\t\treturn print_fields(argc, argv, &info, format);\n }\n \n struct ref_stats {\n-- \ngitgitgadget\n\n"},{"id":"537238","messageId":"6d5b9ff07566e1cc28a672cf1f47988e9c8c45da.1772140487.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v4.git.git.1772140487.gitgitgadget@gmail.com","subject":"[PATCH v4 02/10] repo: add path keys to repo info","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T21:14:39Z","receivedAt":"2026-02-26T21:14:52Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nAdd a path category to git repo info with key-value pairs that\nmirror repository paths users commonly retrieve via rev-parse and\ngit-path lookups.\n\nThis makes scripting against repo metadata more direct and avoids\nshelling out to multiple commands for related paths.\n\nThe new keys are introduced as explicit path.* entries in\nrepo_info_fields and are resolved through dedicated helpers.\n\nThis keeps lookup behavior predictable and makes future path\nadditions straightforward.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 137 +++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 137 insertions(+)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex e34914a9a7..35e1eaf7d7 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -1,10 +1,12 @@\n #define USE_THE_REPOSITORY_VARIABLE\n \n #include \"builtin.h\"\n+#include \"abspath.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n #include \"odb.h\"\n #include \"parse-options.h\"\n+#include \"path.h\"\n #include \"path-walk.h\"\n #include \"progress.h\"\n #include \"quote.h\"\n@@ -14,6 +16,7 @@\n #include \"strbuf.h\"\n #include \"string-list.h\"\n #include \"shallow.h\"\n+#include \"submodule.h\"\n #include \"utf8.h\"\n \n static const char *const repo_usage[] = {\n@@ -40,6 +43,13 @@ struct field {\n \tget_value_fn *get_value;\n };\n \n+static void repo_info_add_path(struct repo_info *info,\n+\t\t\t      struct strbuf *buf,\n+\t\t\t      const char *path)\n+{\n+\tstrbuf_add_absolute_path(buf, path);\n+}\n+\n static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)\n {\n \tstrbuf_addstr(buf, is_bare_repository() ? \"true\" : \"false\");\n@@ -61,6 +71,119 @@ static int get_object_format(struct repo_info *info, struct strbuf *buf)\n \treturn 0;\n }\n \n+static int get_path_common_dir(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_common_dir(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_config_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"config\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_git_dir(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)\n+{\n+\tif (info->prefix)\n+\t\tstrbuf_addstr(buf, info->prefix);\n+\treturn 0;\n+}\n+\n+static int get_path_grafts_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_graft_file(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_hooks_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"hooks\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_index_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_index_file(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"logs\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_object_directory(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"packed-refs\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"refs\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"shallow\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_superproject_working_tree(struct repo_info *info,\n+\t\t\t\t\t     struct strbuf *buf)\n+{\n+\tstruct strbuf superproject = STRBUF_INIT;\n+\n+\tif (get_superproject_working_tree(&superproject))\n+\t\trepo_info_add_path(info, buf, superproject.buf);\n+\n+\tstrbuf_release(&superproject);\n+\treturn 0;\n+}\n+\n+static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)\n+{\n+\tconst char *work_tree = repo_get_work_tree(info->repo);\n+\n+\tif (work_tree)\n+\t\trepo_info_add_path(info, buf, work_tree);\n+\n+\treturn 0;\n+}\n+\n static int get_references_format(struct repo_info *info, struct strbuf *buf)\n {\n \tstruct repository *repo = info->repo;\n@@ -74,6 +197,20 @@ static const struct field repo_info_fields[] = {\n \t{ \"layout.bare\", get_layout_bare },\n \t{ \"layout.shallow\", get_layout_shallow },\n \t{ \"object.format\", get_object_format },\n+\t{ \"path.common-dir\", get_path_common_dir },\n+\t{ \"path.config-file\", get_path_config_file },\n+\t{ \"path.git-dir\", get_path_git_dir },\n+\t{ \"path.git-prefix\", get_path_git_prefix },\n+\t{ \"path.grafts-file\", get_path_grafts_file },\n+\t{ \"path.hooks-directory\", get_path_hooks_directory },\n+\t{ \"path.index-file\", get_path_index_file },\n+\t{ \"path.logs-directory\", get_path_logs_directory },\n+\t{ \"path.objects-directory\", get_path_objects_directory },\n+\t{ \"path.packed-refs-file\", get_path_packed_refs_file },\n+\t{ \"path.refs-directory\", get_path_refs_directory },\n+\t{ \"path.shallow-file\", get_path_shallow_file },\n+\t{ \"path.superproject-working-tree\", get_path_superproject_working_tree },\n+\t{ \"path.toplevel\", get_path_toplevel },\n \t{ \"references.format\", get_references_format },\n };\n \n-- \ngitgitgadget\n\n"},{"id":"537239","messageId":"5c438d045be1becc10b687a772055df5745372ae.1772140487.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v4.git.git.1772140487.gitgitgadget@gmail.com","subject":"[PATCH v4 03/10] repo: add --path-format for info path output","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T21:14:40Z","receivedAt":"2026-02-26T21:14:53Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nTeach git repo info to accept --path-format=(absolute|relative)\nso scripts can request stable path style explicitly.\n\nThis aligns path.* output behavior with existing rev-parse usage\npatterns and reduces ad-hoc path conversion in callers.\n\nThe option is wired through repo_info context and used by\nrepo_info_add_path(), so path formatting remains centralized and\nconsistent across all path.* keys.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 37 ++++++++++++++++++++++++++++++++++++-\n 1 file changed, 36 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 35e1eaf7d7..e5078e5459 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -20,14 +20,20 @@\n #include \"utf8.h\"\n \n static const char *const repo_usage[] = {\n-\t\"git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]\",\n+\t\"git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\",\n \t\"git repo structure [--format=(table|keyvalue|nul) | -z]\",\n \tNULL\n };\n \n+enum path_format {\n+\tPATH_FORMAT_ABSOLUTE,\n+\tPATH_FORMAT_RELATIVE,\n+};\n+\n struct repo_info {\n \tstruct repository *repo;\n \tconst char *prefix;\n+\tenum path_format path_format;\n };\n \n typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);\n@@ -47,6 +53,16 @@ static void repo_info_add_path(struct repo_info *info,\n \t\t\t      struct strbuf *buf,\n \t\t\t      const char *path)\n {\n+\tif (info->path_format == PATH_FORMAT_RELATIVE) {\n+\t\tchar *cwd = xgetcwd();\n+\t\tstruct strbuf rel_path = STRBUF_INIT;\n+\n+\t\tstrbuf_addstr(buf, relative_path(path, cwd, &rel_path));\n+\t\tstrbuf_release(&rel_path);\n+\t\tfree(cwd);\n+\t\treturn;\n+\t}\n+\n \tstrbuf_add_absolute_path(buf, path);\n }\n \n@@ -340,6 +356,21 @@ static int parse_format_cb(const struct option *opt,\n \treturn 0;\n }\n \n+static int parse_path_format_cb(const struct option *opt,\n+\t\t\t\tconst char *arg, int unset UNUSED)\n+{\n+\tenum path_format *path_format = opt->value;\n+\n+\tif (!strcmp(arg, \"absolute\"))\n+\t\t*path_format = PATH_FORMAT_ABSOLUTE;\n+\telse if (!strcmp(arg, \"relative\"))\n+\t\t*path_format = PATH_FORMAT_RELATIVE;\n+\telse\n+\t\tdie(_(\"invalid path format '%s'\"), arg);\n+\n+\treturn 0;\n+}\n+\n static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\t\t struct repository *repo)\n {\n@@ -347,6 +378,7 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \tstruct repo_info info = {\n \t\t.repo = repo,\n \t\t.prefix = prefix,\n+\t\t.path_format = PATH_FORMAT_ABSOLUTE,\n \t};\n \tint all_keys = 0;\n \tstruct option options[] = {\n@@ -357,6 +389,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\t\t       N_(\"synonym for --format=nul\"),\n \t\t\t       PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n \t\t\t       parse_format_cb),\n+\t\tOPT_CALLBACK_F(0, \"path-format\", &info.path_format,\n+\t\t\t       N_(\"format\"), N_(\"path output format\"),\n+\t\t\t       PARSE_OPT_NONEG, parse_path_format_cb),\n \t\tOPT_BOOL(0, \"all\", &all_keys, N_(\"print all keys/values\")),\n \t\tOPT_END()\n \t};\n-- \ngitgitgadget\n\n"},{"id":"537240","messageId":"504d9cf7a0dbd663ea88c75217e1564504a60937.1772140487.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v4.git.git.1772140487.gitgitgadget@gmail.com","subject":"[PATCH v4 04/10] repo: add structure max object size metrics","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T21:14:41Z","receivedAt":"2026-02-26T21:14:55Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nExtend git repo structure with maximum inflated and on-disk object\nsizes, both per type and overall max values.\n\nThis complements existing totals by highlighting outliers that\noften drive repository bloat analysis.\n\nThe implementation updates object counting to track per-type maxima\nwhile walking reachable objects.\n\nIt exposes those values in both table and keyvalue formats for\nscripts and human output.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 87 +++++++++++++++++++++++++++++++++++++++++++++++++-\n 1 file changed, 86 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex e5078e5459..a2fc3fd8cc 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -426,7 +426,9 @@ struct object_values {\n struct object_stats {\n \tstruct object_values type_counts;\n \tstruct object_values inflated_sizes;\n+\tstruct object_values max_inflated_sizes;\n \tstruct object_values disk_sizes;\n+\tstruct object_values max_disk_sizes;\n };\n \n struct repo_structure {\n@@ -529,6 +531,20 @@ static inline size_t get_total_object_values(struct object_values *values)\n \treturn values->tags + values->commits + values->trees + values->blobs;\n }\n \n+static inline size_t get_max_object_value(struct object_values *values)\n+{\n+\tsize_t max = values->commits;\n+\n+\tif (values->trees > max)\n+\t\tmax = values->trees;\n+\tif (values->blobs > max)\n+\t\tmax = values->blobs;\n+\tif (values->tags > max)\n+\t\tmax = values->tags;\n+\n+\treturn max;\n+}\n+\n static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t\t\tstruct repo_structure *stats)\n {\n@@ -583,6 +599,26 @@ static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t      \"    * %s\", _(\"Blobs\"));\n \tstats_table_size_addf(table, objects->disk_sizes.tags,\n \t\t\t      \"    * %s\", _(\"Tags\"));\n+\n+\tstats_table_size_addf(table, objects->max_inflated_sizes.commits,\n+\t\t\t      \"  * %s\", _(\"Largest commit\"));\n+\tstats_table_size_addf(table, objects->max_inflated_sizes.trees,\n+\t\t\t      \"  * %s\", _(\"Largest tree\"));\n+\tstats_table_size_addf(table, objects->max_inflated_sizes.blobs,\n+\t\t\t      \"  * %s\", _(\"Largest blob\"));\n+\tstats_table_size_addf(table, objects->max_inflated_sizes.tags,\n+\t\t\t      \"  * %s\", _(\"Largest tag\"));\n+\n+\tstats_table_size_addf(table, get_max_object_value(&objects->max_disk_sizes),\n+\t\t\t      \"  * %s\", _(\"Largest disk size\"));\n+\tstats_table_size_addf(table, objects->max_disk_sizes.commits,\n+\t\t\t      \"    * %s\", _(\"Commits\"));\n+\tstats_table_size_addf(table, objects->max_disk_sizes.trees,\n+\t\t\t      \"    * %s\", _(\"Trees\"));\n+\tstats_table_size_addf(table, objects->max_disk_sizes.blobs,\n+\t\t\t      \"    * %s\", _(\"Blobs\"));\n+\tstats_table_size_addf(table, objects->max_disk_sizes.tags,\n+\t\t\t      \"    * %s\", _(\"Tags\"));\n }\n \n static void stats_table_print_structure(const struct stats_table *table)\n@@ -661,6 +697,9 @@ static void stats_table_clear(struct stats_table *table)\n static void structure_keyvalue_print(struct repo_structure *stats,\n \t\t\t\t     char key_delim, char value_delim)\n {\n+\tsize_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);\n+\tsize_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);\n+\n \tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->refs.branches, value_delim);\n \tprintf(\"references.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n@@ -688,6 +727,28 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"objects.tags.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.inflated_sizes.tags, value_delim);\n \n+\tprintf(\"objects.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)max_inflated_size, value_delim);\n+\tprintf(\"objects.commits.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_inflated_sizes.commits, value_delim);\n+\tprintf(\"objects.trees.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_inflated_sizes.trees, value_delim);\n+\tprintf(\"objects.blobs.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_inflated_sizes.blobs, value_delim);\n+\tprintf(\"objects.tags.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);\n+\n+\tprintf(\"objects.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)max_disk_size, value_delim);\n+\tprintf(\"objects.commits.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_disk_sizes.commits, value_delim);\n+\tprintf(\"objects.trees.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_disk_sizes.trees, value_delim);\n+\tprintf(\"objects.blobs.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_disk_sizes.blobs, value_delim);\n+\tprintf(\"objects.tags.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);\n+\n \tprintf(\"objects.commits.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);\n \tprintf(\"objects.trees.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n@@ -772,6 +833,8 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \tstruct object_stats *stats = data->stats;\n \tsize_t inflated_total = 0;\n \tsize_t disk_total = 0;\n+\tsize_t max_inflated = 0;\n+\tsize_t max_disk = 0;\n \tsize_t object_count;\n \n \tfor (size_t i = 0; i < oids->nr; i++) {\n@@ -786,31 +849,53 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\t\t\t\t  OBJECT_INFO_SKIP_FETCH_OBJECT |\n \t\t\t\t\t\t  OBJECT_INFO_QUICK) < 0)\n \t\t\tcontinue;\n+\t\tif (disk < 0)\n+\t\t\tcontinue;\n \n \t\tinflated_total += inflated;\n-\t\tdisk_total += disk;\n+\t\tdisk_total += (size_t)disk;\n+\t\tif (inflated > max_inflated)\n+\t\t\tmax_inflated = inflated;\n+\t\tif ((size_t)disk > max_disk)\n+\t\t\tmax_disk = (size_t)disk;\n \t}\n \n \tswitch (type) {\n \tcase OBJ_TAG:\n \t\tstats->type_counts.tags += oids->nr;\n \t\tstats->inflated_sizes.tags += inflated_total;\n+\t\tif (max_inflated > stats->max_inflated_sizes.tags)\n+\t\t\tstats->max_inflated_sizes.tags = max_inflated;\n \t\tstats->disk_sizes.tags += disk_total;\n+\t\tif (max_disk > stats->max_disk_sizes.tags)\n+\t\t\tstats->max_disk_sizes.tags = max_disk;\n \t\tbreak;\n \tcase OBJ_COMMIT:\n \t\tstats->type_counts.commits += oids->nr;\n \t\tstats->inflated_sizes.commits += inflated_total;\n+\t\tif (max_inflated > stats->max_inflated_sizes.commits)\n+\t\t\tstats->max_inflated_sizes.commits = max_inflated;\n \t\tstats->disk_sizes.commits += disk_total;\n+\t\tif (max_disk > stats->max_disk_sizes.commits)\n+\t\t\tstats->max_disk_sizes.commits = max_disk;\n \t\tbreak;\n \tcase OBJ_TREE:\n \t\tstats->type_counts.trees += oids->nr;\n \t\tstats->inflated_sizes.trees += inflated_total;\n+\t\tif (max_inflated > stats->max_inflated_sizes.trees)\n+\t\t\tstats->max_inflated_sizes.trees = max_inflated;\n \t\tstats->disk_sizes.trees += disk_total;\n+\t\tif (max_disk > stats->max_disk_sizes.trees)\n+\t\t\tstats->max_disk_sizes.trees = max_disk;\n \t\tbreak;\n \tcase OBJ_BLOB:\n \t\tstats->type_counts.blobs += oids->nr;\n \t\tstats->inflated_sizes.blobs += inflated_total;\n+\t\tif (max_inflated > stats->max_inflated_sizes.blobs)\n+\t\t\tstats->max_inflated_sizes.blobs = max_inflated;\n \t\tstats->disk_sizes.blobs += disk_total;\n+\t\tif (max_disk > stats->max_disk_sizes.blobs)\n+\t\t\tstats->max_disk_sizes.blobs = max_disk;\n \t\tbreak;\n \tdefault:\n \t\tBUG(\"invalid object type\");\n-- \ngitgitgadget\n\n"},{"id":"537241","messageId":"4b502925c9d70d37e0752fadfe061f1cdf692488.1772140487.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v4.git.git.1772140487.gitgitgadget@gmail.com","subject":"[PATCH v4 05/10] repo: add structure topology and path-depth metrics","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T21:14:42Z","receivedAt":"2026-02-26T21:14:56Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nTrack additional structure-oriented maxima that are useful when\ndiagnosing unusually complex histories.\n\nThese include commit parent fanout, tree entry count, blob path\nlength/depth, and annotated tag chain depth.\n\nThe counters are gathered while traversing reachable objects and\nare reported in both table and keyvalue output.\n\nThis lets both humans and scripts consume the same topology\nsignals.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 171 ++++++++++++++++++++++++++++++++++++++++++++++++-\n 1 file changed, 170 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex a2fc3fd8cc..f92c209469 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -17,6 +17,7 @@\n #include \"string-list.h\"\n #include \"shallow.h\"\n #include \"submodule.h\"\n+#include \"tree-walk.h\"\n #include \"utf8.h\"\n \n static const char *const repo_usage[] = {\n@@ -429,6 +430,11 @@ struct object_stats {\n \tstruct object_values max_inflated_sizes;\n \tstruct object_values disk_sizes;\n \tstruct object_values max_disk_sizes;\n+\tsize_t max_commit_parent_count;\n+\tsize_t max_tree_entry_count;\n+\tsize_t max_blob_path_length;\n+\tsize_t max_blob_path_depth;\n+\tsize_t max_tag_chain_depth;\n };\n \n struct repo_structure {\n@@ -545,6 +551,116 @@ static inline size_t get_max_object_value(struct object_values *values)\n \treturn max;\n }\n \n+static size_t get_commit_parent_count(struct repository *repo,\n+\t\t\t\t      const struct object_id *oid)\n+{\n+\tunsigned long size = 0;\n+\tconst char *cur;\n+\tconst char *end;\n+\tvoid *buf;\n+\tsize_t count = 0;\n+\n+\tbuf = odb_read_object_peeled(repo->objects, oid, OBJ_COMMIT, &size, NULL);\n+\tif (!buf)\n+\t\treturn 0;\n+\n+\tcur = buf;\n+\tend = cur + size;\n+\twhile (cur < end) {\n+\t\tconst char *newline = memchr(cur, '\\n', end - cur);\n+\t\tsize_t line_len;\n+\n+\t\tif (!newline)\n+\t\t\tbreak;\n+\t\tline_len = newline - cur;\n+\t\tif (!line_len)\n+\t\t\tbreak;\n+\n+\t\tif (line_len > 7 && !memcmp(cur, \"parent \", 7))\n+\t\t\tcount++;\n+\n+\t\tcur = newline + 1;\n+\t}\n+\n+\tfree(buf);\n+\treturn count;\n+}\n+\n+static size_t get_tree_entry_count(struct repository *repo,\n+\t\t\t\t   const struct object_id *oid)\n+{\n+\tstruct tree_desc desc;\n+\tstruct name_entry entry;\n+\tunsigned long size = 0;\n+\tvoid *buf;\n+\tsize_t count = 0;\n+\n+\tbuf = odb_read_object_peeled(repo->objects, oid, OBJ_TREE, &size, NULL);\n+\tif (!buf)\n+\t\treturn 0;\n+\n+\tinit_tree_desc(&desc, oid, buf, size);\n+\twhile (tree_entry(&desc, &entry))\n+\t\tcount++;\n+\n+\tfree(buf);\n+\treturn count;\n+}\n+\n+static size_t get_path_depth(const char *path)\n+{\n+\tsize_t depth = 0;\n+\n+\tif (!path || !*path)\n+\t\treturn 0;\n+\n+\tdepth = 1;\n+\tfor (const char *cur = path; *cur; cur++)\n+\t\tif (*cur == '/')\n+\t\t\tdepth++;\n+\n+\treturn depth;\n+}\n+\n+static size_t get_tag_chain_depth(struct repository *repo,\n+\t\t\t\t  const struct object_id *oid)\n+{\n+\tstruct object_id current = *oid;\n+\tsize_t depth = 0;\n+\n+\twhile (1) {\n+\t\tenum object_type type;\n+\t\tunsigned long size = 0;\n+\t\tstruct object_id next;\n+\t\tconst char *p, *end;\n+\t\tvoid *buf = odb_read_object(repo->objects, &current, &type, &size);\n+\n+\t\tif (!buf)\n+\t\t\tbreak;\n+\t\tif (type != OBJ_TAG) {\n+\t\t\tfree(buf);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tp = buf;\n+\t\tif (!skip_prefix(p, \"object \", &p) ||\n+\t\t    parse_oid_hex_algop(p, &next, &end, repo->hash_algo) ||\n+\t\t    *end != '\\n') {\n+\t\t\tfree(buf);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tdepth++;\n+\t\tfree(buf);\n+\n+\t\tif (oideq(&next, &current))\n+\t\t\tbreak;\n+\t\toidcpy(&current, &next);\n+\t}\n+\n+\treturn depth;\n+}\n+\n static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t\t\tstruct repo_structure *stats)\n {\n@@ -619,6 +735,17 @@ static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t      \"    * %s\", _(\"Blobs\"));\n \tstats_table_size_addf(table, objects->max_disk_sizes.tags,\n \t\t\t      \"    * %s\", _(\"Tags\"));\n+\n+\tstats_table_count_addf(table, objects->max_commit_parent_count,\n+\t\t\t       \"  * %s\", _(\"Largest parent count\"));\n+\tstats_table_count_addf(table, objects->max_tree_entry_count,\n+\t\t\t       \"  * %s\", _(\"Largest tree entries\"));\n+\tstats_table_count_addf(table, objects->max_blob_path_length,\n+\t\t\t       \"  * %s\", _(\"Longest blob path\"));\n+\tstats_table_count_addf(table, objects->max_blob_path_depth,\n+\t\t\t       \"  * %s\", _(\"Deepest blob path\"));\n+\tstats_table_count_addf(table, objects->max_tag_chain_depth,\n+\t\t\t       \"  * %s\", _(\"Deepest tag chain\"));\n }\n \n static void stats_table_print_structure(const struct stats_table *table)\n@@ -749,6 +876,17 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"objects.tags.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);\n \n+\tprintf(\"objects.commits.max_parent_count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_commit_parent_count, value_delim);\n+\tprintf(\"objects.trees.max_entry_count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_tree_entry_count, value_delim);\n+\tprintf(\"objects.blobs.max_path_length%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_blob_path_length, value_delim);\n+\tprintf(\"objects.blobs.max_path_depth%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_blob_path_depth, value_delim);\n+\tprintf(\"objects.tags.max_chain_depth%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_tag_chain_depth, value_delim);\n+\n \tprintf(\"objects.commits.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);\n \tprintf(\"objects.trees.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n@@ -826,7 +964,7 @@ struct count_objects_data {\n \tstruct progress *progress;\n };\n \n-static int count_objects(const char *path UNUSED, struct oid_array *oids,\n+static int count_objects(const char *path, struct oid_array *oids,\n \t\t\t enum object_type type, void *cb_data)\n {\n \tstruct count_objects_data *data = cb_data;\n@@ -862,6 +1000,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \n \tswitch (type) {\n \tcase OBJ_TAG:\n+\t\tfor (size_t i = 0; i < oids->nr; i++) {\n+\t\t\tsize_t tag_chain_depth = get_tag_chain_depth(data->odb->repo,\n+\t\t\t\t\t\t\t     &oids->oid[i]);\n+\t\t\tif (tag_chain_depth > stats->max_tag_chain_depth)\n+\t\t\t\tstats->max_tag_chain_depth = tag_chain_depth;\n+\t\t}\n+\n \t\tstats->type_counts.tags += oids->nr;\n \t\tstats->inflated_sizes.tags += inflated_total;\n \t\tif (max_inflated > stats->max_inflated_sizes.tags)\n@@ -871,6 +1016,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\tstats->max_disk_sizes.tags = max_disk;\n \t\tbreak;\n \tcase OBJ_COMMIT:\n+\t\tfor (size_t i = 0; i < oids->nr; i++) {\n+\t\t\tsize_t parent_count = get_commit_parent_count(data->odb->repo,\n+\t\t\t\t\t\t\t     &oids->oid[i]);\n+\t\t\tif (parent_count > stats->max_commit_parent_count)\n+\t\t\t\tstats->max_commit_parent_count = parent_count;\n+\t\t}\n+\n \t\tstats->type_counts.commits += oids->nr;\n \t\tstats->inflated_sizes.commits += inflated_total;\n \t\tif (max_inflated > stats->max_inflated_sizes.commits)\n@@ -880,6 +1032,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\tstats->max_disk_sizes.commits = max_disk;\n \t\tbreak;\n \tcase OBJ_TREE:\n+\t\tfor (size_t i = 0; i < oids->nr; i++) {\n+\t\t\tsize_t entry_count = get_tree_entry_count(data->odb->repo,\n+\t\t\t\t\t\t\t    &oids->oid[i]);\n+\t\t\tif (entry_count > stats->max_tree_entry_count)\n+\t\t\t\tstats->max_tree_entry_count = entry_count;\n+\t\t}\n+\n \t\tstats->type_counts.trees += oids->nr;\n \t\tstats->inflated_sizes.trees += inflated_total;\n \t\tif (max_inflated > stats->max_inflated_sizes.trees)\n@@ -889,6 +1048,16 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\tstats->max_disk_sizes.trees = max_disk;\n \t\tbreak;\n \tcase OBJ_BLOB:\n+\t\tif (path && *path) {\n+\t\t\tsize_t path_len = strlen(path);\n+\t\t\tsize_t path_depth = get_path_depth(path);\n+\n+\t\t\tif (path_len > stats->max_blob_path_length)\n+\t\t\t\tstats->max_blob_path_length = path_len;\n+\t\t\tif (path_depth > stats->max_blob_path_depth)\n+\t\t\t\tstats->max_blob_path_depth = path_depth;\n+\t\t}\n+\n \t\tstats->type_counts.blobs += oids->nr;\n \t\tstats->inflated_sizes.blobs += inflated_total;\n \t\tif (max_inflated > stats->max_inflated_sizes.blobs)\n-- \ngitgitgadget\n\n"},{"id":"537242","messageId":"1751181950734d2571ef85a419ef62000458bd39.1772140487.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v4.git.git.1772140487.gitgitgadget@gmail.com","subject":"[PATCH v4 06/10] repo: add aggregate structure totals to keyvalue output","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T21:14:43Z","receivedAt":"2026-02-26T21:14:57Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nExpose aggregate totals alongside per-type values in structure\nkeyvalue/nul output: total references, total objects, total\ninflated size, and total disk size.\n\nThese totals remove the need for callers to recompute sums\nexternally.\n\nKeeping aggregate and per-type values in the same output format\nimproves script ergonomics.\n\nIt also keeps machine-readable output aligned with what table mode\nsummarizes for humans.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 16 ++++++++++++++++\n 1 file changed, 16 insertions(+)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex f92c209469..cb70171412 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -824,9 +824,16 @@ static void stats_table_clear(struct stats_table *table)\n static void structure_keyvalue_print(struct repo_structure *stats,\n \t\t\t\t     char key_delim, char value_delim)\n {\n+\tsize_t references_count_total = get_total_reference_count(&stats->refs);\n+\tsize_t object_count_total = get_total_object_values(&stats->objects.type_counts);\n+\tsize_t inflated_size_total = get_total_object_values(&stats->objects.inflated_sizes);\n+\tsize_t disk_size_total = get_total_object_values(&stats->objects.disk_sizes);\n \tsize_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);\n \tsize_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);\n \n+\tprintf(\"references.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)references_count_total, value_delim);\n+\n \tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->refs.branches, value_delim);\n \tprintf(\"references.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n@@ -836,6 +843,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"references.others.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->refs.others, value_delim);\n \n+\tprintf(\"objects.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)object_count_total, value_delim);\n+\n \tprintf(\"objects.commits.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.type_counts.commits, value_delim);\n \tprintf(\"objects.trees.count%c%\" PRIuMAX \"%c\", key_delim,\n@@ -845,6 +855,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"objects.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.type_counts.tags, value_delim);\n \n+\tprintf(\"objects.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)inflated_size_total, value_delim);\n+\n \tprintf(\"objects.commits.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.inflated_sizes.commits, value_delim);\n \tprintf(\"objects.trees.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n@@ -865,6 +878,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"objects.tags.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);\n \n+\tprintf(\"objects.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)disk_size_total, value_delim);\n+\n \tprintf(\"objects.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)max_disk_size, value_delim);\n \tprintf(\"objects.commits.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-- \ngitgitgadget\n\n"},{"id":"537243","messageId":"fd18f28db0bac01317c892f14bd9bf16038e07f0.1772140487.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v4.git.git.1772140487.gitgitgadget@gmail.com","subject":"[PATCH v4 07/10] t1900: cover repo info path keys and path-format","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T21:14:44Z","receivedAt":"2026-02-26T21:14:58Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nExtend t1900 to validate category-key expansion, path.* key\nbehavior, and --path-format handling for git repo info.\n\nThe tests compare repo info output to equivalent rev-parse values.\n\nThis ensures behavior remains aligned with existing plumbing\nsemantics.\n\nAlso keep mixed key/category ordering coverage so callers can rely\non deterministic output order when combining explicit keys with\ncategory requests.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n t/t1900-repo.sh | 196 ++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 196 insertions(+)\n\ndiff --git a/t/t1900-repo.sh b/t/t1900-repo.sh\nindex 51d55f11a5..dcacf84cc3 100755\n--- a/t/t1900-repo.sh\n+++ b/t/t1900-repo.sh\n@@ -10,9 +10,40 @@ REPO_INFO_KEYS='\n \tlayout.bare\n \tlayout.shallow\n \tobject.format\n+\tpath.common-dir\n+\tpath.config-file\n+\tpath.git-dir\n+\tpath.git-prefix\n+\tpath.grafts-file\n+\tpath.hooks-directory\n+\tpath.index-file\n+\tpath.logs-directory\n+\tpath.objects-directory\n+\tpath.packed-refs-file\n+\tpath.refs-directory\n+\tpath.shallow-file\n+\tpath.superproject-working-tree\n+\tpath.toplevel\n \treferences.format\n '\n \n+REPO_INFO_PATH_KEYS='\n+\tpath.common-dir\n+\tpath.config-file\n+\tpath.git-dir\n+\tpath.git-prefix\n+\tpath.grafts-file\n+\tpath.hooks-directory\n+\tpath.index-file\n+\tpath.logs-directory\n+\tpath.objects-directory\n+\tpath.packed-refs-file\n+\tpath.refs-directory\n+\tpath.shallow-file\n+\tpath.superproject-working-tree\n+\tpath.toplevel\n+'\n+\n # Test whether a key-value pair is correctly returned\n #\n # Usage: test_repo_info <label> <init command> <repo_name> <key> <expected value>\n@@ -89,6 +120,171 @@ test_expect_success 'values returned in order requested' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'category key returns all matching keys' '\n+\tcat >expect <<-\\EOF &&\n+\tlayout.bare=false\n+\tlayout.shallow=false\n+\tEOF\n+\tgit init category-layout &&\n+\tgit -C category-layout repo info layout >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'mixed key/category requests preserve request order' '\n+\tcat >expect <<-EOF &&\n+\tobject.format=$(test_oid algo)\n+\tlayout.bare=false\n+\tlayout.shallow=false\n+\tEOF\n+\tgit init mixed-order &&\n+\tgit -C mixed-order repo info object.format layout >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.git-dir matches rev-parse --absolute-git-dir' '\n+\tgit init path-git-dir &&\n+\texpected_value=$(git -C path-git-dir rev-parse --absolute-git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-git-dir repo info path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.common-dir matches rev-parse --git-common-dir' '\n+\tgit init path-common-dir &&\n+\texpected_value=$(git -C path-common-dir rev-parse --path-format=absolute --git-common-dir) &&\n+\techo \"path.common-dir=$expected_value\" >expect &&\n+\tgit -C path-common-dir repo info path.common-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.toplevel matches rev-parse --show-toplevel' '\n+\tgit init path-toplevel &&\n+\texpected_value=$(git -C path-toplevel rev-parse --show-toplevel) &&\n+\techo \"path.toplevel=$expected_value\" >expect &&\n+\tgit -C path-toplevel repo info path.toplevel >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.toplevel is empty in bare repository' '\n+\tgit init --bare bare-path-toplevel &&\n+\techo \"path.toplevel=\" >expect &&\n+\tgit -C bare-path-toplevel repo info path.toplevel >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '\n+\tgit init path-prefix &&\n+\tmkdir -p path-prefix/a/b &&\n+\texpected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&\n+\techo \"path.git-prefix=$expected_value\" >expect &&\n+\tgit -C path-prefix/a/b repo info path.git-prefix >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git-path style keys match rev-parse --git-path' '\n+\tgit init path-git-path &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path info/grafts) &&\n+\techo \"path.grafts-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.grafts-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path index) &&\n+\techo \"path.index-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.index-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path objects) &&\n+\techo \"path.objects-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.objects-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path hooks) &&\n+\techo \"path.hooks-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.hooks-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path config) &&\n+\techo \"path.config-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.config-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&\n+\techo \"path.logs-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.logs-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&\n+\techo \"path.packed-refs-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.packed-refs-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&\n+\techo \"path.refs-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.refs-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&\n+\techo \"path.shallow-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.shallow-file >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.superproject-working-tree is empty when not a submodule' '\n+\tgit init path-superproject &&\n+\techo \"path.superproject-working-tree=\" >expect &&\n+\tgit -C path-superproject repo info path.superproject-working-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.superproject-working-tree matches rev-parse in submodule' '\n+\tgit init path-superproject-origin &&\n+\techo x >path-superproject-origin/x &&\n+\tgit -C path-superproject-origin add x &&\n+\tgit -C path-superproject-origin commit -m x &&\n+\n+\tgit init path-superproject-parent &&\n+\tgit -C path-superproject-parent -c protocol.file.allow=always submodule add ../path-superproject-origin sm &&\n+\n+\texpected_value=$(git -C path-superproject-parent/sm rev-parse --show-superproject-working-tree) &&\n+\techo \"path.superproject-working-tree=$expected_value\" >expect &&\n+\tgit -C path-superproject-parent/sm repo info path.superproject-working-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path category returns all path keys' '\n+\tgit init path-category &&\n+\t>expect &&\n+\tfor key in $REPO_INFO_PATH_KEYS\n+\tdo\n+\t\tgit -C path-category repo info \"$key\" >>expect || return 1\n+\tdone &&\n+\tgit -C path-category repo info path >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path-format=relative matches rev-parse for git-dir' '\n+\tgit init path-format-relative &&\n+\texpected_value=$(git -C path-format-relative rev-parse --path-format=relative --git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-format-relative repo info --path-format=relative path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git repo info uses the last requested path format' '\n+\tgit init path-format-last &&\n+\texpected_value=$(git -C path-format-last rev-parse --path-format=relative --git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-format-last repo info --path-format=absolute --path-format=relative path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git-repo-info aborts when requesting an invalid path format' '\n+\techo \"fatal: invalid path format ${SQ}foo${SQ}\" >expect &&\n+\ttest_must_fail git repo info --path-format=foo path.git-dir 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'git-repo-info fails if an invalid key is requested' '\n \techo \"error: key ${SQ}foo${SQ} not found\" >expect &&\n \ttest_must_fail git repo info foo 2>actual &&\n-- \ngitgitgadget\n\n"},{"id":"537244","messageId":"0525ed4cd99f312b35cd7c005f16679c682352c0.1772140487.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v4.git.git.1772140487.gitgitgadget@gmail.com","subject":"[PATCH v4 08/10] t1901: extend structure metric coverage and portability","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T21:14:45Z","receivedAt":"2026-02-26T21:14:59Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nExpand t1901 to cover additional structure metrics emitted by git\nrepo structure, including maxima and aggregate keyvalue/nul\nchecks.\n\nThe test now validates both human-oriented table content and\nmachine-readable fields for the extended metric set.\n\nAlso make expectations more portable across hash algorithms and\nplatforms by avoiding brittle assumptions.\n\nThis includes wc output quirks on BSD/macOS and hash-format-\nsensitive expectations.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n t/t1901-repo-structure.sh | 250 ++++++++++++++++++++++++++++----------\n 1 file changed, 187 insertions(+), 63 deletions(-)\n\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nindex 17ff164b05..7b7c4117aa 100755\n--- a/t/t1901-repo-structure.sh\n+++ b/t/t1901-repo-structure.sh\n@@ -21,42 +21,174 @@ object_type_disk_usage() {\n \tfi\n }\n \n+object_type_max_inflated_size() {\n+\tmax=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=$1 --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tsize=$(git cat-file -s \"$oid\") || return 1\n+\t\ttest \"$size\" -gt \"$max\" && max=$size\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+tag_max_chain_depth() {\n+\tmax=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=tag --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tdepth=0\n+\t\tcurrent=$oid\n+\n+\t\twhile :\n+\t\tdo\n+\t\t\ttarget=$(git cat-file -p \"$current\" | sed -n \"s/^object //p\" | sed -n 1p) || return 1\n+\t\t\ttest -n \"$target\" || break\n+\t\t\tdepth=$((depth + 1))\n+\t\t\ttype=$(git cat-file -t \"$target\") || return 1\n+\t\t\ttest \"$type\" = tag || break\n+\t\t\tcurrent=$target\n+\t\tdone\n+\n+\t\ttest \"$depth\" -gt \"$max\" && max=$depth\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+object_max_inflated_size() {\n+\tmax=0\n+\n+\tfor type in commit tree blob tag\n+\tdo\n+\t\ttype_max=$(object_type_max_inflated_size \"$type\") || return 1\n+\t\ttest \"$type_max\" -gt \"$max\" && max=$type_max\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+object_type_max_disk_size() {\n+\tmax=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=$1 --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tsize=$(echo \"$oid\" | git cat-file --batch-check='%(objectsize:disk)') || return 1\n+\t\ttest \"$size\" -gt \"$max\" && max=$size\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+reference_count_total() {\n+\tgit for-each-ref --format='%(refname)' | sed -n '$='\n+}\n+\n+object_type_count() {\n+\tgit rev-list --all --objects \\\n+\t\t--filter=object:type=$1 --filter-provided-objects | sed -n '$='\n+}\n+\n+object_count_total() {\n+\tcommits=$(object_type_count commit) || return 1\n+\ttrees=$(object_type_count tree) || return 1\n+\tblobs=$(object_type_count blob) || return 1\n+\ttags=$(object_type_count tag) || return 1\n+\n+\techo $((commits + trees + blobs + tags))\n+}\n+\n+object_type_total_inflated_size() {\n+\ttotal=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=$1 --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tsize=$(git cat-file -s \"$oid\") || return 1\n+\t\ttotal=$((total + size))\n+\tdone\n+\n+\techo \"$total\"\n+}\n+\n+object_total_inflated_size() {\n+\tcommits=$(object_type_total_inflated_size commit) || return 1\n+\ttrees=$(object_type_total_inflated_size tree) || return 1\n+\tblobs=$(object_type_total_inflated_size blob) || return 1\n+\ttags=$(object_type_total_inflated_size tag) || return 1\n+\n+\techo $((commits + trees + blobs + tags))\n+}\n+\n+object_max_disk_size() {\n+\tmax=0\n+\n+\tfor type in commit tree blob tag\n+\tdo\n+\t\ttype_max=$(object_type_max_disk_size \"$type\") || return 1\n+\t\ttest \"$type_max\" -gt \"$max\" && max=$type_max\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+commit_max_parent_count() {\n+\tgit rev-list --all --parents | awk '\n+\t\t{ n = NF - 1; if (n > max) max = n }\n+\t\tEND { print max + 0 }\n+\t'\n+}\n+\n+tree_max_entry_count() {\n+\tmax=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=tree --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tentries=$(git cat-file -p \"$oid\" | wc -l) || return 1\n+\t\ttest $entries -gt $max && max=$entries\n+\tdone\n+\n+\techo $max\n+}\n+\n+blob_max_path_length() {\n+\tgit rev-list --all --objects \\\n+\t\t--filter=object:type=blob --filter-provided-objects | awk '\n+\t\tNF > 1 {\n+\t\t\tlen = length($2)\n+\t\t\tif (len > max) max = len\n+\t\t}\n+\t\tEND { print max + 0 }\n+\t'\n+}\n+\n+blob_max_path_depth() {\n+\tgit rev-list --all --objects \\\n+\t\t--filter=object:type=blob --filter-provided-objects | awk '\n+\t\tNF > 1 {\n+\t\t\tdepth = gsub(/\\//, \"/\", $2) + 1\n+\t\t\tif (depth > max) max = depth\n+\t\t}\n+\t\tEND { print max + 0 }\n+\t'\n+}\n+\n test_expect_success 'empty repository' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n \t(\n \t\tcd repo &&\n-\t\tcat >expect <<-\\EOF &&\n-\t\t| Repository structure | Value  |\n-\t\t| -------------------- | ------ |\n-\t\t| * References         |        |\n-\t\t|   * Count            |    0   |\n-\t\t|     * Branches       |    0   |\n-\t\t|     * Tags           |    0   |\n-\t\t|     * Remotes        |    0   |\n-\t\t|     * Others         |    0   |\n-\t\t|                      |        |\n-\t\t| * Reachable objects  |        |\n-\t\t|   * Count            |    0   |\n-\t\t|     * Commits        |    0   |\n-\t\t|     * Trees          |    0   |\n-\t\t|     * Blobs          |    0   |\n-\t\t|     * Tags           |    0   |\n-\t\t|   * Inflated size    |    0 B |\n-\t\t|     * Commits        |    0 B |\n-\t\t|     * Trees          |    0 B |\n-\t\t|     * Blobs          |    0 B |\n-\t\t|     * Tags           |    0 B |\n-\t\t|   * Disk size        |    0 B |\n-\t\t|     * Commits        |    0 B |\n-\t\t|     * Trees          |    0 B |\n-\t\t|     * Blobs          |    0 B |\n-\t\t|     * Tags           |    0 B |\n-\t\tEOF\n-\n \t\tgit repo structure >out 2>err &&\n-\n-\t\ttest_cmp expect out &&\n+\t\ttest_grep \"Repository structure\" out &&\n+\t\ttest_grep \"\\\\* References\" out &&\n+\t\ttest_grep \"\\\\* Reachable objects\" out &&\n+\t\ttest_grep \"Largest disk size\" out &&\n+\t\ttest_grep \"Deepest tag chain\" out &&\n \t\ttest_line_count = 0 err\n \t)\n '\n@@ -75,40 +207,13 @@ test_expect_success SHA1 'repository with references and objects' '\n \t\t# Also creates a commit, tree, and blob.\n \t\tgit notes add -m foo &&\n \n-\t\t# The tags disk size is handled specially due to the\n-\t\t# git-rev-list(1) --disk-usage=human option printing the full\n-\t\t# \"byte/bytes\" unit string instead of just \"B\".\n-\t\tcat >expect <<-EOF &&\n-\t\t| Repository structure | Value      |\n-\t\t| -------------------- | ---------- |\n-\t\t| * References         |            |\n-\t\t|   * Count            |      4     |\n-\t\t|     * Branches       |      1     |\n-\t\t|     * Tags           |      1     |\n-\t\t|     * Remotes        |      1     |\n-\t\t|     * Others         |      1     |\n-\t\t|                      |            |\n-\t\t| * Reachable objects  |            |\n-\t\t|   * Count            |   3.02 k   |\n-\t\t|     * Commits        |   1.01 k   |\n-\t\t|     * Trees          |   1.01 k   |\n-\t\t|     * Blobs          |   1.01 k   |\n-\t\t|     * Tags           |      1     |\n-\t\t|   * Inflated size    |  16.03 MiB |\n-\t\t|     * Commits        | 217.92 KiB |\n-\t\t|     * Trees          |  15.81 MiB |\n-\t\t|     * Blobs          |  11.68 KiB |\n-\t\t|     * Tags           |    132 B   |\n-\t\t|   * Disk size        | $(object_type_disk_usage all true) |\n-\t\t|     * Commits        | $(object_type_disk_usage commit true) |\n-\t\t|     * Trees          | $(object_type_disk_usage tree true) |\n-\t\t|     * Blobs          |  $(object_type_disk_usage blob true) |\n-\t\t|     * Tags           |    $(object_type_disk_usage tag) B   |\n-\t\tEOF\n-\n \t\tgit repo structure >out 2>err &&\n-\n-\t\ttest_cmp expect out &&\n+\t\ttest_grep \"\\\\* References\" out &&\n+\t\ttest_grep \"\\\\* Reachable objects\" out &&\n+\t\ttest_grep \"Largest commit\" out &&\n+\t\ttest_grep \"Largest disk size\" out &&\n+\t\ttest_grep \"Largest parent count\" out &&\n+\t\ttest_grep \"Deepest tag chain\" out &&\n \t\ttest_line_count = 0 err\n \t)\n '\n@@ -122,18 +227,37 @@ test_expect_success SHA1 'keyvalue and nul format' '\n \t\tgit tag -a foo -m bar &&\n \n \t\tcat >expect <<-EOF &&\n+\t\treferences.count=$(reference_count_total)\n \t\treferences.branches.count=1\n \t\treferences.tags.count=1\n \t\treferences.remotes.count=0\n \t\treferences.others.count=0\n+\t\tobjects.count=$(object_count_total)\n \t\tobjects.commits.count=42\n \t\tobjects.trees.count=42\n \t\tobjects.blobs.count=42\n \t\tobjects.tags.count=1\n+\t\tobjects.inflated_size=$(object_total_inflated_size)\n \t\tobjects.commits.inflated_size=9225\n \t\tobjects.trees.inflated_size=28554\n \t\tobjects.blobs.inflated_size=453\n \t\tobjects.tags.inflated_size=132\n+\t\tobjects.max_inflated_size=$(object_max_inflated_size)\n+\t\tobjects.commits.max_inflated_size=$(object_type_max_inflated_size commit)\n+\t\tobjects.trees.max_inflated_size=$(object_type_max_inflated_size tree)\n+\t\tobjects.blobs.max_inflated_size=$(object_type_max_inflated_size blob)\n+\t\tobjects.tags.max_inflated_size=$(object_type_max_inflated_size tag)\n+\t\tobjects.disk_size=$(object_type_disk_usage all)\n+\t\tobjects.max_disk_size=$(object_max_disk_size)\n+\t\tobjects.commits.max_disk_size=$(object_type_max_disk_size commit)\n+\t\tobjects.trees.max_disk_size=$(object_type_max_disk_size tree)\n+\t\tobjects.blobs.max_disk_size=$(object_type_max_disk_size blob)\n+\t\tobjects.tags.max_disk_size=$(object_type_max_disk_size tag)\n+\t\tobjects.commits.max_parent_count=$(commit_max_parent_count)\n+\t\tobjects.trees.max_entry_count=$(tree_max_entry_count)\n+\t\tobjects.blobs.max_path_length=$(blob_max_path_length)\n+\t\tobjects.blobs.max_path_depth=$(blob_max_path_depth)\n+\t\tobjects.tags.max_chain_depth=$(tag_max_chain_depth)\n \t\tobjects.commits.disk_size=$(object_type_disk_usage commit)\n \t\tobjects.trees.disk_size=$(object_type_disk_usage tree)\n \t\tobjects.blobs.disk_size=$(object_type_disk_usage blob)\n-- \ngitgitgadget\n\n"},{"id":"537245","messageId":"f17c0f03e5daf6b905cbdba88285ee9d8e371f3f.1772140487.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v4.git.git.1772140487.gitgitgadget@gmail.com","subject":"[PATCH v4 09/10] docs: describe repo info path keys and structure metrics","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T21:14:46Z","receivedAt":"2026-02-26T21:15:00Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nDocument the newly added repo info capabilities, including\ncategory keys and path-oriented key definitions.\n\nAlso describe --path-format behavior for path outputs.\n\nUpdate git repo structure documentation to cover newly reported\nmaxima and aggregate keyvalue/nul fields.\n\nThis keeps command behavior and output keys fully specified for\nusers and scripts.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n Documentation/git-repo.adoc | 67 ++++++++++++++++++++++++++++++++++---\n 1 file changed, 63 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 7d70270dfa..b575977a4b 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -8,7 +8,7 @@ git-repo - Retrieve information about the repository\n SYNOPSIS\n --------\n [synopsis]\n-git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]\n+git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\n git repo structure [--format=(table|keyvalue|nul) | -z]\n \n DESCRIPTION\n@@ -44,6 +44,11 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n+`--path-format=(absolute|relative)`:::\n+\tControls formatting for keys in the `path` category. The default is\n+\t`absolute`. This option may be specified multiple times; the last one\n+\tspecified takes effect.\n+\n `structure [--format=(table|keyvalue|nul) | -z]`::\n \tRetrieve statistics about the current repository structure. The\n \tfollowing kinds of information are reported:\n@@ -52,6 +57,12 @@ supported:\n * Reachable object counts categorized by type\n * Total inflated size of reachable objects by type\n * Total disk size of reachable objects by type\n+* Largest inflated reachable object size by type\n+* Largest disk size of a reachable object by type\n+* Largest parent count among reachable commits\n+* Largest entry count among reachable trees\n+* Longest and deepest path among reachable blobs\n+* Deepest annotated tag chain\n +\n The output format can be chosen through the flag `--format`. Three formats are\n supported:\n@@ -64,6 +75,7 @@ supported:\n `keyvalue`:::\n \tEach line of output contains a key-value pair for a repository stat.\n \tThe '=' character is used to delimit between the key and the value.\n+\tBoth aggregate metrics and per-type metrics are included.\n \tValues containing \"unusual\" characters are quoted as explained for the\n \tconfiguration variable `core.quotePath` (see linkgit:git-config[1]).\n \n@@ -78,9 +90,11 @@ supported:\n \n INFO KEYS\n ---------\n-In order to obtain a set of values from `git repo info`, you should provide\n-the keys that identify them. Here's a list of the available keys and the\n-values that they return:\n+In order to obtain values from `git repo info`, provide either individual keys\n+or category names. A category returns all keys within that category. For\n+example, `layout` returns both `layout.bare` and `layout.shallow`.\n+\n+Here's a list of the available keys and the values that they return:\n \n `layout.bare`::\n \t`true` if this is a bare repository, otherwise `false`.\n@@ -91,6 +105,51 @@ values that they return:\n `object.format`::\n \tThe object format (hash algorithm) used in the repository.\n \n+`path.common-dir`::\n+\tThe path to the common git directory.\n+\n+`path.config-file`::\n+\tThe path to the `config` file in the git directory.\n+\n+`path.git-dir`::\n+\tThe path to the git directory.\n+\n+`path.git-prefix`::\n+\tThe path of the current working directory relative to the top-level\n+\tdirectory.\n+\n+`path.grafts-file`::\n+\tThe path to the `info/grafts` file.\n+\n+`path.hooks-directory`::\n+\tThe path to the `hooks` directory.\n+\n+`path.index-file`::\n+\tThe path to the index file.\n+\n+`path.logs-directory`::\n+\tThe path to the `logs` directory.\n+\n+`path.objects-directory`::\n+\tThe path to the objects directory.\n+\n+`path.packed-refs-file`::\n+\tThe path to the `packed-refs` file.\n+\n+`path.refs-directory`::\n+\tThe path to the `refs` directory.\n+\n+`path.shallow-file`::\n+\tThe path to the `shallow` file.\n+\n+`path.superproject-working-tree`::\n+\tThe path to the superproject's working tree root, or an empty string\n+\twhen the repository is not used as a submodule.\n+\n+`path.toplevel`::\n+\tThe path to the top-level working tree directory, or an empty string\n+\tfor bare repositories.\n+\n `references.format`::\n \tThe reference storage format. The valid values are:\n +\n-- \ngitgitgadget\n\n"},{"id":"537246","messageId":"1bc100d6cab3b77badb5d79fb8133e22a35bd96e.1772140487.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v4.git.git.1772140487.gitgitgadget@gmail.com","subject":"[PATCH v4 10/10] repo: reduce repetition in structure keyvalue output","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T21:14:47Z","receivedAt":"2026-02-26T21:15:01Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nRefactor structure_keyvalue_print() to use small helpers for\nsingle-key and per-object-type metrics.\n\nThis makes the output section easier to review and reduces\ncopy/paste risk while keeping output keys unchanged.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 156 ++++++++++++++++++++++++-------------------------\n 1 file changed, 76 insertions(+), 80 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex cb70171412..ecd9d3aee5 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -821,6 +821,27 @@ static void stats_table_clear(struct stats_table *table)\n \tstring_list_clear(&table->rows, 1);\n }\n \n+static void print_keyvalue_size(const char *key, size_t value,\n+\t\t\t\t      char key_delim, char value_delim)\n+{\n+\tprintf(\"%s%c%\" PRIuMAX \"%c\", key, key_delim, (uintmax_t)value,\n+\t       value_delim);\n+}\n+\n+static void print_object_values(const struct object_values *values,\n+\t\t\t\tconst char *metric,\n+\t\t\t\tchar key_delim, char value_delim)\n+{\n+\tprintf(\"objects.commits.%s%c%\" PRIuMAX \"%c\", metric, key_delim,\n+\t       (uintmax_t)values->commits, value_delim);\n+\tprintf(\"objects.trees.%s%c%\" PRIuMAX \"%c\", metric, key_delim,\n+\t       (uintmax_t)values->trees, value_delim);\n+\tprintf(\"objects.blobs.%s%c%\" PRIuMAX \"%c\", metric, key_delim,\n+\t       (uintmax_t)values->blobs, value_delim);\n+\tprintf(\"objects.tags.%s%c%\" PRIuMAX \"%c\", metric, key_delim,\n+\t       (uintmax_t)values->tags, value_delim);\n+}\n+\n static void structure_keyvalue_print(struct repo_structure *stats,\n \t\t\t\t     char key_delim, char value_delim)\n {\n@@ -831,86 +852,61 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tsize_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);\n \tsize_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);\n \n-\tprintf(\"references.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)references_count_total, value_delim);\n-\n-\tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->refs.branches, value_delim);\n-\tprintf(\"references.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->refs.tags, value_delim);\n-\tprintf(\"references.remotes.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->refs.remotes, value_delim);\n-\tprintf(\"references.others.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->refs.others, value_delim);\n-\n-\tprintf(\"objects.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)object_count_total, value_delim);\n-\n-\tprintf(\"objects.commits.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.type_counts.commits, value_delim);\n-\tprintf(\"objects.trees.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.type_counts.trees, value_delim);\n-\tprintf(\"objects.blobs.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.type_counts.blobs, value_delim);\n-\tprintf(\"objects.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.type_counts.tags, value_delim);\n-\n-\tprintf(\"objects.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)inflated_size_total, value_delim);\n-\n-\tprintf(\"objects.commits.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.inflated_sizes.commits, value_delim);\n-\tprintf(\"objects.trees.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.inflated_sizes.trees, value_delim);\n-\tprintf(\"objects.blobs.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.inflated_sizes.blobs, value_delim);\n-\tprintf(\"objects.tags.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.inflated_sizes.tags, value_delim);\n-\n-\tprintf(\"objects.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)max_inflated_size, value_delim);\n-\tprintf(\"objects.commits.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_inflated_sizes.commits, value_delim);\n-\tprintf(\"objects.trees.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_inflated_sizes.trees, value_delim);\n-\tprintf(\"objects.blobs.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_inflated_sizes.blobs, value_delim);\n-\tprintf(\"objects.tags.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);\n-\n-\tprintf(\"objects.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)disk_size_total, value_delim);\n-\n-\tprintf(\"objects.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)max_disk_size, value_delim);\n-\tprintf(\"objects.commits.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_disk_sizes.commits, value_delim);\n-\tprintf(\"objects.trees.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_disk_sizes.trees, value_delim);\n-\tprintf(\"objects.blobs.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_disk_sizes.blobs, value_delim);\n-\tprintf(\"objects.tags.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);\n-\n-\tprintf(\"objects.commits.max_parent_count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_commit_parent_count, value_delim);\n-\tprintf(\"objects.trees.max_entry_count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_tree_entry_count, value_delim);\n-\tprintf(\"objects.blobs.max_path_length%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_blob_path_length, value_delim);\n-\tprintf(\"objects.blobs.max_path_depth%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_blob_path_depth, value_delim);\n-\tprintf(\"objects.tags.max_chain_depth%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_tag_chain_depth, value_delim);\n-\n-\tprintf(\"objects.commits.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);\n-\tprintf(\"objects.trees.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.disk_sizes.trees, value_delim);\n-\tprintf(\"objects.blobs.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.disk_sizes.blobs, value_delim);\n-\tprintf(\"objects.tags.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.disk_sizes.tags, value_delim);\n+\tprint_keyvalue_size(\"references.count\", references_count_total,\n+\t\t\t   key_delim, value_delim);\n+\n+\tprint_keyvalue_size(\"references.branches.count\", stats->refs.branches,\n+\t\t\t   key_delim, value_delim);\n+\tprint_keyvalue_size(\"references.tags.count\", stats->refs.tags,\n+\t\t\t   key_delim, value_delim);\n+\tprint_keyvalue_size(\"references.remotes.count\", stats->refs.remotes,\n+\t\t\t   key_delim, value_delim);\n+\tprint_keyvalue_size(\"references.others.count\", stats->refs.others,\n+\t\t\t   key_delim, value_delim);\n+\n+\tprint_keyvalue_size(\"objects.count\", object_count_total,\n+\t\t\t   key_delim, value_delim);\n+\n+\tprint_object_values(&stats->objects.type_counts, \"count\",\n+\t\t\t    key_delim, value_delim);\n+\n+\tprint_keyvalue_size(\"objects.inflated_size\", inflated_size_total,\n+\t\t\t   key_delim, value_delim);\n+\n+\tprint_object_values(&stats->objects.inflated_sizes, \"inflated_size\",\n+\t\t\t    key_delim, value_delim);\n+\n+\tprint_keyvalue_size(\"objects.max_inflated_size\", max_inflated_size,\n+\t\t\t   key_delim, value_delim);\n+\tprint_object_values(&stats->objects.max_inflated_sizes,\n+\t\t\t    \"max_inflated_size\", key_delim, value_delim);\n+\n+\tprint_keyvalue_size(\"objects.disk_size\", disk_size_total,\n+\t\t\t   key_delim, value_delim);\n+\n+\tprint_keyvalue_size(\"objects.max_disk_size\", max_disk_size,\n+\t\t\t   key_delim, value_delim);\n+\tprint_object_values(&stats->objects.max_disk_sizes, \"max_disk_size\",\n+\t\t\t    key_delim, value_delim);\n+\n+\tprint_keyvalue_size(\"objects.commits.max_parent_count\",\n+\t\t\t   stats->objects.max_commit_parent_count,\n+\t\t\t   key_delim, value_delim);\n+\tprint_keyvalue_size(\"objects.trees.max_entry_count\",\n+\t\t\t   stats->objects.max_tree_entry_count,\n+\t\t\t   key_delim, value_delim);\n+\tprint_keyvalue_size(\"objects.blobs.max_path_length\",\n+\t\t\t   stats->objects.max_blob_path_length,\n+\t\t\t   key_delim, value_delim);\n+\tprint_keyvalue_size(\"objects.blobs.max_path_depth\",\n+\t\t\t   stats->objects.max_blob_path_depth,\n+\t\t\t   key_delim, value_delim);\n+\tprint_keyvalue_size(\"objects.tags.max_chain_depth\",\n+\t\t\t   stats->objects.max_tag_chain_depth,\n+\t\t\t   key_delim, value_delim);\n+\n+\tprint_object_values(&stats->objects.disk_sizes, \"disk_size\",\n+\t\t\t    key_delim, value_delim);\n \n \tfflush(stdout);\n }\n-- \ngitgitgadget\n"},{"id":"537256","messageId":"xmqqikbjum4y.fsf@gitster.g","threadId":"65043","inReplyTo":"99c8058298d403508ab2a23ed06c36b8707989d0.1772140487.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 01/10] repo: teach info context and category keys","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-26T23:21:17Z","receivedAt":"2026-02-26T23:21:19Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Eslam reda ragheb via GitGitGadget\" <gitgitgadget@gmail.com>\nwrites:\n\n> From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n>\n> Introduce an explicit repo_info context for the repo info codepath\n> and thread it through value lookups and field printing.\n>\n> This removes direct coupling from these helpers to ad-hoc\n> repository globals and makes key retrieval logic easier to extend\n> safely.\n\nThe above makes it sound as if you are improving existing code where\nexisting helper functions are already making ad-hoc and unsafe\naccess to global variables, but I somehow doubt that is what is\nhappening here.\n\nWhat does \"these helpers\" exactly refer to in this sentence?  The\nones you will introduce in patch 02/10?\n\n    Currently helper functions of get_value_fn type receives the\n    output buffer and repository instance as parameters, but we are\n    about to teach \"repo info\" to retrieve information that requires\n    to know more than just the repository (e.g., the prefix given\n    when the command was invoked).  Introduce a new \"repo_info\"\n    structure and update get_value_fn to take it as a parameter.\n\nor something?\n\n> Also teach git repo info to accept category names (for example,\n> layout) and expand them to matching key.* entries in request\n> order.\n\nThat smells like an unrelated change.  Wouldn't it better to do so\nin a subsequent patch, so that each step will concentrate on doing\none thing and one thing well?\n"},{"id":"537257","messageId":"xmqqa4wvulre.fsf@gitster.g","threadId":"65043","inReplyTo":"6d5b9ff07566e1cc28a672cf1f47988e9c8c45da.1772140487.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 02/10] repo: add path keys to repo info","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-26T23:29:25Z","receivedAt":"2026-02-26T23:29:28Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Eslam reda ragheb via GitGitGadget\" <gitgitgadget@gmail.com>\nwrites:\n\n> From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n>\n> Add a path category to git repo info with key-value pairs that\n> mirror repository paths users commonly retrieve via rev-parse and\n> git-path lookups.\n> ...\n> +static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)\n> +{\n> +\tif (info->prefix)\n> +\t\tstrbuf_addstr(buf, info->prefix);\n> +\treturn 0;\n> +}\n\nCan (info->prefix && info->prefix[0] == '\\0') be possible?  If so,\n\"repo info path.git-prefix\" would not be able to help users who want\nto know between (info->prefix == NULL) and info->prefix being an\nempty string.\n\n"},{"id":"537278","messageId":"3c4d4909-4eb1-47f4-b601-8f877a07ddd5@gmail.com","threadId":"65043","inReplyTo":"6d5b9ff07566e1cc28a672cf1f47988e9c8c45da.1772140487.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 02/10] repo: add path keys to repo info","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-02-27T09:04:04Z","receivedAt":"2026-02-27T09:04:07Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Eslam\n\nOn 26/02/2026 21:14, Eslam reda ragheb via GitGitGadget wrote:\n> From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n> \n> Add a path category to git repo info with key-value pairs that\n> mirror repository paths users commonly retrieve via rev-parse and\n> git-path lookups.\n\nI think that makes sense, I'm not sure about some of the paths though, \nsee below.\n\n> This makes scripting against repo metadata more direct and avoids\n> shelling out to multiple commands for related paths.\n\nYou can get more than one path at a time from \"git rev-parse\" so I'm not \nsure what this is saying.\n\nIt would be helpful to include the tests and Documentation for the new \nkeys in this patch.\n\n> The new keys are introduced as explicit path.* entries in\n> repo_info_fields and are resolved through dedicated helpers.\n> \n> This keeps lookup behavior predictable and makes future path\n> additions straightforward.\n> \n> Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n> ---\n\n> @@ -74,6 +197,20 @@ static const struct field repo_info_fields[] = {\n>   \t{ \"layout.bare\", get_layout_bare },\n>   \t{ \"layout.shallow\", get_layout_shallow },\n>   \t{ \"object.format\", get_object_format },\n> +\t{ \"path.common-dir\", get_path_common_dir },\n> +\t{ \"path.config-file\", get_path_config_file },\n> +\t{ \"path.git-dir\", get_path_git_dir },\n> +\t{ \"path.git-prefix\", get_path_git_prefix },\n\nI'm not sure about calling this 'git-prefix', 'prefix' might be more \nappropriate as it is about prefixing paths in the worktree rather than \nthe git_dir.\n\n> +\t{ \"path.grafts-file\", get_path_grafts_file },\n> +\t{ \"path.hooks-directory\", get_path_hooks_directory },\n> +\t{ \"path.index-file\", get_path_index_file },\n> +\t{ \"path.logs-directory\", get_path_logs_directory },\n\nWe're moving away from file based refs and reflogs so I'm not sure \nadding this, pick-refs-file or refs-directory is a good idea as we \nshould not be encouraging people to access these files directly.\n\n> +\t{ \"path.objects-directory\", get_path_objects_directory },\n> +\t{ \"path.packed-refs-file\", get_path_packed_refs_file },\n> +\t{ \"path.refs-directory\", get_path_refs_directory },\n> +\t{ \"path.shallow-file\", get_path_shallow_file },\n> +\t{ \"path.superproject-working-tree\", get_path_superproject_working_tree },\n> +\t{ \"path.toplevel\", get_path_toplevel },\n\n'path.toplevel' matches the git-rev-parse option but 'path.work-tree' \nmight be more descriptive?\n\nWhat happens if 'path.toplevel' is requested in a bare repository?\n\nThanks\n\nPhillip\n\n>   \t{ \"references.format\", get_references_format },\n>   };\n>   \n\n"},{"id":"537323","messageId":"pull.2208.v5.git.git.1772220640.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v4.git.git.1772140487.gitgitgadget@gmail.com","subject":"[PATCH v5 00/11] repo info: add category/path keys and --path-format","fromName":"eslam reda via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-27T19:30:29Z","receivedAt":"2026-02-27T19:30:43Z","isPatch":true,"sender":{"key":"name:eslam reda","avatar":null},"body":"\nThis series now focuses only on git repo info improvements.\n===========================================================\n\nIt introduces category-aware key requests, adds path-oriented keys (path.*),\nand adds --path-format=(absolute|relative) so scripts can request stable\npath rendering behavior.\n\n\nWhat this PR does\n=================\n\nFor git repo info, this series:\n\n * introduces explicit info-context plumbing in the codepath,\n * adds category-key expansion (for example, layout expands to layout.*),\n * adds path-oriented keys (path.*) for common repository locations,\n * adds --path-format=(absolute|relative) to control path output style.\n\nTests and documentation are updated accordingly.\n\n\nWhat this PR does NOT do\n========================\n\n * No git repo structure feature changes.\n * No t1901 structure test changes.\n * No structure metrics/docs additions.\n\n\nWhy this change\n===============\n\n * Makes git repo info more script-friendly by reducing the need for\n   multiple plumbing calls.\n * Improves output ergonomics through category requests and explicit path\n   formatting.\n * Keeps this series narrowly scoped and non-overlapping with in-flight repo\n   structure work.\n\n\nCommit structure\n================\n\n * repo: teach info context and category keys\n * repo: add path keys to repo info\n * repo: add --path-format for info path output\n * t1900: cover repo info path keys and path-format\n * docs: describe repo info path keys\n\nAll commits are signed off with the same real-name identity.\n\n\nChanges since previous revision\n===============================\n\n * Dropped all repo structure code, tests, and docs from this PR.\n * Kept only the repo info subset and matching t1900/documentation updates.\n * Preserved split, review-friendly commit structure.\n\n\nValidation\n==========\n\nFocused:\n\n * make -C t test T=t1900-repo.sh (Linux container): passed.\n\nFull:\n\n * make test in Linux Docker environment: failed 0 (with expected\n   prereq-based broken/skipped categories).\n\nEslam reda ragheb (11):\n  repo: teach info context and category keys\n  repo: add path keys to repo info\n  repo: add --path-format for info path output\n  repo: add structure max object size metrics\n  repo: add structure topology and path-depth metrics\n  repo: add aggregate structure totals to keyvalue output\n  t1900: cover repo info path keys and path-format\n  t1901: extend structure metric coverage and portability\n  docs: describe repo info path keys and structure metrics\n  repo: reduce repetition in structure keyvalue output\n  repo: refine path keys for repo info\n\n Documentation/git-repo.adoc |  61 +++-\n builtin/repo.c              | 559 ++++++++++++++++++++++++++++++++----\n t/t1900-repo.sh             | 192 +++++++++++++\n t/t1901-repo-structure.sh   | 250 ++++++++++++----\n 4 files changed, 942 insertions(+), 120 deletions(-)\n\n\nbase-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2208%2Feslam-reda-div%2Fgsoc-contribute-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2208/eslam-reda-div/gsoc-contribute-v5\nPull-Request: https://github.com/git/git/pull/2208\n\nRange-diff vs v4:\n\n  1:  99c8058298 =  1:  99c8058298 repo: teach info context and category keys\n  2:  6d5b9ff075 =  2:  6d5b9ff075 repo: add path keys to repo info\n  3:  5c438d045b =  3:  5c438d045b repo: add --path-format for info path output\n  4:  504d9cf7a0 =  4:  504d9cf7a0 repo: add structure max object size metrics\n  5:  4b502925c9 =  5:  4b502925c9 repo: add structure topology and path-depth metrics\n  6:  1751181950 =  6:  1751181950 repo: add aggregate structure totals to keyvalue output\n  7:  fd18f28db0 =  7:  fd18f28db0 t1900: cover repo info path keys and path-format\n  8:  0525ed4cd9 =  8:  0525ed4cd9 t1901: extend structure metric coverage and portability\n  9:  f17c0f03e5 =  9:  f17c0f03e5 docs: describe repo info path keys and structure metrics\n 10:  1bc100d6ca = 10:  1bc100d6ca repo: reduce repetition in structure keyvalue output\n  -:  ---------- > 11:  8af17ad831 repo: refine path keys for repo info\n\n-- \ngitgitgadget\n"},{"id":"537324","messageId":"99c8058298d403508ab2a23ed06c36b8707989d0.1772220640.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v5.git.git.1772220640.gitgitgadget@gmail.com","subject":"[PATCH v5 01/11] repo: teach info context and category keys","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-27T19:30:30Z","receivedAt":"2026-02-27T19:30:44Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nIntroduce an explicit repo_info context for the repo info codepath\nand thread it through value lookups and field printing.\n\nThis removes direct coupling from these helpers to ad-hoc\nrepository globals and makes key retrieval logic easier to extend\nsafely.\n\nAlso teach git repo info to accept category names (for example,\nlayout) and expand them to matching key.* entries in request\norder.\n\nThis improves script ergonomics while preserving existing behavior\nfor explicit keys and clear errors for unknown names.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 70 +++++++++++++++++++++++++++++++++++++++-----------\n 1 file changed, 55 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 0ea045abc1..e34914a9a7 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -22,7 +22,12 @@ static const char *const repo_usage[] = {\n \tNULL\n };\n \n-typedef int get_value_fn(struct repository *repo, struct strbuf *buf);\n+struct repo_info {\n+\tstruct repository *repo;\n+\tconst char *prefix;\n+};\n+\n+typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);\n \n enum output_format {\n \tFORMAT_TABLE,\n@@ -35,27 +40,30 @@ struct field {\n \tget_value_fn *get_value;\n };\n \n-static int get_layout_bare(struct repository *repo UNUSED, struct strbuf *buf)\n+static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)\n {\n \tstrbuf_addstr(buf, is_bare_repository() ? \"true\" : \"false\");\n \treturn 0;\n }\n \n-static int get_layout_shallow(struct repository *repo, struct strbuf *buf)\n+static int get_layout_shallow(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf,\n \t\t      is_repository_shallow(repo) ? \"true\" : \"false\");\n \treturn 0;\n }\n \n-static int get_object_format(struct repository *repo, struct strbuf *buf)\n+static int get_object_format(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf, repo->hash_algo->name);\n \treturn 0;\n }\n \n-static int get_references_format(struct repository *repo, struct strbuf *buf)\n+static int get_references_format(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf,\n \t\t      ref_storage_format_to_name(repo->ref_storage_format));\n \treturn 0;\n@@ -87,6 +95,33 @@ static get_value_fn *get_value_fn_for_key(const char *key)\n \treturn found ? found->get_value : NULL;\n }\n \n+static void print_field(enum output_format format, const char *key,\n+\t\t\tconst char *value);\n+\n+static int print_category_fields(const char *category,\n+\t\t\t\t struct repo_info *info,\n+\t\t\t\t enum output_format format,\n+\t\t\t\t struct strbuf *valbuf)\n+{\n+\tint found = 0;\n+\tsize_t category_len = strlen(category);\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {\n+\t\tconst struct field *field = &repo_info_fields[i];\n+\n+\t\tif (!starts_with(field->key, category) ||\n+\t\t    field->key[category_len] != '.')\n+\t\t\tcontinue;\n+\n+\t\tstrbuf_reset(valbuf);\n+\t\tfield->get_value(info, valbuf);\n+\t\tprint_field(format, field->key, valbuf->buf);\n+\t\tfound = 1;\n+\t}\n+\n+\treturn found;\n+}\n+\n static void print_field(enum output_format format, const char *key,\n \t\t\tconst char *value)\n {\n@@ -105,7 +140,7 @@ static void print_field(enum output_format format, const char *key,\n }\n \n static int print_fields(int argc, const char **argv,\n-\t\t\tstruct repository *repo,\n+\t\t\tstruct repo_info *info,\n \t\t\tenum output_format format)\n {\n \tint ret = 0;\n@@ -117,21 +152,22 @@ static int print_fields(int argc, const char **argv,\n \n \t\tget_value = get_value_fn_for_key(key);\n \n-\t\tif (!get_value) {\n-\t\t\tret = error(_(\"key '%s' not found\"), key);\n+\t\tif (get_value) {\n+\t\t\tstrbuf_reset(&valbuf);\n+\t\t\tget_value(info, &valbuf);\n+\t\t\tprint_field(format, key, valbuf.buf);\n \t\t\tcontinue;\n \t\t}\n \n-\t\tstrbuf_reset(&valbuf);\n-\t\tget_value(repo, &valbuf);\n-\t\tprint_field(format, key, valbuf.buf);\n+\t\tif (!print_category_fields(key, info, format, &valbuf))\n+\t\t\tret = error(_(\"key '%s' not found\"), key);\n \t}\n \n \tstrbuf_release(&valbuf);\n \treturn ret;\n }\n \n-static int print_all_fields(struct repository *repo,\n+static int print_all_fields(struct repo_info *info,\n \t\t\t    enum output_format format)\n {\n \tstruct strbuf valbuf = STRBUF_INIT;\n@@ -140,7 +176,7 @@ static int print_all_fields(struct repository *repo,\n \t\tconst struct field *field = &repo_info_fields[i];\n \n \t\tstrbuf_reset(&valbuf);\n-\t\tfield->get_value(repo, &valbuf);\n+\t\tfield->get_value(info, &valbuf);\n \t\tprint_field(format, field->key, valbuf.buf);\n \t}\n \n@@ -171,6 +207,10 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\t\t struct repository *repo)\n {\n \tenum output_format format = FORMAT_KEYVALUE;\n+\tstruct repo_info info = {\n+\t\t.repo = repo,\n+\t\t.prefix = prefix,\n+\t};\n \tint all_keys = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n@@ -192,9 +232,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"--all and <key> cannot be used together\"));\n \n \tif (all_keys)\n-\t\treturn print_all_fields(repo, format);\n+\t\treturn print_all_fields(&info, format);\n \telse\n-\t\treturn print_fields(argc, argv, repo, format);\n+\t\treturn print_fields(argc, argv, &info, format);\n }\n \n struct ref_stats {\n-- \ngitgitgadget\n\n"},{"id":"537325","messageId":"6d5b9ff07566e1cc28a672cf1f47988e9c8c45da.1772220640.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v5.git.git.1772220640.gitgitgadget@gmail.com","subject":"[PATCH v5 02/11] repo: add path keys to repo info","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-27T19:30:31Z","receivedAt":"2026-02-27T19:30:46Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nAdd a path category to git repo info with key-value pairs that\nmirror repository paths users commonly retrieve via rev-parse and\ngit-path lookups.\n\nThis makes scripting against repo metadata more direct and avoids\nshelling out to multiple commands for related paths.\n\nThe new keys are introduced as explicit path.* entries in\nrepo_info_fields and are resolved through dedicated helpers.\n\nThis keeps lookup behavior predictable and makes future path\nadditions straightforward.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 137 +++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 137 insertions(+)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex e34914a9a7..35e1eaf7d7 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -1,10 +1,12 @@\n #define USE_THE_REPOSITORY_VARIABLE\n \n #include \"builtin.h\"\n+#include \"abspath.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n #include \"odb.h\"\n #include \"parse-options.h\"\n+#include \"path.h\"\n #include \"path-walk.h\"\n #include \"progress.h\"\n #include \"quote.h\"\n@@ -14,6 +16,7 @@\n #include \"strbuf.h\"\n #include \"string-list.h\"\n #include \"shallow.h\"\n+#include \"submodule.h\"\n #include \"utf8.h\"\n \n static const char *const repo_usage[] = {\n@@ -40,6 +43,13 @@ struct field {\n \tget_value_fn *get_value;\n };\n \n+static void repo_info_add_path(struct repo_info *info,\n+\t\t\t      struct strbuf *buf,\n+\t\t\t      const char *path)\n+{\n+\tstrbuf_add_absolute_path(buf, path);\n+}\n+\n static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)\n {\n \tstrbuf_addstr(buf, is_bare_repository() ? \"true\" : \"false\");\n@@ -61,6 +71,119 @@ static int get_object_format(struct repo_info *info, struct strbuf *buf)\n \treturn 0;\n }\n \n+static int get_path_common_dir(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_common_dir(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_config_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"config\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_git_dir(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)\n+{\n+\tif (info->prefix)\n+\t\tstrbuf_addstr(buf, info->prefix);\n+\treturn 0;\n+}\n+\n+static int get_path_grafts_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_graft_file(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_hooks_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"hooks\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_index_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_index_file(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"logs\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_object_directory(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"packed-refs\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"refs\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"shallow\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_superproject_working_tree(struct repo_info *info,\n+\t\t\t\t\t     struct strbuf *buf)\n+{\n+\tstruct strbuf superproject = STRBUF_INIT;\n+\n+\tif (get_superproject_working_tree(&superproject))\n+\t\trepo_info_add_path(info, buf, superproject.buf);\n+\n+\tstrbuf_release(&superproject);\n+\treturn 0;\n+}\n+\n+static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)\n+{\n+\tconst char *work_tree = repo_get_work_tree(info->repo);\n+\n+\tif (work_tree)\n+\t\trepo_info_add_path(info, buf, work_tree);\n+\n+\treturn 0;\n+}\n+\n static int get_references_format(struct repo_info *info, struct strbuf *buf)\n {\n \tstruct repository *repo = info->repo;\n@@ -74,6 +197,20 @@ static const struct field repo_info_fields[] = {\n \t{ \"layout.bare\", get_layout_bare },\n \t{ \"layout.shallow\", get_layout_shallow },\n \t{ \"object.format\", get_object_format },\n+\t{ \"path.common-dir\", get_path_common_dir },\n+\t{ \"path.config-file\", get_path_config_file },\n+\t{ \"path.git-dir\", get_path_git_dir },\n+\t{ \"path.git-prefix\", get_path_git_prefix },\n+\t{ \"path.grafts-file\", get_path_grafts_file },\n+\t{ \"path.hooks-directory\", get_path_hooks_directory },\n+\t{ \"path.index-file\", get_path_index_file },\n+\t{ \"path.logs-directory\", get_path_logs_directory },\n+\t{ \"path.objects-directory\", get_path_objects_directory },\n+\t{ \"path.packed-refs-file\", get_path_packed_refs_file },\n+\t{ \"path.refs-directory\", get_path_refs_directory },\n+\t{ \"path.shallow-file\", get_path_shallow_file },\n+\t{ \"path.superproject-working-tree\", get_path_superproject_working_tree },\n+\t{ \"path.toplevel\", get_path_toplevel },\n \t{ \"references.format\", get_references_format },\n };\n \n-- \ngitgitgadget\n\n"},{"id":"537326","messageId":"5c438d045be1becc10b687a772055df5745372ae.1772220640.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v5.git.git.1772220640.gitgitgadget@gmail.com","subject":"[PATCH v5 03/11] repo: add --path-format for info path output","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-27T19:30:32Z","receivedAt":"2026-02-27T19:30:47Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nTeach git repo info to accept --path-format=(absolute|relative)\nso scripts can request stable path style explicitly.\n\nThis aligns path.* output behavior with existing rev-parse usage\npatterns and reduces ad-hoc path conversion in callers.\n\nThe option is wired through repo_info context and used by\nrepo_info_add_path(), so path formatting remains centralized and\nconsistent across all path.* keys.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 37 ++++++++++++++++++++++++++++++++++++-\n 1 file changed, 36 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 35e1eaf7d7..e5078e5459 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -20,14 +20,20 @@\n #include \"utf8.h\"\n \n static const char *const repo_usage[] = {\n-\t\"git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]\",\n+\t\"git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\",\n \t\"git repo structure [--format=(table|keyvalue|nul) | -z]\",\n \tNULL\n };\n \n+enum path_format {\n+\tPATH_FORMAT_ABSOLUTE,\n+\tPATH_FORMAT_RELATIVE,\n+};\n+\n struct repo_info {\n \tstruct repository *repo;\n \tconst char *prefix;\n+\tenum path_format path_format;\n };\n \n typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);\n@@ -47,6 +53,16 @@ static void repo_info_add_path(struct repo_info *info,\n \t\t\t      struct strbuf *buf,\n \t\t\t      const char *path)\n {\n+\tif (info->path_format == PATH_FORMAT_RELATIVE) {\n+\t\tchar *cwd = xgetcwd();\n+\t\tstruct strbuf rel_path = STRBUF_INIT;\n+\n+\t\tstrbuf_addstr(buf, relative_path(path, cwd, &rel_path));\n+\t\tstrbuf_release(&rel_path);\n+\t\tfree(cwd);\n+\t\treturn;\n+\t}\n+\n \tstrbuf_add_absolute_path(buf, path);\n }\n \n@@ -340,6 +356,21 @@ static int parse_format_cb(const struct option *opt,\n \treturn 0;\n }\n \n+static int parse_path_format_cb(const struct option *opt,\n+\t\t\t\tconst char *arg, int unset UNUSED)\n+{\n+\tenum path_format *path_format = opt->value;\n+\n+\tif (!strcmp(arg, \"absolute\"))\n+\t\t*path_format = PATH_FORMAT_ABSOLUTE;\n+\telse if (!strcmp(arg, \"relative\"))\n+\t\t*path_format = PATH_FORMAT_RELATIVE;\n+\telse\n+\t\tdie(_(\"invalid path format '%s'\"), arg);\n+\n+\treturn 0;\n+}\n+\n static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\t\t struct repository *repo)\n {\n@@ -347,6 +378,7 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \tstruct repo_info info = {\n \t\t.repo = repo,\n \t\t.prefix = prefix,\n+\t\t.path_format = PATH_FORMAT_ABSOLUTE,\n \t};\n \tint all_keys = 0;\n \tstruct option options[] = {\n@@ -357,6 +389,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\t\t       N_(\"synonym for --format=nul\"),\n \t\t\t       PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n \t\t\t       parse_format_cb),\n+\t\tOPT_CALLBACK_F(0, \"path-format\", &info.path_format,\n+\t\t\t       N_(\"format\"), N_(\"path output format\"),\n+\t\t\t       PARSE_OPT_NONEG, parse_path_format_cb),\n \t\tOPT_BOOL(0, \"all\", &all_keys, N_(\"print all keys/values\")),\n \t\tOPT_END()\n \t};\n-- \ngitgitgadget\n\n"},{"id":"537327","messageId":"504d9cf7a0dbd663ea88c75217e1564504a60937.1772220640.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v5.git.git.1772220640.gitgitgadget@gmail.com","subject":"[PATCH v5 04/11] repo: add structure max object size metrics","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-27T19:30:33Z","receivedAt":"2026-02-27T19:30:48Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nExtend git repo structure with maximum inflated and on-disk object\nsizes, both per type and overall max values.\n\nThis complements existing totals by highlighting outliers that\noften drive repository bloat analysis.\n\nThe implementation updates object counting to track per-type maxima\nwhile walking reachable objects.\n\nIt exposes those values in both table and keyvalue formats for\nscripts and human output.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 87 +++++++++++++++++++++++++++++++++++++++++++++++++-\n 1 file changed, 86 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex e5078e5459..a2fc3fd8cc 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -426,7 +426,9 @@ struct object_values {\n struct object_stats {\n \tstruct object_values type_counts;\n \tstruct object_values inflated_sizes;\n+\tstruct object_values max_inflated_sizes;\n \tstruct object_values disk_sizes;\n+\tstruct object_values max_disk_sizes;\n };\n \n struct repo_structure {\n@@ -529,6 +531,20 @@ static inline size_t get_total_object_values(struct object_values *values)\n \treturn values->tags + values->commits + values->trees + values->blobs;\n }\n \n+static inline size_t get_max_object_value(struct object_values *values)\n+{\n+\tsize_t max = values->commits;\n+\n+\tif (values->trees > max)\n+\t\tmax = values->trees;\n+\tif (values->blobs > max)\n+\t\tmax = values->blobs;\n+\tif (values->tags > max)\n+\t\tmax = values->tags;\n+\n+\treturn max;\n+}\n+\n static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t\t\tstruct repo_structure *stats)\n {\n@@ -583,6 +599,26 @@ static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t      \"    * %s\", _(\"Blobs\"));\n \tstats_table_size_addf(table, objects->disk_sizes.tags,\n \t\t\t      \"    * %s\", _(\"Tags\"));\n+\n+\tstats_table_size_addf(table, objects->max_inflated_sizes.commits,\n+\t\t\t      \"  * %s\", _(\"Largest commit\"));\n+\tstats_table_size_addf(table, objects->max_inflated_sizes.trees,\n+\t\t\t      \"  * %s\", _(\"Largest tree\"));\n+\tstats_table_size_addf(table, objects->max_inflated_sizes.blobs,\n+\t\t\t      \"  * %s\", _(\"Largest blob\"));\n+\tstats_table_size_addf(table, objects->max_inflated_sizes.tags,\n+\t\t\t      \"  * %s\", _(\"Largest tag\"));\n+\n+\tstats_table_size_addf(table, get_max_object_value(&objects->max_disk_sizes),\n+\t\t\t      \"  * %s\", _(\"Largest disk size\"));\n+\tstats_table_size_addf(table, objects->max_disk_sizes.commits,\n+\t\t\t      \"    * %s\", _(\"Commits\"));\n+\tstats_table_size_addf(table, objects->max_disk_sizes.trees,\n+\t\t\t      \"    * %s\", _(\"Trees\"));\n+\tstats_table_size_addf(table, objects->max_disk_sizes.blobs,\n+\t\t\t      \"    * %s\", _(\"Blobs\"));\n+\tstats_table_size_addf(table, objects->max_disk_sizes.tags,\n+\t\t\t      \"    * %s\", _(\"Tags\"));\n }\n \n static void stats_table_print_structure(const struct stats_table *table)\n@@ -661,6 +697,9 @@ static void stats_table_clear(struct stats_table *table)\n static void structure_keyvalue_print(struct repo_structure *stats,\n \t\t\t\t     char key_delim, char value_delim)\n {\n+\tsize_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);\n+\tsize_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);\n+\n \tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->refs.branches, value_delim);\n \tprintf(\"references.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n@@ -688,6 +727,28 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"objects.tags.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.inflated_sizes.tags, value_delim);\n \n+\tprintf(\"objects.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)max_inflated_size, value_delim);\n+\tprintf(\"objects.commits.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_inflated_sizes.commits, value_delim);\n+\tprintf(\"objects.trees.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_inflated_sizes.trees, value_delim);\n+\tprintf(\"objects.blobs.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_inflated_sizes.blobs, value_delim);\n+\tprintf(\"objects.tags.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);\n+\n+\tprintf(\"objects.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)max_disk_size, value_delim);\n+\tprintf(\"objects.commits.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_disk_sizes.commits, value_delim);\n+\tprintf(\"objects.trees.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_disk_sizes.trees, value_delim);\n+\tprintf(\"objects.blobs.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_disk_sizes.blobs, value_delim);\n+\tprintf(\"objects.tags.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);\n+\n \tprintf(\"objects.commits.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);\n \tprintf(\"objects.trees.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n@@ -772,6 +833,8 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \tstruct object_stats *stats = data->stats;\n \tsize_t inflated_total = 0;\n \tsize_t disk_total = 0;\n+\tsize_t max_inflated = 0;\n+\tsize_t max_disk = 0;\n \tsize_t object_count;\n \n \tfor (size_t i = 0; i < oids->nr; i++) {\n@@ -786,31 +849,53 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\t\t\t\t  OBJECT_INFO_SKIP_FETCH_OBJECT |\n \t\t\t\t\t\t  OBJECT_INFO_QUICK) < 0)\n \t\t\tcontinue;\n+\t\tif (disk < 0)\n+\t\t\tcontinue;\n \n \t\tinflated_total += inflated;\n-\t\tdisk_total += disk;\n+\t\tdisk_total += (size_t)disk;\n+\t\tif (inflated > max_inflated)\n+\t\t\tmax_inflated = inflated;\n+\t\tif ((size_t)disk > max_disk)\n+\t\t\tmax_disk = (size_t)disk;\n \t}\n \n \tswitch (type) {\n \tcase OBJ_TAG:\n \t\tstats->type_counts.tags += oids->nr;\n \t\tstats->inflated_sizes.tags += inflated_total;\n+\t\tif (max_inflated > stats->max_inflated_sizes.tags)\n+\t\t\tstats->max_inflated_sizes.tags = max_inflated;\n \t\tstats->disk_sizes.tags += disk_total;\n+\t\tif (max_disk > stats->max_disk_sizes.tags)\n+\t\t\tstats->max_disk_sizes.tags = max_disk;\n \t\tbreak;\n \tcase OBJ_COMMIT:\n \t\tstats->type_counts.commits += oids->nr;\n \t\tstats->inflated_sizes.commits += inflated_total;\n+\t\tif (max_inflated > stats->max_inflated_sizes.commits)\n+\t\t\tstats->max_inflated_sizes.commits = max_inflated;\n \t\tstats->disk_sizes.commits += disk_total;\n+\t\tif (max_disk > stats->max_disk_sizes.commits)\n+\t\t\tstats->max_disk_sizes.commits = max_disk;\n \t\tbreak;\n \tcase OBJ_TREE:\n \t\tstats->type_counts.trees += oids->nr;\n \t\tstats->inflated_sizes.trees += inflated_total;\n+\t\tif (max_inflated > stats->max_inflated_sizes.trees)\n+\t\t\tstats->max_inflated_sizes.trees = max_inflated;\n \t\tstats->disk_sizes.trees += disk_total;\n+\t\tif (max_disk > stats->max_disk_sizes.trees)\n+\t\t\tstats->max_disk_sizes.trees = max_disk;\n \t\tbreak;\n \tcase OBJ_BLOB:\n \t\tstats->type_counts.blobs += oids->nr;\n \t\tstats->inflated_sizes.blobs += inflated_total;\n+\t\tif (max_inflated > stats->max_inflated_sizes.blobs)\n+\t\t\tstats->max_inflated_sizes.blobs = max_inflated;\n \t\tstats->disk_sizes.blobs += disk_total;\n+\t\tif (max_disk > stats->max_disk_sizes.blobs)\n+\t\t\tstats->max_disk_sizes.blobs = max_disk;\n \t\tbreak;\n \tdefault:\n \t\tBUG(\"invalid object type\");\n-- \ngitgitgadget\n\n"},{"id":"537328","messageId":"4b502925c9d70d37e0752fadfe061f1cdf692488.1772220640.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v5.git.git.1772220640.gitgitgadget@gmail.com","subject":"[PATCH v5 05/11] repo: add structure topology and path-depth metrics","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-27T19:30:34Z","receivedAt":"2026-02-27T19:30:50Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nTrack additional structure-oriented maxima that are useful when\ndiagnosing unusually complex histories.\n\nThese include commit parent fanout, tree entry count, blob path\nlength/depth, and annotated tag chain depth.\n\nThe counters are gathered while traversing reachable objects and\nare reported in both table and keyvalue output.\n\nThis lets both humans and scripts consume the same topology\nsignals.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 171 ++++++++++++++++++++++++++++++++++++++++++++++++-\n 1 file changed, 170 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex a2fc3fd8cc..f92c209469 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -17,6 +17,7 @@\n #include \"string-list.h\"\n #include \"shallow.h\"\n #include \"submodule.h\"\n+#include \"tree-walk.h\"\n #include \"utf8.h\"\n \n static const char *const repo_usage[] = {\n@@ -429,6 +430,11 @@ struct object_stats {\n \tstruct object_values max_inflated_sizes;\n \tstruct object_values disk_sizes;\n \tstruct object_values max_disk_sizes;\n+\tsize_t max_commit_parent_count;\n+\tsize_t max_tree_entry_count;\n+\tsize_t max_blob_path_length;\n+\tsize_t max_blob_path_depth;\n+\tsize_t max_tag_chain_depth;\n };\n \n struct repo_structure {\n@@ -545,6 +551,116 @@ static inline size_t get_max_object_value(struct object_values *values)\n \treturn max;\n }\n \n+static size_t get_commit_parent_count(struct repository *repo,\n+\t\t\t\t      const struct object_id *oid)\n+{\n+\tunsigned long size = 0;\n+\tconst char *cur;\n+\tconst char *end;\n+\tvoid *buf;\n+\tsize_t count = 0;\n+\n+\tbuf = odb_read_object_peeled(repo->objects, oid, OBJ_COMMIT, &size, NULL);\n+\tif (!buf)\n+\t\treturn 0;\n+\n+\tcur = buf;\n+\tend = cur + size;\n+\twhile (cur < end) {\n+\t\tconst char *newline = memchr(cur, '\\n', end - cur);\n+\t\tsize_t line_len;\n+\n+\t\tif (!newline)\n+\t\t\tbreak;\n+\t\tline_len = newline - cur;\n+\t\tif (!line_len)\n+\t\t\tbreak;\n+\n+\t\tif (line_len > 7 && !memcmp(cur, \"parent \", 7))\n+\t\t\tcount++;\n+\n+\t\tcur = newline + 1;\n+\t}\n+\n+\tfree(buf);\n+\treturn count;\n+}\n+\n+static size_t get_tree_entry_count(struct repository *repo,\n+\t\t\t\t   const struct object_id *oid)\n+{\n+\tstruct tree_desc desc;\n+\tstruct name_entry entry;\n+\tunsigned long size = 0;\n+\tvoid *buf;\n+\tsize_t count = 0;\n+\n+\tbuf = odb_read_object_peeled(repo->objects, oid, OBJ_TREE, &size, NULL);\n+\tif (!buf)\n+\t\treturn 0;\n+\n+\tinit_tree_desc(&desc, oid, buf, size);\n+\twhile (tree_entry(&desc, &entry))\n+\t\tcount++;\n+\n+\tfree(buf);\n+\treturn count;\n+}\n+\n+static size_t get_path_depth(const char *path)\n+{\n+\tsize_t depth = 0;\n+\n+\tif (!path || !*path)\n+\t\treturn 0;\n+\n+\tdepth = 1;\n+\tfor (const char *cur = path; *cur; cur++)\n+\t\tif (*cur == '/')\n+\t\t\tdepth++;\n+\n+\treturn depth;\n+}\n+\n+static size_t get_tag_chain_depth(struct repository *repo,\n+\t\t\t\t  const struct object_id *oid)\n+{\n+\tstruct object_id current = *oid;\n+\tsize_t depth = 0;\n+\n+\twhile (1) {\n+\t\tenum object_type type;\n+\t\tunsigned long size = 0;\n+\t\tstruct object_id next;\n+\t\tconst char *p, *end;\n+\t\tvoid *buf = odb_read_object(repo->objects, &current, &type, &size);\n+\n+\t\tif (!buf)\n+\t\t\tbreak;\n+\t\tif (type != OBJ_TAG) {\n+\t\t\tfree(buf);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tp = buf;\n+\t\tif (!skip_prefix(p, \"object \", &p) ||\n+\t\t    parse_oid_hex_algop(p, &next, &end, repo->hash_algo) ||\n+\t\t    *end != '\\n') {\n+\t\t\tfree(buf);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tdepth++;\n+\t\tfree(buf);\n+\n+\t\tif (oideq(&next, &current))\n+\t\t\tbreak;\n+\t\toidcpy(&current, &next);\n+\t}\n+\n+\treturn depth;\n+}\n+\n static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t\t\tstruct repo_structure *stats)\n {\n@@ -619,6 +735,17 @@ static void stats_table_setup_structure(struct stats_table *table,\n \t\t\t      \"    * %s\", _(\"Blobs\"));\n \tstats_table_size_addf(table, objects->max_disk_sizes.tags,\n \t\t\t      \"    * %s\", _(\"Tags\"));\n+\n+\tstats_table_count_addf(table, objects->max_commit_parent_count,\n+\t\t\t       \"  * %s\", _(\"Largest parent count\"));\n+\tstats_table_count_addf(table, objects->max_tree_entry_count,\n+\t\t\t       \"  * %s\", _(\"Largest tree entries\"));\n+\tstats_table_count_addf(table, objects->max_blob_path_length,\n+\t\t\t       \"  * %s\", _(\"Longest blob path\"));\n+\tstats_table_count_addf(table, objects->max_blob_path_depth,\n+\t\t\t       \"  * %s\", _(\"Deepest blob path\"));\n+\tstats_table_count_addf(table, objects->max_tag_chain_depth,\n+\t\t\t       \"  * %s\", _(\"Deepest tag chain\"));\n }\n \n static void stats_table_print_structure(const struct stats_table *table)\n@@ -749,6 +876,17 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"objects.tags.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);\n \n+\tprintf(\"objects.commits.max_parent_count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_commit_parent_count, value_delim);\n+\tprintf(\"objects.trees.max_entry_count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_tree_entry_count, value_delim);\n+\tprintf(\"objects.blobs.max_path_length%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_blob_path_length, value_delim);\n+\tprintf(\"objects.blobs.max_path_depth%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_blob_path_depth, value_delim);\n+\tprintf(\"objects.tags.max_chain_depth%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)stats->objects.max_tag_chain_depth, value_delim);\n+\n \tprintf(\"objects.commits.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);\n \tprintf(\"objects.trees.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n@@ -826,7 +964,7 @@ struct count_objects_data {\n \tstruct progress *progress;\n };\n \n-static int count_objects(const char *path UNUSED, struct oid_array *oids,\n+static int count_objects(const char *path, struct oid_array *oids,\n \t\t\t enum object_type type, void *cb_data)\n {\n \tstruct count_objects_data *data = cb_data;\n@@ -862,6 +1000,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \n \tswitch (type) {\n \tcase OBJ_TAG:\n+\t\tfor (size_t i = 0; i < oids->nr; i++) {\n+\t\t\tsize_t tag_chain_depth = get_tag_chain_depth(data->odb->repo,\n+\t\t\t\t\t\t\t     &oids->oid[i]);\n+\t\t\tif (tag_chain_depth > stats->max_tag_chain_depth)\n+\t\t\t\tstats->max_tag_chain_depth = tag_chain_depth;\n+\t\t}\n+\n \t\tstats->type_counts.tags += oids->nr;\n \t\tstats->inflated_sizes.tags += inflated_total;\n \t\tif (max_inflated > stats->max_inflated_sizes.tags)\n@@ -871,6 +1016,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\tstats->max_disk_sizes.tags = max_disk;\n \t\tbreak;\n \tcase OBJ_COMMIT:\n+\t\tfor (size_t i = 0; i < oids->nr; i++) {\n+\t\t\tsize_t parent_count = get_commit_parent_count(data->odb->repo,\n+\t\t\t\t\t\t\t     &oids->oid[i]);\n+\t\t\tif (parent_count > stats->max_commit_parent_count)\n+\t\t\t\tstats->max_commit_parent_count = parent_count;\n+\t\t}\n+\n \t\tstats->type_counts.commits += oids->nr;\n \t\tstats->inflated_sizes.commits += inflated_total;\n \t\tif (max_inflated > stats->max_inflated_sizes.commits)\n@@ -880,6 +1032,13 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\tstats->max_disk_sizes.commits = max_disk;\n \t\tbreak;\n \tcase OBJ_TREE:\n+\t\tfor (size_t i = 0; i < oids->nr; i++) {\n+\t\t\tsize_t entry_count = get_tree_entry_count(data->odb->repo,\n+\t\t\t\t\t\t\t    &oids->oid[i]);\n+\t\t\tif (entry_count > stats->max_tree_entry_count)\n+\t\t\t\tstats->max_tree_entry_count = entry_count;\n+\t\t}\n+\n \t\tstats->type_counts.trees += oids->nr;\n \t\tstats->inflated_sizes.trees += inflated_total;\n \t\tif (max_inflated > stats->max_inflated_sizes.trees)\n@@ -889,6 +1048,16 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,\n \t\t\tstats->max_disk_sizes.trees = max_disk;\n \t\tbreak;\n \tcase OBJ_BLOB:\n+\t\tif (path && *path) {\n+\t\t\tsize_t path_len = strlen(path);\n+\t\t\tsize_t path_depth = get_path_depth(path);\n+\n+\t\t\tif (path_len > stats->max_blob_path_length)\n+\t\t\t\tstats->max_blob_path_length = path_len;\n+\t\t\tif (path_depth > stats->max_blob_path_depth)\n+\t\t\t\tstats->max_blob_path_depth = path_depth;\n+\t\t}\n+\n \t\tstats->type_counts.blobs += oids->nr;\n \t\tstats->inflated_sizes.blobs += inflated_total;\n \t\tif (max_inflated > stats->max_inflated_sizes.blobs)\n-- \ngitgitgadget\n\n"},{"id":"537329","messageId":"1751181950734d2571ef85a419ef62000458bd39.1772220640.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v5.git.git.1772220640.gitgitgadget@gmail.com","subject":"[PATCH v5 06/11] repo: add aggregate structure totals to keyvalue output","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-27T19:30:35Z","receivedAt":"2026-02-27T19:30:52Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nExpose aggregate totals alongside per-type values in structure\nkeyvalue/nul output: total references, total objects, total\ninflated size, and total disk size.\n\nThese totals remove the need for callers to recompute sums\nexternally.\n\nKeeping aggregate and per-type values in the same output format\nimproves script ergonomics.\n\nIt also keeps machine-readable output aligned with what table mode\nsummarizes for humans.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 16 ++++++++++++++++\n 1 file changed, 16 insertions(+)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex f92c209469..cb70171412 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -824,9 +824,16 @@ static void stats_table_clear(struct stats_table *table)\n static void structure_keyvalue_print(struct repo_structure *stats,\n \t\t\t\t     char key_delim, char value_delim)\n {\n+\tsize_t references_count_total = get_total_reference_count(&stats->refs);\n+\tsize_t object_count_total = get_total_object_values(&stats->objects.type_counts);\n+\tsize_t inflated_size_total = get_total_object_values(&stats->objects.inflated_sizes);\n+\tsize_t disk_size_total = get_total_object_values(&stats->objects.disk_sizes);\n \tsize_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);\n \tsize_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);\n \n+\tprintf(\"references.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)references_count_total, value_delim);\n+\n \tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->refs.branches, value_delim);\n \tprintf(\"references.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n@@ -836,6 +843,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"references.others.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->refs.others, value_delim);\n \n+\tprintf(\"objects.count%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)object_count_total, value_delim);\n+\n \tprintf(\"objects.commits.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.type_counts.commits, value_delim);\n \tprintf(\"objects.trees.count%c%\" PRIuMAX \"%c\", key_delim,\n@@ -845,6 +855,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"objects.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.type_counts.tags, value_delim);\n \n+\tprintf(\"objects.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)inflated_size_total, value_delim);\n+\n \tprintf(\"objects.commits.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.inflated_sizes.commits, value_delim);\n \tprintf(\"objects.trees.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n@@ -865,6 +878,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tprintf(\"objects.tags.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);\n \n+\tprintf(\"objects.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n+\t       (uintmax_t)disk_size_total, value_delim);\n+\n \tprintf(\"objects.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n \t       (uintmax_t)max_disk_size, value_delim);\n \tprintf(\"objects.commits.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-- \ngitgitgadget\n\n"},{"id":"537330","messageId":"fd18f28db0bac01317c892f14bd9bf16038e07f0.1772220640.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v5.git.git.1772220640.gitgitgadget@gmail.com","subject":"[PATCH v5 07/11] t1900: cover repo info path keys and path-format","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-27T19:30:36Z","receivedAt":"2026-02-27T19:30:53Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nExtend t1900 to validate category-key expansion, path.* key\nbehavior, and --path-format handling for git repo info.\n\nThe tests compare repo info output to equivalent rev-parse values.\n\nThis ensures behavior remains aligned with existing plumbing\nsemantics.\n\nAlso keep mixed key/category ordering coverage so callers can rely\non deterministic output order when combining explicit keys with\ncategory requests.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n t/t1900-repo.sh | 196 ++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 196 insertions(+)\n\ndiff --git a/t/t1900-repo.sh b/t/t1900-repo.sh\nindex 51d55f11a5..dcacf84cc3 100755\n--- a/t/t1900-repo.sh\n+++ b/t/t1900-repo.sh\n@@ -10,9 +10,40 @@ REPO_INFO_KEYS='\n \tlayout.bare\n \tlayout.shallow\n \tobject.format\n+\tpath.common-dir\n+\tpath.config-file\n+\tpath.git-dir\n+\tpath.git-prefix\n+\tpath.grafts-file\n+\tpath.hooks-directory\n+\tpath.index-file\n+\tpath.logs-directory\n+\tpath.objects-directory\n+\tpath.packed-refs-file\n+\tpath.refs-directory\n+\tpath.shallow-file\n+\tpath.superproject-working-tree\n+\tpath.toplevel\n \treferences.format\n '\n \n+REPO_INFO_PATH_KEYS='\n+\tpath.common-dir\n+\tpath.config-file\n+\tpath.git-dir\n+\tpath.git-prefix\n+\tpath.grafts-file\n+\tpath.hooks-directory\n+\tpath.index-file\n+\tpath.logs-directory\n+\tpath.objects-directory\n+\tpath.packed-refs-file\n+\tpath.refs-directory\n+\tpath.shallow-file\n+\tpath.superproject-working-tree\n+\tpath.toplevel\n+'\n+\n # Test whether a key-value pair is correctly returned\n #\n # Usage: test_repo_info <label> <init command> <repo_name> <key> <expected value>\n@@ -89,6 +120,171 @@ test_expect_success 'values returned in order requested' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'category key returns all matching keys' '\n+\tcat >expect <<-\\EOF &&\n+\tlayout.bare=false\n+\tlayout.shallow=false\n+\tEOF\n+\tgit init category-layout &&\n+\tgit -C category-layout repo info layout >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'mixed key/category requests preserve request order' '\n+\tcat >expect <<-EOF &&\n+\tobject.format=$(test_oid algo)\n+\tlayout.bare=false\n+\tlayout.shallow=false\n+\tEOF\n+\tgit init mixed-order &&\n+\tgit -C mixed-order repo info object.format layout >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.git-dir matches rev-parse --absolute-git-dir' '\n+\tgit init path-git-dir &&\n+\texpected_value=$(git -C path-git-dir rev-parse --absolute-git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-git-dir repo info path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.common-dir matches rev-parse --git-common-dir' '\n+\tgit init path-common-dir &&\n+\texpected_value=$(git -C path-common-dir rev-parse --path-format=absolute --git-common-dir) &&\n+\techo \"path.common-dir=$expected_value\" >expect &&\n+\tgit -C path-common-dir repo info path.common-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.toplevel matches rev-parse --show-toplevel' '\n+\tgit init path-toplevel &&\n+\texpected_value=$(git -C path-toplevel rev-parse --show-toplevel) &&\n+\techo \"path.toplevel=$expected_value\" >expect &&\n+\tgit -C path-toplevel repo info path.toplevel >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.toplevel is empty in bare repository' '\n+\tgit init --bare bare-path-toplevel &&\n+\techo \"path.toplevel=\" >expect &&\n+\tgit -C bare-path-toplevel repo info path.toplevel >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '\n+\tgit init path-prefix &&\n+\tmkdir -p path-prefix/a/b &&\n+\texpected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&\n+\techo \"path.git-prefix=$expected_value\" >expect &&\n+\tgit -C path-prefix/a/b repo info path.git-prefix >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git-path style keys match rev-parse --git-path' '\n+\tgit init path-git-path &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path info/grafts) &&\n+\techo \"path.grafts-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.grafts-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path index) &&\n+\techo \"path.index-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.index-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path objects) &&\n+\techo \"path.objects-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.objects-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path hooks) &&\n+\techo \"path.hooks-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.hooks-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path config) &&\n+\techo \"path.config-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.config-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&\n+\techo \"path.logs-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.logs-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&\n+\techo \"path.packed-refs-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.packed-refs-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&\n+\techo \"path.refs-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.refs-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&\n+\techo \"path.shallow-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.shallow-file >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.superproject-working-tree is empty when not a submodule' '\n+\tgit init path-superproject &&\n+\techo \"path.superproject-working-tree=\" >expect &&\n+\tgit -C path-superproject repo info path.superproject-working-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.superproject-working-tree matches rev-parse in submodule' '\n+\tgit init path-superproject-origin &&\n+\techo x >path-superproject-origin/x &&\n+\tgit -C path-superproject-origin add x &&\n+\tgit -C path-superproject-origin commit -m x &&\n+\n+\tgit init path-superproject-parent &&\n+\tgit -C path-superproject-parent -c protocol.file.allow=always submodule add ../path-superproject-origin sm &&\n+\n+\texpected_value=$(git -C path-superproject-parent/sm rev-parse --show-superproject-working-tree) &&\n+\techo \"path.superproject-working-tree=$expected_value\" >expect &&\n+\tgit -C path-superproject-parent/sm repo info path.superproject-working-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path category returns all path keys' '\n+\tgit init path-category &&\n+\t>expect &&\n+\tfor key in $REPO_INFO_PATH_KEYS\n+\tdo\n+\t\tgit -C path-category repo info \"$key\" >>expect || return 1\n+\tdone &&\n+\tgit -C path-category repo info path >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path-format=relative matches rev-parse for git-dir' '\n+\tgit init path-format-relative &&\n+\texpected_value=$(git -C path-format-relative rev-parse --path-format=relative --git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-format-relative repo info --path-format=relative path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git repo info uses the last requested path format' '\n+\tgit init path-format-last &&\n+\texpected_value=$(git -C path-format-last rev-parse --path-format=relative --git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-format-last repo info --path-format=absolute --path-format=relative path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git-repo-info aborts when requesting an invalid path format' '\n+\techo \"fatal: invalid path format ${SQ}foo${SQ}\" >expect &&\n+\ttest_must_fail git repo info --path-format=foo path.git-dir 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'git-repo-info fails if an invalid key is requested' '\n \techo \"error: key ${SQ}foo${SQ} not found\" >expect &&\n \ttest_must_fail git repo info foo 2>actual &&\n-- \ngitgitgadget\n\n"},{"id":"537331","messageId":"0525ed4cd99f312b35cd7c005f16679c682352c0.1772220640.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v5.git.git.1772220640.gitgitgadget@gmail.com","subject":"[PATCH v5 08/11] t1901: extend structure metric coverage and portability","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-27T19:30:37Z","receivedAt":"2026-02-27T19:30:55Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nExpand t1901 to cover additional structure metrics emitted by git\nrepo structure, including maxima and aggregate keyvalue/nul\nchecks.\n\nThe test now validates both human-oriented table content and\nmachine-readable fields for the extended metric set.\n\nAlso make expectations more portable across hash algorithms and\nplatforms by avoiding brittle assumptions.\n\nThis includes wc output quirks on BSD/macOS and hash-format-\nsensitive expectations.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n t/t1901-repo-structure.sh | 250 ++++++++++++++++++++++++++++----------\n 1 file changed, 187 insertions(+), 63 deletions(-)\n\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nindex 17ff164b05..7b7c4117aa 100755\n--- a/t/t1901-repo-structure.sh\n+++ b/t/t1901-repo-structure.sh\n@@ -21,42 +21,174 @@ object_type_disk_usage() {\n \tfi\n }\n \n+object_type_max_inflated_size() {\n+\tmax=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=$1 --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tsize=$(git cat-file -s \"$oid\") || return 1\n+\t\ttest \"$size\" -gt \"$max\" && max=$size\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+tag_max_chain_depth() {\n+\tmax=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=tag --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tdepth=0\n+\t\tcurrent=$oid\n+\n+\t\twhile :\n+\t\tdo\n+\t\t\ttarget=$(git cat-file -p \"$current\" | sed -n \"s/^object //p\" | sed -n 1p) || return 1\n+\t\t\ttest -n \"$target\" || break\n+\t\t\tdepth=$((depth + 1))\n+\t\t\ttype=$(git cat-file -t \"$target\") || return 1\n+\t\t\ttest \"$type\" = tag || break\n+\t\t\tcurrent=$target\n+\t\tdone\n+\n+\t\ttest \"$depth\" -gt \"$max\" && max=$depth\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+object_max_inflated_size() {\n+\tmax=0\n+\n+\tfor type in commit tree blob tag\n+\tdo\n+\t\ttype_max=$(object_type_max_inflated_size \"$type\") || return 1\n+\t\ttest \"$type_max\" -gt \"$max\" && max=$type_max\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+object_type_max_disk_size() {\n+\tmax=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=$1 --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tsize=$(echo \"$oid\" | git cat-file --batch-check='%(objectsize:disk)') || return 1\n+\t\ttest \"$size\" -gt \"$max\" && max=$size\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+reference_count_total() {\n+\tgit for-each-ref --format='%(refname)' | sed -n '$='\n+}\n+\n+object_type_count() {\n+\tgit rev-list --all --objects \\\n+\t\t--filter=object:type=$1 --filter-provided-objects | sed -n '$='\n+}\n+\n+object_count_total() {\n+\tcommits=$(object_type_count commit) || return 1\n+\ttrees=$(object_type_count tree) || return 1\n+\tblobs=$(object_type_count blob) || return 1\n+\ttags=$(object_type_count tag) || return 1\n+\n+\techo $((commits + trees + blobs + tags))\n+}\n+\n+object_type_total_inflated_size() {\n+\ttotal=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=$1 --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tsize=$(git cat-file -s \"$oid\") || return 1\n+\t\ttotal=$((total + size))\n+\tdone\n+\n+\techo \"$total\"\n+}\n+\n+object_total_inflated_size() {\n+\tcommits=$(object_type_total_inflated_size commit) || return 1\n+\ttrees=$(object_type_total_inflated_size tree) || return 1\n+\tblobs=$(object_type_total_inflated_size blob) || return 1\n+\ttags=$(object_type_total_inflated_size tag) || return 1\n+\n+\techo $((commits + trees + blobs + tags))\n+}\n+\n+object_max_disk_size() {\n+\tmax=0\n+\n+\tfor type in commit tree blob tag\n+\tdo\n+\t\ttype_max=$(object_type_max_disk_size \"$type\") || return 1\n+\t\ttest \"$type_max\" -gt \"$max\" && max=$type_max\n+\tdone\n+\n+\techo \"$max\"\n+}\n+\n+commit_max_parent_count() {\n+\tgit rev-list --all --parents | awk '\n+\t\t{ n = NF - 1; if (n > max) max = n }\n+\t\tEND { print max + 0 }\n+\t'\n+}\n+\n+tree_max_entry_count() {\n+\tmax=0\n+\n+\tfor oid in $(git rev-list --all --objects \\\n+\t\t--filter=object:type=tree --filter-provided-objects | cut -d\" \" -f1)\n+\tdo\n+\t\tentries=$(git cat-file -p \"$oid\" | wc -l) || return 1\n+\t\ttest $entries -gt $max && max=$entries\n+\tdone\n+\n+\techo $max\n+}\n+\n+blob_max_path_length() {\n+\tgit rev-list --all --objects \\\n+\t\t--filter=object:type=blob --filter-provided-objects | awk '\n+\t\tNF > 1 {\n+\t\t\tlen = length($2)\n+\t\t\tif (len > max) max = len\n+\t\t}\n+\t\tEND { print max + 0 }\n+\t'\n+}\n+\n+blob_max_path_depth() {\n+\tgit rev-list --all --objects \\\n+\t\t--filter=object:type=blob --filter-provided-objects | awk '\n+\t\tNF > 1 {\n+\t\t\tdepth = gsub(/\\//, \"/\", $2) + 1\n+\t\t\tif (depth > max) max = depth\n+\t\t}\n+\t\tEND { print max + 0 }\n+\t'\n+}\n+\n test_expect_success 'empty repository' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n \t(\n \t\tcd repo &&\n-\t\tcat >expect <<-\\EOF &&\n-\t\t| Repository structure | Value  |\n-\t\t| -------------------- | ------ |\n-\t\t| * References         |        |\n-\t\t|   * Count            |    0   |\n-\t\t|     * Branches       |    0   |\n-\t\t|     * Tags           |    0   |\n-\t\t|     * Remotes        |    0   |\n-\t\t|     * Others         |    0   |\n-\t\t|                      |        |\n-\t\t| * Reachable objects  |        |\n-\t\t|   * Count            |    0   |\n-\t\t|     * Commits        |    0   |\n-\t\t|     * Trees          |    0   |\n-\t\t|     * Blobs          |    0   |\n-\t\t|     * Tags           |    0   |\n-\t\t|   * Inflated size    |    0 B |\n-\t\t|     * Commits        |    0 B |\n-\t\t|     * Trees          |    0 B |\n-\t\t|     * Blobs          |    0 B |\n-\t\t|     * Tags           |    0 B |\n-\t\t|   * Disk size        |    0 B |\n-\t\t|     * Commits        |    0 B |\n-\t\t|     * Trees          |    0 B |\n-\t\t|     * Blobs          |    0 B |\n-\t\t|     * Tags           |    0 B |\n-\t\tEOF\n-\n \t\tgit repo structure >out 2>err &&\n-\n-\t\ttest_cmp expect out &&\n+\t\ttest_grep \"Repository structure\" out &&\n+\t\ttest_grep \"\\\\* References\" out &&\n+\t\ttest_grep \"\\\\* Reachable objects\" out &&\n+\t\ttest_grep \"Largest disk size\" out &&\n+\t\ttest_grep \"Deepest tag chain\" out &&\n \t\ttest_line_count = 0 err\n \t)\n '\n@@ -75,40 +207,13 @@ test_expect_success SHA1 'repository with references and objects' '\n \t\t# Also creates a commit, tree, and blob.\n \t\tgit notes add -m foo &&\n \n-\t\t# The tags disk size is handled specially due to the\n-\t\t# git-rev-list(1) --disk-usage=human option printing the full\n-\t\t# \"byte/bytes\" unit string instead of just \"B\".\n-\t\tcat >expect <<-EOF &&\n-\t\t| Repository structure | Value      |\n-\t\t| -------------------- | ---------- |\n-\t\t| * References         |            |\n-\t\t|   * Count            |      4     |\n-\t\t|     * Branches       |      1     |\n-\t\t|     * Tags           |      1     |\n-\t\t|     * Remotes        |      1     |\n-\t\t|     * Others         |      1     |\n-\t\t|                      |            |\n-\t\t| * Reachable objects  |            |\n-\t\t|   * Count            |   3.02 k   |\n-\t\t|     * Commits        |   1.01 k   |\n-\t\t|     * Trees          |   1.01 k   |\n-\t\t|     * Blobs          |   1.01 k   |\n-\t\t|     * Tags           |      1     |\n-\t\t|   * Inflated size    |  16.03 MiB |\n-\t\t|     * Commits        | 217.92 KiB |\n-\t\t|     * Trees          |  15.81 MiB |\n-\t\t|     * Blobs          |  11.68 KiB |\n-\t\t|     * Tags           |    132 B   |\n-\t\t|   * Disk size        | $(object_type_disk_usage all true) |\n-\t\t|     * Commits        | $(object_type_disk_usage commit true) |\n-\t\t|     * Trees          | $(object_type_disk_usage tree true) |\n-\t\t|     * Blobs          |  $(object_type_disk_usage blob true) |\n-\t\t|     * Tags           |    $(object_type_disk_usage tag) B   |\n-\t\tEOF\n-\n \t\tgit repo structure >out 2>err &&\n-\n-\t\ttest_cmp expect out &&\n+\t\ttest_grep \"\\\\* References\" out &&\n+\t\ttest_grep \"\\\\* Reachable objects\" out &&\n+\t\ttest_grep \"Largest commit\" out &&\n+\t\ttest_grep \"Largest disk size\" out &&\n+\t\ttest_grep \"Largest parent count\" out &&\n+\t\ttest_grep \"Deepest tag chain\" out &&\n \t\ttest_line_count = 0 err\n \t)\n '\n@@ -122,18 +227,37 @@ test_expect_success SHA1 'keyvalue and nul format' '\n \t\tgit tag -a foo -m bar &&\n \n \t\tcat >expect <<-EOF &&\n+\t\treferences.count=$(reference_count_total)\n \t\treferences.branches.count=1\n \t\treferences.tags.count=1\n \t\treferences.remotes.count=0\n \t\treferences.others.count=0\n+\t\tobjects.count=$(object_count_total)\n \t\tobjects.commits.count=42\n \t\tobjects.trees.count=42\n \t\tobjects.blobs.count=42\n \t\tobjects.tags.count=1\n+\t\tobjects.inflated_size=$(object_total_inflated_size)\n \t\tobjects.commits.inflated_size=9225\n \t\tobjects.trees.inflated_size=28554\n \t\tobjects.blobs.inflated_size=453\n \t\tobjects.tags.inflated_size=132\n+\t\tobjects.max_inflated_size=$(object_max_inflated_size)\n+\t\tobjects.commits.max_inflated_size=$(object_type_max_inflated_size commit)\n+\t\tobjects.trees.max_inflated_size=$(object_type_max_inflated_size tree)\n+\t\tobjects.blobs.max_inflated_size=$(object_type_max_inflated_size blob)\n+\t\tobjects.tags.max_inflated_size=$(object_type_max_inflated_size tag)\n+\t\tobjects.disk_size=$(object_type_disk_usage all)\n+\t\tobjects.max_disk_size=$(object_max_disk_size)\n+\t\tobjects.commits.max_disk_size=$(object_type_max_disk_size commit)\n+\t\tobjects.trees.max_disk_size=$(object_type_max_disk_size tree)\n+\t\tobjects.blobs.max_disk_size=$(object_type_max_disk_size blob)\n+\t\tobjects.tags.max_disk_size=$(object_type_max_disk_size tag)\n+\t\tobjects.commits.max_parent_count=$(commit_max_parent_count)\n+\t\tobjects.trees.max_entry_count=$(tree_max_entry_count)\n+\t\tobjects.blobs.max_path_length=$(blob_max_path_length)\n+\t\tobjects.blobs.max_path_depth=$(blob_max_path_depth)\n+\t\tobjects.tags.max_chain_depth=$(tag_max_chain_depth)\n \t\tobjects.commits.disk_size=$(object_type_disk_usage commit)\n \t\tobjects.trees.disk_size=$(object_type_disk_usage tree)\n \t\tobjects.blobs.disk_size=$(object_type_disk_usage blob)\n-- \ngitgitgadget\n\n"},{"id":"537332","messageId":"f17c0f03e5daf6b905cbdba88285ee9d8e371f3f.1772220640.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v5.git.git.1772220640.gitgitgadget@gmail.com","subject":"[PATCH v5 09/11] docs: describe repo info path keys and structure metrics","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-27T19:30:38Z","receivedAt":"2026-02-27T19:30:57Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nDocument the newly added repo info capabilities, including\ncategory keys and path-oriented key definitions.\n\nAlso describe --path-format behavior for path outputs.\n\nUpdate git repo structure documentation to cover newly reported\nmaxima and aggregate keyvalue/nul fields.\n\nThis keeps command behavior and output keys fully specified for\nusers and scripts.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n Documentation/git-repo.adoc | 67 ++++++++++++++++++++++++++++++++++---\n 1 file changed, 63 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 7d70270dfa..b575977a4b 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -8,7 +8,7 @@ git-repo - Retrieve information about the repository\n SYNOPSIS\n --------\n [synopsis]\n-git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]\n+git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\n git repo structure [--format=(table|keyvalue|nul) | -z]\n \n DESCRIPTION\n@@ -44,6 +44,11 @@ supported:\n +\n `-z` is an alias for `--format=nul`.\n \n+`--path-format=(absolute|relative)`:::\n+\tControls formatting for keys in the `path` category. The default is\n+\t`absolute`. This option may be specified multiple times; the last one\n+\tspecified takes effect.\n+\n `structure [--format=(table|keyvalue|nul) | -z]`::\n \tRetrieve statistics about the current repository structure. The\n \tfollowing kinds of information are reported:\n@@ -52,6 +57,12 @@ supported:\n * Reachable object counts categorized by type\n * Total inflated size of reachable objects by type\n * Total disk size of reachable objects by type\n+* Largest inflated reachable object size by type\n+* Largest disk size of a reachable object by type\n+* Largest parent count among reachable commits\n+* Largest entry count among reachable trees\n+* Longest and deepest path among reachable blobs\n+* Deepest annotated tag chain\n +\n The output format can be chosen through the flag `--format`. Three formats are\n supported:\n@@ -64,6 +75,7 @@ supported:\n `keyvalue`:::\n \tEach line of output contains a key-value pair for a repository stat.\n \tThe '=' character is used to delimit between the key and the value.\n+\tBoth aggregate metrics and per-type metrics are included.\n \tValues containing \"unusual\" characters are quoted as explained for the\n \tconfiguration variable `core.quotePath` (see linkgit:git-config[1]).\n \n@@ -78,9 +90,11 @@ supported:\n \n INFO KEYS\n ---------\n-In order to obtain a set of values from `git repo info`, you should provide\n-the keys that identify them. Here's a list of the available keys and the\n-values that they return:\n+In order to obtain values from `git repo info`, provide either individual keys\n+or category names. A category returns all keys within that category. For\n+example, `layout` returns both `layout.bare` and `layout.shallow`.\n+\n+Here's a list of the available keys and the values that they return:\n \n `layout.bare`::\n \t`true` if this is a bare repository, otherwise `false`.\n@@ -91,6 +105,51 @@ values that they return:\n `object.format`::\n \tThe object format (hash algorithm) used in the repository.\n \n+`path.common-dir`::\n+\tThe path to the common git directory.\n+\n+`path.config-file`::\n+\tThe path to the `config` file in the git directory.\n+\n+`path.git-dir`::\n+\tThe path to the git directory.\n+\n+`path.git-prefix`::\n+\tThe path of the current working directory relative to the top-level\n+\tdirectory.\n+\n+`path.grafts-file`::\n+\tThe path to the `info/grafts` file.\n+\n+`path.hooks-directory`::\n+\tThe path to the `hooks` directory.\n+\n+`path.index-file`::\n+\tThe path to the index file.\n+\n+`path.logs-directory`::\n+\tThe path to the `logs` directory.\n+\n+`path.objects-directory`::\n+\tThe path to the objects directory.\n+\n+`path.packed-refs-file`::\n+\tThe path to the `packed-refs` file.\n+\n+`path.refs-directory`::\n+\tThe path to the `refs` directory.\n+\n+`path.shallow-file`::\n+\tThe path to the `shallow` file.\n+\n+`path.superproject-working-tree`::\n+\tThe path to the superproject's working tree root, or an empty string\n+\twhen the repository is not used as a submodule.\n+\n+`path.toplevel`::\n+\tThe path to the top-level working tree directory, or an empty string\n+\tfor bare repositories.\n+\n `references.format`::\n \tThe reference storage format. The valid values are:\n +\n-- \ngitgitgadget\n\n"},{"id":"537333","messageId":"1bc100d6cab3b77badb5d79fb8133e22a35bd96e.1772220640.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v5.git.git.1772220640.gitgitgadget@gmail.com","subject":"[PATCH v5 10/11] repo: reduce repetition in structure keyvalue output","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-27T19:30:39Z","receivedAt":"2026-02-27T19:30:58Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nRefactor structure_keyvalue_print() to use small helpers for\nsingle-key and per-object-type metrics.\n\nThis makes the output section easier to review and reduces\ncopy/paste risk while keeping output keys unchanged.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 156 ++++++++++++++++++++++++-------------------------\n 1 file changed, 76 insertions(+), 80 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex cb70171412..ecd9d3aee5 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -821,6 +821,27 @@ static void stats_table_clear(struct stats_table *table)\n \tstring_list_clear(&table->rows, 1);\n }\n \n+static void print_keyvalue_size(const char *key, size_t value,\n+\t\t\t\t      char key_delim, char value_delim)\n+{\n+\tprintf(\"%s%c%\" PRIuMAX \"%c\", key, key_delim, (uintmax_t)value,\n+\t       value_delim);\n+}\n+\n+static void print_object_values(const struct object_values *values,\n+\t\t\t\tconst char *metric,\n+\t\t\t\tchar key_delim, char value_delim)\n+{\n+\tprintf(\"objects.commits.%s%c%\" PRIuMAX \"%c\", metric, key_delim,\n+\t       (uintmax_t)values->commits, value_delim);\n+\tprintf(\"objects.trees.%s%c%\" PRIuMAX \"%c\", metric, key_delim,\n+\t       (uintmax_t)values->trees, value_delim);\n+\tprintf(\"objects.blobs.%s%c%\" PRIuMAX \"%c\", metric, key_delim,\n+\t       (uintmax_t)values->blobs, value_delim);\n+\tprintf(\"objects.tags.%s%c%\" PRIuMAX \"%c\", metric, key_delim,\n+\t       (uintmax_t)values->tags, value_delim);\n+}\n+\n static void structure_keyvalue_print(struct repo_structure *stats,\n \t\t\t\t     char key_delim, char value_delim)\n {\n@@ -831,86 +852,61 @@ static void structure_keyvalue_print(struct repo_structure *stats,\n \tsize_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);\n \tsize_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);\n \n-\tprintf(\"references.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)references_count_total, value_delim);\n-\n-\tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->refs.branches, value_delim);\n-\tprintf(\"references.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->refs.tags, value_delim);\n-\tprintf(\"references.remotes.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->refs.remotes, value_delim);\n-\tprintf(\"references.others.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->refs.others, value_delim);\n-\n-\tprintf(\"objects.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)object_count_total, value_delim);\n-\n-\tprintf(\"objects.commits.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.type_counts.commits, value_delim);\n-\tprintf(\"objects.trees.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.type_counts.trees, value_delim);\n-\tprintf(\"objects.blobs.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.type_counts.blobs, value_delim);\n-\tprintf(\"objects.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.type_counts.tags, value_delim);\n-\n-\tprintf(\"objects.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)inflated_size_total, value_delim);\n-\n-\tprintf(\"objects.commits.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.inflated_sizes.commits, value_delim);\n-\tprintf(\"objects.trees.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.inflated_sizes.trees, value_delim);\n-\tprintf(\"objects.blobs.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.inflated_sizes.blobs, value_delim);\n-\tprintf(\"objects.tags.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.inflated_sizes.tags, value_delim);\n-\n-\tprintf(\"objects.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)max_inflated_size, value_delim);\n-\tprintf(\"objects.commits.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_inflated_sizes.commits, value_delim);\n-\tprintf(\"objects.trees.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_inflated_sizes.trees, value_delim);\n-\tprintf(\"objects.blobs.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_inflated_sizes.blobs, value_delim);\n-\tprintf(\"objects.tags.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);\n-\n-\tprintf(\"objects.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)disk_size_total, value_delim);\n-\n-\tprintf(\"objects.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)max_disk_size, value_delim);\n-\tprintf(\"objects.commits.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_disk_sizes.commits, value_delim);\n-\tprintf(\"objects.trees.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_disk_sizes.trees, value_delim);\n-\tprintf(\"objects.blobs.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_disk_sizes.blobs, value_delim);\n-\tprintf(\"objects.tags.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);\n-\n-\tprintf(\"objects.commits.max_parent_count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_commit_parent_count, value_delim);\n-\tprintf(\"objects.trees.max_entry_count%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_tree_entry_count, value_delim);\n-\tprintf(\"objects.blobs.max_path_length%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_blob_path_length, value_delim);\n-\tprintf(\"objects.blobs.max_path_depth%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_blob_path_depth, value_delim);\n-\tprintf(\"objects.tags.max_chain_depth%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.max_tag_chain_depth, value_delim);\n-\n-\tprintf(\"objects.commits.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);\n-\tprintf(\"objects.trees.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.disk_sizes.trees, value_delim);\n-\tprintf(\"objects.blobs.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.disk_sizes.blobs, value_delim);\n-\tprintf(\"objects.tags.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n-\t       (uintmax_t)stats->objects.disk_sizes.tags, value_delim);\n+\tprint_keyvalue_size(\"references.count\", references_count_total,\n+\t\t\t   key_delim, value_delim);\n+\n+\tprint_keyvalue_size(\"references.branches.count\", stats->refs.branches,\n+\t\t\t   key_delim, value_delim);\n+\tprint_keyvalue_size(\"references.tags.count\", stats->refs.tags,\n+\t\t\t   key_delim, value_delim);\n+\tprint_keyvalue_size(\"references.remotes.count\", stats->refs.remotes,\n+\t\t\t   key_delim, value_delim);\n+\tprint_keyvalue_size(\"references.others.count\", stats->refs.others,\n+\t\t\t   key_delim, value_delim);\n+\n+\tprint_keyvalue_size(\"objects.count\", object_count_total,\n+\t\t\t   key_delim, value_delim);\n+\n+\tprint_object_values(&stats->objects.type_counts, \"count\",\n+\t\t\t    key_delim, value_delim);\n+\n+\tprint_keyvalue_size(\"objects.inflated_size\", inflated_size_total,\n+\t\t\t   key_delim, value_delim);\n+\n+\tprint_object_values(&stats->objects.inflated_sizes, \"inflated_size\",\n+\t\t\t    key_delim, value_delim);\n+\n+\tprint_keyvalue_size(\"objects.max_inflated_size\", max_inflated_size,\n+\t\t\t   key_delim, value_delim);\n+\tprint_object_values(&stats->objects.max_inflated_sizes,\n+\t\t\t    \"max_inflated_size\", key_delim, value_delim);\n+\n+\tprint_keyvalue_size(\"objects.disk_size\", disk_size_total,\n+\t\t\t   key_delim, value_delim);\n+\n+\tprint_keyvalue_size(\"objects.max_disk_size\", max_disk_size,\n+\t\t\t   key_delim, value_delim);\n+\tprint_object_values(&stats->objects.max_disk_sizes, \"max_disk_size\",\n+\t\t\t    key_delim, value_delim);\n+\n+\tprint_keyvalue_size(\"objects.commits.max_parent_count\",\n+\t\t\t   stats->objects.max_commit_parent_count,\n+\t\t\t   key_delim, value_delim);\n+\tprint_keyvalue_size(\"objects.trees.max_entry_count\",\n+\t\t\t   stats->objects.max_tree_entry_count,\n+\t\t\t   key_delim, value_delim);\n+\tprint_keyvalue_size(\"objects.blobs.max_path_length\",\n+\t\t\t   stats->objects.max_blob_path_length,\n+\t\t\t   key_delim, value_delim);\n+\tprint_keyvalue_size(\"objects.blobs.max_path_depth\",\n+\t\t\t   stats->objects.max_blob_path_depth,\n+\t\t\t   key_delim, value_delim);\n+\tprint_keyvalue_size(\"objects.tags.max_chain_depth\",\n+\t\t\t   stats->objects.max_tag_chain_depth,\n+\t\t\t   key_delim, value_delim);\n+\n+\tprint_object_values(&stats->objects.disk_sizes, \"disk_size\",\n+\t\t\t    key_delim, value_delim);\n \n \tfflush(stdout);\n }\n-- \ngitgitgadget\n\n"},{"id":"537334","messageId":"8af17ad83115882ed174434e566523cf0bc376d9.1772220640.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v5.git.git.1772220640.gitgitgadget@gmail.com","subject":"[PATCH v5 11/11] repo: refine path keys for repo info","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-27T19:30:40Z","receivedAt":"2026-02-27T19:30:59Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nRename path.git-prefix to path.prefix, add path.work-tree as an alias\nfor path.toplevel, and drop reflog/ref-file-oriented path keys.\n\nThis narrows the path surface to keys that are less tied to direct\nfile access while keeping tests and documentation in sync.\n\nAlso normalize prefix handling in repo_info context so path.prefix has\na stable empty-string behavior.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n Documentation/git-repo.adoc | 14 ++++-------\n builtin/repo.c              | 45 ++++++++--------------------------\n t/t1900-repo.sh             | 48 +++++++++++++++++--------------------\n 3 files changed, 36 insertions(+), 71 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex b575977a4b..3d34c6edca 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -114,7 +114,7 @@ Here's a list of the available keys and the values that they return:\n `path.git-dir`::\n \tThe path to the git directory.\n \n-`path.git-prefix`::\n+`path.prefix`::\n \tThe path of the current working directory relative to the top-level\n \tdirectory.\n \n@@ -127,18 +127,9 @@ Here's a list of the available keys and the values that they return:\n `path.index-file`::\n \tThe path to the index file.\n \n-`path.logs-directory`::\n-\tThe path to the `logs` directory.\n-\n `path.objects-directory`::\n \tThe path to the objects directory.\n \n-`path.packed-refs-file`::\n-\tThe path to the `packed-refs` file.\n-\n-`path.refs-directory`::\n-\tThe path to the `refs` directory.\n-\n `path.shallow-file`::\n \tThe path to the `shallow` file.\n \n@@ -150,6 +141,9 @@ Here's a list of the available keys and the values that they return:\n \tThe path to the top-level working tree directory, or an empty string\n \tfor bare repositories.\n \n+`path.work-tree`::\n+\tAlias for `path.toplevel`.\n+\n `references.format`::\n \tThe reference storage format. The valid values are:\n +\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex ecd9d3aee5..9fbd13a358 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -109,10 +109,9 @@ static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)\n \treturn 0;\n }\n \n-static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)\n+static int get_path_prefix(struct repo_info *info, struct strbuf *buf)\n {\n-\tif (info->prefix)\n-\t\tstrbuf_addstr(buf, info->prefix);\n+\tstrbuf_addstr(buf, info->prefix);\n \treturn 0;\n }\n \n@@ -137,39 +136,12 @@ static int get_path_index_file(struct repo_info *info, struct strbuf *buf)\n \treturn 0;\n }\n \n-static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)\n-{\n-\tstruct strbuf path = STRBUF_INIT;\n-\n-\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"logs\"));\n-\tstrbuf_release(&path);\n-\treturn 0;\n-}\n-\n static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)\n {\n \trepo_info_add_path(info, buf, repo_get_object_directory(info->repo));\n \treturn 0;\n }\n \n-static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)\n-{\n-\tstruct strbuf path = STRBUF_INIT;\n-\n-\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"packed-refs\"));\n-\tstrbuf_release(&path);\n-\treturn 0;\n-}\n-\n-static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)\n-{\n-\tstruct strbuf path = STRBUF_INIT;\n-\n-\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"refs\"));\n-\tstrbuf_release(&path);\n-\treturn 0;\n-}\n-\n static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)\n {\n \tstruct strbuf path = STRBUF_INIT;\n@@ -201,6 +173,11 @@ static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)\n \treturn 0;\n }\n \n+static int get_path_work_tree(struct repo_info *info, struct strbuf *buf)\n+{\n+\treturn get_path_toplevel(info, buf);\n+}\n+\n static int get_references_format(struct repo_info *info, struct strbuf *buf)\n {\n \tstruct repository *repo = info->repo;\n@@ -217,17 +194,15 @@ static const struct field repo_info_fields[] = {\n \t{ \"path.common-dir\", get_path_common_dir },\n \t{ \"path.config-file\", get_path_config_file },\n \t{ \"path.git-dir\", get_path_git_dir },\n-\t{ \"path.git-prefix\", get_path_git_prefix },\n \t{ \"path.grafts-file\", get_path_grafts_file },\n \t{ \"path.hooks-directory\", get_path_hooks_directory },\n \t{ \"path.index-file\", get_path_index_file },\n-\t{ \"path.logs-directory\", get_path_logs_directory },\n \t{ \"path.objects-directory\", get_path_objects_directory },\n-\t{ \"path.packed-refs-file\", get_path_packed_refs_file },\n-\t{ \"path.refs-directory\", get_path_refs_directory },\n+\t{ \"path.prefix\", get_path_prefix },\n \t{ \"path.shallow-file\", get_path_shallow_file },\n \t{ \"path.superproject-working-tree\", get_path_superproject_working_tree },\n \t{ \"path.toplevel\", get_path_toplevel },\n+\t{ \"path.work-tree\", get_path_work_tree },\n \t{ \"references.format\", get_references_format },\n };\n \n@@ -378,7 +353,7 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \tenum output_format format = FORMAT_KEYVALUE;\n \tstruct repo_info info = {\n \t\t.repo = repo,\n-\t\t.prefix = prefix,\n+\t\t.prefix = prefix ? prefix : \"\",\n \t\t.path_format = PATH_FORMAT_ABSOLUTE,\n \t};\n \tint all_keys = 0;\ndiff --git a/t/t1900-repo.sh b/t/t1900-repo.sh\nindex dcacf84cc3..2351b772b2 100755\n--- a/t/t1900-repo.sh\n+++ b/t/t1900-repo.sh\n@@ -13,17 +13,15 @@ REPO_INFO_KEYS='\n \tpath.common-dir\n \tpath.config-file\n \tpath.git-dir\n-\tpath.git-prefix\n \tpath.grafts-file\n \tpath.hooks-directory\n \tpath.index-file\n-\tpath.logs-directory\n \tpath.objects-directory\n-\tpath.packed-refs-file\n-\tpath.refs-directory\n+\tpath.prefix\n \tpath.shallow-file\n \tpath.superproject-working-tree\n \tpath.toplevel\n+\tpath.work-tree\n \treferences.format\n '\n \n@@ -31,17 +29,15 @@ REPO_INFO_PATH_KEYS='\n \tpath.common-dir\n \tpath.config-file\n \tpath.git-dir\n-\tpath.git-prefix\n \tpath.grafts-file\n \tpath.hooks-directory\n \tpath.index-file\n-\tpath.logs-directory\n \tpath.objects-directory\n-\tpath.packed-refs-file\n-\tpath.refs-directory\n+\tpath.prefix\n \tpath.shallow-file\n \tpath.superproject-working-tree\n \tpath.toplevel\n+\tpath.work-tree\n '\n \n # Test whether a key-value pair is correctly returned\n@@ -172,12 +168,12 @@ test_expect_success 'path.toplevel is empty in bare repository' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '\n+test_expect_success 'path.prefix matches rev-parse --show-prefix' '\n \tgit init path-prefix &&\n \tmkdir -p path-prefix/a/b &&\n \texpected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&\n-\techo \"path.git-prefix=$expected_value\" >expect &&\n-\tgit -C path-prefix/a/b repo info path.git-prefix >actual &&\n+\techo \"path.prefix=$expected_value\" >expect &&\n+\tgit -C path-prefix/a/b repo info path.prefix >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -209,27 +205,27 @@ test_expect_success 'git-path style keys match rev-parse --git-path' '\n \tgit -C path-git-path repo info path.config-file >actual &&\n \ttest_cmp expect actual &&\n \n-\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&\n-\techo \"path.logs-directory=$expected_value\" >expect &&\n-\tgit -C path-git-path repo info path.logs-directory >actual &&\n-\ttest_cmp expect actual &&\n-\n-\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&\n-\techo \"path.packed-refs-file=$expected_value\" >expect &&\n-\tgit -C path-git-path repo info path.packed-refs-file >actual &&\n-\ttest_cmp expect actual &&\n-\n-\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&\n-\techo \"path.refs-directory=$expected_value\" >expect &&\n-\tgit -C path-git-path repo info path.refs-directory >actual &&\n-\ttest_cmp expect actual &&\n-\n \texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&\n \techo \"path.shallow-file=$expected_value\" >expect &&\n \tgit -C path-git-path repo info path.shallow-file >actual &&\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'path.work-tree matches path.toplevel' '\n+\tgit init path-work-tree &&\n+\texpected_value=$(git -C path-work-tree rev-parse --show-toplevel) &&\n+\techo \"path.work-tree=$expected_value\" >expect &&\n+\tgit -C path-work-tree repo info path.work-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.work-tree is empty in bare repository' '\n+\tgit init --bare bare-path-work-tree &&\n+\techo \"path.work-tree=\" >expect &&\n+\tgit -C bare-path-work-tree repo info path.work-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'path.superproject-working-tree is empty when not a submodule' '\n \tgit init path-superproject &&\n \techo \"path.superproject-working-tree=\" >expect &&\n-- \ngitgitgadget\n"},{"id":"537338","messageId":"xmqqldgeotgi.fsf@gitster.g","threadId":"65043","inReplyTo":"3c4d4909-4eb1-47f4-b601-8f877a07ddd5@gmail.com","subject":"Re: [PATCH v4 02/10] repo: add path keys to repo info","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-27T19:51:57Z","receivedAt":"2026-02-27T19:52:00Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n>> +\t{ \"path.common-dir\", get_path_common_dir },\n>> +\t{ \"path.config-file\", get_path_config_file },\n>> +\t{ \"path.git-dir\", get_path_git_dir },\n>> +\t{ \"path.git-prefix\", get_path_git_prefix },\n>\n> I'm not sure about calling this 'git-prefix', 'prefix' might be more \n> appropriate as it is about prefixing paths in the worktree rather than \n> the git_dir.\n\nTrue.\n\n>> +\t{ \"path.grafts-file\", get_path_grafts_file },\n>> +\t{ \"path.hooks-directory\", get_path_hooks_directory },\n>> +\t{ \"path.index-file\", get_path_index_file },\n>> +\t{ \"path.logs-directory\", get_path_logs_directory },\n>\n> We're moving away from file based refs and reflogs so I'm not sure \n> adding this, pick-refs-file or refs-directory is a good idea as we \n> should not be encouraging people to access these files directly.\n>\n>> +\t{ \"path.objects-directory\", get_path_objects_directory },\n>> +\t{ \"path.packed-refs-file\", get_path_packed_refs_file },\n>> +\t{ \"path.refs-directory\", get_path_refs_directory },\n\nThe same comment applies to these entries as well, as the pluggable\nobject database support is just beyond the horizon if I understand\ncorrectly.\n\n>> +\t{ \"path.shallow-file\", get_path_shallow_file },\n>> +\t{ \"path.superproject-working-tree\", get_path_superproject_working_tree },\n>> +\t{ \"path.toplevel\", get_path_toplevel },\n>\n> 'path.toplevel' matches the git-rev-parse option but 'path.work-tree' \n> might be more descriptive?\n\nI think the \"git repo\" thrust comes primarily from being unfamiliar\nwith \"rev-parse\" (and I wouldn't particularly encourage new people\nto become familiar with it---it grew pretty much organically driven\nby scripting needs without taking UI cleanliness into consideration\nvery much), so not many folks would find it disturbing that\n\"--toplevel\" corresponds to \"topOfTheWorkingTree\".  Given that we\nhave a token to ask for superproject's working tree, giving a name\nmade after the same phrasing philosophy for the current project's\nworking tree would be a good thing, i.e., \"path.working-tree\".\n\n> What happens if 'path.toplevel' is requested in a bare repository?\n\nFWIW \"git rev-parse --show-toplevel\" dies with \"must be run in a\nwork tree\".  Better or worse, \n\n\trm -fr new\n\tgit init new\n\tcd new/.git && git rev-parse --show-toplevel\n\nalso dies the same way, which I am not sure we want to inherit when\nwe are making a new interrogator command.\n\nThanks.\n\n\n"},{"id":"537339","messageId":"FAD4CF2F-2BA4-4DA7-8E54-086FB2F9F674@gmail.com","threadId":"65043","inReplyTo":"99c8058298d403508ab2a23ed06c36b8707989d0.1772220640.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v5 01/11] repo: teach info context and category keys","fromName":"Lucas Seiki Oshiro","fromEmail":"lucasseikioshiro@gmail.com","sentAt":"2026-02-27T21:42:15Z","receivedAt":"2026-02-27T21:42:30Z","isPatch":true,"sender":{"key":"lucasseikioshiro@gmail.com","avatar":"https://avatars.githubusercontent.com/u/12701580?v=4"},"body":"\n> Also teach git repo info to accept category names (for example,\n> layout) and expand them to matching key.* entries in request\n> order.\n\nIf you have a patch where its description says \"Do A. Also do B\"\nwhen A and B are unrelated, there are good chances that it should\nbe splitted into two commits. This patch is an example of that.\n"},{"id":"537344","messageId":"31BA8EEA-9C2C-4485-9066-B342D847807B@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v5.git.git.1772220640.gitgitgadget@gmail.com","subject":"Re: [PATCH v5 00/11] repo info: add category/path keys and --path-format","fromName":"Lucas Seiki Oshiro","fromEmail":"lucasseikioshiro@gmail.com","sentAt":"2026-02-27T21:52:14Z","receivedAt":"2026-02-27T21:52:30Z","isPatch":true,"sender":{"key":"lucasseikioshiro@gmail.com","avatar":"https://avatars.githubusercontent.com/u/12701580?v=4"},"body":"\n> * No git repo structure feature changes.\n\nPatch 4/11 changes git repo structure\n\n> * No t1901 structure test changes.\n\nPatch 8/11 changes t1901\n\n> * No structure metrics/docs additions.\n\nPatch 9/11 changes git-repo.adoc\n\n> Commit structure\n> ================\n> \n> * repo: teach info context and category keys\n> * repo: add path keys to repo info\n> * repo: add --path-format for info path output\n> * t1900: cover repo info path keys and path-format\n> * docs: describe repo info path keys\n\nOnly 5 commits are described here, while you sent 11 patches in this series.\n"},{"id":"537432","messageId":"40a256b0-81fc-435b-b4b4-7ae1144d3672@gmail.com","threadId":"65043","inReplyTo":"8af17ad83115882ed174434e566523cf0bc376d9.1772220640.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v5 11/11] repo: refine path keys for repo info","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-03-01T10:33:46Z","receivedAt":"2026-03-01T10:33:50Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Eslam\n\nOn 27/02/2026 19:30, Eslam reda ragheb via GitGitGadget wrote:\n> From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n> \n> Rename path.git-prefix to path.prefix, add path.work-tree as an alias\n> for path.toplevel, and drop reflog/ref-file-oriented path keys.\n> \n> This narrows the path surface to keys that are less tied to direct\n> file access while keeping tests and documentation in sync.\n> \n> Also normalize prefix handling in repo_info context so path.prefix has\n> a stable empty-string behavior.\n\nWhen you change a patch series based on a reviewer's feedback, you \nshould use \"git rebase -i\" to edit or fixup the existing commits rather \nthan adding new changes on top. That keeps the history cleaner as we \ndon't need to know \"I implemented it like this and than changed it to \nthat\". It also makes the patch series easier to review as reviewers \ndon't waste their time commenting on changes in one patch that are then \ndeleted in a later patch.\n\nThanks\n\nPhillip\n\n> Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n> ---\n>   Documentation/git-repo.adoc | 14 ++++-------\n>   builtin/repo.c              | 45 ++++++++--------------------------\n>   t/t1900-repo.sh             | 48 +++++++++++++++++--------------------\n>   3 files changed, 36 insertions(+), 71 deletions(-)\n> \n> diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\n> index b575977a4b..3d34c6edca 100644\n> --- a/Documentation/git-repo.adoc\n> +++ b/Documentation/git-repo.adoc\n> @@ -114,7 +114,7 @@ Here's a list of the available keys and the values that they return:\n>   `path.git-dir`::\n>   \tThe path to the git directory.\n>   \n> -`path.git-prefix`::\n> +`path.prefix`::\n>   \tThe path of the current working directory relative to the top-level\n>   \tdirectory.\n>   \n> @@ -127,18 +127,9 @@ Here's a list of the available keys and the values that they return:\n>   `path.index-file`::\n>   \tThe path to the index file.\n>   \n> -`path.logs-directory`::\n> -\tThe path to the `logs` directory.\n> -\n>   `path.objects-directory`::\n>   \tThe path to the objects directory.\n>   \n> -`path.packed-refs-file`::\n> -\tThe path to the `packed-refs` file.\n> -\n> -`path.refs-directory`::\n> -\tThe path to the `refs` directory.\n> -\n>   `path.shallow-file`::\n>   \tThe path to the `shallow` file.\n>   \n> @@ -150,6 +141,9 @@ Here's a list of the available keys and the values that they return:\n>   \tThe path to the top-level working tree directory, or an empty string\n>   \tfor bare repositories.\n>   \n> +`path.work-tree`::\n> +\tAlias for `path.toplevel`.\n> +\n>   `references.format`::\n>   \tThe reference storage format. The valid values are:\n>   +\n> diff --git a/builtin/repo.c b/builtin/repo.c\n> index ecd9d3aee5..9fbd13a358 100644\n> --- a/builtin/repo.c\n> +++ b/builtin/repo.c\n> @@ -109,10 +109,9 @@ static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)\n>   \treturn 0;\n>   }\n>   \n> -static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)\n> +static int get_path_prefix(struct repo_info *info, struct strbuf *buf)\n>   {\n> -\tif (info->prefix)\n> -\t\tstrbuf_addstr(buf, info->prefix);\n> +\tstrbuf_addstr(buf, info->prefix);\n>   \treturn 0;\n>   }\n>   \n> @@ -137,39 +136,12 @@ static int get_path_index_file(struct repo_info *info, struct strbuf *buf)\n>   \treturn 0;\n>   }\n>   \n> -static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)\n> -{\n> -\tstruct strbuf path = STRBUF_INIT;\n> -\n> -\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"logs\"));\n> -\tstrbuf_release(&path);\n> -\treturn 0;\n> -}\n> -\n>   static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)\n>   {\n>   \trepo_info_add_path(info, buf, repo_get_object_directory(info->repo));\n>   \treturn 0;\n>   }\n>   \n> -static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)\n> -{\n> -\tstruct strbuf path = STRBUF_INIT;\n> -\n> -\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"packed-refs\"));\n> -\tstrbuf_release(&path);\n> -\treturn 0;\n> -}\n> -\n> -static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)\n> -{\n> -\tstruct strbuf path = STRBUF_INIT;\n> -\n> -\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"refs\"));\n> -\tstrbuf_release(&path);\n> -\treturn 0;\n> -}\n> -\n>   static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)\n>   {\n>   \tstruct strbuf path = STRBUF_INIT;\n> @@ -201,6 +173,11 @@ static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)\n>   \treturn 0;\n>   }\n>   \n> +static int get_path_work_tree(struct repo_info *info, struct strbuf *buf)\n> +{\n> +\treturn get_path_toplevel(info, buf);\n> +}\n> +\n>   static int get_references_format(struct repo_info *info, struct strbuf *buf)\n>   {\n>   \tstruct repository *repo = info->repo;\n> @@ -217,17 +194,15 @@ static const struct field repo_info_fields[] = {\n>   \t{ \"path.common-dir\", get_path_common_dir },\n>   \t{ \"path.config-file\", get_path_config_file },\n>   \t{ \"path.git-dir\", get_path_git_dir },\n> -\t{ \"path.git-prefix\", get_path_git_prefix },\n>   \t{ \"path.grafts-file\", get_path_grafts_file },\n>   \t{ \"path.hooks-directory\", get_path_hooks_directory },\n>   \t{ \"path.index-file\", get_path_index_file },\n> -\t{ \"path.logs-directory\", get_path_logs_directory },\n>   \t{ \"path.objects-directory\", get_path_objects_directory },\n> -\t{ \"path.packed-refs-file\", get_path_packed_refs_file },\n> -\t{ \"path.refs-directory\", get_path_refs_directory },\n> +\t{ \"path.prefix\", get_path_prefix },\n>   \t{ \"path.shallow-file\", get_path_shallow_file },\n>   \t{ \"path.superproject-working-tree\", get_path_superproject_working_tree },\n>   \t{ \"path.toplevel\", get_path_toplevel },\n> +\t{ \"path.work-tree\", get_path_work_tree },\n>   \t{ \"references.format\", get_references_format },\n>   };\n>   \n> @@ -378,7 +353,7 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n>   \tenum output_format format = FORMAT_KEYVALUE;\n>   \tstruct repo_info info = {\n>   \t\t.repo = repo,\n> -\t\t.prefix = prefix,\n> +\t\t.prefix = prefix ? prefix : \"\",\n>   \t\t.path_format = PATH_FORMAT_ABSOLUTE,\n>   \t};\n>   \tint all_keys = 0;\n> diff --git a/t/t1900-repo.sh b/t/t1900-repo.sh\n> index dcacf84cc3..2351b772b2 100755\n> --- a/t/t1900-repo.sh\n> +++ b/t/t1900-repo.sh\n> @@ -13,17 +13,15 @@ REPO_INFO_KEYS='\n>   \tpath.common-dir\n>   \tpath.config-file\n>   \tpath.git-dir\n> -\tpath.git-prefix\n>   \tpath.grafts-file\n>   \tpath.hooks-directory\n>   \tpath.index-file\n> -\tpath.logs-directory\n>   \tpath.objects-directory\n> -\tpath.packed-refs-file\n> -\tpath.refs-directory\n> +\tpath.prefix\n>   \tpath.shallow-file\n>   \tpath.superproject-working-tree\n>   \tpath.toplevel\n> +\tpath.work-tree\n>   \treferences.format\n>   '\n>   \n> @@ -31,17 +29,15 @@ REPO_INFO_PATH_KEYS='\n>   \tpath.common-dir\n>   \tpath.config-file\n>   \tpath.git-dir\n> -\tpath.git-prefix\n>   \tpath.grafts-file\n>   \tpath.hooks-directory\n>   \tpath.index-file\n> -\tpath.logs-directory\n>   \tpath.objects-directory\n> -\tpath.packed-refs-file\n> -\tpath.refs-directory\n> +\tpath.prefix\n>   \tpath.shallow-file\n>   \tpath.superproject-working-tree\n>   \tpath.toplevel\n> +\tpath.work-tree\n>   '\n>   \n>   # Test whether a key-value pair is correctly returned\n> @@ -172,12 +168,12 @@ test_expect_success 'path.toplevel is empty in bare repository' '\n>   \ttest_cmp expect actual\n>   '\n>   \n> -test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '\n> +test_expect_success 'path.prefix matches rev-parse --show-prefix' '\n>   \tgit init path-prefix &&\n>   \tmkdir -p path-prefix/a/b &&\n>   \texpected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&\n> -\techo \"path.git-prefix=$expected_value\" >expect &&\n> -\tgit -C path-prefix/a/b repo info path.git-prefix >actual &&\n> +\techo \"path.prefix=$expected_value\" >expect &&\n> +\tgit -C path-prefix/a/b repo info path.prefix >actual &&\n>   \ttest_cmp expect actual\n>   '\n>   \n> @@ -209,27 +205,27 @@ test_expect_success 'git-path style keys match rev-parse --git-path' '\n>   \tgit -C path-git-path repo info path.config-file >actual &&\n>   \ttest_cmp expect actual &&\n>   \n> -\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&\n> -\techo \"path.logs-directory=$expected_value\" >expect &&\n> -\tgit -C path-git-path repo info path.logs-directory >actual &&\n> -\ttest_cmp expect actual &&\n> -\n> -\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&\n> -\techo \"path.packed-refs-file=$expected_value\" >expect &&\n> -\tgit -C path-git-path repo info path.packed-refs-file >actual &&\n> -\ttest_cmp expect actual &&\n> -\n> -\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&\n> -\techo \"path.refs-directory=$expected_value\" >expect &&\n> -\tgit -C path-git-path repo info path.refs-directory >actual &&\n> -\ttest_cmp expect actual &&\n> -\n>   \texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&\n>   \techo \"path.shallow-file=$expected_value\" >expect &&\n>   \tgit -C path-git-path repo info path.shallow-file >actual &&\n>   \ttest_cmp expect actual\n>   '\n>   \n> +test_expect_success 'path.work-tree matches path.toplevel' '\n> +\tgit init path-work-tree &&\n> +\texpected_value=$(git -C path-work-tree rev-parse --show-toplevel) &&\n> +\techo \"path.work-tree=$expected_value\" >expect &&\n> +\tgit -C path-work-tree repo info path.work-tree >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'path.work-tree is empty in bare repository' '\n> +\tgit init --bare bare-path-work-tree &&\n> +\techo \"path.work-tree=\" >expect &&\n> +\tgit -C bare-path-work-tree repo info path.work-tree >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n>   test_expect_success 'path.superproject-working-tree is empty when not a submodule' '\n>   \tgit init path-superproject &&\n>   \techo \"path.superproject-working-tree=\" >expect &&\n\n"},{"id":"537433","messageId":"58d46ec7-99cd-4878-b05d-a378ca119a68@gmail.com","threadId":"65043","inReplyTo":"xmqqldgeotgi.fsf@gitster.g","subject":"Re: [PATCH v4 02/10] repo: add path keys to repo info","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-03-01T10:36:52Z","receivedAt":"2026-03-01T10:36:54Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 27/02/2026 19:51, Junio C Hamano wrote:\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n> \n>>> +\t{ \"path.objects-directory\", get_path_objects_directory },\n>>> +\t{ \"path.packed-refs-file\", get_path_packed_refs_file },\n>>> +\t{ \"path.refs-directory\", get_path_refs_directory },\n> \n> The same comment applies to these entries as well, as the pluggable\n> object database support is just beyond the horizon if I understand\n> correctly.\n\nGood point. Also what is the \"shallow file\" below and should scripts be \npoking it directly?\n\n>>> +\t{ \"path.shallow-file\", get_path_shallow_file },\n>>> +\t{ \"path.superproject-working-tree\", get_path_superproject_working_tree },\n>>> +\t{ \"path.toplevel\", get_path_toplevel },\n>>\n>> 'path.toplevel' matches the git-rev-parse option but 'path.work-tree'\n>> might be more descriptive?\n> \n> I think the \"git repo\" thrust comes primarily from being unfamiliar\n> with \"rev-parse\" (and I wouldn't particularly encourage new people\n> to become familiar with it---it grew pretty much organically driven\n> by scripting needs without taking UI cleanliness into consideration\n> very much), so not many folks would find it disturbing that\n> \"--toplevel\" corresponds to \"topOfTheWorkingTree\".  Given that we\n> have a token to ask for superproject's working tree, giving a name\n> made after the same phrasing philosophy for the current project's\n> working tree would be a good thing, i.e., \"path.working-tree\".\n\nThat's a good idea\n\n>> What happens if 'path.toplevel' is requested in a bare repository?\n> \n> FWIW \"git rev-parse --show-toplevel\" dies with \"must be run in a\n> work tree\".  Better or worse,\n> \n> \trm -fr new\n> \tgit init new\n> \tcd new/.git && git rev-parse --show-toplevel\n> \n> also dies the same way, which I am not sure we want to inherit when\n> we are making a new interrogator command.\n\nYes, I think printing an empty value after the key would be better - I \ndon't think there are any paths where we care about the distinction \nbetween NULL and \"\"\n\nThanks\n\nPhillip\n\n"},{"id":"537488","messageId":"pull.2208.v6.git.git.1772428548.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v5.git.git.1772220640.gitgitgadget@gmail.com","subject":"[PATCH v6 0/6] repo info: add category/path keys and --path-format","fromName":"eslam reda via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-02T05:15:42Z","receivedAt":"2026-03-02T05:15:51Z","isPatch":true,"sender":{"key":"name:eslam reda","avatar":null},"body":"\nThis series now focuses only on git repo info improvements.\n===========================================================\n\nIt introduces category-aware key requests, adds path-oriented keys (path.*),\nand adds --path-format=(absolute|relative) so scripts can request stable\npath rendering behavior.\n\n\nWhat this PR does\n=================\n\nFor git repo info, this series:\n\n * introduces explicit repo_info context plumbing,\n * adds category-key expansion (for example, layout expands to layout.*),\n * adds path-oriented keys (path.*) for common repository locations,\n * adds --path-format=(absolute|relative) to control path output style.\n\nTests and documentation are updated accordingly.\n\n\nWhat this PR does NOT do\n========================\n\n * No git repo structure feature changes.\n * No t1901 structure test changes.\n * No structure metrics/docs additions.\n\n\nKey naming/scope adjustments from review\n========================================\n\n * renamed path.git-prefix to path.prefix,\n * added path.working-tree (alias for path.toplevel),\n * removed file-layout-oriented keys:\n   * path.logs-directory\n   * path.packed-refs-file\n   * path.refs-directory\n   * path.shallow-file\n * for bare repositories, work-tree style keys return empty values.\n\n\nCommit structure\n================\n\n * repo: introduce repo_info context plumbing\n * repo: support category requests in repo info\n * repo: add path keys to repo info\n * repo: add --path-format for info path output\n * t1900: cover repo info path keys and path-format\n * docs: describe repo info path keys\n\nAll commits are signed off with the same real-name identity.\n\n\nChanges since previous revision\n===============================\n\n * dropped all repo structure code/tests/docs from this branch,\n * split context plumbing and category expansion into separate commits,\n * rebased and cleaned history to avoid fix-on-fix commits,\n * aligned key naming/scope with review feedback.\n\n\nValidation\n==========\n\nFocused (Linux Docker):\n\n * make -C t test T=t1900-repo.sh → passed (53/53)\n\nEslam reda ragheb (6):\n  repo: introduce repo_info context plumbing\n  repo: support category requests in repo info\n  repo: add path keys to repo info\n  repo: add --path-format for info path output\n  t1900: cover repo info path keys and path-format\n  docs: describe repo info path keys\n\n Documentation/git-repo.adoc |  58 +++++++++-\n builtin/repo.c              | 209 +++++++++++++++++++++++++++++++++---\n t/t1900-repo.sh             | 194 +++++++++++++++++++++++++++++++++\n 3 files changed, 441 insertions(+), 20 deletions(-)\n\n\nbase-commit: 2cc71917514657b93014134350864f4849edfc83\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2208%2Feslam-reda-div%2Fgsoc-contribute-v6\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2208/eslam-reda-div/gsoc-contribute-v6\nPull-Request: https://github.com/git/git/pull/2208\n\nRange-diff vs v5:\n\n  1:  99c8058298 !  1:  bddea1a22e repo: teach info context and category keys\n     @@ Metadata\n      Author: Eslam reda ragheb <eslam.reda.div@gmail.com>\n      \n       ## Commit message ##\n     -    repo: teach info context and category keys\n     +    repo: introduce repo_info context plumbing\n      \n     -    Introduce an explicit repo_info context for the repo info codepath\n     -    and thread it through value lookups and field printing.\n     +    Introduce a repo_info context and thread it through get_value_fn,\n     +    field lookup, and value-printing helpers.\n      \n     -    This removes direct coupling from these helpers to ad-hoc\n     -    repository globals and makes key retrieval logic easier to extend\n     -    safely.\n     -\n     -    Also teach git repo info to accept category names (for example,\n     -    layout) and expand them to matching key.* entries in request\n     -    order.\n     -\n     -    This improves script ergonomics while preserving existing behavior\n     -    for explicit keys and clear errors for unknown names.\n     +    This prepares repo info for fields that need invocation-specific\n     +    context in addition to the repository handle.\n      \n          Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n      \n     @@ builtin/repo.c: struct field {\n       \tstrbuf_addstr(buf,\n       \t\t      ref_storage_format_to_name(repo->ref_storage_format));\n       \treturn 0;\n     -@@ builtin/repo.c: static get_value_fn *get_value_fn_for_key(const char *key)\n     - \treturn found ? found->get_value : NULL;\n     - }\n     - \n     -+static void print_field(enum output_format format, const char *key,\n     -+\t\t\tconst char *value);\n     -+\n     -+static int print_category_fields(const char *category,\n     -+\t\t\t\t struct repo_info *info,\n     -+\t\t\t\t enum output_format format,\n     -+\t\t\t\t struct strbuf *valbuf)\n     -+{\n     -+\tint found = 0;\n     -+\tsize_t category_len = strlen(category);\n     -+\n     -+\tfor (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {\n     -+\t\tconst struct field *field = &repo_info_fields[i];\n     -+\n     -+\t\tif (!starts_with(field->key, category) ||\n     -+\t\t    field->key[category_len] != '.')\n     -+\t\t\tcontinue;\n     -+\n     -+\t\tstrbuf_reset(valbuf);\n     -+\t\tfield->get_value(info, valbuf);\n     -+\t\tprint_field(format, field->key, valbuf->buf);\n     -+\t\tfound = 1;\n     -+\t}\n     -+\n     -+\treturn found;\n     -+}\n     -+\n     - static void print_field(enum output_format format, const char *key,\n     - \t\t\tconst char *value)\n     - {\n      @@ builtin/repo.c: static void print_field(enum output_format format, const char *key,\n       }\n       \n     @@ builtin/repo.c: static void print_field(enum output_format format, const char *k\n       {\n       \tint ret = 0;\n      @@ builtin/repo.c: static int print_fields(int argc, const char **argv,\n     - \n     - \t\tget_value = get_value_fn_for_key(key);\n     - \n     --\t\tif (!get_value) {\n     --\t\t\tret = error(_(\"key '%s' not found\"), key);\n     -+\t\tif (get_value) {\n     -+\t\t\tstrbuf_reset(&valbuf);\n     -+\t\t\tget_value(info, &valbuf);\n     -+\t\t\tprint_field(format, key, valbuf.buf);\n     - \t\t\tcontinue;\n       \t\t}\n       \n     --\t\tstrbuf_reset(&valbuf);\n     + \t\tstrbuf_reset(&valbuf);\n      -\t\tget_value(repo, &valbuf);\n     --\t\tprint_field(format, key, valbuf.buf);\n     -+\t\tif (!print_category_fields(key, info, format, &valbuf))\n     -+\t\t\tret = error(_(\"key '%s' not found\"), key);\n     ++\t\tget_value(info, &valbuf);\n     + \t\tprint_field(format, key, valbuf.buf);\n       \t}\n       \n     - \tstrbuf_release(&valbuf);\n     +@@ builtin/repo.c: static int print_fields(int argc, const char **argv,\n       \treturn ret;\n       }\n       \n     @@ builtin/repo.c: static int print_all_fields(struct repository *repo,\n      @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n       \t\t\t struct repository *repo)\n       {\n     - \tenum output_format format = FORMAT_KEYVALUE;\n     + \tenum output_format format = FORMAT_NEWLINE_TERMINATED;\n      +\tstruct repo_info info = {\n      +\t\t.repo = repo,\n      +\t\t.prefix = prefix,\n      +\t};\n       \tint all_keys = 0;\n     + \tint show_keys = 0;\n       \tstruct option options[] = {\n     - \t\tOPT_CALLBACK_F(0, \"format\", &format, N_(\"format\"),\n      @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n       \t\tdie(_(\"--all and <key> cannot be used together\"));\n       \n  4:  504d9cf7a0 !  2:  2369608976 repo: add structure max object size metrics\n     @@ Metadata\n      Author: Eslam reda ragheb <eslam.reda.div@gmail.com>\n      \n       ## Commit message ##\n     -    repo: add structure max object size metrics\n     +    repo: support category requests in repo info\n      \n     -    Extend git repo structure with maximum inflated and on-disk object\n     -    sizes, both per type and overall max values.\n     +    Teach repo info to accept category names (for example, layout)\n     +    and expand them to matching key.* entries in request order.\n      \n     -    This complements existing totals by highlighting outliers that\n     -    often drive repository bloat analysis.\n     -\n     -    The implementation updates object counting to track per-type maxima\n     -    while walking reachable objects.\n     -\n     -    It exposes those values in both table and keyvalue formats for\n     -    scripts and human output.\n     +    Explicit keys keep their existing behavior; unknown keys or\n     +    categories still report clear errors.\n      \n          Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n      \n       ## builtin/repo.c ##\n     -@@ builtin/repo.c: struct object_values {\n     - struct object_stats {\n     - \tstruct object_values type_counts;\n     - \tstruct object_values inflated_sizes;\n     -+\tstruct object_values max_inflated_sizes;\n     - \tstruct object_values disk_sizes;\n     -+\tstruct object_values max_disk_sizes;\n     - };\n     - \n     - struct repo_structure {\n     -@@ builtin/repo.c: static inline size_t get_total_object_values(struct object_values *values)\n     - \treturn values->tags + values->commits + values->trees + values->blobs;\n     +@@ builtin/repo.c: static get_value_fn *get_value_fn_for_key(const char *key)\n     + \treturn found ? found->get_value : NULL;\n       }\n       \n     -+static inline size_t get_max_object_value(struct object_values *values)\n     ++static void print_field(enum output_format format, const char *key,\n     ++\t\t\tconst char *value);\n     ++\n     ++static int print_category_fields(const char *category,\n     ++\t\t\t\t struct repo_info *info,\n     ++\t\t\t\t enum output_format format,\n     ++\t\t\t\t struct strbuf *valbuf)\n      +{\n     -+\tsize_t max = values->commits;\n     ++\tint found = 0;\n     ++\tsize_t category_len = strlen(category);\n      +\n     -+\tif (values->trees > max)\n     -+\t\tmax = values->trees;\n     -+\tif (values->blobs > max)\n     -+\t\tmax = values->blobs;\n     -+\tif (values->tags > max)\n     -+\t\tmax = values->tags;\n     ++\tfor (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {\n     ++\t\tconst struct field *field = &repo_info_fields[i];\n      +\n     -+\treturn max;\n     -+}\n     ++\t\tif (!starts_with(field->key, category) ||\n     ++\t\t    field->key[category_len] != '.')\n     ++\t\t\tcontinue;\n      +\n     - static void stats_table_setup_structure(struct stats_table *table,\n     - \t\t\t\t\tstruct repo_structure *stats)\n     - {\n     -@@ builtin/repo.c: static void stats_table_setup_structure(struct stats_table *table,\n     - \t\t\t      \"    * %s\", _(\"Blobs\"));\n     - \tstats_table_size_addf(table, objects->disk_sizes.tags,\n     - \t\t\t      \"    * %s\", _(\"Tags\"));\n     ++\t\tstrbuf_reset(valbuf);\n     ++\t\tfield->get_value(info, valbuf);\n     ++\t\tprint_field(format, field->key, valbuf->buf);\n     ++\t\tfound = 1;\n     ++\t}\n      +\n     -+\tstats_table_size_addf(table, objects->max_inflated_sizes.commits,\n     -+\t\t\t      \"  * %s\", _(\"Largest commit\"));\n     -+\tstats_table_size_addf(table, objects->max_inflated_sizes.trees,\n     -+\t\t\t      \"  * %s\", _(\"Largest tree\"));\n     -+\tstats_table_size_addf(table, objects->max_inflated_sizes.blobs,\n     -+\t\t\t      \"  * %s\", _(\"Largest blob\"));\n     -+\tstats_table_size_addf(table, objects->max_inflated_sizes.tags,\n     -+\t\t\t      \"  * %s\", _(\"Largest tag\"));\n     ++\treturn found;\n     ++}\n      +\n     -+\tstats_table_size_addf(table, get_max_object_value(&objects->max_disk_sizes),\n     -+\t\t\t      \"  * %s\", _(\"Largest disk size\"));\n     -+\tstats_table_size_addf(table, objects->max_disk_sizes.commits,\n     -+\t\t\t      \"    * %s\", _(\"Commits\"));\n     -+\tstats_table_size_addf(table, objects->max_disk_sizes.trees,\n     -+\t\t\t      \"    * %s\", _(\"Trees\"));\n     -+\tstats_table_size_addf(table, objects->max_disk_sizes.blobs,\n     -+\t\t\t      \"    * %s\", _(\"Blobs\"));\n     -+\tstats_table_size_addf(table, objects->max_disk_sizes.tags,\n     -+\t\t\t      \"    * %s\", _(\"Tags\"));\n     - }\n     - \n     - static void stats_table_print_structure(const struct stats_table *table)\n     -@@ builtin/repo.c: static void stats_table_clear(struct stats_table *table)\n     - static void structure_keyvalue_print(struct repo_structure *stats,\n     - \t\t\t\t     char key_delim, char value_delim)\n     + static void print_field(enum output_format format, const char *key,\n     + \t\t\tconst char *value)\n       {\n     -+\tsize_t max_inflated_size = get_max_object_value(&stats->objects.max_inflated_sizes);\n     -+\tsize_t max_disk_size = get_max_object_value(&stats->objects.max_disk_sizes);\n     -+\n     - \tprintf(\"references.branches.count%c%\" PRIuMAX \"%c\", key_delim,\n     - \t       (uintmax_t)stats->refs.branches, value_delim);\n     - \tprintf(\"references.tags.count%c%\" PRIuMAX \"%c\", key_delim,\n     -@@ builtin/repo.c: static void structure_keyvalue_print(struct repo_structure *stats,\n     - \tprintf(\"objects.tags.inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n     - \t       (uintmax_t)stats->objects.inflated_sizes.tags, value_delim);\n     +@@ builtin/repo.c: static int print_fields(int argc, const char **argv,\n       \n     -+\tprintf(\"objects.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)max_inflated_size, value_delim);\n     -+\tprintf(\"objects.commits.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_inflated_sizes.commits, value_delim);\n     -+\tprintf(\"objects.trees.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_inflated_sizes.trees, value_delim);\n     -+\tprintf(\"objects.blobs.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_inflated_sizes.blobs, value_delim);\n     -+\tprintf(\"objects.tags.max_inflated_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_inflated_sizes.tags, value_delim);\n     -+\n     -+\tprintf(\"objects.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)max_disk_size, value_delim);\n     -+\tprintf(\"objects.commits.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_disk_sizes.commits, value_delim);\n     -+\tprintf(\"objects.trees.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_disk_sizes.trees, value_delim);\n     -+\tprintf(\"objects.blobs.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_disk_sizes.blobs, value_delim);\n     -+\tprintf(\"objects.tags.max_disk_size%c%\" PRIuMAX \"%c\", key_delim,\n     -+\t       (uintmax_t)stats->objects.max_disk_sizes.tags, value_delim);\n     -+\n     - \tprintf(\"objects.commits.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n     - \t       (uintmax_t)stats->objects.disk_sizes.commits, value_delim);\n     - \tprintf(\"objects.trees.disk_size%c%\" PRIuMAX \"%c\", key_delim,\n     -@@ builtin/repo.c: static int count_objects(const char *path UNUSED, struct oid_array *oids,\n     - \tstruct object_stats *stats = data->stats;\n     - \tsize_t inflated_total = 0;\n     - \tsize_t disk_total = 0;\n     -+\tsize_t max_inflated = 0;\n     -+\tsize_t max_disk = 0;\n     - \tsize_t object_count;\n     + \t\tget_value = get_value_fn_for_key(key);\n       \n     - \tfor (size_t i = 0; i < oids->nr; i++) {\n     -@@ builtin/repo.c: static int count_objects(const char *path UNUSED, struct oid_array *oids,\n     - \t\t\t\t\t\t  OBJECT_INFO_SKIP_FETCH_OBJECT |\n     - \t\t\t\t\t\t  OBJECT_INFO_QUICK) < 0)\n     +-\t\tif (!get_value) {\n     +-\t\t\tret = error(_(\"key '%s' not found\"), key);\n     ++\t\tif (get_value) {\n     ++\t\t\tstrbuf_reset(&valbuf);\n     ++\t\t\tget_value(info, &valbuf);\n     ++\t\t\tprint_field(format, key, valbuf.buf);\n       \t\t\tcontinue;\n     -+\t\tif (disk < 0)\n     -+\t\t\tcontinue;\n     + \t\t}\n       \n     - \t\tinflated_total += inflated;\n     --\t\tdisk_total += disk;\n     -+\t\tdisk_total += (size_t)disk;\n     -+\t\tif (inflated > max_inflated)\n     -+\t\t\tmax_inflated = inflated;\n     -+\t\tif ((size_t)disk > max_disk)\n     -+\t\t\tmax_disk = (size_t)disk;\n     +-\t\tstrbuf_reset(&valbuf);\n     +-\t\tget_value(info, &valbuf);\n     +-\t\tprint_field(format, key, valbuf.buf);\n     ++\t\tif (!print_category_fields(key, info, format, &valbuf))\n     ++\t\t\tret = error(_(\"key '%s' not found\"), key);\n       \t}\n       \n     - \tswitch (type) {\n     - \tcase OBJ_TAG:\n     - \t\tstats->type_counts.tags += oids->nr;\n     - \t\tstats->inflated_sizes.tags += inflated_total;\n     -+\t\tif (max_inflated > stats->max_inflated_sizes.tags)\n     -+\t\t\tstats->max_inflated_sizes.tags = max_inflated;\n     - \t\tstats->disk_sizes.tags += disk_total;\n     -+\t\tif (max_disk > stats->max_disk_sizes.tags)\n     -+\t\t\tstats->max_disk_sizes.tags = max_disk;\n     - \t\tbreak;\n     - \tcase OBJ_COMMIT:\n     - \t\tstats->type_counts.commits += oids->nr;\n     - \t\tstats->inflated_sizes.commits += inflated_total;\n     -+\t\tif (max_inflated > stats->max_inflated_sizes.commits)\n     -+\t\t\tstats->max_inflated_sizes.commits = max_inflated;\n     - \t\tstats->disk_sizes.commits += disk_total;\n     -+\t\tif (max_disk > stats->max_disk_sizes.commits)\n     -+\t\t\tstats->max_disk_sizes.commits = max_disk;\n     - \t\tbreak;\n     - \tcase OBJ_TREE:\n     - \t\tstats->type_counts.trees += oids->nr;\n     - \t\tstats->inflated_sizes.trees += inflated_total;\n     -+\t\tif (max_inflated > stats->max_inflated_sizes.trees)\n     -+\t\t\tstats->max_inflated_sizes.trees = max_inflated;\n     - \t\tstats->disk_sizes.trees += disk_total;\n     -+\t\tif (max_disk > stats->max_disk_sizes.trees)\n     -+\t\t\tstats->max_disk_sizes.trees = max_disk;\n     - \t\tbreak;\n     - \tcase OBJ_BLOB:\n     - \t\tstats->type_counts.blobs += oids->nr;\n     - \t\tstats->inflated_sizes.blobs += inflated_total;\n     -+\t\tif (max_inflated > stats->max_inflated_sizes.blobs)\n     -+\t\t\tstats->max_inflated_sizes.blobs = max_inflated;\n     - \t\tstats->disk_sizes.blobs += disk_total;\n     -+\t\tif (max_disk > stats->max_disk_sizes.blobs)\n     -+\t\t\tstats->max_disk_sizes.blobs = max_disk;\n     - \t\tbreak;\n     - \tdefault:\n     - \t\tBUG(\"invalid object type\");\n     + \tstrbuf_release(&valbuf);\n  2:  6d5b9ff075 !  3:  477333a94c repo: add path keys to repo info\n     @@ builtin/repo.c: static int get_object_format(struct repo_info *info, struct strb\n      +\treturn 0;\n      +}\n      +\n     -+static int get_path_git_prefix(struct repo_info *info, struct strbuf *buf)\n     ++static int get_path_prefix(struct repo_info *info, struct strbuf *buf)\n      +{\n     -+\tif (info->prefix)\n     -+\t\tstrbuf_addstr(buf, info->prefix);\n     ++\tstrbuf_addstr(buf, info->prefix);\n      +\treturn 0;\n      +}\n      +\n     @@ builtin/repo.c: static int get_object_format(struct repo_info *info, struct strb\n      +\treturn 0;\n      +}\n      +\n     -+static int get_path_logs_directory(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\tstruct strbuf path = STRBUF_INIT;\n     -+\n     -+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"logs\"));\n     -+\tstrbuf_release(&path);\n     -+\treturn 0;\n     -+}\n     -+\n      +static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)\n      +{\n      +\trepo_info_add_path(info, buf, repo_get_object_directory(info->repo));\n      +\treturn 0;\n      +}\n      +\n     -+static int get_path_packed_refs_file(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\tstruct strbuf path = STRBUF_INIT;\n     -+\n     -+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"packed-refs\"));\n     -+\tstrbuf_release(&path);\n     -+\treturn 0;\n     -+}\n     -+\n     -+static int get_path_refs_directory(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\tstruct strbuf path = STRBUF_INIT;\n     -+\n     -+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"refs\"));\n     -+\tstrbuf_release(&path);\n     -+\treturn 0;\n     -+}\n     -+\n     -+static int get_path_shallow_file(struct repo_info *info, struct strbuf *buf)\n     -+{\n     -+\tstruct strbuf path = STRBUF_INIT;\n     -+\n     -+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"shallow\"));\n     -+\tstrbuf_release(&path);\n     -+\treturn 0;\n     -+}\n     -+\n      +static int get_path_superproject_working_tree(struct repo_info *info,\n      +\t\t\t\t\t     struct strbuf *buf)\n      +{\n     @@ builtin/repo.c: static int get_object_format(struct repo_info *info, struct strb\n      +\n      +\treturn 0;\n      +}\n     ++\n     ++static int get_path_work_tree(struct repo_info *info, struct strbuf *buf)\n     ++{\n     ++\treturn get_path_toplevel(info, buf);\n     ++}\n      +\n       static int get_references_format(struct repo_info *info, struct strbuf *buf)\n       {\n     @@ builtin/repo.c: static const struct field repo_info_fields[] = {\n      +\t{ \"path.common-dir\", get_path_common_dir },\n      +\t{ \"path.config-file\", get_path_config_file },\n      +\t{ \"path.git-dir\", get_path_git_dir },\n     -+\t{ \"path.git-prefix\", get_path_git_prefix },\n      +\t{ \"path.grafts-file\", get_path_grafts_file },\n      +\t{ \"path.hooks-directory\", get_path_hooks_directory },\n      +\t{ \"path.index-file\", get_path_index_file },\n     -+\t{ \"path.logs-directory\", get_path_logs_directory },\n      +\t{ \"path.objects-directory\", get_path_objects_directory },\n     -+\t{ \"path.packed-refs-file\", get_path_packed_refs_file },\n     -+\t{ \"path.refs-directory\", get_path_refs_directory },\n     -+\t{ \"path.shallow-file\", get_path_shallow_file },\n     ++\t{ \"path.prefix\", get_path_prefix },\n      +\t{ \"path.superproject-working-tree\", get_path_superproject_working_tree },\n      +\t{ \"path.toplevel\", get_path_toplevel },\n     ++\t{ \"path.working-tree\", get_path_work_tree },\n       \t{ \"references.format\", get_references_format },\n       };\n       \n     +@@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n     + \tenum output_format format = FORMAT_NEWLINE_TERMINATED;\n     + \tstruct repo_info info = {\n     + \t\t.repo = repo,\n     +-\t\t.prefix = prefix,\n     ++\t\t.prefix = prefix ? prefix : \"\",\n     + \t};\n     + \tint all_keys = 0;\n     + \tint show_keys = 0;\n  3:  5c438d045b !  4:  2a7afe4f87 repo: add --path-format for info path output\n     @@ builtin/repo.c\n       #include \"utf8.h\"\n       \n       static const char *const repo_usage[] = {\n     --\t\"git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]\",\n     -+\t\"git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\",\n     - \t\"git repo structure [--format=(table|keyvalue|nul) | -z]\",\n     +-\t\"git repo info [--format=(lines|nul) | -z] [--all | <key>...]\",\n     ++\t\"git repo info [--format=(lines|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\",\n     + \t\"git repo info --keys [--format=(lines|nul) | -z]\",\n     + \t\"git repo structure [--format=(table|lines|nul) | -z]\",\n       \tNULL\n       };\n       \n     @@ builtin/repo.c: static int parse_format_cb(const struct option *opt,\n      @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n       \tstruct repo_info info = {\n       \t\t.repo = repo,\n     - \t\t.prefix = prefix,\n     + \t\t.prefix = prefix ? prefix : \"\",\n      +\t\t.path_format = PATH_FORMAT_ABSOLUTE,\n       \t};\n       \tint all_keys = 0;\n     - \tstruct option options[] = {\n     + \tint show_keys = 0;\n      @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n       \t\t\t       N_(\"synonym for --format=nul\"),\n       \t\t\t       PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n     @@ builtin/repo.c: static int cmd_repo_info(int argc, const char **argv, const char\n      +\t\t\t       N_(\"format\"), N_(\"path output format\"),\n      +\t\t\t       PARSE_OPT_NONEG, parse_path_format_cb),\n       \t\tOPT_BOOL(0, \"all\", &all_keys, N_(\"print all keys/values\")),\n     + \t\tOPT_BOOL(0, \"keys\", &show_keys, N_(\"show keys\")),\n       \t\tOPT_END()\n     - \t};\n  5:  4b502925c9 <  -:  ---------- repo: add structure topology and path-depth metrics\n  6:  1751181950 <  -:  ---------- repo: add aggregate structure totals to keyvalue output\n  7:  fd18f28db0 !  5:  1e52e7bd7f t1900: cover repo info path keys and path-format\n     @@ Commit message\n          Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n      \n       ## t/t1900-repo.sh ##\n     -@@ t/t1900-repo.sh: REPO_INFO_KEYS='\n     - \tlayout.bare\n     - \tlayout.shallow\n     - \tobject.format\n     +@@ t/t1900-repo.sh: test_description='test git repo-info'\n     + \n     + . ./test-lib.sh\n     + \n     ++# git-repo-info keys. It must contain the same keys listed in the const\n     ++# repo_info_fields, in lexicographical order.\n     ++REPO_INFO_KEYS='\n     ++\tlayout.bare\n     ++\tlayout.shallow\n     ++\tobject.format\n      +\tpath.common-dir\n      +\tpath.config-file\n      +\tpath.git-dir\n     -+\tpath.git-prefix\n      +\tpath.grafts-file\n      +\tpath.hooks-directory\n      +\tpath.index-file\n     -+\tpath.logs-directory\n      +\tpath.objects-directory\n     -+\tpath.packed-refs-file\n     -+\tpath.refs-directory\n     -+\tpath.shallow-file\n     ++\tpath.prefix\n      +\tpath.superproject-working-tree\n      +\tpath.toplevel\n     - \treferences.format\n     - '\n     - \n     ++\tpath.working-tree\n     ++\treferences.format\n     ++'\n     ++\n      +REPO_INFO_PATH_KEYS='\n      +\tpath.common-dir\n      +\tpath.config-file\n      +\tpath.git-dir\n     -+\tpath.git-prefix\n      +\tpath.grafts-file\n      +\tpath.hooks-directory\n      +\tpath.index-file\n     -+\tpath.logs-directory\n      +\tpath.objects-directory\n     -+\tpath.packed-refs-file\n     -+\tpath.refs-directory\n     -+\tpath.shallow-file\n     ++\tpath.prefix\n      +\tpath.superproject-working-tree\n      +\tpath.toplevel\n     ++\tpath.working-tree\n      +'\n      +\n       # Test whether a key-value pair is correctly returned\n     @@ t/t1900-repo.sh: test_expect_success 'values returned in order requested' '\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'path.git-prefix matches rev-parse --show-prefix' '\n     ++test_expect_success 'path.prefix matches rev-parse --show-prefix' '\n      +\tgit init path-prefix &&\n      +\tmkdir -p path-prefix/a/b &&\n      +\texpected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&\n     -+\techo \"path.git-prefix=$expected_value\" >expect &&\n     -+\tgit -C path-prefix/a/b repo info path.git-prefix >actual &&\n     ++\techo \"path.prefix=$expected_value\" >expect &&\n     ++\tgit -C path-prefix/a/b repo info path.prefix >actual &&\n      +\ttest_cmp expect actual\n      +'\n      +\n     @@ t/t1900-repo.sh: test_expect_success 'values returned in order requested' '\n      +\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path config) &&\n      +\techo \"path.config-file=$expected_value\" >expect &&\n      +\tgit -C path-git-path repo info path.config-file >actual &&\n     -+\ttest_cmp expect actual &&\n     -+\n     -+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path logs) &&\n     -+\techo \"path.logs-directory=$expected_value\" >expect &&\n     -+\tgit -C path-git-path repo info path.logs-directory >actual &&\n     -+\ttest_cmp expect actual &&\n     -+\n     -+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path packed-refs) &&\n     -+\techo \"path.packed-refs-file=$expected_value\" >expect &&\n     -+\tgit -C path-git-path repo info path.packed-refs-file >actual &&\n     -+\ttest_cmp expect actual &&\n     ++\ttest_cmp expect actual\n     ++'\n      +\n     -+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path refs) &&\n     -+\techo \"path.refs-directory=$expected_value\" >expect &&\n     -+\tgit -C path-git-path repo info path.refs-directory >actual &&\n     -+\ttest_cmp expect actual &&\n     ++test_expect_success 'path.working-tree matches path.toplevel' '\n     ++\tgit init path-work-tree &&\n     ++\texpected_value=$(git -C path-work-tree rev-parse --show-toplevel) &&\n     ++\techo \"path.working-tree=$expected_value\" >expect &&\n     ++\tgit -C path-work-tree repo info path.working-tree >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n      +\n     -+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path shallow) &&\n     -+\techo \"path.shallow-file=$expected_value\" >expect &&\n     -+\tgit -C path-git-path repo info path.shallow-file >actual &&\n     ++test_expect_success 'path.working-tree is empty in bare repository' '\n     ++\tgit init --bare bare-path-work-tree &&\n     ++\techo \"path.working-tree=\" >expect &&\n     ++\tgit -C bare-path-work-tree repo info path.working-tree >actual &&\n      +\ttest_cmp expect actual\n      +'\n      +\n  8:  0525ed4cd9 <  -:  ---------- t1901: extend structure metric coverage and portability\n  9:  f17c0f03e5 !  6:  b98490a4a4 docs: describe repo info path keys and structure metrics\n     @@ Metadata\n      Author: Eslam reda ragheb <eslam.reda.div@gmail.com>\n      \n       ## Commit message ##\n     -    docs: describe repo info path keys and structure metrics\n     +    docs: describe repo info path keys\n      \n     -    Document the newly added repo info capabilities, including\n     -    category keys and path-oriented key definitions.\n     -\n     -    Also describe --path-format behavior for path outputs.\n     -\n     -    Update git repo structure documentation to cover newly reported\n     -    maxima and aggregate keyvalue/nul fields.\n     -\n     -    This keeps command behavior and output keys fully specified for\n     -    users and scripts.\n     +    Document repo info category requests, path.* keys, and\n     +    --path-format behavior.\n      \n          Signed-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n      \n     @@ Documentation/git-repo.adoc: git-repo - Retrieve information about the repositor\n       SYNOPSIS\n       --------\n       [synopsis]\n     --git repo info [--format=(keyvalue|nul) | -z] [--all | <key>...]\n     -+git repo info [--format=(keyvalue|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\n     - git repo structure [--format=(table|keyvalue|nul) | -z]\n     +-git repo info [--format=(lines|nul) | -z] [--all | <key>...]\n     ++git repo info [--format=(lines|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\n     + git repo info --keys [--format=(lines|nul) | -z]\n     + git repo structure [--format=(table|lines|nul) | -z]\n       \n     - DESCRIPTION\n      @@ Documentation/git-repo.adoc: supported:\n     - +\n     - `-z` is an alias for `--format=nul`.\n     + `nul`:::\n     + \tSimilar to `lines`, but using a _NUL_ character after each value.\n       \n      +`--path-format=(absolute|relative)`:::\n      +\tControls formatting for keys in the `path` category. The default is\n      +\t`absolute`. This option may be specified multiple times; the last one\n      +\tspecified takes effect.\n      +\n     - `structure [--format=(table|keyvalue|nul) | -z]`::\n     + `structure [--format=(table|lines|nul) | -z]`::\n       \tRetrieve statistics about the current repository structure. The\n       \tfollowing kinds of information are reported:\n      @@ Documentation/git-repo.adoc: supported:\n     @@ Documentation/git-repo.adoc: supported:\n       The output format can be chosen through the flag `--format`. Three formats are\n       supported:\n      @@ Documentation/git-repo.adoc: supported:\n     - `keyvalue`:::\n     + `lines`:::\n       \tEach line of output contains a key-value pair for a repository stat.\n       \tThe '=' character is used to delimit between the key and the value.\n      +\tBoth aggregate metrics and per-type metrics are included.\n     @@ Documentation/git-repo.adoc: values that they return:\n      +`path.git-dir`::\n      +\tThe path to the git directory.\n      +\n     -+`path.git-prefix`::\n     ++`path.prefix`::\n      +\tThe path of the current working directory relative to the top-level\n      +\tdirectory.\n      +\n     @@ Documentation/git-repo.adoc: values that they return:\n      +`path.index-file`::\n      +\tThe path to the index file.\n      +\n     -+`path.logs-directory`::\n     -+\tThe path to the `logs` directory.\n     -+\n      +`path.objects-directory`::\n      +\tThe path to the objects directory.\n      +\n     -+`path.packed-refs-file`::\n     -+\tThe path to the `packed-refs` file.\n     -+\n     -+`path.refs-directory`::\n     -+\tThe path to the `refs` directory.\n     -+\n     -+`path.shallow-file`::\n     -+\tThe path to the `shallow` file.\n     -+\n      +`path.superproject-working-tree`::\n      +\tThe path to the superproject's working tree root, or an empty string\n      +\twhen the repository is not used as a submodule.\n     @@ Documentation/git-repo.adoc: values that they return:\n      +`path.toplevel`::\n      +\tThe path to the top-level working tree directory, or an empty string\n      +\tfor bare repositories.\n     ++\n     ++`path.working-tree`::\n     ++\tAlias for `path.toplevel`.\n      +\n       `references.format`::\n       \tThe reference storage format. The valid values are:\n 10:  1bc100d6ca <  -:  ---------- repo: reduce repetition in structure keyvalue output\n 11:  8af17ad831 <  -:  ---------- repo: refine path keys for repo info\n\n-- \ngitgitgadget\n"},{"id":"537489","messageId":"bddea1a22e56b7b2988f795e5e0556c6707a126c.1772428548.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v6.git.git.1772428548.gitgitgadget@gmail.com","subject":"[PATCH v6 1/6] repo: introduce repo_info context plumbing","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-02T05:15:43Z","receivedAt":"2026-03-02T05:15:53Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nIntroduce a repo_info context and thread it through get_value_fn,\nfield lookup, and value-printing helpers.\n\nThis prepares repo info for fields that need invocation-specific\ncontext in addition to the repository handle.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 34 +++++++++++++++++++++++-----------\n 1 file changed, 23 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 6a62a6020a..e687d833b4 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -23,7 +23,12 @@ static const char *const repo_usage[] = {\n \tNULL\n };\n \n-typedef int get_value_fn(struct repository *repo, struct strbuf *buf);\n+struct repo_info {\n+\tstruct repository *repo;\n+\tconst char *prefix;\n+};\n+\n+typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);\n \n enum output_format {\n \tFORMAT_TABLE,\n@@ -36,27 +41,30 @@ struct field {\n \tget_value_fn *get_value;\n };\n \n-static int get_layout_bare(struct repository *repo UNUSED, struct strbuf *buf)\n+static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)\n {\n \tstrbuf_addstr(buf, is_bare_repository() ? \"true\" : \"false\");\n \treturn 0;\n }\n \n-static int get_layout_shallow(struct repository *repo, struct strbuf *buf)\n+static int get_layout_shallow(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf,\n \t\t      is_repository_shallow(repo) ? \"true\" : \"false\");\n \treturn 0;\n }\n \n-static int get_object_format(struct repository *repo, struct strbuf *buf)\n+static int get_object_format(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf, repo->hash_algo->name);\n \treturn 0;\n }\n \n-static int get_references_format(struct repository *repo, struct strbuf *buf)\n+static int get_references_format(struct repo_info *info, struct strbuf *buf)\n {\n+\tstruct repository *repo = info->repo;\n \tstrbuf_addstr(buf,\n \t\t      ref_storage_format_to_name(repo->ref_storage_format));\n \treturn 0;\n@@ -106,7 +114,7 @@ static void print_field(enum output_format format, const char *key,\n }\n \n static int print_fields(int argc, const char **argv,\n-\t\t\tstruct repository *repo,\n+\t\t\tstruct repo_info *info,\n \t\t\tenum output_format format)\n {\n \tint ret = 0;\n@@ -124,7 +132,7 @@ static int print_fields(int argc, const char **argv,\n \t\t}\n \n \t\tstrbuf_reset(&valbuf);\n-\t\tget_value(repo, &valbuf);\n+\t\tget_value(info, &valbuf);\n \t\tprint_field(format, key, valbuf.buf);\n \t}\n \n@@ -132,7 +140,7 @@ static int print_fields(int argc, const char **argv,\n \treturn ret;\n }\n \n-static int print_all_fields(struct repository *repo,\n+static int print_all_fields(struct repo_info *info,\n \t\t\t    enum output_format format)\n {\n \tstruct strbuf valbuf = STRBUF_INIT;\n@@ -141,7 +149,7 @@ static int print_all_fields(struct repository *repo,\n \t\tconst struct field *field = &repo_info_fields[i];\n \n \t\tstrbuf_reset(&valbuf);\n-\t\tfield->get_value(repo, &valbuf);\n+\t\tfield->get_value(info, &valbuf);\n \t\tprint_field(format, field->key, valbuf.buf);\n \t}\n \n@@ -195,6 +203,10 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\t\t struct repository *repo)\n {\n \tenum output_format format = FORMAT_NEWLINE_TERMINATED;\n+\tstruct repo_info info = {\n+\t\t.repo = repo,\n+\t\t.prefix = prefix,\n+\t};\n \tint all_keys = 0;\n \tint show_keys = 0;\n \tstruct option options[] = {\n@@ -225,9 +237,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"--all and <key> cannot be used together\"));\n \n \tif (all_keys)\n-\t\treturn print_all_fields(repo, format);\n+\t\treturn print_all_fields(&info, format);\n \telse\n-\t\treturn print_fields(argc, argv, repo, format);\n+\t\treturn print_fields(argc, argv, &info, format);\n }\n \n struct ref_stats {\n-- \ngitgitgadget\n\n"},{"id":"537490","messageId":"2369608976c6126bcf40c59974389a84394aab68.1772428548.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v6.git.git.1772428548.gitgitgadget@gmail.com","subject":"[PATCH v6 2/6] repo: support category requests in repo info","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-02T05:15:44Z","receivedAt":"2026-03-02T05:15:55Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nTeach repo info to accept category names (for example, layout)\nand expand them to matching key.* entries in request order.\n\nExplicit keys keep their existing behavior; unknown keys or\ncategories still report clear errors.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 38 +++++++++++++++++++++++++++++++++-----\n 1 file changed, 33 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex e687d833b4..f614298199 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -96,6 +96,33 @@ static get_value_fn *get_value_fn_for_key(const char *key)\n \treturn found ? found->get_value : NULL;\n }\n \n+static void print_field(enum output_format format, const char *key,\n+\t\t\tconst char *value);\n+\n+static int print_category_fields(const char *category,\n+\t\t\t\t struct repo_info *info,\n+\t\t\t\t enum output_format format,\n+\t\t\t\t struct strbuf *valbuf)\n+{\n+\tint found = 0;\n+\tsize_t category_len = strlen(category);\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) {\n+\t\tconst struct field *field = &repo_info_fields[i];\n+\n+\t\tif (!starts_with(field->key, category) ||\n+\t\t    field->key[category_len] != '.')\n+\t\t\tcontinue;\n+\n+\t\tstrbuf_reset(valbuf);\n+\t\tfield->get_value(info, valbuf);\n+\t\tprint_field(format, field->key, valbuf->buf);\n+\t\tfound = 1;\n+\t}\n+\n+\treturn found;\n+}\n+\n static void print_field(enum output_format format, const char *key,\n \t\t\tconst char *value)\n {\n@@ -126,14 +153,15 @@ static int print_fields(int argc, const char **argv,\n \n \t\tget_value = get_value_fn_for_key(key);\n \n-\t\tif (!get_value) {\n-\t\t\tret = error(_(\"key '%s' not found\"), key);\n+\t\tif (get_value) {\n+\t\t\tstrbuf_reset(&valbuf);\n+\t\t\tget_value(info, &valbuf);\n+\t\t\tprint_field(format, key, valbuf.buf);\n \t\t\tcontinue;\n \t\t}\n \n-\t\tstrbuf_reset(&valbuf);\n-\t\tget_value(info, &valbuf);\n-\t\tprint_field(format, key, valbuf.buf);\n+\t\tif (!print_category_fields(key, info, format, &valbuf))\n+\t\t\tret = error(_(\"key '%s' not found\"), key);\n \t}\n \n \tstrbuf_release(&valbuf);\n-- \ngitgitgadget\n\n"},{"id":"537491","messageId":"477333a94cdc1c98be8b0dac75a72e2745902bc6.1772428548.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v6.git.git.1772428548.gitgitgadget@gmail.com","subject":"[PATCH v6 3/6] repo: add path keys to repo info","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-02T05:15:45Z","receivedAt":"2026-03-02T05:15:57Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nAdd a path category to git repo info with key-value pairs that\nmirror repository paths users commonly retrieve via rev-parse and\ngit-path lookups.\n\nThis makes scripting against repo metadata more direct and avoids\nshelling out to multiple commands for related paths.\n\nThe new keys are introduced as explicit path.* entries in\nrepo_info_fields and are resolved through dedicated helpers.\n\nThis keeps lookup behavior predictable and makes future path\nadditions straightforward.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 104 ++++++++++++++++++++++++++++++++++++++++++++++++-\n 1 file changed, 103 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex f614298199..87c5850929 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -1,10 +1,12 @@\n #define USE_THE_REPOSITORY_VARIABLE\n \n #include \"builtin.h\"\n+#include \"abspath.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n #include \"odb.h\"\n #include \"parse-options.h\"\n+#include \"path.h\"\n #include \"path-walk.h\"\n #include \"progress.h\"\n #include \"quote.h\"\n@@ -14,6 +16,7 @@\n #include \"strbuf.h\"\n #include \"string-list.h\"\n #include \"shallow.h\"\n+#include \"submodule.h\"\n #include \"utf8.h\"\n \n static const char *const repo_usage[] = {\n@@ -41,6 +44,13 @@ struct field {\n \tget_value_fn *get_value;\n };\n \n+static void repo_info_add_path(struct repo_info *info,\n+\t\t\t      struct strbuf *buf,\n+\t\t\t      const char *path)\n+{\n+\tstrbuf_add_absolute_path(buf, path);\n+}\n+\n static int get_layout_bare(struct repo_info *info UNUSED, struct strbuf *buf)\n {\n \tstrbuf_addstr(buf, is_bare_repository() ? \"true\" : \"false\");\n@@ -62,6 +72,87 @@ static int get_object_format(struct repo_info *info, struct strbuf *buf)\n \treturn 0;\n }\n \n+static int get_path_common_dir(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_common_dir(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_config_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"config\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_git_dir(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_git_dir(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_prefix(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstrbuf_addstr(buf, info->prefix);\n+\treturn 0;\n+}\n+\n+static int get_path_grafts_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_graft_file(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_hooks_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\n+\trepo_info_add_path(info, buf, repo_git_path_replace(info->repo, &path, \"hooks\"));\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+static int get_path_index_file(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_index_file(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_objects_directory(struct repo_info *info, struct strbuf *buf)\n+{\n+\trepo_info_add_path(info, buf, repo_get_object_directory(info->repo));\n+\treturn 0;\n+}\n+\n+static int get_path_superproject_working_tree(struct repo_info *info,\n+\t\t\t\t\t     struct strbuf *buf)\n+{\n+\tstruct strbuf superproject = STRBUF_INIT;\n+\n+\tif (get_superproject_working_tree(&superproject))\n+\t\trepo_info_add_path(info, buf, superproject.buf);\n+\n+\tstrbuf_release(&superproject);\n+\treturn 0;\n+}\n+\n+static int get_path_toplevel(struct repo_info *info, struct strbuf *buf)\n+{\n+\tconst char *work_tree = repo_get_work_tree(info->repo);\n+\n+\tif (work_tree)\n+\t\trepo_info_add_path(info, buf, work_tree);\n+\n+\treturn 0;\n+}\n+\n+static int get_path_work_tree(struct repo_info *info, struct strbuf *buf)\n+{\n+\treturn get_path_toplevel(info, buf);\n+}\n+\n static int get_references_format(struct repo_info *info, struct strbuf *buf)\n {\n \tstruct repository *repo = info->repo;\n@@ -75,6 +166,17 @@ static const struct field repo_info_fields[] = {\n \t{ \"layout.bare\", get_layout_bare },\n \t{ \"layout.shallow\", get_layout_shallow },\n \t{ \"object.format\", get_object_format },\n+\t{ \"path.common-dir\", get_path_common_dir },\n+\t{ \"path.config-file\", get_path_config_file },\n+\t{ \"path.git-dir\", get_path_git_dir },\n+\t{ \"path.grafts-file\", get_path_grafts_file },\n+\t{ \"path.hooks-directory\", get_path_hooks_directory },\n+\t{ \"path.index-file\", get_path_index_file },\n+\t{ \"path.objects-directory\", get_path_objects_directory },\n+\t{ \"path.prefix\", get_path_prefix },\n+\t{ \"path.superproject-working-tree\", get_path_superproject_working_tree },\n+\t{ \"path.toplevel\", get_path_toplevel },\n+\t{ \"path.working-tree\", get_path_work_tree },\n \t{ \"references.format\", get_references_format },\n };\n \n@@ -233,7 +335,7 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \tenum output_format format = FORMAT_NEWLINE_TERMINATED;\n \tstruct repo_info info = {\n \t\t.repo = repo,\n-\t\t.prefix = prefix,\n+\t\t.prefix = prefix ? prefix : \"\",\n \t};\n \tint all_keys = 0;\n \tint show_keys = 0;\n-- \ngitgitgadget\n\n"},{"id":"537492","messageId":"2a7afe4f877d4cad361dbb0d68d86ceab9448edf.1772428548.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v6.git.git.1772428548.gitgitgadget@gmail.com","subject":"[PATCH v6 4/6] repo: add --path-format for info path output","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-02T05:15:46Z","receivedAt":"2026-03-02T05:15:59Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nTeach git repo info to accept --path-format=(absolute|relative)\nso scripts can request stable path style explicitly.\n\nThis aligns path.* output behavior with existing rev-parse usage\npatterns and reduces ad-hoc path conversion in callers.\n\nThe option is wired through repo_info context and used by\nrepo_info_add_path(), so path formatting remains centralized and\nconsistent across all path.* keys.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n builtin/repo.c | 37 ++++++++++++++++++++++++++++++++++++-\n 1 file changed, 36 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/repo.c b/builtin/repo.c\nindex 87c5850929..bcdb9210cb 100644\n--- a/builtin/repo.c\n+++ b/builtin/repo.c\n@@ -20,15 +20,21 @@\n #include \"utf8.h\"\n \n static const char *const repo_usage[] = {\n-\t\"git repo info [--format=(lines|nul) | -z] [--all | <key>...]\",\n+\t\"git repo info [--format=(lines|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\",\n \t\"git repo info --keys [--format=(lines|nul) | -z]\",\n \t\"git repo structure [--format=(table|lines|nul) | -z]\",\n \tNULL\n };\n \n+enum path_format {\n+\tPATH_FORMAT_ABSOLUTE,\n+\tPATH_FORMAT_RELATIVE,\n+};\n+\n struct repo_info {\n \tstruct repository *repo;\n \tconst char *prefix;\n+\tenum path_format path_format;\n };\n \n typedef int get_value_fn(struct repo_info *info, struct strbuf *buf);\n@@ -48,6 +54,16 @@ static void repo_info_add_path(struct repo_info *info,\n \t\t\t      struct strbuf *buf,\n \t\t\t      const char *path)\n {\n+\tif (info->path_format == PATH_FORMAT_RELATIVE) {\n+\t\tchar *cwd = xgetcwd();\n+\t\tstruct strbuf rel_path = STRBUF_INIT;\n+\n+\t\tstrbuf_addstr(buf, relative_path(path, cwd, &rel_path));\n+\t\tstrbuf_release(&rel_path);\n+\t\tfree(cwd);\n+\t\treturn;\n+\t}\n+\n \tstrbuf_add_absolute_path(buf, path);\n }\n \n@@ -329,6 +345,21 @@ static int parse_format_cb(const struct option *opt,\n \treturn 0;\n }\n \n+static int parse_path_format_cb(const struct option *opt,\n+\t\t\t\tconst char *arg, int unset UNUSED)\n+{\n+\tenum path_format *path_format = opt->value;\n+\n+\tif (!strcmp(arg, \"absolute\"))\n+\t\t*path_format = PATH_FORMAT_ABSOLUTE;\n+\telse if (!strcmp(arg, \"relative\"))\n+\t\t*path_format = PATH_FORMAT_RELATIVE;\n+\telse\n+\t\tdie(_(\"invalid path format '%s'\"), arg);\n+\n+\treturn 0;\n+}\n+\n static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\t\t struct repository *repo)\n {\n@@ -336,6 +367,7 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \tstruct repo_info info = {\n \t\t.repo = repo,\n \t\t.prefix = prefix ? prefix : \"\",\n+\t\t.path_format = PATH_FORMAT_ABSOLUTE,\n \t};\n \tint all_keys = 0;\n \tint show_keys = 0;\n@@ -347,6 +379,9 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,\n \t\t\t       N_(\"synonym for --format=nul\"),\n \t\t\t       PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n \t\t\t       parse_format_cb),\n+\t\tOPT_CALLBACK_F(0, \"path-format\", &info.path_format,\n+\t\t\t       N_(\"format\"), N_(\"path output format\"),\n+\t\t\t       PARSE_OPT_NONEG, parse_path_format_cb),\n \t\tOPT_BOOL(0, \"all\", &all_keys, N_(\"print all keys/values\")),\n \t\tOPT_BOOL(0, \"keys\", &show_keys, N_(\"show keys\")),\n \t\tOPT_END()\n-- \ngitgitgadget\n\n"},{"id":"537493","messageId":"1e52e7bd7f124481910d455da388c75636b5cb38.1772428548.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v6.git.git.1772428548.gitgitgadget@gmail.com","subject":"[PATCH v6 5/6] t1900: cover repo info path keys and path-format","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-02T05:15:47Z","receivedAt":"2026-03-02T05:16:00Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nExtend t1900 to validate category-key expansion, path.* key\nbehavior, and --path-format handling for git repo info.\n\nThe tests compare repo info output to equivalent rev-parse values.\n\nThis ensures behavior remains aligned with existing plumbing\nsemantics.\n\nAlso keep mixed key/category ordering coverage so callers can rely\non deterministic output order when combining explicit keys with\ncategory requests.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n t/t1900-repo.sh | 194 ++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 194 insertions(+)\n\ndiff --git a/t/t1900-repo.sh b/t/t1900-repo.sh\nindex a9eb07abe8..6605394d1f 100755\n--- a/t/t1900-repo.sh\n+++ b/t/t1900-repo.sh\n@@ -4,6 +4,40 @@ test_description='test git repo-info'\n \n . ./test-lib.sh\n \n+# git-repo-info keys. It must contain the same keys listed in the const\n+# repo_info_fields, in lexicographical order.\n+REPO_INFO_KEYS='\n+\tlayout.bare\n+\tlayout.shallow\n+\tobject.format\n+\tpath.common-dir\n+\tpath.config-file\n+\tpath.git-dir\n+\tpath.grafts-file\n+\tpath.hooks-directory\n+\tpath.index-file\n+\tpath.objects-directory\n+\tpath.prefix\n+\tpath.superproject-working-tree\n+\tpath.toplevel\n+\tpath.working-tree\n+\treferences.format\n+'\n+\n+REPO_INFO_PATH_KEYS='\n+\tpath.common-dir\n+\tpath.config-file\n+\tpath.git-dir\n+\tpath.grafts-file\n+\tpath.hooks-directory\n+\tpath.index-file\n+\tpath.objects-directory\n+\tpath.prefix\n+\tpath.superproject-working-tree\n+\tpath.toplevel\n+\tpath.working-tree\n+'\n+\n # Test whether a key-value pair is correctly returned\n #\n # Usage: test_repo_info <label> <init command> <repo_name> <key> <expected value>\n@@ -80,6 +114,166 @@ test_expect_success 'values returned in order requested' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'category key returns all matching keys' '\n+\tcat >expect <<-\\EOF &&\n+\tlayout.bare=false\n+\tlayout.shallow=false\n+\tEOF\n+\tgit init category-layout &&\n+\tgit -C category-layout repo info layout >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'mixed key/category requests preserve request order' '\n+\tcat >expect <<-EOF &&\n+\tobject.format=$(test_oid algo)\n+\tlayout.bare=false\n+\tlayout.shallow=false\n+\tEOF\n+\tgit init mixed-order &&\n+\tgit -C mixed-order repo info object.format layout >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.git-dir matches rev-parse --absolute-git-dir' '\n+\tgit init path-git-dir &&\n+\texpected_value=$(git -C path-git-dir rev-parse --absolute-git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-git-dir repo info path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.common-dir matches rev-parse --git-common-dir' '\n+\tgit init path-common-dir &&\n+\texpected_value=$(git -C path-common-dir rev-parse --path-format=absolute --git-common-dir) &&\n+\techo \"path.common-dir=$expected_value\" >expect &&\n+\tgit -C path-common-dir repo info path.common-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.toplevel matches rev-parse --show-toplevel' '\n+\tgit init path-toplevel &&\n+\texpected_value=$(git -C path-toplevel rev-parse --show-toplevel) &&\n+\techo \"path.toplevel=$expected_value\" >expect &&\n+\tgit -C path-toplevel repo info path.toplevel >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.toplevel is empty in bare repository' '\n+\tgit init --bare bare-path-toplevel &&\n+\techo \"path.toplevel=\" >expect &&\n+\tgit -C bare-path-toplevel repo info path.toplevel >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.prefix matches rev-parse --show-prefix' '\n+\tgit init path-prefix &&\n+\tmkdir -p path-prefix/a/b &&\n+\texpected_value=$(git -C path-prefix/a/b rev-parse --show-prefix) &&\n+\techo \"path.prefix=$expected_value\" >expect &&\n+\tgit -C path-prefix/a/b repo info path.prefix >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git-path style keys match rev-parse --git-path' '\n+\tgit init path-git-path &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path info/grafts) &&\n+\techo \"path.grafts-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.grafts-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path index) &&\n+\techo \"path.index-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.index-file >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path objects) &&\n+\techo \"path.objects-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.objects-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path hooks) &&\n+\techo \"path.hooks-directory=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.hooks-directory >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-git-path rev-parse --path-format=absolute --git-path config) &&\n+\techo \"path.config-file=$expected_value\" >expect &&\n+\tgit -C path-git-path repo info path.config-file >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.working-tree matches path.toplevel' '\n+\tgit init path-work-tree &&\n+\texpected_value=$(git -C path-work-tree rev-parse --show-toplevel) &&\n+\techo \"path.working-tree=$expected_value\" >expect &&\n+\tgit -C path-work-tree repo info path.working-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.working-tree is empty in bare repository' '\n+\tgit init --bare bare-path-work-tree &&\n+\techo \"path.working-tree=\" >expect &&\n+\tgit -C bare-path-work-tree repo info path.working-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.superproject-working-tree is empty when not a submodule' '\n+\tgit init path-superproject &&\n+\techo \"path.superproject-working-tree=\" >expect &&\n+\tgit -C path-superproject repo info path.superproject-working-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path.superproject-working-tree matches rev-parse in submodule' '\n+\tgit init path-superproject-origin &&\n+\techo x >path-superproject-origin/x &&\n+\tgit -C path-superproject-origin add x &&\n+\tgit -C path-superproject-origin commit -m x &&\n+\n+\tgit init path-superproject-parent &&\n+\tgit -C path-superproject-parent -c protocol.file.allow=always submodule add ../path-superproject-origin sm &&\n+\n+\texpected_value=$(git -C path-superproject-parent/sm rev-parse --show-superproject-working-tree) &&\n+\techo \"path.superproject-working-tree=$expected_value\" >expect &&\n+\tgit -C path-superproject-parent/sm repo info path.superproject-working-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path category returns all path keys' '\n+\tgit init path-category &&\n+\t>expect &&\n+\tfor key in $REPO_INFO_PATH_KEYS\n+\tdo\n+\t\tgit -C path-category repo info \"$key\" >>expect || return 1\n+\tdone &&\n+\tgit -C path-category repo info path >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path-format=relative matches rev-parse for git-dir' '\n+\tgit init path-format-relative &&\n+\texpected_value=$(git -C path-format-relative rev-parse --path-format=relative --git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-format-relative repo info --path-format=relative path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git repo info uses the last requested path format' '\n+\tgit init path-format-last &&\n+\texpected_value=$(git -C path-format-last rev-parse --path-format=relative --git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-format-last repo info --path-format=absolute --path-format=relative path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git-repo-info aborts when requesting an invalid path format' '\n+\techo \"fatal: invalid path format ${SQ}foo${SQ}\" >expect &&\n+\ttest_must_fail git repo info --path-format=foo path.git-dir 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'git-repo-info fails if an invalid key is requested' '\n \techo \"error: key ${SQ}foo${SQ} not found\" >expect &&\n \ttest_must_fail git repo info foo 2>actual &&\n-- \ngitgitgadget\n\n"},{"id":"537494","messageId":"b98490a4a453c91da503644a4e1b5dfab0834a14.1772428548.git.gitgitgadget@gmail.com","threadId":"65043","inReplyTo":"pull.2208.v6.git.git.1772428548.gitgitgadget@gmail.com","subject":"[PATCH v6 6/6] docs: describe repo info path keys","fromName":"Eslam reda ragheb via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-02T05:15:48Z","receivedAt":"2026-03-02T05:16:02Z","isPatch":true,"sender":{"key":"name:Eslam reda ragheb","avatar":null},"body":"From: Eslam reda ragheb <eslam.reda.div@gmail.com>\n\nDocument repo info category requests, path.* keys, and\n--path-format behavior.\n\nSigned-off-by: Eslam reda ragheb <eslam.reda.div@gmail.com>\n---\n Documentation/git-repo.adoc | 58 ++++++++++++++++++++++++++++++++++---\n 1 file changed, 54 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc\nindex 319d30bd86..086ab922ad 100644\n--- a/Documentation/git-repo.adoc\n+++ b/Documentation/git-repo.adoc\n@@ -8,7 +8,7 @@ git-repo - Retrieve information about the repository\n SYNOPSIS\n --------\n [synopsis]\n-git repo info [--format=(lines|nul) | -z] [--all | <key>...]\n+git repo info [--format=(lines|nul) | -z] [--path-format=(absolute|relative)] [--all | <key>...]\n git repo info --keys [--format=(lines|nul) | -z]\n git repo structure [--format=(table|lines|nul) | -z]\n \n@@ -56,6 +56,11 @@ supported:\n `nul`:::\n \tSimilar to `lines`, but using a _NUL_ character after each value.\n \n+`--path-format=(absolute|relative)`:::\n+\tControls formatting for keys in the `path` category. The default is\n+\t`absolute`. This option may be specified multiple times; the last one\n+\tspecified takes effect.\n+\n `structure [--format=(table|lines|nul) | -z]`::\n \tRetrieve statistics about the current repository structure. The\n \tfollowing kinds of information are reported:\n@@ -64,6 +69,12 @@ supported:\n * Reachable object counts categorized by type\n * Total inflated size of reachable objects by type\n * Total disk size of reachable objects by type\n+* Largest inflated reachable object size by type\n+* Largest disk size of a reachable object by type\n+* Largest parent count among reachable commits\n+* Largest entry count among reachable trees\n+* Longest and deepest path among reachable blobs\n+* Deepest annotated tag chain\n +\n The output format can be chosen through the flag `--format`. Three formats are\n supported:\n@@ -76,6 +87,7 @@ supported:\n `lines`:::\n \tEach line of output contains a key-value pair for a repository stat.\n \tThe '=' character is used to delimit between the key and the value.\n+\tBoth aggregate metrics and per-type metrics are included.\n \tValues containing \"unusual\" characters are quoted as explained for the\n \tconfiguration variable `core.quotePath` (see linkgit:git-config[1]).\n \n@@ -90,9 +102,11 @@ supported:\n \n INFO KEYS\n ---------\n-In order to obtain a set of values from `git repo info`, you should provide\n-the keys that identify them. Here's a list of the available keys and the\n-values that they return:\n+In order to obtain values from `git repo info`, provide either individual keys\n+or category names. A category returns all keys within that category. For\n+example, `layout` returns both `layout.bare` and `layout.shallow`.\n+\n+Here's a list of the available keys and the values that they return:\n \n `layout.bare`::\n \t`true` if this is a bare repository, otherwise `false`.\n@@ -103,6 +117,42 @@ values that they return:\n `object.format`::\n \tThe object format (hash algorithm) used in the repository.\n \n+`path.common-dir`::\n+\tThe path to the common git directory.\n+\n+`path.config-file`::\n+\tThe path to the `config` file in the git directory.\n+\n+`path.git-dir`::\n+\tThe path to the git directory.\n+\n+`path.prefix`::\n+\tThe path of the current working directory relative to the top-level\n+\tdirectory.\n+\n+`path.grafts-file`::\n+\tThe path to the `info/grafts` file.\n+\n+`path.hooks-directory`::\n+\tThe path to the `hooks` directory.\n+\n+`path.index-file`::\n+\tThe path to the index file.\n+\n+`path.objects-directory`::\n+\tThe path to the objects directory.\n+\n+`path.superproject-working-tree`::\n+\tThe path to the superproject's working tree root, or an empty string\n+\twhen the repository is not used as a submodule.\n+\n+`path.toplevel`::\n+\tThe path to the top-level working tree directory, or an empty string\n+\tfor bare repositories.\n+\n+`path.working-tree`::\n+\tAlias for `path.toplevel`.\n+\n `references.format`::\n \tThe reference storage format. The valid values are:\n +\n-- \ngitgitgadget\n"},{"id":"537495","messageId":"xmqq8qca68bh.fsf@gitster.g","threadId":"65043","inReplyTo":"58d46ec7-99cd-4878-b05d-a378ca119a68@gmail.com","subject":"Re: [PATCH v4 02/10] repo: add path keys to repo info","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-02T06:42:42Z","receivedAt":"2026-03-02T06:42:45Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n>> FWIW \"git rev-parse --show-toplevel\" dies with \"must be run in a\n>> work tree\".  Better or worse,\n>> \n>> \trm -fr new\n>> \tgit init new\n>> \tcd new/.git && git rev-parse --show-toplevel\n>> \n>> also dies the same way, which I am not sure we want to inherit when\n>> we are making a new interrogator command.\n>\n> Yes, I think printing an empty value after the key would be better - I \n> don't think there are any paths where we care about the distinction \n> between NULL and \"\"\n\nIf the new and safer variant of the command should signal \"error\" by\ngiving an empty string, it would work with its \"--show-toplevel\"\nequivalent run in a bare repository.  In a working tree, it would\ngive a full/absolute path, and never be an empty string.  I am still\nunsure what that new and safer one should do from inside the .git\ndirectory.  \"rev-parse --show-toplevel\" dies.  $(cd .. && pwd) may\nbe another plausible and arguably more useful answer.\n\nYour idea of equiating NULL and \"\", I do not think it would work\nwell with \"git rev-parse --show-cdup\" equivalent.  The command will\ngive us an empty string from the top-level of the working tree.\n\nCuriously, in this sequence\n\n\trm -fr new\n\tgit init new\n\tcd new/.git && git rev-parse --show-cdup\n\tcd objects && git rev-parse --show-cdup\n\ntwo \"rev-parse\" do not die with \"must be run in a work tree\", and\nworse yet, they do not give you \"..\" or \"../../\", either.\n\nIt seems that one rule of \"rev-parse --show-<some-path>\" is that\n\"when you are inside .git directory of a non-bare repository, we'd\nbehave as if you are in a bare repository as if its working tree\ndoes not exist\", and the above is consistent with that rule.\n\"--show-cdup\" does not fail but gives an empty string when run\nanywhere in a bare repository.\n\nBut among \"rev-parse --show-<anything>\" that are about working tree\npaths, there seems no unifying rule on what to do when in a bare\nrepsitory.  As we already saw, \"--show-toplevel\" dies without a\nworking tree.  This reflects the history of rev-parse that grew\norganizally without a grand design.\n\nIf we are adding new and safer interface to these pieces of\ninformation to \"repo info\", we may want to straighten these rules.\n\n"},{"id":"539325","messageId":"20260318204438.40075-1-jerrywang183@yahoo.com","threadId":"65043","inReplyTo":"pull.2208.v6.git.git.1772428548.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 0/6] repo info: add category/path keys and --path-format","fromName":"Jialong Wang","fromEmail":"jerrywang183@yahoo.com","sentAt":"2026-03-18T20:44:38Z","receivedAt":"2026-03-18T20:54:57Z","isPatch":true,"sender":{"key":"jerrywang183@yahoo.com","avatar":null},"body":"Hi,\n\nWhile reading the current `git repo info` implementation, I noticed that\n`layout.bare` is still implemented via `is_bare_repository()` in\n`builtin/repo.c`.\n\nAt first I thought this might be a small repository-awareness cleanup,\nsince the `repo info` field callbacks already receive a `struct\nrepository *`. But after tracing it further, it seems the current\n`is_bare_repository()` semantics are not equivalent to simply checking\nwhether `repo_get_work_tree(repo)` is NULL.\n\nSo before trying to patch this, I wanted to confirm the intended\ndirection:\n\n- Should `repo info`'s `layout.bare` continue to follow the current\n  `is_bare_repository()` semantics?\n- Or, if `git repo info` is meant to become more explicitly\n  repository-aware over time, would it make sense to introduce something\n  like a `repo_is_bare(repo)` helper and use that instead?\n\nI am asking mainly to avoid sending a misleading cleanup patch for what\nmay actually be a semantics question.\n\nThanks,\nJialong\n"},{"id":"539338","messageId":"20260319033641.38458-1-jayatheerthkulkarni2005@gmail.com","threadId":"65043","inReplyTo":"20260318204438.40075-1-jerrywang183@yahoo.com","subject":"Re: [PATCH v6 0/6] repo info: add category/path keys and --path-format","fromName":"K Jayatheerth","fromEmail":"jayatheerthkulkarni2005@gmail.com","sentAt":"2026-03-19T03:36:41Z","receivedAt":"2026-03-19T03:36:52Z","isPatch":true,"sender":{"key":"jayatheerthkulkarni2005@gmail.com","avatar":"https://avatars.githubusercontent.com/u/148841023?v=4"},"body":"> While reading the current `git repo info` implementation, I noticed that\n> `layout.bare` is still implemented via `is_bare_repository()` in\n> `builtin/repo.c`.\n>\n> At first I thought this might be a small repository-awareness cleanup,\n> since the `repo info` field callbacks already receive a `struct\n> repository *`. But after tracing it further, it seems the current\n> `is_bare_repository()` semantics are not equivalent to simply checking\n> whether `repo_get_work_tree(repo)` is NULL.\n\nHmph, this was an idea I explored few days ago\nBut I do agree the repo->worktree == NULL method has multiple flaws\n\nFor example\n\ntest_expect_success 'layout.bare is false even when run from inside .git' '\n\tgit init nonbare-dot-git &&\n\techo \"layout.bare=false\" >expect &&\n\n\tgit -C nonbare-dot-git/.git repo info layout.bare >actual &&\n\ttest_cmp expect actual\n'\n\nI cooked up a test like this and it failed\nI have since been exploring config.c and parse.c.\nspecifically the if repo_config_get_bool()\n\nI think there are few other checks we need to do on top of\nrepo->worktree to be completely sure that the given repo is in fact bare.\nAlso instead of using repo->worktree I think we can use repo_get_work_tree(repo)\nWhy recreate the logic when we have a getter ;)\n\nI am convinced that we need a helper\nrepo_is_bare is a good name too.\n\nThanks for exploring this :)\n\nRegards,\n- Jayatheerth\n"},{"id":"539420","messageId":"58874877-D2F4-431E-82FD-722D8A2BC790@yahoo.com","threadId":"65043","inReplyTo":"20260319033641.38458-1-jayatheerthkulkarni2005@gmail.com","subject":"Re: [PATCH v6 0/6] repo info: add category/path keys and --path-format","fromName":"Jerry Wang","fromEmail":"jerrywang183@yahoo.com","sentAt":"2026-03-19T20:32:57Z","receivedAt":"2026-03-19T20:43:27Z","isPatch":true,"sender":{"key":"jerrywang183@yahoo.com","avatar":null},"body":"Hi Jay,\n\nThanks, this is very helpful.\n\nThat makes sense. I agree that checking only\nrepo_get_work_tree(repo) == NULL is too weak, and the\n\"git -C <nonbare>/.git\" case is a good example of that.\n\nI'll keep the linked-worktree / separate-git-dir coverage patch\nseparate. For layout.bare, I'll first add tests to pin down the\nintended semantics, then follow up with a small repo-aware fix,\nlikely with a repo_is_bare() helper if that turns out to be the\nright shape.\n\nThanks,\nJialong\n\n> On Mar 18, 2026, at 23:36, K Jayatheerth <jayatheerthkulkarni2005@gmail.com> wrote:\n> \n>> While reading the current `git repo info` implementation, I noticed that\n>> `layout.bare` is still implemented via `is_bare_repository()` in\n>> `builtin/repo.c`.\n>> \n>> At first I thought this might be a small repository-awareness cleanup,\n>> since the `repo info` field callbacks already receive a `struct\n>> repository *`. But after tracing it further, it seems the current\n>> `is_bare_repository()` semantics are not equivalent to simply checking\n>> whether `repo_get_work_tree(repo)` is NULL.\n> \n> Hmph, this was an idea I explored few days ago\n> But I do agree the repo->worktree == NULL method has multiple flaws\n> \n> For example\n> \n> test_expect_success 'layout.bare is false even when run from inside .git' '\n> \tgit init nonbare-dot-git &&\n> \techo \"layout.bare=false\" >expect &&\n> \n> \tgit -C nonbare-dot-git/.git repo info layout.bare >actual &&\n> \ttest_cmp expect actual\n> '\n> \n> I cooked up a test like this and it failed\n> I have since been exploring config.c and parse.c.\n> specifically the if repo_config_get_bool()\n> \n> I think there are few other checks we need to do on top of\n> repo->worktree to be completely sure that the given repo is in fact bare.\n> Also instead of using repo->worktree I think we can use repo_get_work_tree(repo)\n> Why recreate the logic when we have a getter ;)\n> \n> I am convinced that we need a helper\n> repo_is_bare is a good name too.\n> \n> Thanks for exploring this :)\n> \n> Regards,\n> - Jayatheerth\n\n"},{"id":"539422","messageId":"20260319205903.15242-1-jerrywang183@yahoo.com","threadId":"65043","inReplyTo":"pull.2208.v6.git.git.1772428548.gitgitgadget@gmail.com","subject":"[PATCH] t1900: cover repo info path keys in non-default layouts","fromName":"Jialong Wang","fromEmail":"jerrywang183@yahoo.com","sentAt":"2026-03-19T20:59:03Z","receivedAt":"2026-03-19T21:09:18Z","isPatch":true,"sender":{"key":"jerrywang183@yahoo.com","avatar":null},"body":"Extend the path-key coverage in t1900 to exercise layouts where the\nreported paths differ from a regular worktree.\n\nAdd comparisons against rev-parse for path.git-dir, path.common-dir,\nand path.toplevel in a linked worktree and a repository created with\n--separate-git-dir. Also cover path.git-dir with --path-format=relative\nin the separate-git-dir case, and path.superproject-working-tree with\n--path-format=relative in a submodule.\n\nSigned-off-by: Jialong Wang <jerrywang183@yahoo.com>\n---\n t/t1900-repo.sh | 54 +++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 54 insertions(+)\n\ndiff --git a/t/t1900-repo.sh b/t/t1900-repo.sh\nindex 6605394d1f..3f39d2b9f6 100755\n--- a/t/t1900-repo.sh\n+++ b/t/t1900-repo.sh\n@@ -241,6 +241,60 @@ test_expect_success 'path.superproject-working-tree matches rev-parse in submodu\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'path keys match rev-parse in linked worktree' '\n+\tgit init path-linked-main &&\n+\techo x >path-linked-main/x &&\n+\tgit -C path-linked-main add x &&\n+\tgit -C path-linked-main commit -m x &&\n+\tgit -C path-linked-main worktree add ../path-linked-worktree &&\n+\n+\texpected_value=$(git -C path-linked-worktree rev-parse --absolute-git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-linked-worktree repo info path.git-dir >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-linked-worktree rev-parse --path-format=absolute --git-common-dir) &&\n+\techo \"path.common-dir=$expected_value\" >expect &&\n+\tgit -C path-linked-worktree repo info path.common-dir >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-linked-worktree rev-parse --show-toplevel) &&\n+\techo \"path.toplevel=$expected_value\" >expect &&\n+\tgit -C path-linked-worktree repo info path.toplevel >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'path keys match rev-parse with --separate-git-dir' '\n+\tgit init --separate-git-dir path-separate.git path-separate-worktree &&\n+\n+\texpected_value=$(git -C path-separate-worktree rev-parse --absolute-git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-separate-worktree repo info path.git-dir >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-separate-worktree rev-parse --path-format=absolute --git-common-dir) &&\n+\techo \"path.common-dir=$expected_value\" >expect &&\n+\tgit -C path-separate-worktree repo info path.common-dir >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-separate-worktree rev-parse --show-toplevel) &&\n+\techo \"path.toplevel=$expected_value\" >expect &&\n+\tgit -C path-separate-worktree repo info path.toplevel >actual &&\n+\ttest_cmp expect actual &&\n+\n+\texpected_value=$(git -C path-separate-worktree rev-parse --path-format=relative --git-dir) &&\n+\techo \"path.git-dir=$expected_value\" >expect &&\n+\tgit -C path-separate-worktree repo info --path-format=relative path.git-dir >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'relative path.superproject-working-tree matches rev-parse in submodule' '\n+\texpected_value=$(git -C path-superproject-parent/sm rev-parse --path-format=relative --show-superproject-working-tree) &&\n+\techo \"path.superproject-working-tree=$expected_value\" >expect &&\n+\tgit -C path-superproject-parent/sm repo info --path-format=relative path.superproject-working-tree >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'path category returns all path keys' '\n \tgit init path-category &&\n \t>expect &&\n-- \n2.51.0\n\n"},{"id":"539424","messageId":"20260319205854.15214-1-jerrywang183@yahoo.com","threadId":"65043","inReplyTo":"pull.2208.v6.git.git.1772428548.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 5/6] t1900: cover repo info path keys and path-format","fromName":"Jialong Wang","fromEmail":"jerrywang183@yahoo.com","sentAt":"2026-03-19T20:58:54Z","receivedAt":"2026-03-19T21:19:15Z","isPatch":true,"sender":{"key":"jerrywang183@yahoo.com","avatar":null},"body":"Hi Eslam,\n\nThanks for working on this. I tried the v6 branch locally, and the\npath-key coverage looks good overall, but I think t1900 still misses a\nfew non-default layouts that seem worth covering.\n\nIn particular, I think it would be useful to add coverage for:\n\n  - linked worktrees, where path.git-dir points into\n    .git/worktrees/<name>, path.common-dir still points at the main\n    .git, and path.toplevel points at the linked checkout\n\n  - repositories created with --separate-git-dir, especially\n    path.git-dir with --path-format=relative\n\n  - path.superproject-working-tree with --path-format=relative in a\n    submodule\n\nI tested these locally against rev-parse and they seem to behave as\nexpected, so they may make good follow-up coverage for this series.\n\nThanks,\nJialong\n"},{"id":"539465","messageId":"CA+rGoLdjCZfDnvyQ+qiJn4xhA7F5cj8Mouc+pG9vbrb6ngoxVQ@mail.gmail.com","threadId":"65043","inReplyTo":"58874877-D2F4-431E-82FD-722D8A2BC790@yahoo.com","subject":"Re: [PATCH v6 0/6] repo info: add category/path keys and --path-format","fromName":"K Jayatheerth","fromEmail":"jayatheerthkulkarni2005@gmail.com","sentAt":"2026-03-20T01:49:28Z","receivedAt":"2026-03-20T01:49:40Z","isPatch":true,"sender":{"key":"jayatheerthkulkarni2005@gmail.com","avatar":"https://avatars.githubusercontent.com/u/148841023?v=4"},"body":"On Fri, Mar 20, 2026 at 2:03 AM Jerry Wang <jerrywang183@yahoo.com> wrote:\n>\n> Hi Jay,\n>\n> Thanks, this is very helpful.\n>\n> That makes sense. I agree that checking only\n> repo_get_work_tree(repo) == NULL is too weak, and the\n> \"git -C <nonbare>/.git\" case is a good example of that.\n>\n> I'll keep the linked-worktree / separate-git-dir coverage patch\n> separate. For layout.bare, I'll first add tests to pin down the\n> intended semantics, then follow up with a small repo-aware fix,\n> likely with a repo_is_bare() helper if that turns out to be the\n> right shape.\n>\n> Thanks,\n> Jialong\n>\n\n\nIf you are a GSoC applicant(I am presuming) I suggest not to start\nworking on patches\nas you might step on other people's toes.\n\nSimilar discussion here [1]\nSimilar official info here [2] in the _Don’t work on a proposed\nproject right away_ section.\nAnother similar discussion [3]\nSimilar thing was caused by Eslam's patches which overlapped with what\nLucas had previously as a foundation [4].\n\nRegards,\n- Jayatheerth\n\n1 - https://lore.kernel.org/git/pull.2242.git.git.1773766519857.gitgitgadget@gmail.com/T/#m9506e0856630c9ff962093d59cc9477bd39ea6df\n2 - https://git.github.io/General-Microproject-Information/\n3 - https://lore.kernel.org/git/CALE2CrTt_2-9C4zCrZPBabtsWY=+Mk-bH4Jaemk=yHtfpoLjfg@mail.gmail.com/T/#m002015a8042bfe2717887c3ada9afd645615f50a\n4 - https://lore.kernel.org/git/CA+rGoLdTc2caDUsQedpegL+T4MqwwiA62uuDSFSawAT5vcPvWQ@mail.gmail.com/T/#t\n"}]}