From cbb2e60f1a620cabb4157ed8ea29884dc52d5d5b Mon Sep 17 00:00:00 2001 From: K Jayatheerth Date: Mon, 15 Jun 2026 10:21:09 +0530 Subject: [PATCH 1/4] path: introduce append_formatted_path() for shared path formatting The path-formatting logic in builtin/rev-parse.c is tightly coupled to that command and writes directly to stdout, making it impossible for other builtins to reuse. Extract the core algorithm into append_formatted_path() in path.c and expose a path_format enum in path.h so that any builtin can format paths consistently without duplicating logic. Mentored-by: Justin Tobler Mentored-by: Lucas Seiki Oshiro Signed-off-by: K Jayatheerth Signed-off-by: Junio C Hamano --- path.c | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ path.h | 36 ++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/path.c b/path.c index d7e17bf174..5e83e3e4f6 100644 --- a/path.c +++ b/path.c @@ -1579,6 +1579,76 @@ char *xdg_cache_home(const char *filename) return NULL; } +void append_formatted_path(struct strbuf *dest, const char *path, + const char *prefix, enum path_format format) +{ + switch (format) { + case PATH_FORMAT_DEFAULT: + case PATH_FORMAT_UNMODIFIED: + strbuf_addstr(dest, path); + break; + + case PATH_FORMAT_RELATIVE: { + struct strbuf relative_buf = STRBUF_INIT; + struct strbuf real_path = STRBUF_INIT; + struct strbuf real_prefix = STRBUF_INIT; + char *cwd = NULL; + + /* + * We don't ever produce a relative path if prefix is NULL, + * so set the prefix to the current directory so that we can + * produce a relative path whenever possible. + */ + if (!prefix) + prefix = cwd = xgetcwd(); + + if (!is_absolute_path(path)) { + strbuf_realpath_forgiving(&real_path, path, 1); + path = real_path.buf; + } + if (!is_absolute_path(prefix)) { + strbuf_realpath_forgiving(&real_prefix, prefix, 1); + prefix = real_prefix.buf; + } + + strbuf_addstr(dest, relative_path(path, prefix, &relative_buf)); + + strbuf_release(&relative_buf); + strbuf_release(&real_path); + strbuf_release(&real_prefix); + free(cwd); + break; + } + + case PATH_FORMAT_RELATIVE_IF_SHARED: { + struct strbuf relative_buf = STRBUF_INIT; + + /* + * If we're using RELATIVE_IF_SHARED mode, then we want an + * absolute path unless the two share a common prefix, so don't + * default the prefix to the current working directory. Doing so + * would cause a relative path to always be produced if possible. + */ + strbuf_addstr(dest, relative_path(path, prefix, &relative_buf)); + strbuf_release(&relative_buf); + break; + } + + case PATH_FORMAT_CANONICAL: { + struct strbuf canonical_buf = STRBUF_INIT; + + strbuf_realpath_forgiving(&canonical_buf, path, 1); + strbuf_addbuf(dest, &canonical_buf); + + strbuf_release(&canonical_buf); + break; + } + + default: + BUG("unknown path_format value %d", format); + } +} + REPO_GIT_PATH_FUNC(squash_msg, "SQUASH_MSG") REPO_GIT_PATH_FUNC(merge_msg, "MERGE_MSG") REPO_GIT_PATH_FUNC(merge_rr, "MERGE_RR") diff --git a/path.h b/path.h index 0434ba5e07..6aca53b100 100644 --- a/path.h +++ b/path.h @@ -262,6 +262,42 @@ enum scld_error safe_create_leading_directories_no_share(char *path); int safe_create_file_with_leading_directories(struct repository *repo, const char *path); +/** + * The formatting strategy to apply when writing a path into a buffer. + */ +enum path_format { + /* + * Represents the default formatting behavior. Treated as + * PATH_FORMAT_UNMODIFIED by append_formatted_path(). + */ + PATH_FORMAT_DEFAULT, + + /* Output the path exactly as-is without any modifications. */ + PATH_FORMAT_UNMODIFIED, + + /* Output a path relative to the provided directory prefix. */ + PATH_FORMAT_RELATIVE, + + /* Output a relative path only if the path shares a root with the prefix. */ + PATH_FORMAT_RELATIVE_IF_SHARED, + + /* Output a fully resolved, absolute canonical path. */ + PATH_FORMAT_CANONICAL +}; + +/** + * Format a path according to the specified formatting strategy and append + * the result to the given strbuf. + * + * `dest` : The string buffer to append the formatted path to. + * `path` : The path string that needs to be formatted. + * `prefix` : The directory prefix to calculate relative offsets against. + * Pass NULL to default to the current working directory where applicable. + * `format` : The formatting behavior rule to execute. + */ +void append_formatted_path(struct strbuf *dest, const char *path, + const char *prefix, enum path_format format); + # ifdef USE_THE_REPOSITORY_VARIABLE # include "strbuf.h" # include "repository.h" From 53ff8ed0e4f5f1371a34f2c6051aa4802422d91c Mon Sep 17 00:00:00 2001 From: K Jayatheerth Date: Mon, 15 Jun 2026 10:21:10 +0530 Subject: [PATCH 2/4] rev-parse: use append_formatted_path() for path formatting Now that path formatting logic lives in a shared helper, keeping a duplicate implementation in rev-parse is unnecessary and risks the two diverging over time. Replace the local format_type and default_type enums and the hand-rolled formatting logic with a call to append_formatted_path(). Introduce PATH_FORMAT_DEFAULT as the initial value of arg_path_format so that per-path fallback behavior is resolved in print_path() rather than leaked into the shared helper. Mentored-by: Justin Tobler Mentored-by: Lucas Seiki Oshiro Signed-off-by: K Jayatheerth Signed-off-by: Junio C Hamano --- builtin/rev-parse.c | 103 ++++++++++---------------------------------- 1 file changed, 23 insertions(+), 80 deletions(-) diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c index bb882678fe..7ffa4b54b7 100644 --- a/builtin/rev-parse.c +++ b/builtin/rev-parse.c @@ -633,73 +633,16 @@ static void handle_ref_opt(const char *pattern, const char *prefix) clear_ref_exclusions(&ref_excludes); } -enum format_type { - /* We would like a relative path. */ - FORMAT_RELATIVE, - /* We would like a canonical absolute path. */ - FORMAT_CANONICAL, - /* We would like the default behavior. */ - FORMAT_DEFAULT, -}; - -enum default_type { - /* Our default is a relative path. */ - DEFAULT_RELATIVE, - /* Our default is a relative path if there's a shared root. */ - DEFAULT_RELATIVE_IF_SHARED, - /* Our default is a canonical absolute path. */ - DEFAULT_CANONICAL, - /* Our default is not to modify the item. */ - DEFAULT_UNMODIFIED, -}; - -static void print_path(const char *path, const char *prefix, enum format_type format, enum default_type def) +static void print_path(const char *path, const char *prefix, + enum path_format arg_path_format, enum path_format def_format) { - char *cwd = NULL; - /* - * We don't ever produce a relative path if prefix is NULL, so set the - * prefix to the current directory so that we can produce a relative - * path whenever possible. If we're using RELATIVE_IF_SHARED mode, then - * we want an absolute path unless the two share a common prefix, so don't - * set it in that case, since doing so causes a relative path to always - * be produced if possible. - */ - if (!prefix && (format != FORMAT_DEFAULT || def != DEFAULT_RELATIVE_IF_SHARED)) - prefix = cwd = xgetcwd(); - if (format == FORMAT_DEFAULT && def == DEFAULT_UNMODIFIED) { - puts(path); - } else if (format == FORMAT_RELATIVE || - (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE)) { - /* - * In order for relative_path to work as expected, we need to - * make sure that both paths are absolute paths. If we don't, - * we can end up with an unexpected absolute path that the user - * didn't want. - */ - struct strbuf buf = STRBUF_INIT, realbuf = STRBUF_INIT, prefixbuf = STRBUF_INIT; - if (!is_absolute_path(path)) { - strbuf_realpath_forgiving(&realbuf, path, 1); - path = realbuf.buf; - } - if (!is_absolute_path(prefix)) { - strbuf_realpath_forgiving(&prefixbuf, prefix, 1); - prefix = prefixbuf.buf; - } - puts(relative_path(path, prefix, &buf)); - strbuf_release(&buf); - strbuf_release(&realbuf); - strbuf_release(&prefixbuf); - } else if (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE_IF_SHARED) { - struct strbuf buf = STRBUF_INIT; - puts(relative_path(path, prefix, &buf)); - strbuf_release(&buf); - } else { - struct strbuf buf = STRBUF_INIT; - strbuf_realpath_forgiving(&buf, path, 1); - puts(buf.buf); - strbuf_release(&buf); - } - free(cwd); + struct strbuf sb = STRBUF_INIT; + enum path_format fmt = (arg_path_format != PATH_FORMAT_DEFAULT) ? arg_path_format : def_format; + + append_formatted_path(&sb, path, prefix, fmt); + puts(sb.buf); + + strbuf_release(&sb); } int cmd_rev_parse(int argc, @@ -718,7 +661,7 @@ int cmd_rev_parse(int argc, const char *name = NULL; struct strbuf buf = STRBUF_INIT; int seen_end_of_options = 0; - enum format_type format = FORMAT_DEFAULT; + enum path_format arg_path_format = PATH_FORMAT_DEFAULT; show_usage_if_asked(argc, argv, builtin_rev_parse_usage); @@ -798,8 +741,8 @@ int cmd_rev_parse(int argc, die(_("--git-path requires an argument")); print_path(repo_git_path_replace(the_repository, &buf, "%s", argv[i + 1]), prefix, - format, - DEFAULT_RELATIVE_IF_SHARED); + arg_path_format, + PATH_FORMAT_RELATIVE_IF_SHARED); i++; continue; } @@ -821,9 +764,9 @@ int cmd_rev_parse(int argc, if (!arg) die(_("--path-format requires an argument")); if (!strcmp(arg, "absolute")) { - format = FORMAT_CANONICAL; + arg_path_format = PATH_FORMAT_CANONICAL; } else if (!strcmp(arg, "relative")) { - format = FORMAT_RELATIVE; + arg_path_format = PATH_FORMAT_RELATIVE; } else { die(_("unknown argument to --path-format: %s"), arg); } @@ -996,7 +939,7 @@ int cmd_rev_parse(int argc, if (!strcmp(arg, "--show-toplevel")) { const char *work_tree = repo_get_work_tree(the_repository); if (work_tree) - print_path(work_tree, prefix, format, DEFAULT_UNMODIFIED); + print_path(work_tree, prefix, arg_path_format, PATH_FORMAT_UNMODIFIED); else die(_("this operation must be run in a work tree")); continue; @@ -1004,7 +947,7 @@ int cmd_rev_parse(int argc, if (!strcmp(arg, "--show-superproject-working-tree")) { struct strbuf superproject = STRBUF_INIT; if (get_superproject_working_tree(&superproject)) - print_path(superproject.buf, prefix, format, DEFAULT_UNMODIFIED); + print_path(superproject.buf, prefix, arg_path_format, PATH_FORMAT_UNMODIFIED); strbuf_release(&superproject); continue; } @@ -1039,18 +982,18 @@ int cmd_rev_parse(int argc, const char *gitdir = getenv(GIT_DIR_ENVIRONMENT); char *cwd; int len; - enum format_type wanted = format; + enum path_format wanted = arg_path_format; if (arg[2] == 'g') { /* --git-dir */ if (gitdir) { - print_path(gitdir, prefix, format, DEFAULT_UNMODIFIED); + print_path(gitdir, prefix, arg_path_format, PATH_FORMAT_UNMODIFIED); continue; } if (!prefix) { - print_path(".git", prefix, format, DEFAULT_UNMODIFIED); + print_path(".git", prefix, arg_path_format, PATH_FORMAT_UNMODIFIED); continue; } } else { /* --absolute-git-dir */ - wanted = FORMAT_CANONICAL; + wanted = PATH_FORMAT_CANONICAL; if (!gitdir && !prefix) gitdir = ".git"; if (gitdir) { @@ -1066,11 +1009,11 @@ int cmd_rev_parse(int argc, strbuf_reset(&buf); strbuf_addf(&buf, "%s%s.git", cwd, len && cwd[len-1] != '/' ? "/" : ""); free(cwd); - print_path(buf.buf, prefix, wanted, DEFAULT_CANONICAL); + print_path(buf.buf, prefix, wanted, PATH_FORMAT_CANONICAL); continue; } if (!strcmp(arg, "--git-common-dir")) { - print_path(repo_get_common_dir(the_repository), prefix, format, DEFAULT_RELATIVE_IF_SHARED); + print_path(repo_get_common_dir(the_repository), prefix, arg_path_format, PATH_FORMAT_RELATIVE_IF_SHARED); continue; } if (!strcmp(arg, "--is-inside-git-dir")) { @@ -1100,7 +1043,7 @@ int cmd_rev_parse(int argc, if (the_repository->index->split_index) { const struct object_id *oid = &the_repository->index->split_index->base_oid; const char *path = repo_git_path_replace(the_repository, &buf, "sharedindex.%s", oid_to_hex(oid)); - print_path(path, prefix, format, DEFAULT_RELATIVE); + print_path(path, prefix, arg_path_format, PATH_FORMAT_RELATIVE); } continue; } From eea209169a99d8250f820c53cdaf1b2c905016e0 Mon Sep 17 00:00:00 2001 From: K Jayatheerth Date: Mon, 15 Jun 2026 10:21:11 +0530 Subject: [PATCH 3/4] repo: add path.commondir with absolute and relative suffix formatting Scripts working with worktree setups need a reliable way to discover the common directory, which diverges from the git directory when multiple worktrees are in use. There is no way to retrieve this path from git repo info today. Introduce path.commondir.absolute and path.commondir.relative keys. Exposing explicit format variants rather than a single key with a default avoids ambiguity for scripts that require predictable output. Add a test helper test_repo_info_path that creates isolated repositories per test case to prevent state leaks, captures the repo root before changing directories to avoid eval, and accepts an optional init_command to cover environment variable overrides such as GIT_COMMON_DIR and GIT_DIR. Mentored-by: Justin Tobler Mentored-by: Lucas Seiki Oshiro Signed-off-by: K Jayatheerth Signed-off-by: Junio C Hamano --- Documentation/git-repo.adoc | 9 ++++++ builtin/repo.c | 26 ++++++++++++++++ t/t1900-repo-info.sh | 61 +++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc index 42262c1983..890c34051d 100644 --- a/Documentation/git-repo.adoc +++ b/Documentation/git-repo.adoc @@ -104,6 +104,15 @@ values that they return: `object.format`:: The object format (hash algorithm) used in the repository. +`path.commondir.absolute`:: + The canonical absolute path to the Git repository's common + directory (the shared `.git` directory containing objects, + refs, and global configuration). + +`path.commondir.relative`:: + The path to the Git repository's common directory relative to + the current working directory. + `references.format`:: The reference storage format. The valid values are: + diff --git a/builtin/repo.c b/builtin/repo.c index 71a5c1c29c..c4cc3bf3fc 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -7,12 +7,14 @@ #include "hex.h" #include "odb.h" #include "parse-options.h" +#include "path.h" #include "path-walk.h" #include "progress.h" #include "quote.h" #include "ref-filter.h" #include "refs.h" #include "revision.h" +#include "setup.h" #include "strbuf.h" #include "string-list.h" #include "shallow.h" @@ -75,6 +77,28 @@ static int get_object_format(struct repository *repo, struct strbuf *buf) return 0; } +static int get_path_commondir_absolute(struct repository *repo, struct strbuf *buf) +{ + const char *common_dir = repo_get_common_dir(repo); + + if (!common_dir) + return error(_("unable to get common directory")); + + append_formatted_path(buf, common_dir, startup_info->prefix, PATH_FORMAT_CANONICAL); + return 0; +} + +static int get_path_commondir_relative(struct repository *repo, struct strbuf *buf) +{ + const char *common_dir = repo_get_common_dir(repo); + + if (!common_dir) + return error(_("unable to get common directory")); + + append_formatted_path(buf, common_dir, startup_info->prefix, PATH_FORMAT_RELATIVE); + return 0; +} + static int get_references_format(struct repository *repo, struct strbuf *buf) { strbuf_addstr(buf, @@ -87,6 +111,8 @@ static const struct repo_info_field repo_info_field[] = { { "layout.bare", get_layout_bare }, { "layout.shallow", get_layout_shallow }, { "object.format", get_object_format }, + { "path.commondir.absolute", get_path_commondir_absolute }, + { "path.commondir.relative", get_path_commondir_relative }, { "references.format", get_references_format }, }; diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh index 39bb77dda0..0c0228687f 100755 --- a/t/t1900-repo-info.sh +++ b/t/t1900-repo-info.sh @@ -155,4 +155,65 @@ test_expect_success 'git repo info -h shows only repo info usage' ' test_grep ! "git repo structure" actual ' +# Helper function to test path keys in both absolute and relative formats. +# $1: label for the test +# $2: field_name (e.g., commondir) +# $3: unique repo name for isolation +# $4: expect_absolute (suffix appended to repo root) +# $5: expect_relative (the relative path string expected) +# $6: init_command (extra setup like exporting env vars) +test_repo_info_path () { + label=$1 + field_name=$2 + repo_name=$3 + expect_absolute_suffix=$4 + expect_relative=$5 + init_command=$6 + + absolute_root="$repo_name-absolute" + relative_root="$repo_name-relative" + + test_expect_success "setup: $label" ' + git init "$absolute_root" && + git init "$relative_root" && + mkdir -p "$absolute_root/sub" "$relative_root/sub" + ' + + test_expect_success "absolute: $label" ' + ( + cd "$absolute_root/sub" && + ROOT="$(test-tool path-utils real_path ..)" && export ROOT && + eval "$init_command" && + expect_path="$ROOT${expect_absolute_suffix:+/$expect_absolute_suffix}" && + echo "path.$field_name.absolute=$expect_path" >expect && + git repo info "path.$field_name.absolute" >actual && + test_cmp expect actual + ) + ' + + test_expect_success "relative: $label" ' + ( + cd "$relative_root/sub" && + ROOT="$(test-tool path-utils real_path ..)" && export ROOT && + eval "$init_command" && + echo "path.$field_name.relative=$expect_relative" >expect && + git repo info "path.$field_name.relative" >actual && + test_cmp expect actual + ) + ' +} + +test_repo_info_path 'commondir standard' 'commondir' 'commondir-std' \ + '.git' '../.git' + +test_repo_info_path 'commondir with GIT_COMMON_DIR and GIT_DIR' 'commondir' \ + 'commondir-envs' 'custom-common' '../custom-common' \ + 'GIT_COMMON_DIR="$ROOT/custom-common" && export GIT_COMMON_DIR && + GIT_DIR="../.git" && export GIT_DIR && + git init --bare "$ROOT/custom-common"' + +test_repo_info_path 'commondir with only GIT_DIR' 'commondir' \ + 'commondir-only-gitdir' '.git' '../.git' \ + 'GIT_DIR="../.git" && export GIT_DIR' + test_done From 7e3a92cc09ecd867170aa78f27dc5578cf76c058 Mon Sep 17 00:00:00 2001 From: K Jayatheerth Date: Mon, 15 Jun 2026 10:21:12 +0530 Subject: [PATCH 4/4] repo: add path.gitdir with absolute and relative suffix formatting Scripts need a stable way to locate the git directory without parsing rev-parse output or relying on its flag-driven path format selection. There is no way to retrieve this path from git repo info today. Introduce path.gitdir.absolute and path.gitdir.relative keys, consistent with the path.commondir keys added in the previous patch. Reuse the test_repo_info_path helper introduced there to validate both variants. Mentored-by: Justin Tobler Mentored-by: Lucas Seiki Oshiro Signed-off-by: K Jayatheerth Signed-off-by: Junio C Hamano --- Documentation/git-repo.adoc | 6 ++++++ builtin/repo.c | 24 ++++++++++++++++++++++++ t/t1900-repo-info.sh | 7 +++++++ 3 files changed, 37 insertions(+) diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc index 890c34051d..ed7d80c690 100644 --- a/Documentation/git-repo.adoc +++ b/Documentation/git-repo.adoc @@ -113,6 +113,12 @@ values that they return: The path to the Git repository's common directory relative to the current working directory. +`path.gitdir.absolute`:: + The canonical absolute path to the Git repository directory (the `.git` directory). + +`path.gitdir.relative`:: + The path to the Git repository directory relative to the current working directory. + `references.format`:: The reference storage format. The valid values are: + diff --git a/builtin/repo.c b/builtin/repo.c index c4cc3bf3fc..9a312d127a 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -99,6 +99,28 @@ static int get_path_commondir_relative(struct repository *repo, struct strbuf *b return 0; } +static int get_path_gitdir_absolute(struct repository *repo, struct strbuf *buf) +{ + const char *git_dir = repo_get_git_dir(repo); + + if (!git_dir) + return error(_("unable to get git directory")); + + append_formatted_path(buf, git_dir, startup_info->prefix, PATH_FORMAT_CANONICAL); + return 0; +} + +static int get_path_gitdir_relative(struct repository *repo, struct strbuf *buf) +{ + const char *git_dir = repo_get_git_dir(repo); + + if (!git_dir) + return error(_("unable to get git directory")); + + append_formatted_path(buf, git_dir, startup_info->prefix, PATH_FORMAT_RELATIVE); + return 0; +} + static int get_references_format(struct repository *repo, struct strbuf *buf) { strbuf_addstr(buf, @@ -113,6 +135,8 @@ static const struct repo_info_field repo_info_field[] = { { "object.format", get_object_format }, { "path.commondir.absolute", get_path_commondir_absolute }, { "path.commondir.relative", get_path_commondir_relative }, + { "path.gitdir.absolute", get_path_gitdir_absolute }, + { "path.gitdir.relative", get_path_gitdir_relative }, { "references.format", get_references_format }, }; diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh index 0c0228687f..45741fc9f1 100755 --- a/t/t1900-repo-info.sh +++ b/t/t1900-repo-info.sh @@ -216,4 +216,11 @@ test_repo_info_path 'commondir with only GIT_DIR' 'commondir' \ 'commondir-only-gitdir' '.git' '../.git' \ 'GIT_DIR="../.git" && export GIT_DIR' +test_repo_info_path 'gitdir standard' 'gitdir' 'gitdir-std' \ + '.git' '../.git' + +test_repo_info_path 'gitdir with explicit GIT_DIR' 'gitdir' \ + 'gitdir-env' '.git' '../.git' \ + 'GIT_DIR="../.git" && export GIT_DIR' + test_done