From d0abfb048fe0a069a4d3cd42fdd0c99afec141c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= Date: Sun, 15 Feb 2026 20:11:55 +0000 Subject: [PATCH 01/58] shallow: free local object_array allocations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The local object_array 'stack' in get_shallow_commits() function does not free its dynamic elements before the function returns. As a result elements remain allocated and their reference forgotten. Also note, that test 'fetching deepen beyond merged branch' added by 'shallow: handling fetch relative-deepen' patch fails without this correction in linux-leaks and linux-reftable-leaks test runs. Signed-off-by: Samo Pogačnik Signed-off-by: Junio C Hamano --- shallow.c | 1 + 1 file changed, 1 insertion(+) diff --git a/shallow.c b/shallow.c index 186e9178f32c33..d37d2bc1798036 100644 --- a/shallow.c +++ b/shallow.c @@ -198,6 +198,7 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth, } } deep_clear_commit_depth(&depths, free_depth_in_slab); + object_array_clear(&stack); return result; } From 3ef68ff40ebf75bba9c4b05f50197190ff1abda2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= Date: Sun, 15 Feb 2026 20:11:56 +0000 Subject: [PATCH 02/58] shallow: handling fetch relative-deepen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a shallowed repository gets deepened beyond the beginning of a merged branch, we may end up with some shallows that are hidden behind the reachable shallow commits. Added test 'fetching deepen beyond merged branch' exposes that behaviour. An example showing the problem based on added test: 0. Whole initial git repo to be cloned from Graph: * 033585d (HEAD -> main) Merge branch 'branch' |\ | * 984f8b1 (branch) five | * ecb578a four |/ * 0cb5d20 three * 2b4e70d two * 61ba98b one 1. Initial shallow clone --depth=3 (all good) Shallows: 2b4e70da2a10e1d3231a0ae2df396024735601f1 ecb578a3cf37198d122ae5df7efed9abaca17144 Graph: * 033585d (HEAD -> main) Merge branch 'branch' |\ | * 984f8b1 five | * ecb578a (grafted) four * 0cb5d20 three * 2b4e70d (grafted) two 2. Deepen shallow clone with fetch --deepen=1 (NOT OK) Shallows: 0cb5d204f4ef96ed241feb0f2088c9f4794ba758 61ba98be443fd51c542eb66585a1f6d7e15fcdae Graph: * 033585d (HEAD -> main) Merge branch 'branch' |\ | * 984f8b1 five | * ecb578a four |/ * 0cb5d20 (grafted) three --- Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae is not reachable. On the other hand, it seems that equivalent absolute depth driven fetches result in all the correct shallows. That led to this proposal, which unifies absolute and relative deepening in a way that the same get_shallow_commits() call is used in both cases. The difference is only that depth is adapted for relative deepening by measuring equivalent depth of current local shallow commits in the current remote repo. Thus a new function get_shallows_depth() has been added and the function get_reachable_list() became redundant / removed. Same example showing the corrected second step: 2. Deepen shallow clone with fetch --deepen=1 (all good) Shallow: 61ba98be443fd51c542eb66585a1f6d7e15fcdae Graph: * 033585d (HEAD -> main) Merge branch 'branch' |\ | * 984f8b1 five | * ecb578a four |/ * 0cb5d20 three * 2b4e70d two * 61ba98b (grafted) one The get_shallows_depth() function also shares the logic of the get_shallow_commits() function, but it focuses on counting depth of each existing shallow commit. The minimum result is stored as 'data->deepen_relative', which is set not to be zero for relative deepening anyway. That way we can always sum 'data->deepen_relative' and 'depth' values, because 'data->deepen_relative' is always 0 in absolute deepening. To avoid duplicating logic between get_shallows_depth() and get_shallow_commits(), get_shallow_commits() was modified so that it is used by get_shallows_depth(). Signed-off-by: Samo Pogačnik Signed-off-by: Junio C Hamano --- shallow.c | 72 +++++++++++++++++++++++++++++++++++-------- shallow.h | 2 ++ t/t5500-fetch-pack.sh | 23 ++++++++++++++ upload-pack.c | 72 ++----------------------------------------- 4 files changed, 87 insertions(+), 82 deletions(-) diff --git a/shallow.c b/shallow.c index d37d2bc1798036..08fcf6089de940 100644 --- a/shallow.c +++ b/shallow.c @@ -130,11 +130,24 @@ static void free_depth_in_slab(int **ptr) { FREE_AND_NULL(*ptr); } -struct commit_list *get_shallow_commits(struct object_array *heads, int depth, - int shallow_flag, int not_shallow_flag) +/* + * This is a common internal function that can either return a list of + * shallow commits or calculate the current maximum depth of a shallow + * repository, depending on the input parameters. + * + * Depth calculation is triggered by passing the `shallows` parameter. + * In this case, the computed depth is stored in `max_cur_depth` (if it is + * provided), and the function returns NULL. + * + * Otherwise, `max_cur_depth` remains unchanged and the function returns + * a list of shallow commits. + */ +static struct commit_list *get_shallows_or_depth(struct object_array *heads, + struct object_array *shallows, int *max_cur_depth, + int depth, int shallow_flag, int not_shallow_flag) { size_t i = 0; - int cur_depth = 0; + int cur_depth = 0, cur_depth_shallow = 0; struct commit_list *result = NULL; struct object_array stack = OBJECT_ARRAY_INIT; struct commit *commit = NULL; @@ -168,16 +181,30 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth, } parse_commit_or_die(commit); cur_depth++; - if ((depth != INFINITE_DEPTH && cur_depth >= depth) || - (is_repository_shallow(the_repository) && !commit->parents && - (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL && - graft->nr_parent < 0)) { - commit_list_insert(commit, &result); - commit->object.flags |= shallow_flag; - commit = NULL; - continue; + if (shallows) { + for (size_t j = 0; j < shallows->nr; j++) + if (oideq(&commit->object.oid, &shallows->objects[j].item->oid)) + if (!cur_depth_shallow || cur_depth < cur_depth_shallow) + cur_depth_shallow = cur_depth; + + if ((is_repository_shallow(the_repository) && !commit->parents && + (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL && + graft->nr_parent < 0)) { + commit = NULL; + continue; + } + } else { + if ((depth != INFINITE_DEPTH && cur_depth >= depth) || + (is_repository_shallow(the_repository) && !commit->parents && + (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL && + graft->nr_parent < 0)) { + commit_list_insert(commit, &result); + commit->object.flags |= shallow_flag; + commit = NULL; + continue; + } + commit->object.flags |= not_shallow_flag; } - commit->object.flags |= not_shallow_flag; for (p = commit->parents, commit = NULL; p; p = p->next) { int **depth_slot = commit_depth_at(&depths, p->item); if (!*depth_slot) { @@ -200,9 +227,30 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth, deep_clear_commit_depth(&depths, free_depth_in_slab); object_array_clear(&stack); + if (shallows && max_cur_depth) + *max_cur_depth = cur_depth_shallow; return result; } +int get_shallows_depth(struct object_array *heads, struct object_array *shallows) +{ + int max_cur_depth = 0; + get_shallows_or_depth(heads, shallows, &max_cur_depth, 0, 0, 0); + return max_cur_depth; + +} + +struct commit_list *get_shallow_commits(struct object_array *heads, + struct object_array *shallows, int deepen_relative, + int depth, int shallow_flag, int not_shallow_flag) +{ + if (shallows && deepen_relative) { + depth += get_shallows_depth(heads, shallows); + } + return get_shallows_or_depth(heads, NULL, NULL, + depth, shallow_flag, not_shallow_flag); +} + static void show_commit(struct commit *commit, void *data) { commit_list_insert(commit, data); diff --git a/shallow.h b/shallow.h index ad591bd1396854..e3f0df57ad45b8 100644 --- a/shallow.h +++ b/shallow.h @@ -35,7 +35,9 @@ int commit_shallow_file(struct repository *r, struct shallow_lock *lk); /* rollback $GIT_DIR/shallow and reset stat-validity checks */ void rollback_shallow_file(struct repository *r, struct shallow_lock *lk); +int get_shallows_depth(struct object_array *heads, struct object_array *shallows); struct commit_list *get_shallow_commits(struct object_array *heads, + struct object_array *shallows, int deepen_relative, int depth, int shallow_flag, int not_shallow_flag); struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv, int shallow_flag, int not_shallow_flag); diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh index 2677cd5faa8253..5a8b30e1fdaa3e 100755 --- a/t/t5500-fetch-pack.sh +++ b/t/t5500-fetch-pack.sh @@ -955,6 +955,29 @@ test_expect_success 'fetching deepen' ' ) ' +test_expect_success 'fetching deepen beyond merged branch' ' + test_create_repo shallow-deepen-merged && + ( + cd shallow-deepen-merged && + git commit --allow-empty -m one && + git commit --allow-empty -m two && + git commit --allow-empty -m three && + git switch -c branch && + git commit --allow-empty -m four && + git commit --allow-empty -m five && + git switch main && + git merge --no-ff branch && + cd - && + git clone --bare --depth 3 "file://$(pwd)/shallow-deepen-merged" deepen.git && + git -C deepen.git fetch origin --deepen=1 && + git -C deepen.git rev-list --all >actual && + for commit in $(sed "/^$/d" deepen.git/shallow) + do + test_grep "$commit" actual || exit 1 + done + ) +' + test_negotiation_algorithm_default () { test_when_finished rm -rf clientv0 clientv2 && rm -rf server client && diff --git a/upload-pack.c b/upload-pack.c index 2d2b70cbf2dd0b..88dac1b65c81af 100644 --- a/upload-pack.c +++ b/upload-pack.c @@ -704,56 +704,6 @@ static int do_reachable_revlist(struct child_process *cmd, return -1; } -static int get_reachable_list(struct upload_pack_data *data, - struct object_array *reachable) -{ - struct child_process cmd = CHILD_PROCESS_INIT; - int i; - struct object *o; - char namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */ - const unsigned hexsz = the_hash_algo->hexsz; - int ret; - - if (do_reachable_revlist(&cmd, &data->shallows, reachable, - data->allow_uor) < 0) { - ret = -1; - goto out; - } - - while ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz + 1) { - struct object_id oid; - const char *p; - - if (parse_oid_hex(namebuf, &oid, &p) || *p != '\n') - break; - - o = lookup_object(the_repository, &oid); - if (o && o->type == OBJ_COMMIT) { - o->flags &= ~TMP_MARK; - } - } - for (i = get_max_object_index(the_repository); 0 < i; i--) { - o = get_indexed_object(the_repository, i - 1); - if (o && o->type == OBJ_COMMIT && - (o->flags & TMP_MARK)) { - add_object_array(o, NULL, reachable); - o->flags &= ~TMP_MARK; - } - } - close(cmd.out); - - if (finish_command(&cmd)) { - ret = -1; - goto out; - } - - ret = 0; - -out: - child_process_clear(&cmd); - return ret; -} - static int has_unreachable(struct object_array *src, enum allow_uor allow_uor) { struct child_process cmd = CHILD_PROCESS_INIT; @@ -881,29 +831,11 @@ static void deepen(struct upload_pack_data *data, int depth) struct object *object = data->shallows.objects[i].item; object->flags |= NOT_SHALLOW; } - } else if (data->deepen_relative) { - struct object_array reachable_shallows = OBJECT_ARRAY_INIT; - struct commit_list *result; - - /* - * Checking for reachable shallows requires that our refs be - * marked with OUR_REF. - */ - refs_head_ref_namespaced(get_main_ref_store(the_repository), - check_ref, data); - for_each_namespaced_ref_1(check_ref, data); - - get_reachable_list(data, &reachable_shallows); - result = get_shallow_commits(&reachable_shallows, - depth + 1, - SHALLOW, NOT_SHALLOW); - send_shallow(data, result); - free_commit_list(result); - object_array_clear(&reachable_shallows); } else { struct commit_list *result; - result = get_shallow_commits(&data->want_obj, depth, + result = get_shallow_commits(&data->want_obj, &data->shallows, + data->deepen_relative, depth, SHALLOW, NOT_SHALLOW); send_shallow(data, result); free_commit_list(result); From 18e71bbda1e4e8418a590a3a9490ccdaec7deba0 Mon Sep 17 00:00:00 2001 From: Rito Rhymes Date: Mon, 16 Feb 2026 15:53:27 +0000 Subject: [PATCH 03/58] gitweb: add viewport meta tag for mobile devices Without a viewport meta tag, phone browsers render gitweb at desktop width and scale the whole page down to fit the screen. Add a viewport meta tag so the layout viewport tracks device width. This is the baseline needed for mobile CSS fixes in follow-up commits. Signed-off-by: Rito Rhymes Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 1 + 1 file changed, 1 insertion(+) diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index b5490dfecf2da7..fde804593b6ff3 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -4214,6 +4214,7 @@ sub git_header_html { + $title EOF # the stylesheet, favicon etc urls won't work correctly with path_info From 5be380d865972652a2cfd3f1f8d090c87489d904 Mon Sep 17 00:00:00 2001 From: Rito Rhymes Date: Mon, 16 Feb 2026 15:53:28 +0000 Subject: [PATCH 04/58] gitweb: prevent project search bar from overflowing on mobile On narrow screens, the project search input can exceed the available width and force page-wide horizontal scrolling. Add a mobile media query and apply side padding to the search container, then cap the input width to its container with border-box sizing so the form stays within the viewport. Signed-off-by: Rito Rhymes Signed-off-by: Junio C Hamano --- gitweb/static/gitweb.css | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/gitweb/static/gitweb.css b/gitweb/static/gitweb.css index 48d2e5101542ad..0b63acc0e29e9c 100644 --- a/gitweb/static/gitweb.css +++ b/gitweb/static/gitweb.css @@ -684,3 +684,15 @@ div.remote { .kwb { color:#830000; } .kwc { color:#000000; font-weight:bold; } .kwd { color:#010181; } + +@media (max-width: 768px) { + div.projsearch { + padding: 0 8px; + box-sizing: border-box; + } + + div.projsearch input[type="text"] { + max-width: 100%; + box-sizing: border-box; + } +} From fd10720357f01baa8a07ff6fa8e22de198424fd3 Mon Sep 17 00:00:00 2001 From: Rito Rhymes Date: Mon, 16 Feb 2026 15:53:29 +0000 Subject: [PATCH 05/58] gitweb: fix mobile page overflow across log/commit/blob/diff views On mobile-sized viewports, gitweb pages in log/commit/blob/diff views can overflow horizontally due to desktop-oriented paddings and fixed-width preformatted content. Extend the existing mobile media query to rebalance those layouts: reduce or clear paddings in log/commit sections, and allow horizontal scrolling for preformatted blob/diff content instead of forcing page-wide overflow. All layout adjustments in this patch are mobile-scoped, except one global safeguard: set overflow-wrap:anywhere on div.log_body. Log content can contain escaped or non-breaking text that behaves like a single long token and can overflow at any viewport width, including desktop. Signed-off-by: Rito Rhymes Signed-off-by: Junio C Hamano --- gitweb/static/gitweb.css | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/gitweb/static/gitweb.css b/gitweb/static/gitweb.css index 0b63acc0e29e9c..135590b64cbe76 100644 --- a/gitweb/static/gitweb.css +++ b/gitweb/static/gitweb.css @@ -123,6 +123,7 @@ div.title_text { div.log_body { padding: 8px 8px 8px 150px; + overflow-wrap: anywhere; } span.age { @@ -686,6 +687,15 @@ div.remote { .kwd { color:#010181; } @media (max-width: 768px) { + div.page_body { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + div.page_body div.pre { + min-width: max-content; + } + div.projsearch { padding: 0 8px; box-sizing: border-box; @@ -695,4 +705,46 @@ div.remote { max-width: 100%; box-sizing: border-box; } + + div.title_text { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + padding-left: 4px; + padding-right: 4px; + box-sizing: border-box; + } + + div.title_text table.object_header { + width: max-content; + } + + div.log_body { + padding: 8px; + clear: left; + } + + div.patchset div.patch { + width: max-content; + min-width: 100%; + } + + div.diff.header { + padding: 4px 8px 2px 8px; + white-space: nowrap; + overflow-wrap: normal; + } + + div.diff.extended_header { + padding: 2px 8px; + white-space: nowrap; + overflow-wrap: normal; + } + + div.diff.ctx, + div.diff.add, + div.diff.rem, + div.diff.chunk_header { + padding: 0 8px; + white-space: pre; + } } From 34108d7fa3b91ca52f9f99f646c0f1ba4111d357 Mon Sep 17 00:00:00 2001 From: Rito Rhymes Date: Mon, 16 Feb 2026 15:53:30 +0000 Subject: [PATCH 06/58] gitweb: fix mobile footer overflow by wrapping text and clearing floats On narrow screens, footer text can wrap, but the fixed 22px footer height and floated footer blocks can cause overflow. Switch to min-height and add a clearfix on the footer container so it grows to contain wrapped float content cleanly. Signed-off-by: Rito Rhymes Signed-off-by: Junio C Hamano --- gitweb/static/gitweb.css | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gitweb/static/gitweb.css b/gitweb/static/gitweb.css index 135590b64cbe76..824764606333a0 100644 --- a/gitweb/static/gitweb.css +++ b/gitweb/static/gitweb.css @@ -73,11 +73,17 @@ div.page_path { } div.page_footer { - height: 22px; + min-height: 22px; padding: 4px 8px; background-color: #d9d8d1; } +div.page_footer::after { + content: ""; + display: table; + clear: both; +} + div.page_footer_text { line-height: 22px; float: left; From f4e63fd83e5b33f0113cb7e2231d013c515f0a8b Mon Sep 17 00:00:00 2001 From: Rito Rhymes Date: Mon, 16 Feb 2026 15:53:31 +0000 Subject: [PATCH 07/58] gitweb: let page header grow on mobile for long wrapped project names On mobile, long project names in the page header can wrap to multiple lines, but the fixed 25px header height does not grow with wrapped content. Switch the header from fixed height to min-height so it expands as needed while keeping the same baseline height for single-line titles. Signed-off-by: Rito Rhymes Signed-off-by: Junio C Hamano --- gitweb/static/gitweb.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitweb/static/gitweb.css b/gitweb/static/gitweb.css index 824764606333a0..e2e6dd96a2c915 100644 --- a/gitweb/static/gitweb.css +++ b/gitweb/static/gitweb.css @@ -42,7 +42,7 @@ a.list img.avatar { } div.page_header { - height: 25px; + min-height: 25px; padding: 8px; font-size: 150%; font-weight: bold; From cb18484385eb66f6220d2418d62ad790358899d1 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Thu, 19 Feb 2026 14:26:32 +0000 Subject: [PATCH 08/58] wt-status: avoid passing NULL worktree In preparation for removing the repository argument from worktree_git_path() add a function to construct a "struct worktree" from a "struct repository" using its "gitdir" and "worktree" members. This function is then used to avoid passing a NULL worktree to wt_status_check_bisect() and wt_status_check_rebase(). In general the "struct worktree" returned may not correspond to the "current" worktree defined by is_current_worktree() as that function uses "the_repository" rather than "wt->repo" when deciding which worktree is "current". In practice the "struct repository" we pass corresponds to "the_repository" as we only ever operate on a single repository at the moment. wt_status_check_bisect() and wt_status_check_rebase() have the following callers: - branch.c:prepare_checked_out_branches() which loops over all worktrees. - worktree.c:is_worktree_being_rebased() which is called from builtin/branch.c:reject_rebase_or_bisect_branch() that loops over all worktrees and worktree.c:is_shared_symref() which dereferences wt earlier in the function. - wt-status:wt_status_get_state() which is updated to avoid passing a NULL worktree by this patch. This updates the only callers that pass a NULL worktree to worktree_git_path(). A new test is added to check that "git status" detects a rebase in a linked worktree. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- t/t7512-status-help.sh | 9 +++++++++ worktree.c | 20 ++++++++++++++++++++ worktree.h | 6 ++++++ wt-status.c | 15 ++++++++++++--- 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/t/t7512-status-help.sh b/t/t7512-status-help.sh index 25e8e9711f8fef..08e82f79140841 100755 --- a/t/t7512-status-help.sh +++ b/t/t7512-status-help.sh @@ -594,6 +594,15 @@ EOF test_cmp expected actual ' +test_expect_success 'rebase in a linked worktree' ' + test_might_fail git rebase --abort && + git worktree add wt && + test_when_finished "test_might_fail git -C wt rebase --abort; + git worktree remove wt" && + GIT_SEQUENCE_EDITOR="echo break >" git -C wt rebase -i HEAD && + git -C wt status >actual && + test_grep "interactive rebase in progress" actual +' test_expect_success 'prepare am_session' ' git reset --hard main && diff --git a/worktree.c b/worktree.c index 9308389cb6f029..218c332a66dc5c 100644 --- a/worktree.c +++ b/worktree.c @@ -66,6 +66,26 @@ static int is_current_worktree(struct worktree *wt) return is_current; } +struct worktree *get_worktree_from_repository(struct repository *repo) +{ + struct worktree *wt = xcalloc(1, sizeof(*wt)); + char *gitdir = absolute_pathdup(repo->gitdir); + char *commondir = absolute_pathdup(repo->commondir); + + wt->repo = repo; + wt->path = absolute_pathdup(repo->worktree ? repo->worktree + : repo->gitdir); + wt->is_bare = !repo->worktree; + if (fspathcmp(gitdir, commondir)) + wt->id = xstrdup(find_last_dir_sep(gitdir) + 1); + wt->is_current = is_current_worktree(wt); + add_head_info(wt); + + free(gitdir); + free(commondir); + return wt; +} + /* * When in a secondary worktree, and when extensions.worktreeConfig * is true, only $commondir/config and $commondir/worktrees// diff --git a/worktree.h b/worktree.h index e4bcccdc0aef5a..06efe26b835a81 100644 --- a/worktree.h +++ b/worktree.h @@ -38,6 +38,12 @@ struct worktree **get_worktrees(void); */ struct worktree **get_worktrees_without_reading_head(void); +/* + * Construct a struct worktree corresponding to repo->gitdir and + * repo->worktree. + */ +struct worktree *get_worktree_from_repository(struct repository *repo); + /* * Returns 1 if linked worktrees exist, 0 otherwise. */ diff --git a/wt-status.c b/wt-status.c index e12adb26b9f8eb..eceb41fb659746 100644 --- a/wt-status.c +++ b/wt-status.c @@ -1723,6 +1723,9 @@ int wt_status_check_rebase(const struct worktree *wt, { struct stat st; + if (!wt) + BUG("wt_status_check_rebase() called with NULL worktree"); + if (!stat(worktree_git_path(the_repository, wt, "rebase-apply"), &st)) { if (!stat(worktree_git_path(the_repository, wt, "rebase-apply/applying"), &st)) { state->am_in_progress = 1; @@ -1750,6 +1753,9 @@ int wt_status_check_bisect(const struct worktree *wt, { struct stat st; + if (!wt) + BUG("wt_status_check_bisect() called with NULL worktree"); + if (!stat(worktree_git_path(the_repository, wt, "BISECT_LOG"), &st)) { state->bisect_in_progress = 1; state->bisecting_from = get_branch(wt, "BISECT_START"); @@ -1795,18 +1801,19 @@ void wt_status_get_state(struct repository *r, struct stat st; struct object_id oid; enum replay_action action; + struct worktree *wt = get_worktree_from_repository(r); if (!stat(git_path_merge_head(r), &st)) { - wt_status_check_rebase(NULL, state); + wt_status_check_rebase(wt, state); state->merge_in_progress = 1; - } else if (wt_status_check_rebase(NULL, state)) { + } else if (wt_status_check_rebase(wt, state)) { ; /* all set */ } else if (refs_ref_exists(get_main_ref_store(r), "CHERRY_PICK_HEAD") && !repo_get_oid(r, "CHERRY_PICK_HEAD", &oid)) { state->cherry_pick_in_progress = 1; oidcpy(&state->cherry_pick_head_oid, &oid); } - wt_status_check_bisect(NULL, state); + wt_status_check_bisect(wt, state); if (refs_ref_exists(get_main_ref_store(r), "REVERT_HEAD") && !repo_get_oid(r, "REVERT_HEAD", &oid)) { state->revert_in_progress = 1; @@ -1824,6 +1831,8 @@ void wt_status_get_state(struct repository *r, if (get_detached_from) wt_status_get_detached_from(r, state); wt_status_check_sparse_checkout(r, state); + + free_worktree(wt); } static void wt_longstatus_print_state(struct wt_status *s) From a49cb0f093809e3e66566f161aa930f37346775d Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Thu, 19 Feb 2026 14:26:33 +0000 Subject: [PATCH 09/58] path: remove repository argument from worktree_git_path() worktree_git_path() takes a struct repository and a struct worktree which also contains a struct repository. The repository argument was added by a973f60dc7c (path: stop relying on `the_repository` in `worktree_git_path()`, 2024-08-13) and exists because the worktree argument is optional. Having two ways of passing a repository is a potential foot-gun as if the the worktree argument is present the repository argument must match the worktree's repository member. Since the last commit there are no callers that pass a NULL worktree so lets remove the repository argument. This removes the potential confusion and lets us delete a number of uses of "the_repository". worktree_git_path() has the following callers: - builtin/worktree.c:validate_no_submodules() which is called from check_clean_worktree() and move_worktree(), both of which supply a non-NULL worktree. - builtin/fsck.c:cmd_fsck() which loops over all worktrees. - revision.c:add_index_objects_to_pending() which loops over all worktrees. - worktree.c:worktree_lock_reason() which dereferences wt before calling worktree_git_path(). - wt-status.c:wt_status_check_bisect() and wt_status_check_rebase() which are always called with a non-NULL worktree after the last commit. - wt-status.c:git_branch() which is only called by wt_status_check_bisect() and wt_status_check_rebase(). Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- builtin/fsck.c | 2 +- builtin/worktree.c | 4 ++-- path.c | 9 ++++----- path.h | 8 +++----- revision.c | 2 +- worktree.c | 2 +- wt-status.c | 14 +++++++------- 7 files changed, 19 insertions(+), 22 deletions(-) diff --git a/builtin/fsck.c b/builtin/fsck.c index 0512f78a87fe1d..42ba0afb91a905 100644 --- a/builtin/fsck.c +++ b/builtin/fsck.c @@ -1137,7 +1137,7 @@ int cmd_fsck(int argc, * and may get overwritten by other calls * while we're examining the index. */ - path = xstrdup(worktree_git_path(the_repository, wt, "index")); + path = xstrdup(worktree_git_path(wt, "index")); wt_gitdir = get_worktree_git_dir(wt); read_index_from(&istate, path, wt_gitdir); diff --git a/builtin/worktree.c b/builtin/worktree.c index fbdaf2eb2eb85c..1019093bce80fb 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -1191,14 +1191,14 @@ static void validate_no_submodules(const struct worktree *wt) wt_gitdir = get_worktree_git_dir(wt); - if (is_directory(worktree_git_path(the_repository, wt, "modules"))) { + if (is_directory(worktree_git_path(wt, "modules"))) { /* * There could be false positives, e.g. the "modules" * directory exists but is empty. But it's a rare case and * this simpler check is probably good enough for now. */ found_submodules = 1; - } else if (read_index_from(&istate, worktree_git_path(the_repository, wt, "index"), + } else if (read_index_from(&istate, worktree_git_path(wt, "index"), wt_gitdir) > 0) { for (i = 0; i < istate.cache_nr; i++) { struct cache_entry *ce = istate.cache[i]; diff --git a/path.c b/path.c index d726537622cda6..073f631b914745 100644 --- a/path.c +++ b/path.c @@ -486,17 +486,16 @@ const char *mkpath(const char *fmt, ...) return cleanup_path(pathname->buf); } -const char *worktree_git_path(struct repository *r, - const struct worktree *wt, const char *fmt, ...) +const char *worktree_git_path(const struct worktree *wt, const char *fmt, ...) { struct strbuf *pathname = get_pathname(); va_list args; - if (wt && wt->repo != r) - BUG("worktree not connected to expected repository"); + if (!wt) + BUG("%s() called with NULL worktree", __func__); va_start(args, fmt); - repo_git_pathv(r, wt, pathname, fmt, args); + repo_git_pathv(wt->repo, wt, pathname, fmt, args); va_end(args); return pathname->buf; } diff --git a/path.h b/path.h index 0ec95a0b079c90..cbcad254a0a0b5 100644 --- a/path.h +++ b/path.h @@ -66,13 +66,11 @@ const char *repo_git_path_replace(struct repository *repo, /* * Similar to repo_git_path() but can produce paths for a specified - * worktree instead of current one. When no worktree is given, then the path is - * computed relative to main worktree of the given repository. + * worktree instead of current one. */ -const char *worktree_git_path(struct repository *r, - const struct worktree *wt, +const char *worktree_git_path(const struct worktree *wt, const char *fmt, ...) - __attribute__((format (printf, 3, 4))); + __attribute__((format (printf, 2, 3))); /* * The `repo_worktree_path` family of functions will construct a path into a diff --git a/revision.c b/revision.c index 9b131670f79b96..6e9e914d863842 100644 --- a/revision.c +++ b/revision.c @@ -1847,7 +1847,7 @@ void add_index_objects_to_pending(struct rev_info *revs, unsigned int flags) wt_gitdir = get_worktree_git_dir(wt); if (read_index_from(&istate, - worktree_git_path(the_repository, wt, "index"), + worktree_git_path(wt, "index"), wt_gitdir) > 0) do_add_index_objects_to_pending(revs, &istate, flags); diff --git a/worktree.c b/worktree.c index 218c332a66dc5c..6e2f0f78283dbf 100644 --- a/worktree.c +++ b/worktree.c @@ -308,7 +308,7 @@ const char *worktree_lock_reason(struct worktree *wt) if (!wt->lock_reason_valid) { struct strbuf path = STRBUF_INIT; - strbuf_addstr(&path, worktree_git_path(the_repository, wt, "locked")); + strbuf_addstr(&path, worktree_git_path(wt, "locked")); if (file_exists(path.buf)) { struct strbuf lock_reason = STRBUF_INIT; if (strbuf_read_file(&lock_reason, path.buf, 0) < 0) diff --git a/wt-status.c b/wt-status.c index eceb41fb659746..e8234b2fa23e84 100644 --- a/wt-status.c +++ b/wt-status.c @@ -1624,7 +1624,7 @@ static char *get_branch(const struct worktree *wt, const char *path) struct object_id oid; const char *branch_name; - if (strbuf_read_file(&sb, worktree_git_path(the_repository, wt, "%s", path), 0) <= 0) + if (strbuf_read_file(&sb, worktree_git_path(wt, "%s", path), 0) <= 0) goto got_nothing; while (sb.len && sb.buf[sb.len - 1] == '\n') @@ -1726,18 +1726,18 @@ int wt_status_check_rebase(const struct worktree *wt, if (!wt) BUG("wt_status_check_rebase() called with NULL worktree"); - if (!stat(worktree_git_path(the_repository, wt, "rebase-apply"), &st)) { - if (!stat(worktree_git_path(the_repository, wt, "rebase-apply/applying"), &st)) { + if (!stat(worktree_git_path(wt, "rebase-apply"), &st)) { + if (!stat(worktree_git_path(wt, "rebase-apply/applying"), &st)) { state->am_in_progress = 1; - if (!stat(worktree_git_path(the_repository, wt, "rebase-apply/patch"), &st) && !st.st_size) + if (!stat(worktree_git_path(wt, "rebase-apply/patch"), &st) && !st.st_size) state->am_empty_patch = 1; } else { state->rebase_in_progress = 1; state->branch = get_branch(wt, "rebase-apply/head-name"); state->onto = get_branch(wt, "rebase-apply/onto"); } - } else if (!stat(worktree_git_path(the_repository, wt, "rebase-merge"), &st)) { - if (!stat(worktree_git_path(the_repository, wt, "rebase-merge/interactive"), &st)) + } else if (!stat(worktree_git_path(wt, "rebase-merge"), &st)) { + if (!stat(worktree_git_path(wt, "rebase-merge/interactive"), &st)) state->rebase_interactive_in_progress = 1; else state->rebase_in_progress = 1; @@ -1756,7 +1756,7 @@ int wt_status_check_bisect(const struct worktree *wt, if (!wt) BUG("wt_status_check_bisect() called with NULL worktree"); - if (!stat(worktree_git_path(the_repository, wt, "BISECT_LOG"), &st)) { + if (!stat(worktree_git_path(wt, "BISECT_LOG"), &st)) { state->bisect_in_progress = 1; state->bisecting_from = get_branch(wt, "BISECT_START"); return 1; From 3e9cc24e68ef311500406ef4d170be30e36e1231 Mon Sep 17 00:00:00 2001 From: Koji Nakamaru Date: Fri, 20 Feb 2026 01:39:00 +0000 Subject: [PATCH 10/58] osxkeychain: define build targets in the top-level Makefile. The fix for git-credential-osxkeychain in 4580bcd235 (osxkeychain: avoid incorrectly skipping store operation, 2025-11-14) introduced linkage with libgit.a, and its Makefile was adjusted accordingly. However, the build fails as of 864f55e190 because several macOS-specific refinements were applied to the top-level Makefile and config.mak.uname, such as: - 363837afe7 (macOS: make Homebrew use configurable, 2025-12-24) - cee341e9dd (macOS: use iconv from Homebrew if needed and present, 2025-12-24) - d281241518 (utf8.c: enable workaround for iconv under macOS 14/15, 2026-01-12) Since libgit.a and its corresponding header files depend on many flags defined in the top-level Makefile, these flags must be consistently defined when building git-credential-osxkeychain. Continuing to manually adjust the git-credential-osxkeychain Makefile is cumbersome and fragile. Define the build targets for git-credential-osxkeychain in the top-level Makefile and modify its local Makefile to simply rely on those targets. Helped-by: Junio C Hamano Reported-by: D. Ben Knoble Helped-by: Kristoffer Haugsbakk Signed-off-by: Koji Nakamaru Signed-off-by: Junio C Hamano --- Makefile | 21 ++++++++ contrib/credential/osxkeychain/Makefile | 65 +++---------------------- 2 files changed, 27 insertions(+), 59 deletions(-) diff --git a/Makefile b/Makefile index 8aa489f3b6812f..ad4f36cdac9450 100644 --- a/Makefile +++ b/Makefile @@ -2874,6 +2874,10 @@ objects: $(OBJECTS) dep_files := $(foreach f,$(OBJECTS),$(dir $f).depend/$(notdir $f).d) dep_dirs := $(addsuffix .depend,$(sort $(dir $(OBJECTS)))) +ifeq ($(uname_S),Darwin) + dep_dirs += $(addsuffix .depend,$(sort $(dir contrib/credential/osxkeychain/git-credential-osxkeychain.o))) +endif + ifeq ($(COMPUTE_HEADER_DEPENDENCIES),yes) $(dep_dirs): @mkdir -p $@ @@ -4058,3 +4062,20 @@ $(LIBGIT_HIDDEN_EXPORT): $(LIBGIT_PARTIAL_EXPORT) contrib/libgit-sys/libgitpub.a: $(LIBGIT_HIDDEN_EXPORT) $(AR) $(ARFLAGS) $@ $^ + +contrib/credential/osxkeychain/git-credential-osxkeychain: contrib/credential/osxkeychain/git-credential-osxkeychain.o $(LIB_FILE) GIT-LDFLAGS + $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \ + $(filter %.o,$^) $(LIB_FILE) $(EXTLIBS) -framework Security -framework CoreFoundation + +contrib/credential/osxkeychain/git-credential-osxkeychain.o: contrib/credential/osxkeychain/git-credential-osxkeychain.c GIT-CFLAGS + $(QUIET_LINK)$(CC) -o $@ -c $(dep_args) $(compdb_args) $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $< + +install-git-credential-osxkeychain: contrib/credential/osxkeychain/git-credential-osxkeychain + $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(gitexec_instdir_SQ)' + $(INSTALL) $(INSTALL_STRIP) $< '$(DESTDIR_SQ)$(gitexec_instdir_SQ)' + +.PHONY: clean-git-credential-osxkeychain +clean-git-credential-osxkeychain: + $(RM) \ + contrib/credential/osxkeychain/git-credential-osxkeychain \ + contrib/credential/osxkeychain/git-credential-osxkeychain.o diff --git a/contrib/credential/osxkeychain/Makefile b/contrib/credential/osxkeychain/Makefile index c68445b82dc3e5..219b0d7f49e016 100644 --- a/contrib/credential/osxkeychain/Makefile +++ b/contrib/credential/osxkeychain/Makefile @@ -1,66 +1,13 @@ # The default target of this Makefile is... all:: git-credential-osxkeychain -include ../../../config.mak.uname --include ../../../config.mak.autogen --include ../../../config.mak +git-credential-osxkeychain: + $(MAKE) -C ../../.. contrib/credential/osxkeychain/git-credential-osxkeychain -ifdef ZLIB_NG - BASIC_CFLAGS += -DHAVE_ZLIB_NG - ifdef ZLIB_NG_PATH - BASIC_CFLAGS += -I$(ZLIB_NG_PATH)/include - EXTLIBS += $(call libpath_template,$(ZLIB_NG_PATH)/$(lib)) - endif - EXTLIBS += -lz-ng -else - ifdef ZLIB_PATH - BASIC_CFLAGS += -I$(ZLIB_PATH)/include - EXTLIBS += $(call libpath_template,$(ZLIB_PATH)/$(lib)) - endif - EXTLIBS += -lz -endif -ifndef NO_ICONV - ifdef NEEDS_LIBICONV - ifdef ICONVDIR - BASIC_CFLAGS += -I$(ICONVDIR)/include - ICONV_LINK = $(call libpath_template,$(ICONVDIR)/$(lib)) - else - ICONV_LINK = - endif - ifdef NEEDS_LIBINTL_BEFORE_LIBICONV - ICONV_LINK += -lintl - endif - EXTLIBS += $(ICONV_LINK) -liconv - endif -endif -ifndef LIBC_CONTAINS_LIBINTL - EXTLIBS += -lintl -endif - -prefix ?= /usr/local -gitexecdir ?= $(prefix)/libexec/git-core - -CC ?= gcc -CFLAGS ?= -g -O2 -Wall -I../../.. $(BASIC_CFLAGS) -LDFLAGS ?= $(BASIC_LDFLAGS) $(EXTLIBS) -INSTALL ?= install -RM ?= rm -f - -%.o: %.c - $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ -c $< - -git-credential-osxkeychain: git-credential-osxkeychain.o ../../../libgit.a - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) \ - -framework Security -framework CoreFoundation - -install: git-credential-osxkeychain - $(INSTALL) -d -m 755 $(DESTDIR)$(gitexecdir) - $(INSTALL) -m 755 $< $(DESTDIR)$(gitexecdir) - -../../../libgit.a: - cd ../../..; make libgit.a +install: + $(MAKE) -C ../../.. install-git-credential-osxkeychain clean: - $(RM) git-credential-osxkeychain git-credential-osxkeychain.o + $(MAKE) -C ../../.. clean-git-credential-osxkeychain -.PHONY: all install clean +.PHONY: all git-credential-osxkeychain install clean From 999b09348d6302d018165b4b3d289d4579d08e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20Kara=C3=A7ay?= Date: Fri, 20 Feb 2026 09:04:41 +0300 Subject: [PATCH 11/58] mailmap: stop using the_repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'read_mailmap' and 'read_mailmap_blob' functions rely on the global 'the_repository' variable. Update both functions to accept a 'struct repository' parameter. Update all callers to pass 'the_repository' to retain the current behavior. Signed-off-by: Burak Kaan Karaçay Signed-off-by: Junio C Hamano --- builtin/blame.c | 2 +- builtin/cat-file.c | 2 +- builtin/check-mailmap.c | 4 ++-- builtin/commit.c | 2 +- builtin/log.c | 2 +- builtin/shortlog.c | 2 +- mailmap.c | 11 ++++++----- mailmap.h | 6 ++++-- pretty.c | 2 +- ref-filter.c | 2 +- 10 files changed, 19 insertions(+), 16 deletions(-) diff --git a/builtin/blame.c b/builtin/blame.c index eac2fe73201ca2..f3a11eff44ffc7 100644 --- a/builtin/blame.c +++ b/builtin/blame.c @@ -1252,7 +1252,7 @@ int cmd_blame(int argc, sb.xdl_opts = xdl_opts; sb.no_whole_file_rename = no_whole_file_rename; - read_mailmap(&mailmap); + read_mailmap(the_repository, &mailmap); sb.found_guilty_entry = &found_guilty_entry; sb.found_guilty_entry_data = π diff --git a/builtin/cat-file.c b/builtin/cat-file.c index df8e87a81f5eee..d298e95797fdf7 100644 --- a/builtin/cat-file.c +++ b/builtin/cat-file.c @@ -1105,7 +1105,7 @@ int cmd_cat_file(int argc, opt_epts = (opt == 'e' || opt == 'p' || opt == 't' || opt == 's'); if (use_mailmap) - read_mailmap(&mailmap); + read_mailmap(the_repository, &mailmap); switch (batch.objects_filter.choice) { case LOFC_DISABLED: diff --git a/builtin/check-mailmap.c b/builtin/check-mailmap.c index 9cc5c598302657..3f2a39cae0c1d0 100644 --- a/builtin/check-mailmap.c +++ b/builtin/check-mailmap.c @@ -63,9 +63,9 @@ int cmd_check_mailmap(int argc, if (argc == 0 && !use_stdin) die(_("no contacts specified")); - read_mailmap(&mailmap); + read_mailmap(the_repository, &mailmap); if (mailmap_blob) - read_mailmap_blob(&mailmap, mailmap_blob); + read_mailmap_blob(the_repository, &mailmap, mailmap_blob); if (mailmap_file) read_mailmap_file(&mailmap, mailmap_file, 0); diff --git a/builtin/commit.c b/builtin/commit.c index 9e3a09d532bfce..3700f66ba95efd 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1155,7 +1155,7 @@ static const char *find_author_by_nickname(const char *name) setup_revisions(ac, av, &revs, NULL); revs.mailmap = xmalloc(sizeof(struct string_list)); string_list_init_nodup(revs.mailmap); - read_mailmap(revs.mailmap); + read_mailmap(the_repository, revs.mailmap); if (prepare_revision_walk(&revs)) die(_("revision walk setup failed")); diff --git a/builtin/log.c b/builtin/log.c index 8ab6d3a9439afd..ff0227e32d0f14 100644 --- a/builtin/log.c +++ b/builtin/log.c @@ -336,7 +336,7 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix, if (mailmap) { rev->mailmap = xmalloc(sizeof(struct string_list)); string_list_init_nodup(rev->mailmap); - read_mailmap(rev->mailmap); + read_mailmap(the_repository, rev->mailmap); } if (rev->pretty_given && rev->commit_format == CMIT_FMT_RAW) { diff --git a/builtin/shortlog.c b/builtin/shortlog.c index d80bf1a7d055fc..6b2a0b93b5992e 100644 --- a/builtin/shortlog.c +++ b/builtin/shortlog.c @@ -357,7 +357,7 @@ void shortlog_init(struct shortlog *log) { memset(log, 0, sizeof(*log)); - read_mailmap(&log->mailmap); + read_mailmap(the_repository, &log->mailmap); log->list.strdup_strings = 1; log->wrap = DEFAULT_WRAPLEN; diff --git a/mailmap.c b/mailmap.c index 37fd158a516d25..cf70956675c066 100644 --- a/mailmap.c +++ b/mailmap.c @@ -183,7 +183,8 @@ static void read_mailmap_string(struct string_list *map, char *buf) } } -int read_mailmap_blob(struct string_list *map, const char *name) +int read_mailmap_blob(struct repository *repo, struct string_list *map, + const char *name) { struct object_id oid; char *buf; @@ -192,10 +193,10 @@ int read_mailmap_blob(struct string_list *map, const char *name) if (!name) return 0; - if (repo_get_oid(the_repository, name, &oid) < 0) + if (repo_get_oid(repo, name, &oid) < 0) return 0; - buf = odb_read_object(the_repository->objects, &oid, &type, &size); + buf = odb_read_object(repo->objects, &oid, &type, &size); if (!buf) return error("unable to read mailmap object at %s", name); if (type != OBJ_BLOB) { @@ -209,7 +210,7 @@ int read_mailmap_blob(struct string_list *map, const char *name) return 0; } -int read_mailmap(struct string_list *map) +int read_mailmap(struct repository *repo, struct string_list *map) { int err = 0; @@ -224,7 +225,7 @@ int read_mailmap(struct string_list *map) startup_info->have_repository ? MAILMAP_NOFOLLOW : 0); if (startup_info->have_repository) - err |= read_mailmap_blob(map, git_mailmap_blob); + err |= read_mailmap_blob(repo, map, git_mailmap_blob); err |= read_mailmap_file(map, git_mailmap_file, 0); return err; } diff --git a/mailmap.h b/mailmap.h index 908365e1bffafc..fda329d7157e4e 100644 --- a/mailmap.h +++ b/mailmap.h @@ -1,6 +1,7 @@ #ifndef MAILMAP_H #define MAILMAP_H +struct repository; struct string_list; extern char *git_mailmap_file; @@ -11,9 +12,10 @@ extern char *git_mailmap_blob; int read_mailmap_file(struct string_list *map, const char *filename, unsigned flags); -int read_mailmap_blob(struct string_list *map, const char *name); +int read_mailmap_blob(struct repository *repo, struct string_list *map, + const char *name); -int read_mailmap(struct string_list *map); +int read_mailmap(struct repository *repo, struct string_list *map); void clear_mailmap(struct string_list *map); int map_user(struct string_list *map, diff --git a/pretty.c b/pretty.c index e0646bbc5d49cc..ebf4da4834075f 100644 --- a/pretty.c +++ b/pretty.c @@ -781,7 +781,7 @@ static int mailmap_name(const char **email, size_t *email_len, static struct string_list *mail_map; if (!mail_map) { CALLOC_ARRAY(mail_map, 1); - read_mailmap(mail_map); + read_mailmap(the_repository, mail_map); } return mail_map->nr && map_user(mail_map, email, email_len, name, name_len); } diff --git a/ref-filter.c b/ref-filter.c index 3917c4ccd9f73a..d7a23a7b610bad 100644 --- a/ref-filter.c +++ b/ref-filter.c @@ -1753,7 +1753,7 @@ static void grab_person(const char *who, struct atom_value *val, int deref, void (starts_with(name + wholen, "email") && (atom->u.email_option.option & EO_MAILMAP))) { if (!mailmap.items) - read_mailmap(&mailmap); + read_mailmap(the_repository, &mailmap); strbuf_addstr(&mailmap_buf, buf); apply_mailmap_to_header(&mailmap_buf, headers, &mailmap); wholine = find_wholine(who, wholen, mailmap_buf.buf); From 6aea51bc3bf3c5318b97b5bddc405c29f1b23e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20Kara=C3=A7ay?= Date: Fri, 20 Feb 2026 09:04:42 +0300 Subject: [PATCH 12/58] mailmap: drop global config variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'mailmap.file' and 'mailmap.blob' configurations are currently parsed and stored in the global variables 'git_mailmap_file' and 'git_mailmap_blob'. Since these values are typically only needed once when initializing a mailmap, there is no need to keep them as global state throughout the lifetime of the Git process. To reduce global state, remove these global variables and instead use 'repo_config_get_*' functions to read the configuration on demand. Signed-off-by: Burak Kaan Karaçay Signed-off-by: Junio C Hamano --- environment.c | 19 ------------------- mailmap.c | 21 ++++++++++++++------- mailmap.h | 3 --- 3 files changed, 14 insertions(+), 29 deletions(-) diff --git a/environment.c b/environment.c index 0026eb227487dd..2764d8f4817c2a 100644 --- a/environment.c +++ b/environment.c @@ -647,22 +647,6 @@ static int git_default_push_config(const char *var, const char *value) return 0; } -static int git_default_mailmap_config(const char *var, const char *value) -{ - if (!strcmp(var, "mailmap.file")) { - FREE_AND_NULL(git_mailmap_file); - return git_config_pathname(&git_mailmap_file, var, value); - } - - if (!strcmp(var, "mailmap.blob")) { - FREE_AND_NULL(git_mailmap_blob); - return git_config_string(&git_mailmap_blob, var, value); - } - - /* Add other config variables here and to Documentation/config.adoc. */ - return 0; -} - static int git_default_attr_config(const char *var, const char *value) { if (!strcmp(var, "attr.tree")) { @@ -697,9 +681,6 @@ int git_default_config(const char *var, const char *value, if (starts_with(var, "push.")) return git_default_push_config(var, value); - if (starts_with(var, "mailmap.")) - return git_default_mailmap_config(var, value); - if (starts_with(var, "attr.")) return git_default_attr_config(var, value); diff --git a/mailmap.c b/mailmap.c index cf70956675c066..3b2691781d8ff1 100644 --- a/mailmap.c +++ b/mailmap.c @@ -7,9 +7,7 @@ #include "object-name.h" #include "odb.h" #include "setup.h" - -char *git_mailmap_file; -char *git_mailmap_blob; +#include "config.h" struct mailmap_info { char *name; @@ -213,20 +211,29 @@ int read_mailmap_blob(struct repository *repo, struct string_list *map, int read_mailmap(struct repository *repo, struct string_list *map) { int err = 0; + char *mailmap_file = NULL, *mailmap_blob = NULL; + + repo_config_get_pathname(repo, "mailmap.file", &mailmap_file); + repo_config_get_string(repo, "mailmap.blob", &mailmap_blob); map->strdup_strings = 1; map->cmp = namemap_cmp; - if (!git_mailmap_blob && is_bare_repository()) - git_mailmap_blob = xstrdup("HEAD:.mailmap"); + if (!mailmap_blob && is_bare_repository()) + mailmap_blob = xstrdup("HEAD:.mailmap"); if (!startup_info->have_repository || !is_bare_repository()) err |= read_mailmap_file(map, ".mailmap", startup_info->have_repository ? MAILMAP_NOFOLLOW : 0); if (startup_info->have_repository) - err |= read_mailmap_blob(repo, map, git_mailmap_blob); - err |= read_mailmap_file(map, git_mailmap_file, 0); + err |= read_mailmap_blob(repo, map, mailmap_blob); + + err |= read_mailmap_file(map, mailmap_file, 0); + + free(mailmap_file); + free(mailmap_blob); + return err; } diff --git a/mailmap.h b/mailmap.h index fda329d7157e4e..6866cb6f1d6c6f 100644 --- a/mailmap.h +++ b/mailmap.h @@ -4,9 +4,6 @@ struct repository; struct string_list; -extern char *git_mailmap_file; -extern char *git_mailmap_blob; - /* Flags for read_mailmap_file() */ #define MAILMAP_NOFOLLOW (1<<0) From 0fbf380daf1367a5c203eb25b744f55634dab251 Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Wed, 18 Feb 2026 00:15:32 +0000 Subject: [PATCH 13/58] apply: normalize path in --directory argument When passing a relative path like --directory=./some/sub, the leading "./" caused apply to prepend it literally to patch filenames, resulting in an error (invalid path). There may be more cases like this where users pass some/./path to the directory which can easily be normalized to an acceptable path, so these changes try to normalize the path before using it. Signed-off-by: Joaquim Rocha Signed-off-by: Junio C Hamano --- apply.c | 4 ++++ t/t4128-apply-root.sh | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/apply.c b/apply.c index a2ceb3fb40d3b5..7c07d124f4afb2 100644 --- a/apply.c +++ b/apply.c @@ -4963,6 +4963,10 @@ static int apply_option_parse_directory(const struct option *opt, strbuf_reset(&state->root); strbuf_addstr(&state->root, arg); + + if (strbuf_normalize_path(&state->root) < 0) + return error(_("unable to normalize directory: '%s'"), arg); + strbuf_complete(&state->root, '/'); return 0; } diff --git a/t/t4128-apply-root.sh b/t/t4128-apply-root.sh index f6db5a79dd9d35..5eba15fa66b956 100755 --- a/t/t4128-apply-root.sh +++ b/t/t4128-apply-root.sh @@ -43,6 +43,47 @@ test_expect_success 'apply --directory -p (2) ' ' ' +test_expect_success 'apply --directory (./ prefix)' ' + git reset --hard initial && + git apply --directory=./some/sub -p3 --index patch && + echo Bello >expect && + git show :some/sub/dir/file >actual && + test_cmp expect actual && + test_cmp expect some/sub/dir/file +' + +test_expect_success 'apply --directory (double slash)' ' + git reset --hard initial && + git apply --directory=some//sub -p3 --index patch && + echo Bello >expect && + git show :some/sub/dir/file >actual && + test_cmp expect actual && + test_cmp expect some/sub/dir/file +' + +test_expect_success 'apply --directory (./ in the middle)' ' + git reset --hard initial && + git apply --directory=some/./sub -p3 --index patch && + echo Bello >expect && + git show :some/sub/dir/file >actual && + test_cmp expect actual && + test_cmp expect some/sub/dir/file +' + +test_expect_success 'apply --directory (../ in the middle)' ' + git reset --hard initial && + git apply --directory=some/../some/sub -p3 --index patch && + echo Bello >expect && + git show :some/sub/dir/file >actual && + test_cmp expect actual && + test_cmp expect some/sub/dir/file +' + +test_expect_success 'apply --directory rejects leading ../' ' + test_must_fail git apply --directory=../foo -p3 patch 2>err && + test_grep "unable to normalize directory" err +' + cat > patch << EOF diff --git a/newfile b/newfile new file mode 100644 From 1e50d839f8592daf364778298a61670c4b998654 Mon Sep 17 00:00:00 2001 From: Shreyansh Paliwal Date: Fri, 20 Feb 2026 23:21:26 +0530 Subject: [PATCH 14/58] tree-diff: remove the usage of the_hash_algo global emit_path() uses the global the_hash_algo even though a local repository is already available via struct diff_options *opt. Replace these uses with opt->repo->hash_algo. With no remaining reliance on global states in this file, drop the dependency on 'environment.h' and remove '#define USE_THE_REPOSITORY_VARIABLE'. This follows earlier cleanups to introduce opt->repo in tree-diff.c [1][2]. [1]- https://lore.kernel.org/git/20180921155739.14407-21-pclouds@gmail.com/ [2]- https://lore.kernel.org/git/20260109213021.2546-2-l.s.r@web.de/ Signed-off-by: Shreyansh Paliwal Signed-off-by: Junio C Hamano --- tree-diff.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tree-diff.c b/tree-diff.c index 631ea868124256..2f5c956d0259c7 100644 --- a/tree-diff.c +++ b/tree-diff.c @@ -2,7 +2,6 @@ * Helper functions for tree diff generation */ -#define USE_THE_REPOSITORY_VARIABLE #define DISABLE_SIGN_COMPARE_WARNINGS #include "git-compat-util.h" @@ -11,7 +10,6 @@ #include "hash.h" #include "tree.h" #include "tree-walk.h" -#include "environment.h" #include "repository.h" #include "dir.h" @@ -253,7 +251,7 @@ static void emit_path(struct combine_diff_path ***tail, strbuf_add(base, path, pathlen); p = combine_diff_path_new(base->buf, base->len, mode, - oid ? oid : null_oid(the_hash_algo), + oid ? oid : null_oid(opt->repo->hash_algo), nparent); strbuf_setlen(base, old_baselen); @@ -278,7 +276,7 @@ static void emit_path(struct combine_diff_path ***tail, mode_i = tp[i].entry.mode; } else { - oid_i = null_oid(the_hash_algo); + oid_i = null_oid(opt->repo->hash_algo); mode_i = 0; } From 84325f0730801b7638f1152ea3553530452d5c3b Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 21 Feb 2026 23:59:48 +0000 Subject: [PATCH 15/58] merge,diff: remove the_repository check before prefetching blobs Prefetching of blobs from promisor remotes was added to diff in 7fbbcb21b162 (diff: batch fetching of missing blobs, 2019-04-05). In that commit, https://lore.kernel.org/git/20190405170934.20441-1-jonathantanmy@google.com/ was squashed into https://lore.kernel.org/git/44de02e584f449481e6fb00cf35d74adf0192e9d.1553895166.git.jonathantanmy@google.com/ without the extra explanation about the squashed changes being added to the commit message; in particular, this explanation from that first link is absent: > Also, prefetch only if the repository being diffed is the_repository > (because we do not support lazy fetching for any other repository > anyway). Then, later, this checking was spread from diff.c to diffcore-rename.c and diffcore-break.c by 95acf11a3dc3 (diff: restrict when prefetching occurs, 2020-04-07) and then further split in d331dd3b0c82 (diffcore-rename: allow different missing_object_cb functions, 2021-06-22). I also copied the logic from prefetching blobs from diff.c to merge-ort.c in 2bff554b23e8 (merge-ort: add prefetching for content merges, 2021-06-22). The reason for all these checks was noted above -- we only supported lazy fetching for the_repository. However, that changed with ef830cc43412 (promisor-remote: teach lazy-fetch in any repo, 2021-06-17), so these checks are now unnecessary. Remove them. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- diff.c | 2 +- diffcore-break.c | 2 +- diffcore-rename.c | 4 ++-- merge-ort.c | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/diff.c b/diff.c index 35b903a9a0966a..9091e041b79fee 100644 --- a/diff.c +++ b/diff.c @@ -7176,7 +7176,7 @@ void diffcore_std(struct diff_options *options) * If no prefetching occurs, diffcore_rename() will prefetch if it * decides that it needs inexact rename detection. */ - if (options->repo == the_repository && repo_has_promisor_remote(the_repository) && + if (repo_has_promisor_remote(options->repo) && (options->output_format & output_formats_to_prefetch || options->pickaxe_opts & DIFF_PICKAXE_KINDS_MASK)) diff_queued_diff_prefetch(options->repo); diff --git a/diffcore-break.c b/diffcore-break.c index c4c2173f3096bc..91ae5e8dbb4950 100644 --- a/diffcore-break.c +++ b/diffcore-break.c @@ -69,7 +69,7 @@ static int should_break(struct repository *r, oideq(&src->oid, &dst->oid)) return 0; /* they are the same */ - if (r == the_repository && repo_has_promisor_remote(the_repository)) { + if (repo_has_promisor_remote(r)) { options.missing_object_cb = diff_queued_diff_prefetch; options.missing_object_data = r; } diff --git a/diffcore-rename.c b/diffcore-rename.c index d9476db35acbf7..c797d8ed2f54dd 100644 --- a/diffcore-rename.c +++ b/diffcore-rename.c @@ -987,7 +987,7 @@ static int find_basename_matches(struct diff_options *options, strintmap_set(&dests, base, i); } - if (options->repo == the_repository && repo_has_promisor_remote(the_repository)) { + if (repo_has_promisor_remote(options->repo)) { dpf_options.missing_object_cb = basename_prefetch; dpf_options.missing_object_data = &prefetch_options; } @@ -1574,7 +1574,7 @@ void diffcore_rename_extended(struct diff_options *options, /* Finish setting up dpf_options */ prefetch_options.skip_unmodified = skip_unmodified; - if (options->repo == the_repository && repo_has_promisor_remote(the_repository)) { + if (repo_has_promisor_remote(options->repo)) { dpf_options.missing_object_cb = inexact_prefetch; dpf_options.missing_object_data = &prefetch_options; } diff --git a/merge-ort.c b/merge-ort.c index 0a59d1e596200e..27a58a735d69bb 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -4438,7 +4438,7 @@ static void prefetch_for_content_merges(struct merge_options *opt, struct string_list_item *e; struct oid_array to_fetch = OID_ARRAY_INIT; - if (opt->repo != the_repository || !repo_has_promisor_remote(the_repository)) + if (!repo_has_promisor_remote(opt->repo)) return; for (e = &plist->items[plist->nr-1]; e >= plist->items; --e) { From 4f8108b5ff8937720844cec95d2c5b6b22cec03b Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 21 Feb 2026 23:59:49 +0000 Subject: [PATCH 16/58] merge-ort: pass repository to write_tree() In order to get rid of a usage of the_repository, we need to know the value of opt->repo; pass it along to write_tree(). Once we have the repository, though, we no longer need to pass opt->repo->hash_algo->rawsz, we can have write_tree() look up that value itself. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-ort.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/merge-ort.c b/merge-ort.c index 27a58a735d69bb..289a61822f2314 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -3822,15 +3822,16 @@ static int tree_entry_order(const void *a_, const void *b_) b->string, strlen(b->string), bmi->result.mode); } -static int write_tree(struct object_id *result_oid, +static int write_tree(struct repository *repo, + struct object_id *result_oid, struct string_list *versions, - unsigned int offset, - size_t hash_size) + unsigned int offset) { size_t maxlen = 0, extra; unsigned int nr; struct strbuf buf = STRBUF_INIT; int i, ret = 0; + size_t hash_size = repo->hash_algo->rawsz; assert(offset <= versions->nr); nr = versions->nr - offset; @@ -3856,7 +3857,7 @@ static int write_tree(struct object_id *result_oid, } /* Write this object file out, and record in result_oid */ - if (odb_write_object(the_repository->objects, buf.buf, + if (odb_write_object(repo->objects, buf.buf, buf.len, OBJ_TREE, result_oid)) ret = -1; strbuf_release(&buf); @@ -4026,8 +4027,8 @@ static int write_completed_directory(struct merge_options *opt, dir_info->is_null = 0; dir_info->result.mode = S_IFDIR; if (record_tree && - write_tree(&dir_info->result.oid, &info->versions, offset, - opt->repo->hash_algo->rawsz) < 0) + write_tree(opt->repo, &dir_info->result.oid, &info->versions, + offset) < 0) ret = -1; } @@ -4573,8 +4574,7 @@ static int process_entries(struct merge_options *opt, BUG("dir_metadata accounting completely off; shouldn't happen"); } if (record_tree && - write_tree(result_oid, &dir_metadata.versions, 0, - opt->repo->hash_algo->rawsz) < 0) + write_tree(opt->repo, result_oid, &dir_metadata.versions, 0) < 0) ret = -1; cleanup: string_list_clear(&plist, 0); From b54590b4634936ffc57a994d33ae053ccc7fcdfd Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 21 Feb 2026 23:59:50 +0000 Subject: [PATCH 17/58] merge-ort: replace the_repository with opt->repo We have a perfectly valid repository available and do not need to use the_repository. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-ort.c | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/merge-ort.c b/merge-ort.c index 289a61822f2314..9b6a4c312e12c5 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -1732,9 +1732,9 @@ static int collect_merge_info(struct merge_options *opt, info.data = opt; info.show_all_errors = 1; - if (repo_parse_tree(the_repository, merge_base) < 0 || - repo_parse_tree(the_repository, side1) < 0 || - repo_parse_tree(the_repository, side2) < 0) + if (repo_parse_tree(opt->repo, merge_base) < 0 || + repo_parse_tree(opt->repo, side1) < 0 || + repo_parse_tree(opt->repo, side2) < 0) return -1; init_tree_desc(t + 0, &merge_base->object.oid, merge_base->buffer, merge_base->size); @@ -2136,9 +2136,9 @@ static int merge_3way(struct merge_options *opt, name2 = mkpathdup("%s:%s", opt->branch2, pathnames[2]); } - read_mmblob(&orig, the_repository->objects, o); - read_mmblob(&src1, the_repository->objects, a); - read_mmblob(&src2, the_repository->objects, b); + read_mmblob(&orig, opt->repo->objects, o); + read_mmblob(&src1, opt->repo->objects, a); + read_mmblob(&src2, opt->repo->objects, b); merge_status = ll_merge(result_buf, path, &orig, base, &src1, name1, &src2, name2, @@ -2254,7 +2254,7 @@ static int handle_content_merge(struct merge_options *opt, } if (!ret && record_object && - odb_write_object(the_repository->objects, result_buf.ptr, result_buf.size, + odb_write_object(opt->repo->objects, result_buf.ptr, result_buf.size, OBJ_BLOB, &result->oid)) { path_msg(opt, ERROR_OBJECT_WRITE_FAILED, 0, pathnames[0], pathnames[1], pathnames[2], NULL, @@ -3713,7 +3713,7 @@ static int read_oid_strbuf(struct merge_options *opt, void *buf; enum object_type type; unsigned long size; - buf = odb_read_object(the_repository->objects, oid, &type, &size); + buf = odb_read_object(opt->repo->objects, oid, &type, &size); if (!buf) { path_msg(opt, ERROR_OBJECT_READ_FAILED, 0, path, NULL, NULL, NULL, @@ -4619,10 +4619,10 @@ static int checkout(struct merge_options *opt, unpack_opts.verbose_update = (opt->verbosity > 2); unpack_opts.fn = twoway_merge; unpack_opts.preserve_ignored = 0; /* FIXME: !opts->overwrite_ignore */ - if (repo_parse_tree(the_repository, prev) < 0) + if (repo_parse_tree(opt->repo, prev) < 0) return -1; init_tree_desc(&trees[0], &prev->object.oid, prev->buffer, prev->size); - if (repo_parse_tree(the_repository, next) < 0) + if (repo_parse_tree(opt->repo, next) < 0) return -1; init_tree_desc(&trees[1], &next->object.oid, next->buffer, next->size); @@ -5280,7 +5280,7 @@ static void merge_ort_nonrecursive_internal(struct merge_options *opt, if (result->clean >= 0) { if (!opt->mergeability_only) { - result->tree = repo_parse_tree_indirect(the_repository, + result->tree = repo_parse_tree_indirect(opt->repo, &working_tree_oid); if (!result->tree) die(_("unable to read tree (%s)"), @@ -5309,7 +5309,7 @@ static void merge_ort_internal(struct merge_options *opt, struct strbuf merge_base_abbrev = STRBUF_INIT; if (!merge_bases) { - if (repo_get_merge_bases(the_repository, h1, h2, + if (repo_get_merge_bases(opt->repo, h1, h2, &merge_bases) < 0) { result->clean = -1; goto out; @@ -5440,20 +5440,20 @@ static void merge_recursive_config(struct merge_options *opt, int ui) { char *value = NULL; int renormalize = 0; - repo_config_get_int(the_repository, "merge.verbosity", &opt->verbosity); - repo_config_get_int(the_repository, "diff.renamelimit", &opt->rename_limit); - repo_config_get_int(the_repository, "merge.renamelimit", &opt->rename_limit); - repo_config_get_bool(the_repository, "merge.renormalize", &renormalize); + repo_config_get_int(opt->repo, "merge.verbosity", &opt->verbosity); + repo_config_get_int(opt->repo, "diff.renamelimit", &opt->rename_limit); + repo_config_get_int(opt->repo, "merge.renamelimit", &opt->rename_limit); + repo_config_get_bool(opt->repo, "merge.renormalize", &renormalize); opt->renormalize = renormalize; - if (!repo_config_get_string(the_repository, "diff.renames", &value)) { + if (!repo_config_get_string(opt->repo, "diff.renames", &value)) { opt->detect_renames = git_config_rename("diff.renames", value); free(value); } - if (!repo_config_get_string(the_repository, "merge.renames", &value)) { + if (!repo_config_get_string(opt->repo, "merge.renames", &value)) { opt->detect_renames = git_config_rename("merge.renames", value); free(value); } - if (!repo_config_get_string(the_repository, "merge.directoryrenames", &value)) { + if (!repo_config_get_string(opt->repo, "merge.directoryrenames", &value)) { int boolval = git_parse_maybe_bool(value); if (0 <= boolval) { opt->detect_directory_renames = boolval ? @@ -5466,7 +5466,7 @@ static void merge_recursive_config(struct merge_options *opt, int ui) free(value); } if (ui) { - if (!repo_config_get_string(the_repository, "diff.algorithm", &value)) { + if (!repo_config_get_string(opt->repo, "diff.algorithm", &value)) { long diff_algorithm = parse_algorithm_value(value); if (diff_algorithm < 0) die(_("unknown value for config '%s': %s"), "diff.algorithm", value); @@ -5474,7 +5474,7 @@ static void merge_recursive_config(struct merge_options *opt, int ui) free(value); } } - repo_config(the_repository, git_xmerge_config, NULL); + repo_config(opt->repo, git_xmerge_config, NULL); } static void init_merge_options(struct merge_options *opt, From 5eae39beb10efeff23c58c1a9f4665ed4c498065 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 21 Feb 2026 23:59:51 +0000 Subject: [PATCH 18/58] merge-ort: replace the_hash_algo with opt->repo->hash_algo We have a perfectly valid repository available and do not need to use the_hash_algo (a shorthand for the_repository->hash_algo), so use the known repository instead. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-ort.c | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/merge-ort.c b/merge-ort.c index 9b6a4c312e12c5..60b4675f39d8c5 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -1857,7 +1857,7 @@ static int merge_submodule(struct merge_options *opt, BUG("submodule deleted on one side; this should be handled outside of merge_submodule()"); if ((sub_not_initialized = repo_submodule_init(&subrepo, - opt->repo, path, null_oid(the_hash_algo)))) { + opt->repo, path, null_oid(opt->repo->hash_algo)))) { path_msg(opt, CONFLICT_SUBMODULE_NOT_INITIALIZED, 0, path, NULL, NULL, NULL, _("Failed to merge submodule %s (not checked out)"), @@ -2240,7 +2240,7 @@ static int handle_content_merge(struct merge_options *opt, two_way = ((S_IFMT & o->mode) != (S_IFMT & a->mode)); merge_status = merge_3way(opt, path, - two_way ? null_oid(the_hash_algo) : &o->oid, + two_way ? null_oid(opt->repo->hash_algo) : &o->oid, &a->oid, &b->oid, pathnames, extra_marker_size, &result_buf); @@ -2272,7 +2272,7 @@ static int handle_content_merge(struct merge_options *opt, } else if (S_ISGITLINK(a->mode)) { int two_way = ((S_IFMT & o->mode) != (S_IFMT & a->mode)); clean = merge_submodule(opt, pathnames[0], - two_way ? null_oid(the_hash_algo) : &o->oid, + two_way ? null_oid(opt->repo->hash_algo) : &o->oid, &a->oid, &b->oid, &result->oid); if (clean < 0) return -1; @@ -2786,7 +2786,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt, assert(!new_ci->match_mask); new_ci->dirmask = 0; new_ci->stages[1].mode = 0; - oidcpy(&new_ci->stages[1].oid, null_oid(the_hash_algo)); + oidcpy(&new_ci->stages[1].oid, null_oid(opt->repo->hash_algo)); /* * Now that we have the file information in new_ci, make sure @@ -2799,7 +2799,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt, continue; /* zero out any entries related to files */ ci->stages[i].mode = 0; - oidcpy(&ci->stages[i].oid, null_oid(the_hash_algo)); + oidcpy(&ci->stages[i].oid, null_oid(opt->repo->hash_algo)); } /* Now we want to focus on new_ci, so reassign ci to it. */ @@ -3214,7 +3214,7 @@ static int process_renames(struct merge_options *opt, if (type_changed) { /* rename vs. typechange */ /* Mark the original as resolved by removal */ - memcpy(&oldinfo->stages[0].oid, null_oid(the_hash_algo), + memcpy(&oldinfo->stages[0].oid, null_oid(opt->repo->hash_algo), sizeof(oldinfo->stages[0].oid)); oldinfo->stages[0].mode = 0; oldinfo->filemask &= 0x06; @@ -4102,7 +4102,7 @@ static int process_entry(struct merge_options *opt, if (ci->filemask & (1 << i)) continue; ci->stages[i].mode = 0; - oidcpy(&ci->stages[i].oid, null_oid(the_hash_algo)); + oidcpy(&ci->stages[i].oid, null_oid(opt->repo->hash_algo)); } } else if (ci->df_conflict && ci->merged.result.mode != 0) { /* @@ -4149,7 +4149,7 @@ static int process_entry(struct merge_options *opt, continue; /* zero out any entries related to directories */ new_ci->stages[i].mode = 0; - oidcpy(&new_ci->stages[i].oid, null_oid(the_hash_algo)); + oidcpy(&new_ci->stages[i].oid, null_oid(opt->repo->hash_algo)); } /* @@ -4271,11 +4271,11 @@ static int process_entry(struct merge_options *opt, new_ci->merged.result.mode = ci->stages[2].mode; oidcpy(&new_ci->merged.result.oid, &ci->stages[2].oid); new_ci->stages[1].mode = 0; - oidcpy(&new_ci->stages[1].oid, null_oid(the_hash_algo)); + oidcpy(&new_ci->stages[1].oid, null_oid(opt->repo->hash_algo)); new_ci->filemask = 5; if ((S_IFMT & b_mode) != (S_IFMT & o_mode)) { new_ci->stages[0].mode = 0; - oidcpy(&new_ci->stages[0].oid, null_oid(the_hash_algo)); + oidcpy(&new_ci->stages[0].oid, null_oid(opt->repo->hash_algo)); new_ci->filemask = 4; } @@ -4283,11 +4283,11 @@ static int process_entry(struct merge_options *opt, ci->merged.result.mode = ci->stages[1].mode; oidcpy(&ci->merged.result.oid, &ci->stages[1].oid); ci->stages[2].mode = 0; - oidcpy(&ci->stages[2].oid, null_oid(the_hash_algo)); + oidcpy(&ci->stages[2].oid, null_oid(opt->repo->hash_algo)); ci->filemask = 3; if ((S_IFMT & a_mode) != (S_IFMT & o_mode)) { ci->stages[0].mode = 0; - oidcpy(&ci->stages[0].oid, null_oid(the_hash_algo)); + oidcpy(&ci->stages[0].oid, null_oid(opt->repo->hash_algo)); ci->filemask = 2; } @@ -4415,7 +4415,7 @@ static int process_entry(struct merge_options *opt, /* Deleted on both sides */ ci->merged.is_null = 1; ci->merged.result.mode = 0; - oidcpy(&ci->merged.result.oid, null_oid(the_hash_algo)); + oidcpy(&ci->merged.result.oid, null_oid(opt->repo->hash_algo)); assert(!ci->df_conflict); ci->merged.clean = !ci->path_conflict; } From 59257224a34824b948bf3941d66168deeec1dca8 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 21 Feb 2026 23:59:52 +0000 Subject: [PATCH 19/58] merge-ort: prevent the_repository from coming back Due to the use of DEFAULT_ABBREV, we cannot get rid of our usage of USE_THE_REPOSITORY_VARIABLE. However, we have removed all other uses of the_repository in merge-ort a few times. But they keep coming back. Define the_repository to make it a compilation error so that they don't come back any more. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-ort.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/merge-ort.c b/merge-ort.c index 60b4675f39d8c5..00923ce3cd749b 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -53,6 +53,14 @@ #include "unpack-trees.h" #include "xdiff-interface.h" +/* + * We technically need USE_THE_REPOSITORY_VARIABLE above for DEFAULT_ABBREV, + * but do not want more uses of the_repository. Prevent them. + * + * opt->repo is available; use it instead. + */ +#define the_repository DO_NOT_USE_THE_REPOSITORY + /* * We have many arrays of size 3. Whenever we have such an array, the * indices refer to one of the sides of the three-way merge. This is so From 3249d07962f081bd84bace9ca7f808575e2cae56 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Fri, 20 Feb 2026 01:59:48 +0000 Subject: [PATCH 20/58] replay: prevent the_repository from coming back Due to the use of DEFAULT_ABBREV, we cannot get rid of our usage of USE_THE_REPOSITORY_VARIABLE. We have removed all other uses of the_repository before, but without removing that definition, they keep coming back. Define the_repository to make it a compilation error so that they don't come back any more; the repo parameter plumbed through the various functions can be used instead. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- replay.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/replay.c b/replay.c index f97d652f338f1d..a63f6714c4c2ec 100644 --- a/replay.c +++ b/replay.c @@ -11,6 +11,12 @@ #include "strmap.h" #include "tree.h" +/* + * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but + * do not want to use the_repository. + */ +#define the_repository DO_NOT_USE_THE_REPOSITORY + static const char *short_commit_name(struct repository *repo, struct commit *commit) { From 25ede48b5406524d1a6b900e400f593c0b9a34dd Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 23 Feb 2026 12:26:43 +0000 Subject: [PATCH 21/58] config: move show_all_config() In anticipation of using format_config() in this method, move show_all_config() lower in the file without changes. Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- builtin/config.c | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/builtin/config.c b/builtin/config.c index 288ebdfdaaab1c..237f7a934d2f12 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -231,30 +231,6 @@ static void show_config_scope(const struct config_display_options *opts, strbuf_addch(buf, term); } -static int show_all_config(const char *key_, const char *value_, - const struct config_context *ctx, - void *cb) -{ - const struct config_display_options *opts = cb; - const struct key_value_info *kvi = ctx->kvi; - - if (opts->show_origin || opts->show_scope) { - struct strbuf buf = STRBUF_INIT; - if (opts->show_scope) - show_config_scope(opts, kvi, &buf); - if (opts->show_origin) - show_config_origin(opts, kvi, &buf); - /* Use fwrite as "buf" can contain \0's if "end_null" is set. */ - fwrite(buf.buf, 1, buf.len, stdout); - strbuf_release(&buf); - } - if (!opts->omit_values && value_) - printf("%s%c%s%c", key_, opts->delim, value_, opts->term); - else - printf("%s%c", key_, opts->term); - return 0; -} - struct strbuf_list { struct strbuf *items; int nr; @@ -332,6 +308,30 @@ static int format_config(const struct config_display_options *opts, return 0; } +static int show_all_config(const char *key_, const char *value_, + const struct config_context *ctx, + void *cb) +{ + const struct config_display_options *opts = cb; + const struct key_value_info *kvi = ctx->kvi; + + if (opts->show_origin || opts->show_scope) { + struct strbuf buf = STRBUF_INIT; + if (opts->show_scope) + show_config_scope(opts, kvi, &buf); + if (opts->show_origin) + show_config_origin(opts, kvi, &buf); + /* Use fwrite as "buf" can contain \0's if "end_null" is set. */ + fwrite(buf.buf, 1, buf.len, stdout); + strbuf_release(&buf); + } + if (!opts->omit_values && value_) + printf("%s%c%s%c", key_, opts->delim, value_, opts->term); + else + printf("%s%c", key_, opts->term); + return 0; +} + #define GET_VALUE_ALL (1 << 0) #define GET_VALUE_KEY_REGEXP (1 << 1) From 12210d034635e6e558f23505ef3edaedd870097e Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 23 Feb 2026 12:26:44 +0000 Subject: [PATCH 22/58] config: add 'gently' parameter to format_config() This parameter is set to 0 for all current callers and is UNUSED. However, we will start using this option in future changes and in a critical change that requires gentle parsing (not using die()) to try parsing all values in a list. Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- builtin/config.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/builtin/config.c b/builtin/config.c index 237f7a934d2f12..b4c4228311dde1 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -242,10 +242,14 @@ struct strbuf_list { * append it into strbuf `buf`. Returns a negative value on failure, * 0 on success, 1 on a missing optional value (i.e., telling the * caller to pretend that did not exist). + * + * Note: 'gently' is currently ignored, but will be implemented in + * a future change. */ static int format_config(const struct config_display_options *opts, struct strbuf *buf, const char *key_, - const char *value_, const struct key_value_info *kvi) + const char *value_, const struct key_value_info *kvi, + int gently UNUSED) { if (opts->show_scope) show_config_scope(opts, kvi, buf); @@ -372,7 +376,7 @@ static int collect_config(const char *key_, const char *value_, strbuf_init(&values->items[values->nr], 0); status = format_config(data->display_opts, &values->items[values->nr++], - key_, value_, kvi); + key_, value_, kvi, 0); if (status < 0) return status; if (status) { @@ -463,7 +467,7 @@ static int get_value(const struct config_location_options *opts, strbuf_init(item, 0); status = format_config(display_opts, item, key_, - display_opts->default_value, &kvi); + display_opts->default_value, &kvi, 0); if (status < 0) die(_("failed to format default config value: %s"), display_opts->default_value); @@ -743,7 +747,7 @@ static int get_urlmatch(const struct config_location_options *opts, status = format_config(&display_opts, &buf, item->string, matched->value_is_null ? NULL : matched->value.buf, - &matched->kvi); + &matched->kvi, 0); if (!status) fwrite(buf.buf, 1, buf.len, stdout); strbuf_release(&buf); From 1ef1f9d53a1607dd8fd38e0dbae67e405c3b3563 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 23 Feb 2026 12:26:45 +0000 Subject: [PATCH 23/58] config: make 'git config list --type=' work Previously, the --type= argument to 'git config list' was ignored and did nothing. Now, we add the use of format_config() to the show_all_config() function so each key-value pair is attempted to be parsed. This is our first use of the 'gently' parameter with a nonzero value. When listing multiple values, our initial settings for the output format is different. Add a new init helper to specify the fact that keys should be shown and also add the default delimiters as they were unset in some cases. Our intention is that if there is an error in parsing, then the row is not output. This is necessary to avoid the caller needing to build their own validator to understand the difference between valid, canonicalized types and other raw string values. The raw values will always be available to the user if they do not specify the --type= option. The current behavior is more complicated, including error messages on bad parsing or potentially complete failure of the command. We add tests at this point that demonstrate the current behavior so we can witness the fix in future changes that parse these values quietly and gently. This is a change in behavior! We are starting to respect an option that was previously ignored, leading to potential user confusion. This is probably still a good option, since the --type argument did not change behavior at all previously, so users can get the behavior they expect by removing the --type argument or adding the --no-type argument. t1300-config.sh is updated with the current behavior of this formatting logic to justify the upcoming refactoring of format_config() that will incrementally fix some of these cases to be more user-friendly. Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- Documentation/git-config.adoc | 3 ++ builtin/config.c | 35 +++++++------ t/t1300-config.sh | 97 ++++++++++++++++++++++++++++++++++- 3 files changed, 119 insertions(+), 16 deletions(-) diff --git a/Documentation/git-config.adoc b/Documentation/git-config.adoc index ac3b536a155c6e..5300dd4c51257a 100644 --- a/Documentation/git-config.adoc +++ b/Documentation/git-config.adoc @@ -240,6 +240,9 @@ Valid ``'s include: that the given value is canonicalize-able as an ANSI color, but it is written as-is. + +If the command is in `list` mode, then the `--type ` argument will apply +to each listed config value. If the value does not successfully parse in that +format, then it will be omitted from the list. --bool:: --int:: diff --git a/builtin/config.c b/builtin/config.c index b4c4228311dde1..4c4c79188382a3 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -318,21 +318,12 @@ static int show_all_config(const char *key_, const char *value_, { const struct config_display_options *opts = cb; const struct key_value_info *kvi = ctx->kvi; + struct strbuf formatted = STRBUF_INIT; - if (opts->show_origin || opts->show_scope) { - struct strbuf buf = STRBUF_INIT; - if (opts->show_scope) - show_config_scope(opts, kvi, &buf); - if (opts->show_origin) - show_config_origin(opts, kvi, &buf); - /* Use fwrite as "buf" can contain \0's if "end_null" is set. */ - fwrite(buf.buf, 1, buf.len, stdout); - strbuf_release(&buf); - } - if (!opts->omit_values && value_) - printf("%s%c%s%c", key_, opts->delim, value_, opts->term); - else - printf("%s%c", key_, opts->term); + if (format_config(opts, &formatted, key_, value_, kvi, 1) >= 0) + fwrite(formatted.buf, 1, formatted.len, stdout); + + strbuf_release(&formatted); return 0; } @@ -872,6 +863,19 @@ static void display_options_init(struct config_display_options *opts) } } +static void display_options_init_list(struct config_display_options *opts) +{ + opts->show_keys = 1; + + if (opts->end_nul) { + display_options_init(opts); + } else { + opts->term = '\n'; + opts->delim = ' '; + opts->key_delim = '='; + } +} + static int cmd_config_list(int argc, const char **argv, const char *prefix, struct repository *repo UNUSED) { @@ -890,7 +894,7 @@ static int cmd_config_list(int argc, const char **argv, const char *prefix, check_argc(argc, 0, 0); location_options_init(&location_opts, prefix); - display_options_init(&display_opts); + display_options_init_list(&display_opts); setup_auto_pager("config", 1); @@ -1321,6 +1325,7 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix) if (actions == ACTION_LIST) { check_argc(argc, 0, 0); + display_options_init_list(&display_opts); if (config_with_options(show_all_config, &display_opts, &location_opts.source, the_repository, &location_opts.options) < 0) { diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 9850fcd5b567a5..dc744c0baef10c 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -2459,9 +2459,15 @@ done cat >.git/config <<-\EOF && [section] -foo = true +foo = True number = 10 big = 1M +path = ~/dir +red = red +blue = Blue +date = Fri Jun 4 15:46:55 2010 +missing=:(optional)no-such-path +exists=:(optional)expect EOF test_expect_success 'identical modern --type specifiers are allowed' ' @@ -2503,6 +2509,95 @@ test_expect_success 'unset type specifiers may be reset to conflicting ones' ' test_cmp_config 1048576 --type=bool --no-type --type=int section.big ' +test_expect_success 'list --type=int shows only canonicalizable int values' ' + cat >expect <<-EOF && + section.number=10 + section.big=1048576 + EOF + + test_must_fail git config ${mode_prefix}list --type=int +' + +test_expect_success 'list --type=bool shows only canonicalizable bool values' ' + cat >expect <<-EOF && + section.foo=true + section.number=true + section.big=true + EOF + + test_must_fail git config ${mode_prefix}list --type=bool +' + +test_expect_success 'list --type=bool-or-int shows only canonicalizable values' ' + cat >expect <<-EOF && + section.foo=true + section.number=10 + section.big=1048576 + EOF + + test_must_fail git config ${mode_prefix}list --type=bool-or-int +' + +test_expect_success 'list --type=path shows only canonicalizable path values' ' + # TODO: handling of missing path is incorrect here. + cat >expect <<-EOF && + section.foo=True + section.number=10 + section.big=1M + section.path=$HOME/dir + section.red=red + section.blue=Blue + section.date=Fri Jun 4 15:46:55 2010 + section.missing=section.exists=expect + EOF + + git config ${mode_prefix}list --type=path >actual 2>err && + test_cmp expect actual && + test_must_be_empty err +' + +test_expect_success 'list --type=expiry-date shows only canonicalizable dates' ' + cat >expecterr <<-EOF && + error: '\''True'\'' for '\''section.foo'\'' is not a valid timestamp + error: '\''~/dir'\'' for '\''section.path'\'' is not a valid timestamp + error: '\''red'\'' for '\''section.red'\'' is not a valid timestamp + error: '\''Blue'\'' for '\''section.blue'\'' is not a valid timestamp + error: '\'':(optional)no-such-path'\'' for '\''section.missing'\'' is not a valid timestamp + error: '\'':(optional)expect'\'' for '\''section.exists'\'' is not a valid timestamp + EOF + + git config ${mode_prefix}list --type=expiry-date >actual 2>err && + + # section.number and section.big parse as relative dates that could + # have clock skew in their results. + test_grep section.big actual && + test_grep section.number actual && + test_grep "section.date=$(git config --type=expiry-date section.$key)" actual && + test_cmp expecterr err +' + +test_expect_success 'list --type=color shows only canonicalizable color values' ' + cat >expect <<-EOF && + section.number=<> + section.red= + section.blue= + EOF + + cat >expecterr <<-EOF && + error: invalid color value: True + error: invalid color value: 1M + error: invalid color value: ~/dir + error: invalid color value: Fri Jun 4 15:46:55 2010 + error: invalid color value: :(optional)no-such-path + error: invalid color value: :(optional)expect + EOF + + git config ${mode_prefix}list --type=color >actual.raw 2>err && + test_decode_color actual && + test_cmp expect actual && + test_cmp expecterr err +' + test_expect_success '--type rejects unknown specifiers' ' test_must_fail git config --type=nonsense section.foo 2>error && test_grep "unrecognized --type argument" error From d744923fefb294c835d18883bac62f85ff55fc9f Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 23 Feb 2026 12:26:46 +0000 Subject: [PATCH 24/58] config: format int64s gently Move the logic for formatting int64 config values into a helper method and use gentle parsing when needed. Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- builtin/config.c | 27 +++++++++++++++++++++++---- t/t1300-config.sh | 4 +++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/builtin/config.c b/builtin/config.c index 4c4c79188382a3..448b1485637c58 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -237,6 +237,25 @@ struct strbuf_list { int alloc; }; +static int format_config_int64(struct strbuf *buf, + const char *key_, + const char *value_, + const struct key_value_info *kvi, + int gently) +{ + int64_t v = 0; + if (gently) { + if (!git_parse_int64(value_, &v)) + return -1; + } else { + /* may die() */ + v = git_config_int64(key_, value_ ? value_ : "", kvi); + } + + strbuf_addf(buf, "%"PRId64, v); + return 0; +} + /* * Format the configuration key-value pair (`key_`, `value_`) and * append it into strbuf `buf`. Returns a negative value on failure, @@ -249,8 +268,9 @@ struct strbuf_list { static int format_config(const struct config_display_options *opts, struct strbuf *buf, const char *key_, const char *value_, const struct key_value_info *kvi, - int gently UNUSED) + int gently) { + int res = 0; if (opts->show_scope) show_config_scope(opts, kvi, buf); if (opts->show_origin) @@ -262,8 +282,7 @@ static int format_config(const struct config_display_options *opts, strbuf_addch(buf, opts->key_delim); if (opts->type == TYPE_INT) - strbuf_addf(buf, "%"PRId64, - git_config_int64(key_, value_ ? value_ : "", kvi)); + res = format_config_int64(buf, key_, value_, kvi, gently); else if (opts->type == TYPE_BOOL) strbuf_addstr(buf, git_config_bool(key_, value_) ? "true" : "false"); @@ -309,7 +328,7 @@ static int format_config(const struct config_display_options *opts, } } strbuf_addch(buf, opts->term); - return 0; + return res; } static int show_all_config(const char *key_, const char *value_, diff --git a/t/t1300-config.sh b/t/t1300-config.sh index dc744c0baef10c..05a812fd6d27e8 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -2515,7 +2515,9 @@ test_expect_success 'list --type=int shows only canonicalizable int values' ' section.big=1048576 EOF - test_must_fail git config ${mode_prefix}list --type=int + git config ${mode_prefix}list --type=int >actual 2>err && + test_cmp expect actual && + test_must_be_empty err ' test_expect_success 'list --type=bool shows only canonicalizable bool values' ' From 53959a8ba22d80f78daa693dfc2f76fd3afe80e2 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 23 Feb 2026 12:26:47 +0000 Subject: [PATCH 25/58] config: format bools gently Move the logic for formatting bool config values into a helper method and use gentle parsing when needed. This makes 'git config list --type=bool' not fail when coming across a non-boolean value. Such unparseable values are filtered out quietly. Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- builtin/config.c | 21 +++++++++++++++++++-- t/t1300-config.sh | 4 +++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/builtin/config.c b/builtin/config.c index 448b1485637c58..d8b38c51d3ae64 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -256,6 +256,24 @@ static int format_config_int64(struct strbuf *buf, return 0; } +static int format_config_bool(struct strbuf *buf, + const char *key_, + const char *value_, + int gently) +{ + int v = 0; + if (gently) { + if ((v = git_parse_maybe_bool(value_)) < 0) + return -1; + } else { + /* may die() */ + v = git_config_bool(key_, value_); + } + + strbuf_addstr(buf, v ? "true" : "false"); + return 0; +} + /* * Format the configuration key-value pair (`key_`, `value_`) and * append it into strbuf `buf`. Returns a negative value on failure, @@ -284,8 +302,7 @@ static int format_config(const struct config_display_options *opts, if (opts->type == TYPE_INT) res = format_config_int64(buf, key_, value_, kvi, gently); else if (opts->type == TYPE_BOOL) - strbuf_addstr(buf, git_config_bool(key_, value_) ? - "true" : "false"); + res = format_config_bool(buf, key_, value_, gently); else if (opts->type == TYPE_BOOL_OR_INT) { int is_bool, v; v = git_config_bool_or_int(key_, value_, kvi, diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 05a812fd6d27e8..568cfaa3c56295 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -2527,7 +2527,9 @@ test_expect_success 'list --type=bool shows only canonicalizable bool values' ' section.big=true EOF - test_must_fail git config ${mode_prefix}list --type=bool + git config ${mode_prefix}list --type=bool >actual 2>err && + test_cmp expect actual && + test_must_be_empty err ' test_expect_success 'list --type=bool-or-int shows only canonicalizable values' ' From 5fb7bdcca98e63fedee22f16a34ab5fadbee54e0 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 23 Feb 2026 12:26:48 +0000 Subject: [PATCH 26/58] config: format bools or ints gently Move the logic for formatting bool-or-int config values into a helper method and use gentle parsing when needed. Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- builtin/config.c | 40 +++++++++++++++++++++++++++++++--------- t/t1300-config.sh | 4 +++- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/builtin/config.c b/builtin/config.c index d8b38c51d3ae64..491a880e5683e2 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -274,6 +274,34 @@ static int format_config_bool(struct strbuf *buf, return 0; } +static int format_config_bool_or_int(struct strbuf *buf, + const char *key_, + const char *value_, + const struct key_value_info *kvi, + int gently) +{ + int v, is_bool = 0; + + if (gently) { + v = git_parse_maybe_bool_text(value_); + + if (v >= 0) + is_bool = 1; + else if (!git_parse_int(value_, &v)) + return -1; + } else { + v = git_config_bool_or_int(key_, value_, kvi, + &is_bool); + } + + if (is_bool) + strbuf_addstr(buf, v ? "true" : "false"); + else + strbuf_addf(buf, "%d", v); + + return 0; +} + /* * Format the configuration key-value pair (`key_`, `value_`) and * append it into strbuf `buf`. Returns a negative value on failure, @@ -303,15 +331,9 @@ static int format_config(const struct config_display_options *opts, res = format_config_int64(buf, key_, value_, kvi, gently); else if (opts->type == TYPE_BOOL) res = format_config_bool(buf, key_, value_, gently); - else if (opts->type == TYPE_BOOL_OR_INT) { - int is_bool, v; - v = git_config_bool_or_int(key_, value_, kvi, - &is_bool); - if (is_bool) - strbuf_addstr(buf, v ? "true" : "false"); - else - strbuf_addf(buf, "%d", v); - } else if (opts->type == TYPE_BOOL_OR_STR) { + else if (opts->type == TYPE_BOOL_OR_INT) + res = format_config_bool_or_int(buf, key_, value_, kvi, gently); + else if (opts->type == TYPE_BOOL_OR_STR) { int v = git_parse_maybe_bool(value_); if (v < 0) strbuf_addstr(buf, value_); diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 568cfaa3c56295..1fc8e788ee9cc5 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -2539,7 +2539,9 @@ test_expect_success 'list --type=bool-or-int shows only canonicalizable values' section.big=1048576 EOF - test_must_fail git config ${mode_prefix}list --type=bool-or-int + git config ${mode_prefix}list --type=bool-or-int >actual 2>err && + test_cmp expect actual && + test_must_be_empty err ' test_expect_success 'list --type=path shows only canonicalizable path values' ' From 9c7fc23c24cc0cfeaf5fe32a96fbfe2709a3f93d Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 23 Feb 2026 12:26:49 +0000 Subject: [PATCH 27/58] config: format bools or strings in helper Move the logic for formatting bool-or-string config values into a helper. This parsing has always been gentle, so this is not unlocking new behavior. This extraction is only to match the formatting of the other cases that do need a behavior change. Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- builtin/config.c | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/builtin/config.c b/builtin/config.c index 491a880e5683e2..79c139c5b06c1d 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -302,6 +302,18 @@ static int format_config_bool_or_int(struct strbuf *buf, return 0; } +/* This mode is always gentle. */ +static int format_config_bool_or_str(struct strbuf *buf, + const char *value_) +{ + int v = git_parse_maybe_bool(value_); + if (v < 0) + strbuf_addstr(buf, value_); + else + strbuf_addstr(buf, v ? "true" : "false"); + return 0; +} + /* * Format the configuration key-value pair (`key_`, `value_`) and * append it into strbuf `buf`. Returns a negative value on failure, @@ -333,13 +345,9 @@ static int format_config(const struct config_display_options *opts, res = format_config_bool(buf, key_, value_, gently); else if (opts->type == TYPE_BOOL_OR_INT) res = format_config_bool_or_int(buf, key_, value_, kvi, gently); - else if (opts->type == TYPE_BOOL_OR_STR) { - int v = git_parse_maybe_bool(value_); - if (v < 0) - strbuf_addstr(buf, value_); - else - strbuf_addstr(buf, v ? "true" : "false"); - } else if (opts->type == TYPE_PATH) { + else if (opts->type == TYPE_BOOL_OR_STR) + res = format_config_bool_or_str(buf, value_); + else if (opts->type == TYPE_PATH) { char *v; if (git_config_pathname(&v, key_, value_) < 0) return -1; From bcfb9128c9ce87dfeacaffe051257f7a5fc866e9 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 23 Feb 2026 12:26:50 +0000 Subject: [PATCH 28/58] config: format paths gently Move the logic for formatting path config values into a helper method and use gentle parsing when needed. We need to be careful about how to handle the ':(optional)' macro, which as tested in t1311-config-optional.sh must allow for ignoring a missing path when other multiple values exist, but cause 'git config get' to fail if it is the only possible value and thus no result is output. In the case of our list, we need to omit those values silently. This necessitates the use of the 'gently' parameter here. Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- builtin/config.c | 32 ++++++++++++++++++++++---------- t/t1300-config.sh | 3 +-- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/builtin/config.c b/builtin/config.c index 79c139c5b06c1d..2828b6dcf17814 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -314,6 +314,25 @@ static int format_config_bool_or_str(struct strbuf *buf, return 0; } +static int format_config_path(struct strbuf *buf, + const char *key_, + const char *value_, + int gently) +{ + char *v; + + if (git_config_pathname(&v, key_, value_) < 0) + return -1; + + if (v) + strbuf_addstr(buf, v); + else + return gently ? -1 : 1; /* :(optional)no-such-file */ + + free(v); + return 0; +} + /* * Format the configuration key-value pair (`key_`, `value_`) and * append it into strbuf `buf`. Returns a negative value on failure, @@ -347,16 +366,9 @@ static int format_config(const struct config_display_options *opts, res = format_config_bool_or_int(buf, key_, value_, kvi, gently); else if (opts->type == TYPE_BOOL_OR_STR) res = format_config_bool_or_str(buf, value_); - else if (opts->type == TYPE_PATH) { - char *v; - if (git_config_pathname(&v, key_, value_) < 0) - return -1; - if (v) - strbuf_addstr(buf, v); - else - return 1; /* :(optional)no-such-file */ - free((char *)v); - } else if (opts->type == TYPE_EXPIRY_DATE) { + else if (opts->type == TYPE_PATH) + res = format_config_path(buf, key_, value_, gently); + else if (opts->type == TYPE_EXPIRY_DATE) { timestamp_t t; if (git_config_expiry_date(&t, key_, value_) < 0) return -1; diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 1fc8e788ee9cc5..48d9c554d815bf 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -2545,7 +2545,6 @@ test_expect_success 'list --type=bool-or-int shows only canonicalizable values' ' test_expect_success 'list --type=path shows only canonicalizable path values' ' - # TODO: handling of missing path is incorrect here. cat >expect <<-EOF && section.foo=True section.number=10 @@ -2554,7 +2553,7 @@ test_expect_success 'list --type=path shows only canonicalizable path values' ' section.red=red section.blue=Blue section.date=Fri Jun 4 15:46:55 2010 - section.missing=section.exists=expect + section.exists=expect EOF git config ${mode_prefix}list --type=path >actual 2>err && From 9cb4a5e1ba3e586e77b1c026d509f284b4c55764 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 23 Feb 2026 12:26:51 +0000 Subject: [PATCH 29/58] config: format expiry dates quietly Move the logic for formatting expiry date config values into a helper method and use quiet parsing when needed. Note that git_config_expiry_date() will show an error on a bad parse and not die() like most other git_config...() parsers. Thus, we use 'quietly' here instead of 'gently'. There is an unfortunate asymmetry in these two parsing methods, but we need to treat a positive response from parse_expiry_date() as an error or we will get incorrect values. This updates the behavior of 'git config list --type=expiry-date' to be quiet when attempting parsing on non-date values. Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- builtin/config.c | 27 +++++++++++++++++++++------ t/t1300-config.sh | 11 +---------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/builtin/config.c b/builtin/config.c index 2828b6dcf17814..ee77ddc87c629c 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -3,6 +3,7 @@ #include "abspath.h" #include "config.h" #include "color.h" +#include "date.h" #include "editor.h" #include "environment.h" #include "gettext.h" @@ -333,6 +334,23 @@ static int format_config_path(struct strbuf *buf, return 0; } +static int format_config_expiry_date(struct strbuf *buf, + const char *key_, + const char *value_, + int quietly) +{ + timestamp_t t; + if (quietly) { + if (parse_expiry_date(value_, &t)) + return -1; + } else if (git_config_expiry_date(&t, key_, value_) < 0) { + return -1; + } + + strbuf_addf(buf, "%"PRItime, t); + return 0; +} + /* * Format the configuration key-value pair (`key_`, `value_`) and * append it into strbuf `buf`. Returns a negative value on failure, @@ -368,12 +386,9 @@ static int format_config(const struct config_display_options *opts, res = format_config_bool_or_str(buf, value_); else if (opts->type == TYPE_PATH) res = format_config_path(buf, key_, value_, gently); - else if (opts->type == TYPE_EXPIRY_DATE) { - timestamp_t t; - if (git_config_expiry_date(&t, key_, value_) < 0) - return -1; - strbuf_addf(buf, "%"PRItime, t); - } else if (opts->type == TYPE_COLOR) { + else if (opts->type == TYPE_EXPIRY_DATE) + res = format_config_expiry_date(buf, key_, value_, gently); + else if (opts->type == TYPE_COLOR) { char v[COLOR_MAXLEN]; if (git_config_color(v, key_, value_) < 0) return -1; diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 48d9c554d815bf..72bdd6ab03fd75 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -2562,15 +2562,6 @@ test_expect_success 'list --type=path shows only canonicalizable path values' ' ' test_expect_success 'list --type=expiry-date shows only canonicalizable dates' ' - cat >expecterr <<-EOF && - error: '\''True'\'' for '\''section.foo'\'' is not a valid timestamp - error: '\''~/dir'\'' for '\''section.path'\'' is not a valid timestamp - error: '\''red'\'' for '\''section.red'\'' is not a valid timestamp - error: '\''Blue'\'' for '\''section.blue'\'' is not a valid timestamp - error: '\'':(optional)no-such-path'\'' for '\''section.missing'\'' is not a valid timestamp - error: '\'':(optional)expect'\'' for '\''section.exists'\'' is not a valid timestamp - EOF - git config ${mode_prefix}list --type=expiry-date >actual 2>err && # section.number and section.big parse as relative dates that could @@ -2578,7 +2569,7 @@ test_expect_success 'list --type=expiry-date shows only canonicalizable dates' ' test_grep section.big actual && test_grep section.number actual && test_grep "section.date=$(git config --type=expiry-date section.$key)" actual && - test_cmp expecterr err + test_must_be_empty err ' test_expect_success 'list --type=color shows only canonicalizable color values' ' From db45e4908d6711ddc3094cb079c85278691c8267 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 23 Feb 2026 12:26:52 +0000 Subject: [PATCH 30/58] color: add color_parse_quietly() When parsing colors, a failed parse leads to an error message due to the result returning error(). To allow for quiet parsing, create color_parse_quietly(). This is in contrast to an ..._gently() version because the original does not die(), so both options are technically 'gentle'. To accomplish this, convert the implementation of color_parse_mem() into a static color_parse_mem_1() helper that adds a 'quiet' parameter. The color_parse_quietly() method can then use this. Since it is a near equivalent to color_parse(), move that method down in the file so they can be nearby while also appearing after color_parse_mem_1(). Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- color.c | 25 ++++++++++++++++++------- color.h | 1 + 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/color.c b/color.c index 07ac8c9d400906..00b53f97acbcc7 100644 --- a/color.c +++ b/color.c @@ -223,11 +223,6 @@ static int parse_attr(const char *name, size_t len) return -1; } -int color_parse(const char *value, char *dst) -{ - return color_parse_mem(value, strlen(value), dst); -} - /* * Write the ANSI color codes for "c" to "out"; the string should * already have the ANSI escape code in it. "out" should have enough @@ -264,7 +259,8 @@ static int color_empty(const struct color *c) return c->type <= COLOR_NORMAL; } -int color_parse_mem(const char *value, int value_len, char *dst) +static int color_parse_mem_1(const char *value, int value_len, + char *dst, int quiet) { const char *ptr = value; int len = value_len; @@ -365,10 +361,25 @@ int color_parse_mem(const char *value, int value_len, char *dst) OUT(0); return 0; bad: - return error(_("invalid color value: %.*s"), value_len, value); + return quiet ? -1 : error(_("invalid color value: %.*s"), value_len, value); #undef OUT } +int color_parse_mem(const char *value, int value_len, char *dst) +{ + return color_parse_mem_1(value, value_len, dst, 0); +} + +int color_parse(const char *value, char *dst) +{ + return color_parse_mem(value, strlen(value), dst); +} + +int color_parse_quietly(const char *value, char *dst) +{ + return color_parse_mem_1(value, strlen(value), dst, 1); +} + enum git_colorbool git_config_colorbool(const char *var, const char *value) { if (value) { diff --git a/color.h b/color.h index 43e6c9ad0972b3..0d7254030003f0 100644 --- a/color.h +++ b/color.h @@ -118,6 +118,7 @@ bool want_color_fd(int fd, enum git_colorbool var); * terminal. */ int color_parse(const char *value, char *dst); +int color_parse_quietly(const char *value, char *dst); int color_parse_mem(const char *value, int len, char *dst); /* From 2d4ab5a885f365cb66156ff4553f88c000dfa307 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 23 Feb 2026 12:26:53 +0000 Subject: [PATCH 31/58] config: format colors quietly Move the logic for formatting color config value into a helper method and use quiet parsing when needed. This removes error messages when parsing a list of config values that do not match color formats. Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- builtin/config.c | 27 +++++++++++++++++++++------ t/t1300-config.sh | 11 +---------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/builtin/config.c b/builtin/config.c index ee77ddc87c629c..45304076dc4726 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -351,6 +351,24 @@ static int format_config_expiry_date(struct strbuf *buf, return 0; } +static int format_config_color(struct strbuf *buf, + const char *key_, + const char *value_, + int gently) +{ + char v[COLOR_MAXLEN]; + + if (gently) { + if (color_parse_quietly(value_, v) < 0) + return -1; + } else if (git_config_color(v, key_, value_) < 0) { + return -1; + } + + strbuf_addstr(buf, v); + return 0; +} + /* * Format the configuration key-value pair (`key_`, `value_`) and * append it into strbuf `buf`. Returns a negative value on failure, @@ -388,12 +406,9 @@ static int format_config(const struct config_display_options *opts, res = format_config_path(buf, key_, value_, gently); else if (opts->type == TYPE_EXPIRY_DATE) res = format_config_expiry_date(buf, key_, value_, gently); - else if (opts->type == TYPE_COLOR) { - char v[COLOR_MAXLEN]; - if (git_config_color(v, key_, value_) < 0) - return -1; - strbuf_addstr(buf, v); - } else if (value_) { + else if (opts->type == TYPE_COLOR) + res = format_config_color(buf, key_, value_, gently); + else if (value_) { strbuf_addstr(buf, value_); } else { /* Just show the key name; back out delimiter */ diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 72bdd6ab03fd75..128971ee12fa6c 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -2579,19 +2579,10 @@ test_expect_success 'list --type=color shows only canonicalizable color values' section.blue= EOF - cat >expecterr <<-EOF && - error: invalid color value: True - error: invalid color value: 1M - error: invalid color value: ~/dir - error: invalid color value: Fri Jun 4 15:46:55 2010 - error: invalid color value: :(optional)no-such-path - error: invalid color value: :(optional)expect - EOF - git config ${mode_prefix}list --type=color >actual.raw 2>err && test_decode_color actual && test_cmp expect actual && - test_cmp expecterr err + test_must_be_empty err ' test_expect_success '--type rejects unknown specifiers' ' From 645f92a3e9179ebf1ed42dc4fa05cc8dd71e3e9c Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 23 Feb 2026 12:26:54 +0000 Subject: [PATCH 32/58] config: restructure format_config() The recent changes have replaced the bodies of most if/else-if cases with simple helper method calls. This makes it easy to adapt the structure into a clearer switch statement, leaving a simple if/else in the default case. Make things a little simpler to read by reducing the nesting depth via a new goto statement when we want to skip values. Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- builtin/config.c | 64 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/builtin/config.c b/builtin/config.c index 45304076dc4726..2e8bc6590cbb1f 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -124,6 +124,7 @@ struct config_display_options { .key_delim = ' ', \ } +#define TYPE_NONE 0 #define TYPE_BOOL 1 #define TYPE_INT 2 #define TYPE_BOOL_OR_INT 3 @@ -390,32 +391,57 @@ static int format_config(const struct config_display_options *opts, show_config_origin(opts, kvi, buf); if (opts->show_keys) strbuf_addstr(buf, key_); - if (!opts->omit_values) { - if (opts->show_keys) - strbuf_addch(buf, opts->key_delim); - - if (opts->type == TYPE_INT) - res = format_config_int64(buf, key_, value_, kvi, gently); - else if (opts->type == TYPE_BOOL) - res = format_config_bool(buf, key_, value_, gently); - else if (opts->type == TYPE_BOOL_OR_INT) - res = format_config_bool_or_int(buf, key_, value_, kvi, gently); - else if (opts->type == TYPE_BOOL_OR_STR) - res = format_config_bool_or_str(buf, value_); - else if (opts->type == TYPE_PATH) - res = format_config_path(buf, key_, value_, gently); - else if (opts->type == TYPE_EXPIRY_DATE) - res = format_config_expiry_date(buf, key_, value_, gently); - else if (opts->type == TYPE_COLOR) - res = format_config_color(buf, key_, value_, gently); - else if (value_) { + + if (opts->omit_values) + goto terminator; + + if (opts->show_keys) + strbuf_addch(buf, opts->key_delim); + + switch (opts->type) { + case TYPE_INT: + res = format_config_int64(buf, key_, value_, kvi, gently); + break; + + case TYPE_BOOL: + res = format_config_bool(buf, key_, value_, gently); + break; + + case TYPE_BOOL_OR_INT: + res = format_config_bool_or_int(buf, key_, value_, kvi, gently); + break; + + case TYPE_BOOL_OR_STR: + res = format_config_bool_or_str(buf, value_); + break; + + case TYPE_PATH: + res = format_config_path(buf, key_, value_, gently); + break; + + case TYPE_EXPIRY_DATE: + res = format_config_expiry_date(buf, key_, value_, gently); + break; + + case TYPE_COLOR: + res = format_config_color(buf, key_, value_, gently); + break; + + case TYPE_NONE: + if (value_) { strbuf_addstr(buf, value_); } else { /* Just show the key name; back out delimiter */ if (opts->show_keys) strbuf_setlen(buf, buf->len - 1); } + break; + + default: + BUG("undefined type %d", opts->type); } + +terminator: strbuf_addch(buf, opts->term); return res; } From 096aa6099834ce00401a369b34cbff4868ea5704 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 23 Feb 2026 12:26:55 +0000 Subject: [PATCH 33/58] config: use an enum for type The --type= option for 'git config' has previously been defined using macros, but using a typed enum is better for tracking the possible values. Move the definition up to make sure it is defined before a macro uses some of its terms. Update the initializer for config_display_options to explicitly set 'type' to TYPE_NONE even though this is implied by a zero value. This assists in knowing that the switch statement added in the previous change has a complete set of cases for a properly-valued enum. Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- builtin/config.c | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/builtin/config.c b/builtin/config.c index 2e8bc6590cbb1f..7c4857be622904 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -86,6 +86,17 @@ struct config_location_options { .respect_includes_opt = -1, \ } +enum config_type { + TYPE_NONE = 0, + TYPE_BOOL, + TYPE_INT, + TYPE_BOOL_OR_INT, + TYPE_PATH, + TYPE_EXPIRY_DATE, + TYPE_COLOR, + TYPE_BOOL_OR_STR, +}; + #define CONFIG_TYPE_OPTIONS(type) \ OPT_GROUP(N_("Type")), \ OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type), \ @@ -111,7 +122,7 @@ struct config_display_options { int show_origin; int show_scope; int show_keys; - int type; + enum config_type type; char *default_value; /* Populated via `display_options_init()`. */ int term; @@ -122,17 +133,9 @@ struct config_display_options { .term = '\n', \ .delim = '=', \ .key_delim = ' ', \ + .type = TYPE_NONE, \ } -#define TYPE_NONE 0 -#define TYPE_BOOL 1 -#define TYPE_INT 2 -#define TYPE_BOOL_OR_INT 3 -#define TYPE_PATH 4 -#define TYPE_EXPIRY_DATE 5 -#define TYPE_COLOR 6 -#define TYPE_BOOL_OR_STR 7 - #define OPT_CALLBACK_VALUE(s, l, v, h, i) { \ .type = OPTION_CALLBACK, \ .short_name = (s), \ From 09505b11153a20e1c6c572d41db778171dd19cbc Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Tue, 24 Feb 2026 09:45:45 +0100 Subject: [PATCH 34/58] t: fix races caused by background maintenance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Many Git commands spawn git-maintenance(1) to optimize the repository in the background. By default, performing the maintenance is for most of the part asynchronous: we fork the executable and then continue with the rest of our business logic. This is working as expected for our users, but this behaviour is somewhat problematic for our test suite as this is inherently racy. We have many tests that verify the on-disk state of repositories, and those tests may easily race with our background maintenance. In a similar fashion, we may end up with processes that "leak" out of a current test case. Until now this tends to not be much of a problem. Our maintenance uses git-gc(1) by default, which knows to bail out in case there aren't either too many packfiles or too many loose objects. So even if other data structures would need to be optimized, we won't do so unless the object database also needs optimizations. This is about to change though, as a subsequent commit will switch to the "geometric" maintenance strategy as a default. The consequence is that we will run required optimizations even if the object database is well-optimized. And this uncovers races between our test suite and background maintenance all over the place. Disabling maintenance outright in our test suite is not really an option, as it would result in significant divergence from the "real world" and reduce our test coverage. But we've got an alternative up our sleeves: we can ensure that garbage collection runs synchronously by overriding the "maintenance.autoDetach" configuration. Of course that also diverges from the real world, as we now stop testing that background maintenance interacts in a benign way with normal Git commands. But on the other hand this ensures that the maintenance itself does not for example lead to data loss in a more reproducible way. Another concern is that this would make execution of the test suite much slower. But a quick benchmark on my machine demonstrates that this does not seem to be the case: Benchmark 1: meson test (revision = HEAD~) Time (mean ± σ): 131.182 s ± 1.293 s [User: 853.737 s, System: 1160.479 s] Range (min … max): 130.001 s … 132.563 s 3 runs Benchmark 2: meson test (revision = HEAD) Time (mean ± σ): 129.554 s ± 0.507 s [User: 849.040 s, System: 1152.664 s] Range (min … max): 129.000 s … 129.994 s 3 runs Summary meson test (revision = HEAD) ran 1.01 ± 0.01 times faster than meson test (revision = HEAD~) Funny enough, it even seems as if this speeds up test execution ever so slightly, but that may just as well be noise. Introduce a new `GIT_TEST_MAINT_AUTO_DETACH` environment variable that allows us to override the auto-detach behaviour and set that variable in our tests. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- run-command.c | 2 +- t/t5616-partial-clone.sh | 6 +++--- t/t7900-maintenance.sh | 3 +++ t/test-lib.sh | 4 ++++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/run-command.c b/run-command.c index e3e02475ccec50..438a290d30744d 100644 --- a/run-command.c +++ b/run-command.c @@ -1828,7 +1828,7 @@ int prepare_auto_maintenance(int quiet, struct child_process *maint) */ if (repo_config_get_bool(the_repository, "maintenance.autodetach", &auto_detach) && repo_config_get_bool(the_repository, "gc.autodetach", &auto_detach)) - auto_detach = 1; + auto_detach = git_env_bool("GIT_TEST_MAINT_AUTO_DETACH", true); maint->git_cmd = 1; maint->close_object_store = 1; diff --git a/t/t5616-partial-clone.sh b/t/t5616-partial-clone.sh index 1e354e057fa12c..d62760eb92b422 100755 --- a/t/t5616-partial-clone.sh +++ b/t/t5616-partial-clone.sh @@ -229,7 +229,7 @@ test_expect_success 'fetch --refetch triggers repacking' ' GIT_TRACE2_EVENT="$PWD/trace1.event" \ git -C pc1 fetch --refetch origin && - test_subcommand git maintenance run --auto --no-quiet --detach Date: Tue, 24 Feb 2026 09:45:46 +0100 Subject: [PATCH 35/58] t: disable maintenance where we verify object database structure We have a couple of tests that explicitly verify the structure of the object database. Naturally, this structure is dependent on whether or not we run repository maintenance: if it decides to optimize the object database the expected structure is likely to not materialize. Explicitly disable auto-maintenance in such tests so that we are not dependent on decisions made by our maintenance. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/t0081-find-pack.sh | 1 + t/t5316-pack-delta-depth.sh | 1 + t/t5319-multi-pack-index.sh | 1 + t/t5326-multi-pack-bitmaps.sh | 3 ++- t/t5327-multi-pack-bitmaps-rev.sh | 3 ++- t/t5331-pack-objects-stdin.sh | 2 ++ t/t5332-multi-pack-reuse.sh | 1 + t/t5334-incremental-multi-pack-index.sh | 1 + t/t5500-fetch-pack.sh | 3 ++- t/t5616-partial-clone.sh | 1 + t/t7700-repack.sh | 3 +++ 11 files changed, 17 insertions(+), 3 deletions(-) diff --git a/t/t0081-find-pack.sh b/t/t0081-find-pack.sh index 5a628bf7356445..26f017422d7253 100755 --- a/t/t0081-find-pack.sh +++ b/t/t0081-find-pack.sh @@ -68,6 +68,7 @@ test_expect_success 'add more packfiles' ' ' test_expect_success 'add more commits (as loose objects)' ' + test_config maintenance.auto false && test_commit six && test_commit seven && diff --git a/t/t5316-pack-delta-depth.sh b/t/t5316-pack-delta-depth.sh index 03dfb7a61ea978..8a067a45cb4bae 100755 --- a/t/t5316-pack-delta-depth.sh +++ b/t/t5316-pack-delta-depth.sh @@ -48,6 +48,7 @@ test_description='pack-objects breaks long cross-pack delta chains' # repeatedly-modified file to generate the delta chain). test_expect_success 'create series of packs' ' + test_config maintenance.auto false && test-tool genrandom foo 4096 >content && prev= && for i in $(test_seq 1 10) diff --git a/t/t5319-multi-pack-index.sh b/t/t5319-multi-pack-index.sh index faae98c7e76a20..7672d599d4e470 100755 --- a/t/t5319-multi-pack-index.sh +++ b/t/t5319-multi-pack-index.sh @@ -1315,6 +1315,7 @@ test_expect_success 'bitmapped packs are stored via the BTMP chunk' ' git init repo && ( cd repo && + git config set maintenance.auto false && for i in 1 2 3 4 5 do diff --git a/t/t5326-multi-pack-bitmaps.sh b/t/t5326-multi-pack-bitmaps.sh index 892aeb09e4b9d7..62bd973d92afc9 100755 --- a/t/t5326-multi-pack-bitmaps.sh +++ b/t/t5326-multi-pack-bitmaps.sh @@ -93,7 +93,8 @@ test_midx_bitmap_cases () { test_expect_success 'setup test_repository' ' rm -rf * .git && git init && - git config pack.writeBitmapLookupTable '"$writeLookupTable"' + git config pack.writeBitmapLookupTable '"$writeLookupTable"' && + git config maintenance.auto false ' midx_bitmap_core diff --git a/t/t5327-multi-pack-bitmaps-rev.sh b/t/t5327-multi-pack-bitmaps-rev.sh index 9cac03a94bf4b4..cfa12de2a8a7ba 100755 --- a/t/t5327-multi-pack-bitmaps-rev.sh +++ b/t/t5327-multi-pack-bitmaps-rev.sh @@ -30,7 +30,8 @@ test_midx_bitmap_rev () { test_expect_success 'setup bitmap config' ' rm -rf * .git && git init && - git config pack.writeBitmapLookupTable '"$writeLookupTable"' + git config pack.writeBitmapLookupTable '"$writeLookupTable"' && + git config maintenance.auto false ' midx_bitmap_core rev diff --git a/t/t5331-pack-objects-stdin.sh b/t/t5331-pack-objects-stdin.sh index cd949025b982a4..b03f6be1644471 100755 --- a/t/t5331-pack-objects-stdin.sh +++ b/t/t5331-pack-objects-stdin.sh @@ -14,6 +14,7 @@ packed_objects () { test_expect_success 'setup for --stdin-packs tests' ' git init stdin-packs && + git -C stdin-packs config set maintenance.auto false && ( cd stdin-packs && @@ -255,6 +256,7 @@ test_expect_success '--stdin-packs=follow walks into unknown packs' ' git init repo && ( cd repo && + git config set maintenance.auto false && for c in A B C D do diff --git a/t/t5332-multi-pack-reuse.sh b/t/t5332-multi-pack-reuse.sh index 395d09444ced72..881ce668e1d14e 100755 --- a/t/t5332-multi-pack-reuse.sh +++ b/t/t5332-multi-pack-reuse.sh @@ -59,6 +59,7 @@ test_pack_objects_reused () { test_expect_success 'preferred pack is reused for single-pack reuse' ' test_config pack.allowPackReuse single && + git config set maintenance.auto false && for i in A B do diff --git a/t/t5334-incremental-multi-pack-index.sh b/t/t5334-incremental-multi-pack-index.sh index d30d7253d6f6cc..99c7d44d8e9d34 100755 --- a/t/t5334-incremental-multi-pack-index.sh +++ b/t/t5334-incremental-multi-pack-index.sh @@ -15,6 +15,7 @@ midx_chain=$midxdir/multi-pack-index-chain test_expect_success 'convert non-incremental MIDX to incremental' ' test_commit base && + git config set maintenance.auto false && git repack -ad && git multi-pack-index write && diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh index 4bb56c167a52ec..0c88d04d0ad727 100755 --- a/t/t5500-fetch-pack.sh +++ b/t/t5500-fetch-pack.sh @@ -154,7 +154,8 @@ test_expect_success 'clone shallow depth 1 with fsck' ' ' test_expect_success 'clone shallow' ' - git clone --no-single-branch --depth 2 "file://$(pwd)/." shallow + git clone --no-single-branch --depth 2 "file://$(pwd)/." shallow && + git -C shallow config set maintenance.auto false ' test_expect_success 'clone shallow depth count' ' diff --git a/t/t5616-partial-clone.sh b/t/t5616-partial-clone.sh index d62760eb92b422..1c2805accac636 100755 --- a/t/t5616-partial-clone.sh +++ b/t/t5616-partial-clone.sh @@ -585,6 +585,7 @@ test_expect_success 'verify fetch downloads only one pack when updating refs' ' git clone --filter=blob:none "file://$(pwd)/srv.bare" pack-test && ls pack-test/.git/objects/pack/*pack >pack-list && test_line_count = 2 pack-list && + test_config -C pack-test maintenance.auto false && for i in A B C do test_commit -C src $i && diff --git a/t/t7700-repack.sh b/t/t7700-repack.sh index 73b78bdd887d80..acc2589f212727 100755 --- a/t/t7700-repack.sh +++ b/t/t7700-repack.sh @@ -217,6 +217,7 @@ test_expect_success 'repack --keep-pack' ' cd keep-pack && # avoid producing different packs due to delta/base choices git config pack.window 0 && + git config maintenance.auto false && P1=$(commit_and_pack 1) && P2=$(commit_and_pack 2) && P3=$(commit_and_pack 3) && @@ -260,6 +261,7 @@ test_expect_success 'repacking fails when missing .pack actually means missing o # Avoid producing different packs due to delta/base choices git config pack.window 0 && + git config maintenance.auto false && P1=$(commit_and_pack 1) && P2=$(commit_and_pack 2) && P3=$(commit_and_pack 3) && @@ -534,6 +536,7 @@ test_expect_success 'setup for --write-midx tests' ' ( cd midx && git config core.multiPackIndex true && + git config maintenance.auto false && test_commit base ) From ea7d894f449d7f212ddf8a12a67f78467581b23f Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Tue, 24 Feb 2026 09:45:47 +0100 Subject: [PATCH 36/58] t34xx: don't expire reflogs where it matters We have a couple of tests in the t34xx range that rely on reflogs. This never really used to be a problem, but in a subsequent commit we will change the default maintenance strategy from "gc" to "geometric", and this will cause us to drop all reflogs in these tests. This may seem surprising and like a bug at first, but it's actually not. The main difference between these two strategies is that the "gc" strategy will skip all maintenance in case the object database is in a well-optimized state. The "geometric" strategy has separate subtasks though, and the conditions for each of these tasks is evaluated on a case by case basis. This means that even if the object database is in good shape, we may still decide to expire reflogs. So why is that a problem? The issue is that Git's test suite hardcodes the committer and author dates to a date in 2005. Interestingly though, these hardcoded dates not only impact the commits, but also the reflog entries. The consequence is that all newly written reflog entries are immediately considered stale as our reflog expiration threshold is in the range of weeks, only. It follows that executing `git reflog expire` will thus immediately purge all reflog entries. This hasn't been a problem in our test suite by pure chance, as the repository shapes simply didn't cause us to perform actual garbage collection. But with the upcoming "geometric" strategy we _will_ start to execute `git reflog expire`, thus surfacing this issue. Prepare for this by explicitly disabling reflog expiration in tests impacted by this upcoming change. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/t3404-rebase-interactive.sh | 6 ++++++ t/t3406-rebase-message.sh | 6 ++++++ t/t3431-rebase-fork-point.sh | 6 ++++++ t/t3432-rebase-fast-forward.sh | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index e778dd8ae4a6dc..3e44562afaab40 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -31,6 +31,12 @@ Initial setup: . "$TEST_DIRECTORY"/lib-rebase.sh test_expect_success 'setup' ' + # Commit dates are hardcoded to 2005, and the reflog entries will have + # a matching timestamp. Maintenance may thus immediately expire + # reflogs if it was running. + git config set gc.reflogExpire never && + git config set gc.reflogExpireUnreachable never && + git switch -C primary && test_commit A file1 && test_commit B file1 && diff --git a/t/t3406-rebase-message.sh b/t/t3406-rebase-message.sh index a1d7fa7f7c6965..bc51a9d3a70d55 100755 --- a/t/t3406-rebase-message.sh +++ b/t/t3406-rebase-message.sh @@ -8,6 +8,12 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME . ./test-lib.sh test_expect_success 'setup' ' + # Commit dates are hardcoded to 2005, and the reflog entries will have + # a matching timestamp. Maintenance may thus immediately expire + # reflogs if it was running. + git config set gc.reflogExpire never && + git config set gc.reflogExpireUnreachable never && + test_commit O fileO && test_commit X fileX && git branch fast-forward && diff --git a/t/t3431-rebase-fork-point.sh b/t/t3431-rebase-fork-point.sh index be09fc78c16aab..4336f417c27a7c 100755 --- a/t/t3431-rebase-fork-point.sh +++ b/t/t3431-rebase-fork-point.sh @@ -17,6 +17,12 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME # C was formerly part of main but main was rewound to remove C # test_expect_success setup ' + # Commit dates are hardcoded to 2005, and the reflog entries will have + # a matching timestamp. Maintenance may thus immediately expire + # reflogs if it was running. + git config set gc.reflogExpire never && + git config set gc.reflogExpireUnreachable never && + test_commit A && test_commit B && test_commit C && diff --git a/t/t3432-rebase-fast-forward.sh b/t/t3432-rebase-fast-forward.sh index 5086e14c022071..181d19dcc14a5e 100755 --- a/t/t3432-rebase-fast-forward.sh +++ b/t/t3432-rebase-fast-forward.sh @@ -11,6 +11,12 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME . ./test-lib.sh test_expect_success setup ' + # Commit dates are hardcoded to 2005, and the reflog entries will have + # a matching timestamp. Maintenance may thus immediately expire + # reflogs if it was running. + git config set gc.reflogExpire never && + git config set gc.reflogExpireUnreachable never && + test_commit A && test_commit B && test_commit C && From 0894704369579cb99bba21e88e9ec7ff3852deee Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Tue, 24 Feb 2026 09:45:48 +0100 Subject: [PATCH 37/58] t5400: explicitly use "gc" strategy In t5400 we verify that git-receive-pack(1) runs automated repository maintenance in the remote repository. The check is performed indirectly by observing an effect that git-gc(1) would have, namely to prune a temporary object from the object database. In a subsequent commit we're about to switch to the "geometric" strategy by default though, and here we stop observing that effect. Adapt the test to explicitly use the "gc" strategy to prepare for that upcoming change. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/t5400-send-pack.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/t/t5400-send-pack.sh b/t/t5400-send-pack.sh index 83b42ff0733971..b32a0a6aa77e12 100755 --- a/t/t5400-send-pack.sh +++ b/t/t5400-send-pack.sh @@ -187,6 +187,7 @@ test_expect_success 'receive-pack runs auto-gc in remote repo' ' cd child && git config gc.autopacklimit 1 && git config gc.autodetach false && + git config maintenance.strategy gc && git branch test_auto_gc && # And create a file that follows the temporary object naming # convention for the auto-gc to remove From 94f5d9f09e3c25a2c1efafcab7697a3387c80b4b Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Tue, 24 Feb 2026 09:45:49 +0100 Subject: [PATCH 38/58] t5510: explicitly use "gc" strategy One of the tests in t5510 wants to verify that auto-gc does not lock up when fetching into a repository. Adapt it to explicitly pick the "gc" strategy for auto-maintenance. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/t5510-fetch.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index c69afb5a609343..5dcb4b51a47d88 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -1321,6 +1321,7 @@ test_expect_success 'fetching with auto-gc does not lock up' ' git config fetch.unpackLimit 1 && git config gc.autoPackLimit 1 && git config gc.autoDetach false && + git config maintenance.strategy gc && GIT_ASK_YESNO="$TRASH_DIRECTORY/askyesno" git fetch --verbose >fetch.out 2>&1 && test_grep "Auto packing the repository" fetch.out && ! grep "Should I try again" fetch.out From 38ae87c1ba6b070a4ab69d9ae08c39bcbfcba00c Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Tue, 24 Feb 2026 09:45:50 +0100 Subject: [PATCH 39/58] t6500: explicitly use "gc" strategy The test in t6500 explicitly wants to exercise git-gc(1) and is thus highly specific to the actual on-disk state of the repository and specifically of the object database. An upcoming change modifies the default maintenance strategy to be the "geometric" strategy though, which breaks a couple of assumptions. One fix would arguably be to disable auto-maintenance altogether, as we do want to explicitly verify git-gc(1) anyway. But as the whole test suite is about git-gc(1) in the first place it feels more sensible to configure the default maintenance strategy to be "gc". Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/t6500-gc.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/t/t6500-gc.sh b/t/t6500-gc.sh index bef472cb8dc37b..ea9aaad47091a4 100755 --- a/t/t6500-gc.sh +++ b/t/t6500-gc.sh @@ -11,6 +11,7 @@ test_expect_success 'setup' ' # behavior, make sure we always pack everything to one pack by # default git config gc.bigPackThreshold 2g && + git config set --global maintenance.strategy gc && test_oid_init ' From d2fbe9af79148a93bbcc2fa540e21e9fe3594b65 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Tue, 24 Feb 2026 09:45:51 +0100 Subject: [PATCH 40/58] t7900: prepare for switch of the default strategy The t7900 test suite is exercising git-maintenance(1) and is thus of course heavily reliant on the exact maintenance strategy. This reliance comes in two flavors: - One test explicitly wants to verify that git-gc(1) is run as part of `git maintenance run`. This test is adapted by explicitly picking the "gc" strategy. - The other tests assume a specific shape of the object database, which is dependent on whether or not we run auto-maintenance before we come to the actual subject under test. These tests are adapted by disabling auto-maintenance. With these changes t7900 passes with both "gc" and "geometric" default strategies. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/t7900-maintenance.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh index fe344f47ee5c89..4700beacc18281 100755 --- a/t/t7900-maintenance.sh +++ b/t/t7900-maintenance.sh @@ -45,7 +45,8 @@ test_expect_success 'help text' ' test_grep "usage: git maintenance" err ' -test_expect_success 'run [--auto|--quiet]' ' +test_expect_success 'run [--auto|--quiet] with gc strategy' ' + test_config maintenance.strategy gc && GIT_TRACE2_EVENT="$(pwd)/run-no-auto.txt" \ git maintenance run 2>/dev/null && GIT_TRACE2_EVENT="$(pwd)/run-auto.txt" \ @@ -499,6 +500,7 @@ test_expect_success 'maintenance.incremental-repack.auto' ' ( cd incremental-repack-true && git config core.multiPackIndex true && + git config maintenance.auto false && run_incremental_repack_and_verify ) ' @@ -509,6 +511,7 @@ test_expect_success 'maintenance.incremental-repack.auto (when config is unset)' ( cd incremental-repack-unset && test_unconfig core.multiPackIndex && + git config maintenance.auto false && run_incremental_repack_and_verify ) ' @@ -619,6 +622,7 @@ test_expect_success 'geometric repacking with --auto' ' git init repo && ( cd repo && + git config set maintenance.auto false && # An empty repository does not need repacking, except when # explicitly told to do it. From 452b12c2e0fe7a18f9487f8a090ce46bef207177 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Tue, 24 Feb 2026 09:45:52 +0100 Subject: [PATCH 41/58] builtin/maintenance: use "geometric" strategy by default The git-gc(1) command has been introduced in the early days of Git in 30f610b7b0 (Create 'git gc' to perform common maintenance operations., 2006-12-27) as the main repository maintenance utility. And while the tool has of course evolved since then to cover new parts, the basic strategy it uses has never really changed much. It is safe to say that since 2006 the Git ecosystem has changed quite a bit. Repositories tend to be much larger nowadays than they have been almost 20 years ago, and large parts of the industry went crazy for monorepos (for various wildly different definitions of "monorepo"). So the maintenance strategy we used back then may not be the best fit nowadays anymore. Arguably, most of the maintenance tasks that git-gc(1) does are still perfectly fine today: repacking references, expiring various data structures and things like tend to not cause huge problems. But the big exception is the way we repack objects. git-gc(1) by default uses a split strategy: it performs incremental repacks by default, and then whenever we have too many packs we perform a large all-into-one repack. This all-into-one repack is what is causing problems nowadays, as it is an operation that is quite expensive. While it is wasteful in small- and medium-sized repositories, in large repos it may even be prohibitively expensive. We have eventually introduced git-maintenance(1) that was slated as a replacement for git-gc(1). In contrast to git-gc(1), it is much more flexible as it is structured around configurable tasks and strategies. So while its default "gc" strategy still uses git-gc(1) under the hood, it allows us to iterate. A second strategy it knows about is the "incremental" strategy, which we configure when registering a repository for scheduled maintenance. This strategy isn't really a full replacement for git-gc(1) though, as it doesn't know to expire unused data structures. In Git 2.52 we have thus introduced a new "geometric" strategy that is a proper replacement for the old git-gc(1). In contrast to the incremental/all-into-one split used by git-gc(1), the new "geometric" strategy maintains a geometric progression of packfiles, which significantly reduces the number of all-into-one repacks that we have to perform in large repositories. It is thus a much better fit for large repositories than git-gc(1). Note that the "geometric" strategy isn't perfect though: while we perform way less all-into-one repacks compared to git-gc(1), we still have to perform them eventually. But for the largest repositories out there this may not be an option either, as client machines might not be powerful enough to perform such a repack in the first place. These cases would thus still be covered by the "incremental" strategy. Switch the default strategy away from "gc" to "geometric", but retain the "incremental" strategy configured when registering background maintenance with `git maintenance register`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/config/maintenance.adoc | 6 +++--- builtin/gc.c | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc index d0c38f03fabd60..b578856dde1dd4 100644 --- a/Documentation/config/maintenance.adoc +++ b/Documentation/config/maintenance.adoc @@ -30,8 +30,7 @@ The possible strategies are: + * `none`: This strategy implies no tasks are run at all. This is the default strategy for scheduled maintenance. -* `gc`: This strategy runs the `gc` task. This is the default strategy for - manual maintenance. +* `gc`: This strategy runs the `gc` task. * `geometric`: This strategy performs geometric repacking of packfiles and keeps auxiliary data structures up-to-date. The strategy expires data in the reflog and removes worktrees that cannot be located anymore. When the @@ -40,7 +39,8 @@ The possible strategies are: are already part of a cruft pack will be expired. + This repacking strategy is a full replacement for the `gc` strategy and is -recommended for large repositories. +recommended for large repositories. This is the default strategy for manual +maintenance. * `incremental`: This setting optimizes for performing small maintenance activities that do not delete any data. This does not schedule the `gc` task, but runs the `prefetch` and `commit-graph` tasks hourly, the diff --git a/builtin/gc.c b/builtin/gc.c index 4390eee6eca382..fb329c2cffab80 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -1980,7 +1980,7 @@ static void initialize_task_config(struct maintenance_run_opts *opts, strategy = none_strategy; type = MAINTENANCE_TYPE_SCHEDULED; } else { - strategy = gc_strategy; + strategy = geometric_strategy; type = MAINTENANCE_TYPE_MANUAL; } From ebeea3c471c82638170764989d57d9dc24cc7450 Mon Sep 17 00:00:00 2001 From: "D. Ben Knoble" Date: Tue, 24 Feb 2026 09:39:44 -0500 Subject: [PATCH 42/58] build: regenerate config-list.h when Documentation changes The Meson-based build doesn't know when to rebuild config-list.h, so the header is sometimes stale. For example, an old build directory might have config-list.h from before 4173df5187 (submodule: introduce extensions.submodulePathConfig, 2026-01-12), which added submodule..gitdir to the list. Without it, t9902-completion.sh fails. Regenerating the config-list.h artifact from sources fixes the artifact and the test. Since Meson does not have (or want) builtin support for globbing like Make, teach generate-configlist.sh to also generate a list of Documentation files its output depends on, and incorporate that into the Meson build. We honor the undocumented GCC/Clang contract of outputting empty targets for all the dependencies (like they do with -MP). That is, generate lines like build/config-list.h: $SOURCE_DIR/Documentation/config.adoc $SOURCE_DIR/Documentation/config.adoc: We assume that if a user adds a new file under Documentation/config then they will also edit one of the existing files to include that new file, and that will trigger a rebuild. Also mark the generator script as a dependency. While we're at it, teach the Makefile to use the same "the script knows it's dependencies" logic. For Meson, combining the following commands helps debug dependencies: ninja -C -t deps config-list.h ninja -C -t browse config-list.h The former lists all the dependencies discovered from our output ".d" file (the config documentation) and the latter shows the dependency on the script itself, among other useful edges in the dependency graph. Helped-by: Patrick Steinhardt Helped-by: Phillip Wood Signed-off-by: D. Ben Knoble Signed-off-by: Junio C Hamano --- Makefile | 5 +++-- generate-configlist.sh | 16 +++++++++++++++- meson.build | 5 ++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 47ed9fa7fdd1fa..36459299642c53 100644 --- a/Makefile +++ b/Makefile @@ -2687,9 +2687,10 @@ $(BUILT_INS): git$X cp $< $@ config-list.h: generate-configlist.sh + @mkdir -p .depend + $(QUIET_GEN)$(SHELL_PATH) ./generate-configlist.sh . $@ .depend/config-list.h.d -config-list.h: Documentation/*config.adoc Documentation/config/*.adoc - $(QUIET_GEN)$(SHELL_PATH) ./generate-configlist.sh . $@ +-include .depend/config-list.h.d command-list.h: generate-cmdlist.sh command-list.txt diff --git a/generate-configlist.sh b/generate-configlist.sh index 75c39ade20939d..e28054f9e0e9ba 100755 --- a/generate-configlist.sh +++ b/generate-configlist.sh @@ -2,10 +2,11 @@ SOURCE_DIR="$1" OUTPUT="$2" +DEPFILE="$3" if test -z "$SOURCE_DIR" || ! test -d "$SOURCE_DIR" || test -z "$OUTPUT" then - echo >&2 "USAGE: $0 " + echo >&2 "USAGE: $0 []" exit 1 fi @@ -36,3 +37,16 @@ EOF echo print_config_list } >"$OUTPUT" + +if test -n "$DEPFILE" +then + QUOTED_OUTPUT="$(printf '%s\n' "$OUTPUT" | sed 's,[&/\],\\&,g')" + { + printf '%s\n' "$SOURCE_DIR"/Documentation/*config.adoc \ + "$SOURCE_DIR"/Documentation/config/*.adoc | + sed -e 's/[# ]/\\&/g' -e "s/^/$QUOTED_OUTPUT: /" + printf '%s:\n' "$SOURCE_DIR"/Documentation/*config.adoc \ + "$SOURCE_DIR"/Documentation/config/*.adoc | + sed -e 's/[# ]/\\&/g' + } >"$DEPFILE" +fi diff --git a/meson.build b/meson.build index 3a1d12caa4b94f..e4b8f1e33d284e 100644 --- a/meson.build +++ b/meson.build @@ -720,11 +720,14 @@ endif builtin_sources += custom_target( output: 'config-list.h', + depfile: 'config-list.h.d', + depend_files: [ 'generate-configlist.sh' ], command: [ shell, - meson.current_source_dir() + '/generate-configlist.sh', + meson.current_source_dir() / 'generate-configlist.sh', meson.current_source_dir(), '@OUTPUT@', + '@DEPFILE@', ], env: script_environment, ) From f87593ab1a7040f4a132787ee436f67cef3136d0 Mon Sep 17 00:00:00 2001 From: cuiweixie Date: Wed, 25 Feb 2026 02:00:57 +0000 Subject: [PATCH 43/58] fetch: fix wrong evaluation order in URL trailing-slash trimming if i == -1, url[i] will be UB. Signed-off-by: cuiweixie Signed-off-by: Junio C Hamano --- builtin/fetch.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/fetch.c b/builtin/fetch.c index 40a0e8d24434f2..1ced5f22de26af 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -761,7 +761,7 @@ static void display_state_init(struct display_state *display_state, struct ref * display_state->url = xstrdup("foreign"); display_state->url_len = strlen(display_state->url); - for (i = display_state->url_len - 1; display_state->url[i] == '/' && 0 <= i; i--) + for (i = display_state->url_len - 1; 0 <= i && display_state->url[i] == '/'; i--) ; display_state->url_len = i + 1; if (4 < i && !strncmp(".git", display_state->url + i - 3, 4)) From 2c69ff481938a10660c2078cf83235db26773254 Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Wed, 25 Feb 2026 10:40:41 +0100 Subject: [PATCH 44/58] setup: don't modify repo in `create_reference_database()` The `create_reference_database()` function is used to create the reference database during initialization of a repository. The function calls `repo_set_ref_storage_format()` to set the repositories reference format. This is an unexpected side-effect of the function. More so because the function is only called in two locations: 1. During git-init(1) where the value is propagated from the `struct repository_format repo_fmt` value. 2. During git-clone(1) where the value is propagated from the `the_repository` value. The former is valid, however the flow already calls `repo_set_ref_storage_format()`, so this effort is simply duplicated. The latter sets the existing value in `the_repository` back to itself. While this is okay for now, introduction of more fields in `repo_set_ref_storage_format()` would cause issues, especially dynamically allocated strings, where we would free/allocate the same string back into `the_repostiory`. To avoid all this confusion, clean up the function to no longer take in and set the repo's reference storage format. Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- builtin/clone.c | 2 +- setup.c | 7 ++----- setup.h | 3 +-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index b40cee596804f5..cd43bb5aa22a76 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -1442,7 +1442,7 @@ int cmd_clone(int argc, hash_algo = hash_algo_by_ptr(transport_get_hash_algo(transport)); initialize_repository_version(hash_algo, the_repository->ref_storage_format, 1); repo_set_hash_algo(the_repository, hash_algo); - create_reference_database(the_repository->ref_storage_format, NULL, 1); + create_reference_database(NULL, 1); /* * Before fetching from the remote, download and install bundle diff --git a/setup.c b/setup.c index b723f8b33931bd..1fc9ae3872809c 100644 --- a/setup.c +++ b/setup.c @@ -2359,14 +2359,12 @@ static int is_reinit(void) return ret; } -void create_reference_database(enum ref_storage_format ref_storage_format, - const char *initial_branch, int quiet) +void create_reference_database(const char *initial_branch, int quiet) { struct strbuf err = STRBUF_INIT; char *to_free = NULL; int reinit = is_reinit(); - repo_set_ref_storage_format(the_repository, ref_storage_format); if (ref_store_create_on_disk(get_main_ref_store(the_repository), 0, &err)) die("failed to set up refs db: %s", err.buf); @@ -2701,8 +2699,7 @@ int init_db(const char *git_dir, const char *real_git_dir, &repo_fmt, init_shared_repository); if (!(flags & INIT_DB_SKIP_REFDB)) - create_reference_database(repo_fmt.ref_storage_format, - initial_branch, flags & INIT_DB_QUIET); + create_reference_database(initial_branch, flags & INIT_DB_QUIET); create_object_directory(); if (repo_settings_get_shared_repository(the_repository)) { diff --git a/setup.h b/setup.h index d55dcc66086308..ddb9f6701c2df7 100644 --- a/setup.h +++ b/setup.h @@ -240,8 +240,7 @@ int init_db(const char *git_dir, const char *real_git_dir, void initialize_repository_version(int hash_algo, enum ref_storage_format ref_storage_format, int reinit); -void create_reference_database(enum ref_storage_format ref_storage_format, - const char *initial_branch, int quiet); +void create_reference_database(const char *initial_branch, int quiet); /* * NOTE NOTE NOTE!! From 4ffbb02ee4bde38b4792b93cfba48755b394a130 Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Wed, 25 Feb 2026 10:40:42 +0100 Subject: [PATCH 45/58] refs: extract out `refs_create_refdir_stubs()` For Git to recognize a directory as a Git directory, it requires the directory to contain: 1. 'HEAD' file 2. 'objects/' directory 3. 'refs/' directory Here, #1 and #3 are part of the reference storage mechanism, specifically the files backend. Since then, newer backends such as the reftable backend have moved to using their own path ('reftable/') for storing references. But to ensure Git still recognizes the directory as a Git directory, we create stubs. There are two locations where we create stubs: - In 'refs/reftable-backend.c' when creating the reftable backend. - In 'clone.c' before spawning transport helpers. In a following commit, we'll add another instance. So instead of repeating the code, let's extract out this code to `refs_create_refdir_stubs()` and use it. Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- builtin/clone.c | 7 +------ refs.c | 23 +++++++++++++++++++++++ refs.h | 13 +++++++++++++ refs/reftable-backend.c | 14 ++------------ 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index cd43bb5aa22a76..697c5bb5cbd677 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -1225,12 +1225,7 @@ int cmd_clone(int argc, initialize_repository_version(GIT_HASH_UNKNOWN, the_repository->ref_storage_format, 1); - strbuf_addf(&buf, "%s/HEAD", git_dir); - write_file(buf.buf, "ref: refs/heads/.invalid"); - - strbuf_reset(&buf); - strbuf_addf(&buf, "%s/refs", git_dir); - safe_create_dir(the_repository, buf.buf, 1); + refs_create_refdir_stubs(the_repository, git_dir, NULL); /* * additional config can be injected with -c, make sure it's included diff --git a/refs.c b/refs.c index 627b7f8698d044..77b93d655b2773 100644 --- a/refs.c +++ b/refs.c @@ -2163,6 +2163,29 @@ const char *refs_resolve_ref_unsafe(struct ref_store *refs, return NULL; } +void refs_create_refdir_stubs(struct repository *repo, const char *refdir, + const char *refs_heads_content) +{ + struct strbuf path = STRBUF_INIT; + + strbuf_addf(&path, "%s/HEAD", refdir); + write_file(path.buf, "ref: refs/heads/.invalid"); + adjust_shared_perm(repo, path.buf); + + strbuf_reset(&path); + strbuf_addf(&path, "%s/refs", refdir); + safe_create_dir(repo, path.buf, 1); + + if (refs_heads_content) { + strbuf_reset(&path); + strbuf_addf(&path, "%s/refs/heads", refdir); + write_file(path.buf, "%s", refs_heads_content); + adjust_shared_perm(repo, path.buf); + } + + strbuf_release(&path); +} + /* backend functions */ int ref_store_create_on_disk(struct ref_store *refs, int flags, struct strbuf *err) { diff --git a/refs.h b/refs.h index f0abfa1d93633e..a35fdc66420a09 100644 --- a/refs.h +++ b/refs.h @@ -1427,4 +1427,17 @@ void ref_iterator_free(struct ref_iterator *ref_iterator); int do_for_each_ref_iterator(struct ref_iterator *iter, each_ref_fn fn, void *cb_data); +/* + * Git only recognizes a directory as a repository if it contains: + * - HEAD file + * - refs/ folder + * While it is necessary within the files backend, newer backends may not + * follow the same structure. To go around this, we create stubs as necessary. + * + * If provided with a 'refs_heads_content', we create the 'refs/heads/head' file + * with the provided message. + */ +void refs_create_refdir_stubs(struct repository *repo, const char *refdir, + const char *refs_heads_content); + #endif /* REFS_H */ diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index fe74af73afdb7a..d8651fe7794505 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -491,18 +491,8 @@ static int reftable_be_create_on_disk(struct ref_store *ref_store, safe_create_dir(the_repository, sb.buf, 1); strbuf_reset(&sb); - strbuf_addf(&sb, "%s/HEAD", refs->base.gitdir); - write_file(sb.buf, "ref: refs/heads/.invalid"); - adjust_shared_perm(the_repository, sb.buf); - strbuf_reset(&sb); - - strbuf_addf(&sb, "%s/refs", refs->base.gitdir); - safe_create_dir(the_repository, sb.buf, 1); - strbuf_reset(&sb); - - strbuf_addf(&sb, "%s/refs/heads", refs->base.gitdir); - write_file(sb.buf, "this repository uses the reftable format"); - adjust_shared_perm(the_repository, sb.buf); + refs_create_refdir_stubs(the_repository, refs->base.gitdir, + "this repository uses the reftable format"); strbuf_release(&sb); return 0; From 2a32ac429e9faaecaf1c15c18e7873da5754a8d7 Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Wed, 25 Feb 2026 10:40:43 +0100 Subject: [PATCH 46/58] refs: move out stub modification to generic layer When creating the reftable reference backend on disk, we create stubs to ensure that the directory can be recognized as a Git repository. This is done by calling `refs_create_refdir_stubs()`. Move this to the generic layer as this is needed for all backends excluding from the files backends. In an upcoming commit where we introduce alternate reference backend locations, we'll have to also create stubs in the $GIT_DIR irrespective of the backend being used. This commit builds the base to add that logic. Similarly, move the logic for deletion of stubs to the generic layer. The files backend recursively calls the remove function of the 'packed-backend', here skip calling the generic function since that would try to delete stubs. Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- refs.c | 47 +++++++++++++++++++++++++++++++++++++++-- refs/files-backend.c | 6 +++++- refs/reftable-backend.c | 27 ----------------------- 3 files changed, 50 insertions(+), 30 deletions(-) diff --git a/refs.c b/refs.c index 77b93d655b2773..c83af63dc5f7c1 100644 --- a/refs.c +++ b/refs.c @@ -2189,12 +2189,55 @@ void refs_create_refdir_stubs(struct repository *repo, const char *refdir, /* backend functions */ int ref_store_create_on_disk(struct ref_store *refs, int flags, struct strbuf *err) { - return refs->be->create_on_disk(refs, flags, err); + int ret = refs->be->create_on_disk(refs, flags, err); + + if (!ret && + ref_storage_format_by_name(refs->be->name) != REF_STORAGE_FORMAT_FILES) { + struct strbuf msg = STRBUF_INIT; + + strbuf_addf(&msg, "this repository uses the %s format", refs->be->name); + refs_create_refdir_stubs(refs->repo, refs->gitdir, msg.buf); + strbuf_release(&msg); + } + + return ret; } int ref_store_remove_on_disk(struct ref_store *refs, struct strbuf *err) { - return refs->be->remove_on_disk(refs, err); + int ret = refs->be->remove_on_disk(refs, err); + + if (!ret && + ref_storage_format_by_name(refs->be->name) != REF_STORAGE_FORMAT_FILES) { + struct strbuf sb = STRBUF_INIT; + + strbuf_addf(&sb, "%s/HEAD", refs->gitdir); + if (unlink(sb.buf) < 0) { + strbuf_addf(err, "could not delete stub HEAD: %s", + strerror(errno)); + ret = -1; + } + strbuf_reset(&sb); + + strbuf_addf(&sb, "%s/refs/heads", refs->gitdir); + if (unlink(sb.buf) < 0) { + strbuf_addf(err, "could not delete stub heads: %s", + strerror(errno)); + ret = -1; + } + strbuf_reset(&sb); + + strbuf_addf(&sb, "%s/refs", refs->gitdir); + if (rmdir(sb.buf) < 0) { + strbuf_addf(err, "could not delete refs directory: %s", + strerror(errno)); + ret = -1; + } + + strbuf_release(&sb); + } + + return ret; } int repo_resolve_gitlink_ref(struct repository *r, diff --git a/refs/files-backend.c b/refs/files-backend.c index 240d3c3b26e0b5..d3f64232610ae3 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -3700,7 +3700,11 @@ static int files_ref_store_remove_on_disk(struct ref_store *ref_store, if (for_each_root_ref(refs, remove_one_root_ref, &data) < 0) ret = -1; - if (ref_store_remove_on_disk(refs->packed_ref_store, err) < 0) + /* + * Directly access the cleanup functions for packed-refs as the generic function + * would try to clear stubs which isn't required for the files backend. + */ + if (refs->packed_ref_store->be->remove_on_disk(refs->packed_ref_store, err) < 0) ret = -1; strbuf_release(&sb); diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index d8651fe7794505..6ce7f9bb8edb98 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -491,9 +491,6 @@ static int reftable_be_create_on_disk(struct ref_store *ref_store, safe_create_dir(the_repository, sb.buf, 1); strbuf_reset(&sb); - refs_create_refdir_stubs(the_repository, refs->base.gitdir, - "this repository uses the reftable format"); - strbuf_release(&sb); return 0; } @@ -519,30 +516,6 @@ static int reftable_be_remove_on_disk(struct ref_store *ref_store, strerror(errno)); ret = -1; } - strbuf_reset(&sb); - - strbuf_addf(&sb, "%s/HEAD", refs->base.gitdir); - if (unlink(sb.buf) < 0) { - strbuf_addf(err, "could not delete stub HEAD: %s", - strerror(errno)); - ret = -1; - } - strbuf_reset(&sb); - - strbuf_addf(&sb, "%s/refs/heads", refs->base.gitdir); - if (unlink(sb.buf) < 0) { - strbuf_addf(err, "could not delete stub heads: %s", - strerror(errno)); - ret = -1; - } - strbuf_reset(&sb); - - strbuf_addf(&sb, "%s/refs", refs->base.gitdir); - if (rmdir(sb.buf) < 0) { - strbuf_addf(err, "could not delete refs directory: %s", - strerror(errno)); - ret = -1; - } strbuf_release(&sb); return ret; From d74aacd7c41573e586c1a9d7204aaaebf9901bd1 Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Wed, 25 Feb 2026 10:40:44 +0100 Subject: [PATCH 47/58] refs: receive and use the reference storage payload An upcoming commit will add support for providing an URI via the 'extensions.refStorage' config. The URI will contain the reference backend and a corresponding payload. The payload can be then used for providing an alternate locations for the reference backend. To prepare for this, modify the existing backends to accept such an argument when initializing via the 'init()' function. Both the files and reftable backends will parse the information to be filesystem paths to store references. Given that no callers pass any payload yet this is essentially a no-op change for now. To enable this, provide a 'refs_compute_filesystem_location()' function which will parse the current 'gitdir' and the 'payload' to provide the final reference directory and common reference directory (if working in a linked worktree). The documentation and tests will be added alongside the extension of the config variable. Helped-by: Patrick Steinhardt Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- refs.c | 40 +++++++++++++++++++++++++++++++++++++++- refs/files-backend.c | 17 ++++++++++++----- refs/packed-backend.c | 5 +++++ refs/packed-backend.h | 1 + refs/refs-internal.h | 14 ++++++++++++++ refs/reftable-backend.c | 24 ++++++++++++++---------- 6 files changed, 85 insertions(+), 16 deletions(-) diff --git a/refs.c b/refs.c index c83af63dc5f7c1..ba2573eb7a339a 100644 --- a/refs.c +++ b/refs.c @@ -5,6 +5,7 @@ #define USE_THE_REPOSITORY_VARIABLE #include "git-compat-util.h" +#include "abspath.h" #include "advice.h" #include "config.h" #include "environment.h" @@ -2290,7 +2291,7 @@ static struct ref_store *ref_store_init(struct repository *repo, if (!be) BUG("reference backend is unknown"); - refs = be->init(repo, gitdir, flags); + refs = be->init(repo, NULL, gitdir, flags); return refs; } @@ -3468,3 +3469,40 @@ const char *ref_transaction_error_msg(enum ref_transaction_error err) return "unknown failure"; } } + +void refs_compute_filesystem_location(const char *gitdir, const char *payload, + bool *is_worktree, struct strbuf *refdir, + struct strbuf *ref_common_dir) +{ + struct strbuf sb = STRBUF_INIT; + + *is_worktree = get_common_dir_noenv(ref_common_dir, gitdir); + + if (!payload) { + /* + * We can use the 'gitdir' as the 'refdir' without appending the + * worktree path, as the 'gitdir' here is already the worktree + * path and is different from 'commondir' denoted by 'ref_common_dir'. + */ + strbuf_addstr(refdir, gitdir); + return; + } + + if (!is_absolute_path(payload)) { + strbuf_addf(&sb, "%s/%s", ref_common_dir->buf, payload); + strbuf_realpath(ref_common_dir, sb.buf, 1); + } else { + strbuf_realpath(ref_common_dir, payload, 1); + } + + strbuf_addbuf(refdir, ref_common_dir); + + if (*is_worktree) { + const char *wt_id = strrchr(gitdir, '/'); + if (!wt_id) + BUG("worktree path does not contain slash"); + strbuf_addf(refdir, "/worktrees/%s", wt_id + 1); + } + + strbuf_release(&sb); +} diff --git a/refs/files-backend.c b/refs/files-backend.c index d3f64232610ae3..9cde3ba724f8e1 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -106,19 +106,24 @@ static void clear_loose_ref_cache(struct files_ref_store *refs) * set of caches. */ static struct ref_store *files_ref_store_init(struct repository *repo, + const char *payload, const char *gitdir, unsigned int flags) { struct files_ref_store *refs = xcalloc(1, sizeof(*refs)); struct ref_store *ref_store = (struct ref_store *)refs; - struct strbuf sb = STRBUF_INIT; + struct strbuf ref_common_dir = STRBUF_INIT; + struct strbuf refdir = STRBUF_INIT; + bool is_worktree; + + refs_compute_filesystem_location(gitdir, payload, &is_worktree, &refdir, + &ref_common_dir); - base_ref_store_init(ref_store, repo, gitdir, &refs_be_files); + base_ref_store_init(ref_store, repo, refdir.buf, &refs_be_files); refs->store_flags = flags; - get_common_dir_noenv(&sb, gitdir); - refs->gitcommondir = strbuf_detach(&sb, NULL); + refs->gitcommondir = strbuf_detach(&ref_common_dir, NULL); refs->packed_ref_store = - packed_ref_store_init(repo, refs->gitcommondir, flags); + packed_ref_store_init(repo, NULL, refs->gitcommondir, flags); refs->log_all_ref_updates = repo_settings_get_log_all_ref_updates(repo); repo_config_get_bool(repo, "core.prefersymlinkrefs", &refs->prefer_symlink_refs); @@ -126,6 +131,8 @@ static struct ref_store *files_ref_store_init(struct repository *repo, chdir_notify_reparent("files-backend $GIT_COMMONDIR", &refs->gitcommondir); + strbuf_release(&refdir); + return ref_store; } diff --git a/refs/packed-backend.c b/refs/packed-backend.c index 4ea0c1229946bd..e7bb9f10f9df1c 100644 --- a/refs/packed-backend.c +++ b/refs/packed-backend.c @@ -211,7 +211,12 @@ static size_t snapshot_hexsz(const struct snapshot *snapshot) return snapshot->refs->base.repo->hash_algo->hexsz; } +/* + * Since packed-refs is only stored in the common dir, don't parse the + * payload and rely on the files-backend to set 'gitdir' correctly. + */ struct ref_store *packed_ref_store_init(struct repository *repo, + const char *payload UNUSED, const char *gitdir, unsigned int store_flags) { diff --git a/refs/packed-backend.h b/refs/packed-backend.h index 9481d5e7c2e2f5..2c2377a35653ec 100644 --- a/refs/packed-backend.h +++ b/refs/packed-backend.h @@ -14,6 +14,7 @@ struct ref_transaction; */ struct ref_store *packed_ref_store_init(struct repository *repo, + const char *payload, const char *gitdir, unsigned int store_flags); diff --git a/refs/refs-internal.h b/refs/refs-internal.h index c7d2a6e50b7696..4fb8fdb872f442 100644 --- a/refs/refs-internal.h +++ b/refs/refs-internal.h @@ -389,6 +389,7 @@ struct ref_store; * the ref_store and to record the ref_store for later lookup. */ typedef struct ref_store *ref_store_init_fn(struct repository *repo, + const char *payload, const char *gitdir, unsigned int flags); /* @@ -666,4 +667,17 @@ enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs unsigned int initial_transaction, struct strbuf *err); +/* + * Given a gitdir and the reference storage payload provided, retrieve the + * 'refdir' and 'ref_common_dir'. The former is where references should be + * stored for the current worktree, the latter is the common reference + * directory if working with a linked worktree. If working with the main + * worktree, both values will be the same. + * + * This is used by backends that store references in the repository directly. + */ +void refs_compute_filesystem_location(const char *gitdir, const char *payload, + bool *is_worktree, struct strbuf *refdir, + struct strbuf *ref_common_dir); + #endif /* REFS_REFS_INTERNAL_H */ diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index 6ce7f9bb8edb98..0e220d6bb53aad 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -372,18 +372,24 @@ static int reftable_be_fsync(int fd) } static struct ref_store *reftable_be_init(struct repository *repo, + const char *payload, const char *gitdir, unsigned int store_flags) { struct reftable_ref_store *refs = xcalloc(1, sizeof(*refs)); + struct strbuf ref_common_dir = STRBUF_INIT; + struct strbuf refdir = STRBUF_INIT; struct strbuf path = STRBUF_INIT; - int is_worktree; + bool is_worktree; mode_t mask; mask = umask(0); umask(mask); - base_ref_store_init(&refs->base, repo, gitdir, &refs_be_reftable); + refs_compute_filesystem_location(gitdir, payload, &is_worktree, &refdir, + &ref_common_dir); + + base_ref_store_init(&refs->base, repo, refdir.buf, &refs_be_reftable); strmap_init(&refs->worktree_backends); refs->store_flags = store_flags; refs->log_all_ref_updates = repo_settings_get_log_all_ref_updates(repo); @@ -419,14 +425,11 @@ static struct ref_store *reftable_be_init(struct repository *repo, /* * Set up the main reftable stack that is hosted in GIT_COMMON_DIR. * This stack contains both the shared and the main worktree refs. - * - * Note that we don't try to resolve the path in case we have a - * worktree because `get_common_dir_noenv()` already does it for us. */ - is_worktree = get_common_dir_noenv(&path, gitdir); + strbuf_addbuf(&path, &ref_common_dir); if (!is_worktree) { strbuf_reset(&path); - strbuf_realpath(&path, gitdir, 0); + strbuf_realpath(&path, ref_common_dir.buf, 0); } strbuf_addstr(&path, "/reftable"); refs->err = reftable_backend_init(&refs->main_backend, path.buf, @@ -443,10 +446,9 @@ static struct ref_store *reftable_be_init(struct repository *repo, * do it efficiently. */ if (is_worktree) { - strbuf_reset(&path); - strbuf_addf(&path, "%s/reftable", gitdir); + strbuf_addstr(&refdir, "/reftable"); - refs->err = reftable_backend_init(&refs->worktree_backend, path.buf, + refs->err = reftable_backend_init(&refs->worktree_backend, refdir.buf, &refs->write_options); if (refs->err) goto done; @@ -456,6 +458,8 @@ static struct ref_store *reftable_be_init(struct repository *repo, done: assert(refs->err != REFTABLE_API_ERROR); + strbuf_release(&ref_common_dir); + strbuf_release(&refdir); strbuf_release(&path); return &refs->base; } From 01dc84594ee365ee7086fccc7f590ab527730531 Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Wed, 25 Feb 2026 10:40:45 +0100 Subject: [PATCH 48/58] refs: allow reference location in refstorage config The 'extensions.refStorage' config is used to specify the reference backend for a given repository. Both the 'files' and 'reftable' backends utilize the $GIT_DIR as the reference folder by default in `get_main_ref_store()`. Since the reference backends are pluggable, this means that they could work with out-of-tree reference directories too. Extend the 'refStorage' config to also support taking an URI input, where users can specify the reference backend and the location. Add the required changes to obtain and propagate this value to the individual backends. Add the necessary documentation and tests. Traditionally, for linked worktrees, references were stored in the '$GIT_DIR/worktrees/' path. But when using an alternate reference storage path, it doesn't make sense to store the main worktree references in the new path, and the linked worktree references in the $GIT_DIR. So, let's store linked worktree references in '$ALTERNATE_REFERENCE_DIR/worktrees/'. To do this, create the necessary files and folders while also adding stubs in the $GIT_DIR path to ensure that it is still considered a Git directory. Ideally, we would want to pass in a `struct worktree *` to individual backends, instead of passing the `gitdir`. This allows them to handle worktree specific logic. Currently, that is not possible since the worktree code is: - Tied to using the global `the_repository` variable. - Is not setup before the reference database during initialization of the repository. Add a TODO in 'refs.c' to ensure we can eventually make that change. Helped-by: Patrick Steinhardt Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- Documentation/config/extensions.adoc | 16 ++- builtin/worktree.c | 34 ++++++ refs.c | 6 +- repository.c | 9 +- repository.h | 8 +- setup.c | 34 +++++- setup.h | 1 + t/meson.build | 1 + t/t1423-ref-backend.sh | 159 +++++++++++++++++++++++++++ 9 files changed, 259 insertions(+), 9 deletions(-) create mode 100755 t/t1423-ref-backend.sh diff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc index 532456644b770e..329d02b3c4dd04 100644 --- a/Documentation/config/extensions.adoc +++ b/Documentation/config/extensions.adoc @@ -57,10 +57,24 @@ For historical reasons, this extension is respected regardless of the `core.repositoryFormatVersion` setting. refStorage::: - Specify the ref storage format to use. The acceptable values are: + Specify the ref storage format and a corresponding payload. The value + can be either a format name or a URI: + -- +* A format name alone (e.g., `reftable` or `files`). + +* A URI format `://` explicitly specifies both the + format and payload (e.g., `reftable:///foo/bar`). + +Supported format names are: + include::../ref-storage-format.adoc[] + +The payload is passed directly to the reference backend. For the files and +reftable backends, this must be a filesystem path where the references will +be stored. Defaulting to the commondir when no payload is provided. Relative +paths are resolved relative to the `$GIT_DIR`. Future backends may support +other payload schemes, e.g., postgres://127.0.0.1:5432?database=myrepo. -- + Note that this setting should only be set by linkgit:git-init[1] or diff --git a/builtin/worktree.c b/builtin/worktree.c index fbdaf2eb2eb85c..293e80837987b4 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -425,6 +425,39 @@ static int make_worktree_orphan(const char * ref, const struct add_opts *opts, return run_command(&cp); } +/* + * References for worktrees are generally stored in '$GIT_DIR/worktrees/'. + * But when using alternate reference directories, we want to store the worktree + * references in '$ALTERNATE_REFERENCE_DIR/worktrees/'. + * + * Create the necessary folder structure to facilitate the same. But to ensure + * that the former path is still considered a Git directory, add stubs. + */ +static void setup_alternate_ref_dir(struct worktree *wt, const char *wt_git_path) +{ + struct strbuf sb = STRBUF_INIT; + char *path; + + path = wt->repo->ref_storage_payload; + if (!path) + return; + + if (!is_absolute_path(path)) + strbuf_addf(&sb, "%s/", wt->repo->commondir); + + strbuf_addf(&sb, "%s/worktrees", path); + safe_create_dir(wt->repo, sb.buf, 1); + strbuf_addf(&sb, "/%s", wt->id); + safe_create_dir(wt->repo, sb.buf, 1); + strbuf_reset(&sb); + + strbuf_addf(&sb, "this worktree stores references in %s/worktrees/%s", + path, wt->id); + refs_create_refdir_stubs(wt->repo, wt_git_path, sb.buf); + + strbuf_release(&sb); +} + static int add_worktree(const char *path, const char *refname, const struct add_opts *opts) { @@ -518,6 +551,7 @@ static int add_worktree(const char *path, const char *refname, ret = error(_("could not find created worktree '%s'"), name); goto done; } + setup_alternate_ref_dir(wt, sb_repo.buf); wt_refs = get_worktree_ref_store(wt); ret = ref_store_create_on_disk(wt_refs, REF_STORE_CREATE_ON_DISK_IS_WORKTREE, &sb); diff --git a/refs.c b/refs.c index ba2573eb7a339a..ef1902e85c707d 100644 --- a/refs.c +++ b/refs.c @@ -2291,7 +2291,11 @@ static struct ref_store *ref_store_init(struct repository *repo, if (!be) BUG("reference backend is unknown"); - refs = be->init(repo, NULL, gitdir, flags); + /* + * TODO Send in a 'struct worktree' instead of a 'gitdir', and + * allow the backend to handle how it wants to deal with worktrees. + */ + refs = be->init(repo, repo->ref_storage_payload, gitdir, flags); return refs; } diff --git a/repository.c b/repository.c index c7e75215ac2ab2..9815f081efa0aa 100644 --- a/repository.c +++ b/repository.c @@ -193,9 +193,12 @@ void repo_set_compat_hash_algo(struct repository *repo, int algo) } void repo_set_ref_storage_format(struct repository *repo, - enum ref_storage_format format) + enum ref_storage_format format, + const char *payload) { repo->ref_storage_format = format; + free(repo->ref_storage_payload); + repo->ref_storage_payload = xstrdup_or_null(payload); } /* @@ -277,7 +280,8 @@ int repo_init(struct repository *repo, repo_set_hash_algo(repo, format.hash_algo); repo_set_compat_hash_algo(repo, format.compat_hash_algo); - repo_set_ref_storage_format(repo, format.ref_storage_format); + repo_set_ref_storage_format(repo, format.ref_storage_format, + format.ref_storage_payload); repo->repository_format_worktree_config = format.worktree_config; repo->repository_format_relative_worktrees = format.relative_worktrees; repo->repository_format_precious_objects = format.precious_objects; @@ -369,6 +373,7 @@ void repo_clear(struct repository *repo) FREE_AND_NULL(repo->index_file); FREE_AND_NULL(repo->worktree); FREE_AND_NULL(repo->submodule_prefix); + FREE_AND_NULL(repo->ref_storage_payload); odb_free(repo->objects); repo->objects = NULL; diff --git a/repository.h b/repository.h index 6063c4b846d031..95e2333bad9463 100644 --- a/repository.h +++ b/repository.h @@ -150,6 +150,11 @@ struct repository { /* Repository's reference storage format, as serialized on disk. */ enum ref_storage_format ref_storage_format; + /* + * Reference storage information as needed for the backend. This contains + * only the payload from the reference URI without the schema. + */ + char *ref_storage_payload; /* A unique-id for tracing purposes. */ int trace2_repo_id; @@ -204,7 +209,8 @@ void repo_set_worktree(struct repository *repo, const char *path); void repo_set_hash_algo(struct repository *repo, int algo); void repo_set_compat_hash_algo(struct repository *repo, int compat_algo); void repo_set_ref_storage_format(struct repository *repo, - enum ref_storage_format format); + enum ref_storage_format format, + const char *payload); void initialize_repository(struct repository *repo); RESULT_MUST_BE_USED int repo_init(struct repository *r, const char *gitdir, const char *worktree); diff --git a/setup.c b/setup.c index 1fc9ae3872809c..d407f3347b974e 100644 --- a/setup.c +++ b/setup.c @@ -632,6 +632,21 @@ static enum extension_result handle_extension_v0(const char *var, return EXTENSION_UNKNOWN; } +static void parse_reference_uri(const char *value, char **format, + char **payload) +{ + const char *schema_end; + + schema_end = strstr(value, "://"); + if (!schema_end) { + *format = xstrdup(value); + *payload = NULL; + } else { + *format = xstrndup(value, schema_end - value); + *payload = xstrdup_or_null(schema_end + 3); + } +} + /* * Record any new extensions in this function. */ @@ -674,10 +689,17 @@ static enum extension_result handle_extension(const char *var, return EXTENSION_OK; } else if (!strcmp(ext, "refstorage")) { unsigned int format; + char *format_str; if (!value) return config_error_nonbool(var); - format = ref_storage_format_by_name(value); + + parse_reference_uri(value, &format_str, + &data->ref_storage_payload); + + format = ref_storage_format_by_name(format_str); + free(format_str); + if (format == REF_STORAGE_FORMAT_UNKNOWN) return error(_("invalid value for '%s': '%s'"), "extensions.refstorage", value); @@ -850,6 +872,7 @@ void clear_repository_format(struct repository_format *format) string_list_clear(&format->v1_only_extensions, 0); free(format->work_tree); free(format->partial_clone); + free(format->ref_storage_payload); init_repository_format(format); } @@ -1942,7 +1965,8 @@ const char *setup_git_directory_gently(int *nongit_ok) repo_set_compat_hash_algo(the_repository, repo_fmt.compat_hash_algo); repo_set_ref_storage_format(the_repository, - repo_fmt.ref_storage_format); + repo_fmt.ref_storage_format, + repo_fmt.ref_storage_payload); the_repository->repository_format_worktree_config = repo_fmt.worktree_config; the_repository->repository_format_relative_worktrees = @@ -2042,7 +2066,8 @@ void check_repository_format(struct repository_format *fmt) repo_set_hash_algo(the_repository, fmt->hash_algo); repo_set_compat_hash_algo(the_repository, fmt->compat_hash_algo); repo_set_ref_storage_format(the_repository, - fmt->ref_storage_format); + fmt->ref_storage_format, + fmt->ref_storage_payload); the_repository->repository_format_worktree_config = fmt->worktree_config; the_repository->repository_format_relative_worktrees = @@ -2643,7 +2668,8 @@ static void repository_format_configure(struct repository_format *repo_fmt, } else { repo_fmt->ref_storage_format = REF_STORAGE_FORMAT_DEFAULT; } - repo_set_ref_storage_format(the_repository, repo_fmt->ref_storage_format); + repo_set_ref_storage_format(the_repository, repo_fmt->ref_storage_format, + repo_fmt->ref_storage_payload); } int init_db(const char *git_dir, const char *real_git_dir, diff --git a/setup.h b/setup.h index ddb9f6701c2df7..093af39e844def 100644 --- a/setup.h +++ b/setup.h @@ -171,6 +171,7 @@ struct repository_format { int hash_algo; int compat_hash_algo; enum ref_storage_format ref_storage_format; + char *ref_storage_payload; int sparse_index; char *work_tree; struct string_list unknown_extensions; diff --git a/t/meson.build b/t/meson.build index 459c52a48972e4..11fc5a49ee2228 100644 --- a/t/meson.build +++ b/t/meson.build @@ -210,6 +210,7 @@ integration_tests = [ 't1420-lost-found.sh', 't1421-reflog-write.sh', 't1422-show-ref-exists.sh', + 't1423-ref-backend.sh', 't1430-bad-ref-name.sh', 't1450-fsck.sh', 't1451-fsck-buffer.sh', diff --git a/t/t1423-ref-backend.sh b/t/t1423-ref-backend.sh new file mode 100755 index 00000000000000..82cccb7a65f93c --- /dev/null +++ b/t/t1423-ref-backend.sh @@ -0,0 +1,159 @@ +#!/bin/sh + +test_description='Test reference backend URIs' + +. ./test-lib.sh + +# Run a git command with the provided reference storage. Reset the backend +# post running the command. +# Usage: run_with_uri +# is the relative path to the repo to run the command in. +# is the original ref storage of the repo. +# is the new URI to be set for the ref storage. +# is the git subcommand to be run in the repository. +run_with_uri () { + repo=$1 && + backend=$2 && + uri=$3 && + cmd=$4 && + + git -C "$repo" config set core.repositoryformatversion 1 + git -C "$repo" config set extensions.refStorage "$uri" && + git -C "$repo" $cmd && + git -C "$repo" config set extensions.refStorage "$backend" +} + +# Test a repository with a given reference storage by running and comparing +# 'git refs list' before and after setting the new reference backend. If +# err_msg is set, expect the command to fail and grep for the provided err_msg. +# Usage: run_with_uri +# is the relative path to the repo to run the command in. +# is the original ref storage of the repo. +# is the new URI to be set for the ref storage. +# (optional) if set, check if 'git-refs(1)' failed with the provided msg. +test_refs_backend () { + repo=$1 && + backend=$2 && + uri=$3 && + err_msg=$4 && + + git -C "$repo" config set core.repositoryformatversion 1 && + if test -n "$err_msg"; + then + git -C "$repo" config set extensions.refStorage "$uri" && + test_must_fail git -C "$repo" refs list 2>err && + test_grep "$err_msg" err + else + git -C "$repo" refs list >expect && + run_with_uri "$repo" "$backend" "$uri" "refs list" >actual && + test_cmp expect actual + fi +} + +test_expect_success 'URI is invalid' ' + test_when_finished "rm -rf repo" && + git init repo && + test_refs_backend repo files "reftable@/home/reftable" \ + "invalid value for ${SQ}extensions.refstorage${SQ}" +' + +test_expect_success 'URI ends with colon' ' + test_when_finished "rm -rf repo" && + git init repo && + test_refs_backend repo files "reftable:" \ + "invalid value for ${SQ}extensions.refstorage${SQ}" +' + +test_expect_success 'unknown reference backend' ' + test_when_finished "rm -rf repo" && + git init repo && + test_refs_backend repo files "db://.git" \ + "invalid value for ${SQ}extensions.refstorage${SQ}" +' + +ref_formats="files reftable" +for from_format in $ref_formats +do + +for to_format in $ref_formats +do + if test "$from_format" = "$to_format" + then + continue + fi + + + for dir in "$(pwd)/repo/.git" "." + do + + test_expect_success "read from $to_format backend, $dir dir" ' + test_when_finished "rm -rf repo" && + git init --ref-format=$from_format repo && + ( + cd repo && + test_commit 1 && + test_commit 2 && + test_commit 3 && + + git refs migrate --dry-run --ref-format=$to_format >out && + BACKEND_PATH="$dir/$(sed "s/.* ${SQ}.git\/\(.*\)${SQ}/\1/" out)" && + test_refs_backend . $from_format "$to_format://$BACKEND_PATH" "$method" + ) + ' + + test_expect_success "write to $to_format backend, $dir dir" ' + test_when_finished "rm -rf repo" && + git init --ref-format=$from_format repo && + ( + cd repo && + test_commit 1 && + test_commit 2 && + test_commit 3 && + + git refs migrate --dry-run --ref-format=$to_format >out && + BACKEND_PATH="$dir/$(sed "s/.* ${SQ}.git\/\(.*\)${SQ}/\1/" out)" && + + test_refs_backend . $from_format "$to_format://$BACKEND_PATH" && + + git refs list >expect && + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" "tag -d 1" && + git refs list >actual && + test_cmp expect actual && + + git refs list | grep -v "refs/tags/1" >expect && + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" "refs list" >actual && + test_cmp expect actual + ) + ' + + test_expect_success "with worktree and $to_format backend, $dir dir" ' + test_when_finished "rm -rf repo wt" && + git init --ref-format=$from_format repo && + ( + cd repo && + test_commit 1 && + test_commit 2 && + test_commit 3 && + + git refs migrate --dry-run --ref-format=$to_format >out && + BACKEND_PATH="$dir/$(sed "s/.* ${SQ}.git\/\(.*\)${SQ}/\1/" out)" && + + git config set core.repositoryformatversion 1 && + git config set extensions.refStorage "$to_format://$BACKEND_PATH" && + + git worktree add ../wt 2 + ) && + + git -C repo for-each-ref --include-root-refs >expect && + git -C wt for-each-ref --include-root-refs >expect && + ! test_cmp expect actual && + + git -C wt rev-parse 2 >expect && + git -C wt rev-parse HEAD >actual && + test_cmp expect actual + ' + done # closes dir +done # closes to_format +done # closes from_format + +test_done From 53592d68e86814fcc4a8df6cc38340597e56fe5a Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Wed, 25 Feb 2026 10:40:46 +0100 Subject: [PATCH 49/58] refs: add GIT_REFERENCE_BACKEND to specify reference backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Git allows setting a different object directory via 'GIT_OBJECT_DIRECTORY', but provides no equivalent for references. In the previous commit we extended the 'extensions.refStorage' config to also support an URI input for reference backend with location. Let's also add a new environment variable 'GIT_REFERENCE_BACKEND' that takes in the same input as the config variable. Having an environment variable allows us to modify the reference backend and location on the fly for individual Git commands. The environment variable also allows usage of alternate reference directories during 'git-clone(1)' and 'git-init(1)'. Add the config to the repository when created with the environment variable set. When initializing the repository with an alternate reference folder, create the required stubs in the repositories $GIT_DIR. The inverse, i.e. removal of the ref store doesn't clean up the stubs in the $GIT_DIR since that would render it unusable. Removal of ref store is only used when migrating between ref formats and cleanup of the $GIT_DIR doesn't make sense in such a situation. Helped-by: Jean-Noël Avila Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- Documentation/git.adoc | 5 ++ environment.h | 1 + refs.c | 30 +++++-- setup.c | 55 +++++++++++- t/t1423-ref-backend.sh | 187 +++++++++++++++++++++++++++++++++-------- 5 files changed, 233 insertions(+), 45 deletions(-) diff --git a/Documentation/git.adoc b/Documentation/git.adoc index ce099e78b8023e..66442735ea61e0 100644 --- a/Documentation/git.adoc +++ b/Documentation/git.adoc @@ -584,6 +584,11 @@ double-quotes and respecting backslash escapes. E.g., the value repositories will be set to this value. The default is "files". See `--ref-format` in linkgit:git-init[1]. +`GIT_REFERENCE_BACKEND`:: + Specify which reference backend to be used along with its URI. + See `extensions.refStorage` option in linkgit:git-config[1] for more + details. Overrides the config variable when used. + Git Commits ~~~~~~~~~~~ `GIT_AUTHOR_NAME`:: diff --git a/environment.h b/environment.h index 27f657af046a51..540e0a7f6dd51f 100644 --- a/environment.h +++ b/environment.h @@ -42,6 +42,7 @@ #define GIT_OPTIONAL_LOCKS_ENVIRONMENT "GIT_OPTIONAL_LOCKS" #define GIT_TEXT_DOMAIN_DIR_ENVIRONMENT "GIT_TEXTDOMAINDIR" #define GIT_ATTR_SOURCE_ENVIRONMENT "GIT_ATTR_SOURCE" +#define GIT_REFERENCE_BACKEND_ENVIRONMENT "GIT_REFERENCE_BACKEND" /* * Environment variable used to propagate the --no-advice global option to the diff --git a/refs.c b/refs.c index ef1902e85c707d..a700a66f0860e4 100644 --- a/refs.c +++ b/refs.c @@ -2192,13 +2192,17 @@ int ref_store_create_on_disk(struct ref_store *refs, int flags, struct strbuf *e { int ret = refs->be->create_on_disk(refs, flags, err); - if (!ret && - ref_storage_format_by_name(refs->be->name) != REF_STORAGE_FORMAT_FILES) { - struct strbuf msg = STRBUF_INIT; - - strbuf_addf(&msg, "this repository uses the %s format", refs->be->name); - refs_create_refdir_stubs(refs->repo, refs->gitdir, msg.buf); - strbuf_release(&msg); + if (!ret) { + /* Creation of stubs for linked worktrees are handled in the worktree code. */ + if (!(flags & REF_STORE_CREATE_ON_DISK_IS_WORKTREE) && refs->repo->ref_storage_payload) { + refs_create_refdir_stubs(refs->repo, refs->repo->gitdir, + "repository uses alternate refs storage"); + } else if (ref_storage_format_by_name(refs->be->name) != REF_STORAGE_FORMAT_FILES) { + struct strbuf msg = STRBUF_INIT; + strbuf_addf(&msg, "this repository uses the %s format", refs->be->name); + refs_create_refdir_stubs(refs->repo, refs->gitdir, msg.buf); + strbuf_release(&msg); + } } return ret; @@ -2208,10 +2212,18 @@ int ref_store_remove_on_disk(struct ref_store *refs, struct strbuf *err) { int ret = refs->be->remove_on_disk(refs, err); - if (!ret && - ref_storage_format_by_name(refs->be->name) != REF_STORAGE_FORMAT_FILES) { + if (!ret) { + enum ref_storage_format format = ref_storage_format_by_name(refs->be->name); struct strbuf sb = STRBUF_INIT; + /* Backends apart from the files backend create stubs. */ + if (format == REF_STORAGE_FORMAT_FILES) + return ret; + + /* Alternate refs backend require stubs in the gitdir. */ + if (refs->repo->ref_storage_payload) + return ret; + strbuf_addf(&sb, "%s/HEAD", refs->gitdir); if (unlink(sb.buf) < 0) { strbuf_addf(err, "could not delete stub HEAD: %s", diff --git a/setup.c b/setup.c index d407f3347b974e..90cb9be578974a 100644 --- a/setup.c +++ b/setup.c @@ -1838,6 +1838,7 @@ const char *setup_git_directory_gently(int *nongit_ok) static struct strbuf cwd = STRBUF_INIT; struct strbuf dir = STRBUF_INIT, gitdir = STRBUF_INIT, report = STRBUF_INIT; const char *prefix = NULL; + const char *ref_backend_uri; struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT; /* @@ -1995,6 +1996,25 @@ const char *setup_git_directory_gently(int *nongit_ok) setenv(GIT_PREFIX_ENVIRONMENT, "", 1); } + /* + * The env variable should override the repository config + * for 'extensions.refStorage'. + */ + ref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT); + if (ref_backend_uri) { + char *backend, *payload; + enum ref_storage_format format; + + parse_reference_uri(ref_backend_uri, &backend, &payload); + format = ref_storage_format_by_name(backend); + if (format == REF_STORAGE_FORMAT_UNKNOWN) + die(_("unknown ref storage format: '%s'"), backend); + repo_set_ref_storage_format(the_repository, format, payload); + + free(backend); + free(payload); + } + setup_original_cwd(); strbuf_release(&dir); @@ -2337,7 +2357,8 @@ void initialize_repository_version(int hash_algo, * the remote repository's format. */ if (hash_algo != GIT_HASH_SHA1_LEGACY || - ref_storage_format != REF_STORAGE_FORMAT_FILES) + ref_storage_format != REF_STORAGE_FORMAT_FILES || + the_repository->ref_storage_payload) target_version = GIT_REPO_VERSION_READ; if (hash_algo != GIT_HASH_SHA1_LEGACY && hash_algo != GIT_HASH_UNKNOWN) @@ -2346,11 +2367,20 @@ void initialize_repository_version(int hash_algo, else if (reinit) repo_config_set_gently(the_repository, "extensions.objectformat", NULL); - if (ref_storage_format != REF_STORAGE_FORMAT_FILES) + if (the_repository->ref_storage_payload) { + struct strbuf ref_uri = STRBUF_INIT; + + strbuf_addf(&ref_uri, "%s://%s", + ref_storage_format_to_name(ref_storage_format), + the_repository->ref_storage_payload); + repo_config_set(the_repository, "extensions.refstorage", ref_uri.buf); + strbuf_release(&ref_uri); + } else if (ref_storage_format != REF_STORAGE_FORMAT_FILES) { repo_config_set(the_repository, "extensions.refstorage", ref_storage_format_to_name(ref_storage_format)); - else if (reinit) + } else if (reinit) { repo_config_set_gently(the_repository, "extensions.refstorage", NULL); + } if (reinit) { struct strbuf config = STRBUF_INIT; @@ -2623,6 +2653,7 @@ static void repository_format_configure(struct repository_format *repo_fmt, .ignore_repo = 1, .ignore_worktree = 1, }; + const char *ref_backend_uri; const char *env; config_with_options(read_default_format_config, &cfg, NULL, NULL, &opts); @@ -2668,6 +2699,24 @@ static void repository_format_configure(struct repository_format *repo_fmt, } else { repo_fmt->ref_storage_format = REF_STORAGE_FORMAT_DEFAULT; } + + + ref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT); + if (ref_backend_uri) { + char *backend, *payload; + enum ref_storage_format format; + + parse_reference_uri(ref_backend_uri, &backend, &payload); + format = ref_storage_format_by_name(backend); + if (format == REF_STORAGE_FORMAT_UNKNOWN) + die(_("unknown ref storage format: '%s'"), backend); + + repo_fmt->ref_storage_format = format; + repo_fmt->ref_storage_payload = payload; + + free(backend); + } + repo_set_ref_storage_format(the_repository, repo_fmt->ref_storage_format, repo_fmt->ref_storage_payload); } diff --git a/t/t1423-ref-backend.sh b/t/t1423-ref-backend.sh index 82cccb7a65f93c..fd47d77e8e2904 100755 --- a/t/t1423-ref-backend.sh +++ b/t/t1423-ref-backend.sh @@ -11,16 +11,25 @@ test_description='Test reference backend URIs' # is the original ref storage of the repo. # is the new URI to be set for the ref storage. # is the git subcommand to be run in the repository. +# if 'config', set the backend via the 'extensions.refStorage' config. +# if 'env', set the backend via the 'GIT_REFERENCE_BACKEND' env. run_with_uri () { repo=$1 && backend=$2 && uri=$3 && cmd=$4 && + via=$5 && - git -C "$repo" config set core.repositoryformatversion 1 - git -C "$repo" config set extensions.refStorage "$uri" && - git -C "$repo" $cmd && - git -C "$repo" config set extensions.refStorage "$backend" + git -C "$repo" config set core.repositoryformatversion 1 && + if test "$via" = "env" + then + test_env GIT_REFERENCE_BACKEND="$uri" git -C "$repo" $cmd + elif test "$via" = "config" + then + git -C "$repo" config set extensions.refStorage "$uri" && + git -C "$repo" $cmd && + git -C "$repo" config set extensions.refStorage "$backend" + fi } # Test a repository with a given reference storage by running and comparing @@ -30,44 +39,84 @@ run_with_uri () { # is the relative path to the repo to run the command in. # is the original ref storage of the repo. # is the new URI to be set for the ref storage. +# if 'config', set the backend via the 'extensions.refStorage' config. +# if 'env', set the backend via the 'GIT_REFERENCE_BACKEND' env. # (optional) if set, check if 'git-refs(1)' failed with the provided msg. test_refs_backend () { repo=$1 && backend=$2 && uri=$3 && - err_msg=$4 && + via=$4 && + err_msg=$5 && + - git -C "$repo" config set core.repositoryformatversion 1 && if test -n "$err_msg"; then - git -C "$repo" config set extensions.refStorage "$uri" && - test_must_fail git -C "$repo" refs list 2>err && - test_grep "$err_msg" err + if test "$via" = "env" + then + test_env GIT_REFERENCE_BACKEND="$uri" test_must_fail git -C "$repo" refs list 2>err + elif test "$via" = "config" + then + git -C "$repo" config set extensions.refStorage "$uri" && + test_must_fail git -C "$repo" refs list 2>err && + test_grep "$err_msg" err + fi else git -C "$repo" refs list >expect && - run_with_uri "$repo" "$backend" "$uri" "refs list" >actual && + run_with_uri "$repo" "$backend" "$uri" "refs list" "$via">actual && test_cmp expect actual fi } -test_expect_success 'URI is invalid' ' +# Verify that the expected files are present in the gitdir and the refsdir. +# Usage: verify_files_exist +# is the path for the gitdir. +# is the path for the refdir. +verify_files_exist () { + gitdir=$1 && + refdir=$2 && + + # verify that the stubs were added to the $GITDIR. + echo "repository uses alternate refs storage" >expect && + test_cmp expect $gitdir/refs/heads && + echo "ref: refs/heads/.invalid" >expect && + test_cmp expect $gitdir/HEAD + + # verify that backend specific files exist. + case "$GIT_DEFAULT_REF_FORMAT" in + files) + test_path_is_dir $refdir/refs/heads && + test_path_is_file $refdir/HEAD;; + reftable) + test_path_is_dir $refdir/reftable && + test_path_is_file $refdir/reftable/tables.list;; + *) + BUG "unhandled ref format $GIT_DEFAULT_REF_FORMAT";; + esac +} + +methods="config env" +for method in $methods +do + +test_expect_success "$method: URI is invalid" ' test_when_finished "rm -rf repo" && git init repo && - test_refs_backend repo files "reftable@/home/reftable" \ + test_refs_backend repo files "reftable@/home/reftable" "$method" \ "invalid value for ${SQ}extensions.refstorage${SQ}" ' -test_expect_success 'URI ends with colon' ' +test_expect_success "$method: URI ends with colon" ' test_when_finished "rm -rf repo" && git init repo && - test_refs_backend repo files "reftable:" \ + test_refs_backend repo files "reftable:" "$method" \ "invalid value for ${SQ}extensions.refstorage${SQ}" ' -test_expect_success 'unknown reference backend' ' +test_expect_success "$method: unknown reference backend" ' test_when_finished "rm -rf repo" && git init repo && - test_refs_backend repo files "db://.git" \ + test_refs_backend repo files "db://.git" "$method" \ "invalid value for ${SQ}extensions.refstorage${SQ}" ' @@ -86,7 +135,7 @@ do for dir in "$(pwd)/repo/.git" "." do - test_expect_success "read from $to_format backend, $dir dir" ' + test_expect_success "$method: read from $to_format backend, $dir dir" ' test_when_finished "rm -rf repo" && git init --ref-format=$from_format repo && ( @@ -101,7 +150,7 @@ do ) ' - test_expect_success "write to $to_format backend, $dir dir" ' + test_expect_success "$method: write to $to_format backend, $dir dir" ' test_when_finished "rm -rf repo" && git init --ref-format=$from_format repo && ( @@ -113,20 +162,22 @@ do git refs migrate --dry-run --ref-format=$to_format >out && BACKEND_PATH="$dir/$(sed "s/.* ${SQ}.git\/\(.*\)${SQ}/\1/" out)" && - test_refs_backend . $from_format "$to_format://$BACKEND_PATH" && + test_refs_backend . $from_format "$to_format://$BACKEND_PATH" "$method" && git refs list >expect && - run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" "tag -d 1" && + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \ + "tag -d 1" "$method" && git refs list >actual && test_cmp expect actual && git refs list | grep -v "refs/tags/1" >expect && - run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" "refs list" >actual && + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \ + "refs list" "$method" >actual && test_cmp expect actual ) ' - test_expect_success "with worktree and $to_format backend, $dir dir" ' + test_expect_success "$method: with worktree and $to_format backend, $dir dir" ' test_when_finished "rm -rf repo wt" && git init --ref-format=$from_format repo && ( @@ -138,22 +189,92 @@ do git refs migrate --dry-run --ref-format=$to_format >out && BACKEND_PATH="$dir/$(sed "s/.* ${SQ}.git\/\(.*\)${SQ}/\1/" out)" && - git config set core.repositoryformatversion 1 && - git config set extensions.refStorage "$to_format://$BACKEND_PATH" && - - git worktree add ../wt 2 - ) && + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \ + "worktree add ../wt 2" "$method" && - git -C repo for-each-ref --include-root-refs >expect && - git -C wt for-each-ref --include-root-refs >expect && - ! test_cmp expect actual && + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \ + "for-each-ref --include-root-refs" "$method" >actual && + run_with_uri ../wt "$from_format" "$to_format://$BACKEND_PATH" \ + "for-each-ref --include-root-refs" "$method" >expect && + ! test_cmp expect actual && - git -C wt rev-parse 2 >expect && - git -C wt rev-parse HEAD >actual && - test_cmp expect actual + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \ + "rev-parse 2" "$method" >actual && + run_with_uri ../wt "$from_format" "$to_format://$BACKEND_PATH" \ + "rev-parse HEAD" "$method" >expect && + test_cmp expect actual + ) ' done # closes dir + + test_expect_success "migrating repository to $to_format with alternate refs directory" ' + test_when_finished "rm -rf repo refdir" && + mkdir refdir && + GIT_REFERENCE_BACKEND="${from_format}://$(pwd)/refdir" git init repo && + ( + cd repo && + + test_commit 1 && + test_commit 2 && + test_commit 3 && + + git refs migrate --ref-format=$to_format && + git refs list >out && + test_grep "refs/tags/1" out && + test_grep "refs/tags/2" out && + test_grep "refs/tags/3" out + ) + ' + done # closes to_format done # closes from_format +done # closes method + +test_expect_success 'initializing repository with alt ref directory' ' + test_when_finished "rm -rf repo refdir" && + mkdir refdir && + BACKEND="$(test_detect_ref_format)://$(pwd)/refdir" && + GIT_REFERENCE_BACKEND=$BACKEND git init repo && + verify_files_exist repo/.git refdir && + ( + cd repo && + + git config get extensions.refstorage >actual && + echo $BACKEND >expect && + test_cmp expect actual && + + test_commit 1 && + test_commit 2 && + test_commit 3 && + git refs list >out && + test_grep "refs/tags/1" out && + test_grep "refs/tags/2" out && + test_grep "refs/tags/3" out + ) +' + +test_expect_success 'cloning repository with alt ref directory' ' + test_when_finished "rm -rf source repo refdir" && + mkdir refdir && + + git init source && + test_commit -C source 1 && + test_commit -C source 2 && + test_commit -C source 3 && + + BACKEND="$(test_detect_ref_format)://$(pwd)/refdir" && + GIT_REFERENCE_BACKEND=$BACKEND git clone source repo && + + git -C repo config get extensions.refstorage >actual && + echo $BACKEND >expect && + test_cmp expect actual && + + verify_files_exist repo/.git refdir && + + git -C source for-each-ref refs/tags/ >expect && + git -C repo for-each-ref refs/tags/ >actual && + test_cmp expect actual +' + test_done From c63e64e04d43ebdc04204a052858c4801c018e1e Mon Sep 17 00:00:00 2001 From: Lucas Seiki Oshiro Date: Wed, 25 Feb 2026 13:32:10 -0300 Subject: [PATCH 50/58] CodingGuidelines: instruct to name arrays in singular Arrays should be named in the singular form, ensuring that when accessing an element within an array (e.g. dog[0]) it's clear that we're referring to an element instead of a collection. Add a new rule to CodingGuidelines asking for arrays to be named in singular instead of plural. Helped-by: Eric Sunshine Signed-off-by: Lucas Seiki Oshiro Signed-off-by: Junio C Hamano --- Documentation/CodingGuidelines | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Documentation/CodingGuidelines b/Documentation/CodingGuidelines index df72fe01772a18..278083526d1f88 100644 --- a/Documentation/CodingGuidelines +++ b/Documentation/CodingGuidelines @@ -656,6 +656,19 @@ For C programs: unsigned other_field:1; unsigned field_with_longer_name:1; + - Array names should be named in the singular form if the individual items are + subject of use. E.g.: + + char *dog[] = ...; + walk_dog(dog[0]); + walk_dog(dog[1]); + + Cases where the array is employed as a whole rather than as its unit parts, + the plural forms is preferable. E.g: + + char *dogs[] = ...; + walk_all_dogs(dogs); + For Perl programs: - Most of the C guidelines above apply. From 3d4e6d319383d31b319227ed099a7dbd0706e165 Mon Sep 17 00:00:00 2001 From: Lucas Seiki Oshiro Date: Wed, 25 Feb 2026 13:32:11 -0300 Subject: [PATCH 51/58] repo: rename repo_info_fields to repo_info_field Rename repo_info_fields as repo_info_field, following the CodingGuidelines rule for naming arrays in singular. Rename all the references to that array accordingly. Signed-off-by: Lucas Seiki Oshiro Signed-off-by: Junio C Hamano --- builtin/repo.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/builtin/repo.c b/builtin/repo.c index 6a62a6020a5115..aa9a154cd2eebe 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -62,15 +62,15 @@ static int get_references_format(struct repository *repo, struct strbuf *buf) return 0; } -/* repo_info_fields keys must be in lexicographical order */ -static const struct field repo_info_fields[] = { +/* repo_info_field keys must be in lexicographical order */ +static const struct field repo_info_field[] = { { "layout.bare", get_layout_bare }, { "layout.shallow", get_layout_shallow }, { "object.format", get_object_format }, { "references.format", get_references_format }, }; -static int repo_info_fields_cmp(const void *va, const void *vb) +static int repo_info_field_cmp(const void *va, const void *vb) { const struct field *a = va; const struct field *b = vb; @@ -81,10 +81,10 @@ static int repo_info_fields_cmp(const void *va, const void *vb) static get_value_fn *get_value_fn_for_key(const char *key) { const struct field search_key = { key, NULL }; - const struct field *found = bsearch(&search_key, repo_info_fields, - ARRAY_SIZE(repo_info_fields), + const struct field *found = bsearch(&search_key, repo_info_field, + ARRAY_SIZE(repo_info_field), sizeof(*found), - repo_info_fields_cmp); + repo_info_field_cmp); return found ? found->get_value : NULL; } @@ -137,8 +137,8 @@ static int print_all_fields(struct repository *repo, { struct strbuf valbuf = STRBUF_INIT; - for (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) { - const struct field *field = &repo_info_fields[i]; + for (size_t i = 0; i < ARRAY_SIZE(repo_info_field); i++) { + const struct field *field = &repo_info_field[i]; strbuf_reset(&valbuf); field->get_value(repo, &valbuf); @@ -164,8 +164,8 @@ static int print_keys(enum output_format format) die(_("--keys can only be used with --format=lines or --format=nul")); } - for (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) { - const struct field *field = &repo_info_fields[i]; + for (size_t i = 0; i < ARRAY_SIZE(repo_info_field); i++) { + const struct field *field = &repo_info_field[i]; printf("%s%c", field->key, sep); } From 7377a6ef6b9cec293a3f65be7499d5e5ee9c3fae Mon Sep 17 00:00:00 2001 From: Lucas Seiki Oshiro Date: Wed, 25 Feb 2026 13:32:12 -0300 Subject: [PATCH 52/58] repo: replace get_value_fn_for_key by get_repo_info_field Remove the function `get_value_fn_for_key`, which returns a function that retrieves a value for a certain repo info key. Introduce `get_repo_info_field` instead, which returns a struct field. This refactor makes the structure of the function print_fields more consistent to the function print_all_fields, improving its readability. Signed-off-by: Lucas Seiki Oshiro Signed-off-by: Junio C Hamano --- builtin/repo.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/builtin/repo.c b/builtin/repo.c index aa9a154cd2eebe..c60a41ba7bea8c 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -78,14 +78,15 @@ static int repo_info_field_cmp(const void *va, const void *vb) return strcmp(a->key, b->key); } -static get_value_fn *get_value_fn_for_key(const char *key) +static const struct field *get_repo_info_field(const char *key) { const struct field search_key = { key, NULL }; const struct field *found = bsearch(&search_key, repo_info_field, ARRAY_SIZE(repo_info_field), sizeof(*found), repo_info_field_cmp); - return found ? found->get_value : NULL; + + return found; } static void print_field(enum output_format format, const char *key, @@ -113,18 +114,16 @@ static int print_fields(int argc, const char **argv, struct strbuf valbuf = STRBUF_INIT; for (int i = 0; i < argc; i++) { - get_value_fn *get_value; const char *key = argv[i]; + const struct field *field = get_repo_info_field(key); - get_value = get_value_fn_for_key(key); - - if (!get_value) { + if (!field) { ret = error(_("key '%s' not found"), key); continue; } strbuf_reset(&valbuf); - get_value(repo, &valbuf); + field->get_value(repo, &valbuf); print_field(format, key, valbuf.buf); } From 18f16b889cbc7a9659a253c974f857d2133f7397 Mon Sep 17 00:00:00 2001 From: Lucas Seiki Oshiro Date: Wed, 25 Feb 2026 13:32:13 -0300 Subject: [PATCH 53/58] repo: rename struct field to repo_info_field Change the name of the struct field to repo_info_field, making it explicit that it is an internal data type of git-repo-info. Signed-off-by: Lucas Seiki Oshiro Signed-off-by: Junio C Hamano --- builtin/repo.c | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/builtin/repo.c b/builtin/repo.c index c60a41ba7bea8c..f943be74510158 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -31,7 +31,7 @@ enum output_format { FORMAT_NUL_TERMINATED, }; -struct field { +struct repo_info_field { const char *key; get_value_fn *get_value; }; @@ -63,7 +63,7 @@ static int get_references_format(struct repository *repo, struct strbuf *buf) } /* repo_info_field keys must be in lexicographical order */ -static const struct field repo_info_field[] = { +static const struct repo_info_field repo_info_field[] = { { "layout.bare", get_layout_bare }, { "layout.shallow", get_layout_shallow }, { "object.format", get_object_format }, @@ -72,19 +72,20 @@ static const struct field repo_info_field[] = { static int repo_info_field_cmp(const void *va, const void *vb) { - const struct field *a = va; - const struct field *b = vb; + const struct repo_info_field *a = va; + const struct repo_info_field *b = vb; return strcmp(a->key, b->key); } -static const struct field *get_repo_info_field(const char *key) +static const struct repo_info_field *get_repo_info_field(const char *key) { - const struct field search_key = { key, NULL }; - const struct field *found = bsearch(&search_key, repo_info_field, - ARRAY_SIZE(repo_info_field), - sizeof(*found), - repo_info_field_cmp); + const struct repo_info_field search_key = { key, NULL }; + const struct repo_info_field *found = bsearch(&search_key, + repo_info_field, + ARRAY_SIZE(repo_info_field), + sizeof(*found), + repo_info_field_cmp); return found; } @@ -115,7 +116,7 @@ static int print_fields(int argc, const char **argv, for (int i = 0; i < argc; i++) { const char *key = argv[i]; - const struct field *field = get_repo_info_field(key); + const struct repo_info_field *field = get_repo_info_field(key); if (!field) { ret = error(_("key '%s' not found"), key); @@ -137,7 +138,7 @@ static int print_all_fields(struct repository *repo, struct strbuf valbuf = STRBUF_INIT; for (size_t i = 0; i < ARRAY_SIZE(repo_info_field); i++) { - const struct field *field = &repo_info_field[i]; + const struct repo_info_field *field = &repo_info_field[i]; strbuf_reset(&valbuf); field->get_value(repo, &valbuf); @@ -164,7 +165,7 @@ static int print_keys(enum output_format format) } for (size_t i = 0; i < ARRAY_SIZE(repo_info_field); i++) { - const struct field *field = &repo_info_field[i]; + const struct repo_info_field *field = &repo_info_field[i]; printf("%s%c", field->key, sep); } From b62dab3b6d4ba14828893e39a0c480f2f5097f5b Mon Sep 17 00:00:00 2001 From: Lucas Seiki Oshiro Date: Wed, 25 Feb 2026 13:32:14 -0300 Subject: [PATCH 54/58] t1900: rename t1900-repo to t1900-repo-info Since the commit bbb2b93348 (builtin/repo: introduce structure subcommand, 2025-10-21), t1901 specifically tests git-repo-structure. Rename t1900-repo to t1900-repo-info to clarify that it focus solely on git-repo-info subcommand. Signed-off-by: Lucas Seiki Oshiro Signed-off-by: Junio C Hamano --- t/meson.build | 2 +- t/{t1900-repo.sh => t1900-repo-info.sh} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename t/{t1900-repo.sh => t1900-repo-info.sh} (100%) diff --git a/t/meson.build b/t/meson.build index f80e366cff73f3..9867762bacfc9d 100644 --- a/t/meson.build +++ b/t/meson.build @@ -240,7 +240,7 @@ integration_tests = [ 't1700-split-index.sh', 't1701-racy-split-index.sh', 't1800-hook.sh', - 't1900-repo.sh', + 't1900-repo-info.sh', 't1901-repo-structure.sh', 't2000-conflict-when-checking-files-out.sh', 't2002-checkout-cache-u.sh', diff --git a/t/t1900-repo.sh b/t/t1900-repo-info.sh similarity index 100% rename from t/t1900-repo.sh rename to t/t1900-repo-info.sh From 2db3d0a2268c1bdd1b9d1bf8e5f46be3ba7cb8bf Mon Sep 17 00:00:00 2001 From: Lucas Seiki Oshiro Date: Wed, 25 Feb 2026 13:32:15 -0300 Subject: [PATCH 55/58] t1901: adjust nul format output instead of expected value The test 'keyvalue and nul format', as it description says, test both `keyvalue` and `nul` format. These formats are similar, differing only in their field separator (= in the former, LF in the latter) and their record separator (LF in the former, NUL in the latter). This way, both formats can be tested using the same expected output and only replacing the separators in one of the output formats. However, it is not desirable to have a NUL character in the files compared by test_cmp because, if that assetion fails, diff will consider them binary files and won't display the differences properly. Adjust the output of `git repo structure --format=nul` in t1901, matching the --format=keyvalue ones. Compare this output against the same value expected from --format=keyvalue, without using files with NUL characters in test_cmp. Signed-off-by: Lucas Seiki Oshiro Signed-off-by: Junio C Hamano --- t/t1901-repo-structure.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh index a6f2591d9a61ff..a67b38ab178125 100755 --- a/t/t1901-repo-structure.sh +++ b/t/t1901-repo-structure.sh @@ -145,18 +145,18 @@ test_expect_success SHA1 'lines and nul format' ' test_cmp expect out && test_line_count = 0 err && - # Replace key and value delimiters for nul format. - tr "\n=" "\0\n" expect_nul && git repo structure --format=nul >out 2>err && + tr "\012\000" "=\012" actual && - test_cmp expect_nul out && + test_cmp expect actual && test_line_count = 0 err && # "-z", as a synonym to "--format=nul", participates in the # usual "last one wins" rule. git repo structure --format=table -z >out 2>err && + tr "\012\000" "=\012" actual && - test_cmp expect_nul out && + test_cmp expect actual && test_line_count = 0 err ) ' From 906b632c4f8b83c9bcc28e160fec79b912b9c5a3 Mon Sep 17 00:00:00 2001 From: Lucas Seiki Oshiro Date: Wed, 25 Feb 2026 13:32:16 -0300 Subject: [PATCH 56/58] Documentation/git-repo: replace 'NUL' with '_NUL_' Replace all occurrences of "NUL" by "_NUL_" in git-repo.adoc, following the convention used by other documentation files. Signed-off-by: Lucas Seiki Oshiro Signed-off-by: Junio C Hamano --- Documentation/git-repo.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc index 319d30bd86e7f4..f76f579b20f929 100644 --- a/Documentation/git-repo.adoc +++ b/Documentation/git-repo.adoc @@ -40,7 +40,7 @@ supported: `nul`::: similar to `lines`, but using a newline character as the delimiter - between the key and the value and using a NUL character after each value. + between the key and the value and using a _NUL_ character after each value. This format is better suited for being parsed by another applications than `lines`. Unlike in the `lines` format, the values are never quoted. + @@ -80,7 +80,7 @@ supported: configuration variable `core.quotePath` (see linkgit:git-config[1]). `nul`::: - Similar to `lines`, but uses a NUL character to delimit between + Similar to `lines`, but uses a _NUL_ character to delimit between key-value pairs instead of a newline. Also uses a newline character as the delimiter between the key and value instead of '='. Unlike the `lines` format, values containing "unusual" characters are never From 8b97dc367a9dce2c5f14c8012ac1025c3ec70121 Mon Sep 17 00:00:00 2001 From: Lucas Seiki Oshiro Date: Wed, 25 Feb 2026 13:32:17 -0300 Subject: [PATCH 57/58] Documentation/git-repo: capitalize format descriptions The descriptions for the git-repo output formats are in lowercase. Capitalize these descriptions, making them consistent with the rest of the documentation. Signed-off-by: Lucas Seiki Oshiro Signed-off-by: Junio C Hamano --- Documentation/git-repo.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc index f76f579b20f929..5e2968b707eacd 100644 --- a/Documentation/git-repo.adoc +++ b/Documentation/git-repo.adoc @@ -33,13 +33,13 @@ supported: + `lines`::: - output key-value pairs one per line using the `=` character as + Output key-value pairs one per line using the `=` character as the delimiter between the key and the value. Values containing "unusual" characters are quoted as explained for the configuration variable `core.quotePath` (see linkgit:git-config[1]). This is the default. `nul`::: - similar to `lines`, but using a newline character as the delimiter + Similar to `lines`, but using a newline character as the delimiter between the key and the value and using a _NUL_ character after each value. This format is better suited for being parsed by another applications than `lines`. Unlike in the `lines` format, the values are never quoted. From 628a66ccf68d141d57d06e100c3514a54b31d6b7 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 4 Mar 2026 10:50:48 -0800 Subject: [PATCH 58/58] The 11th batch Signed-off-by: Junio C Hamano --- Documentation/RelNotes/2.54.0.adoc | 47 ++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/Documentation/RelNotes/2.54.0.adoc b/Documentation/RelNotes/2.54.0.adoc index 6425d47e00c165..f37c0be602db33 100644 --- a/Documentation/RelNotes/2.54.0.adoc +++ b/Documentation/RelNotes/2.54.0.adoc @@ -47,6 +47,19 @@ UI, Workflows & Features * "git add -p" learned a new mode that allows the user to revisit a file that was already dealt with. + * Allow the directory in which reference backends store their data to + be specified. + + * "gitweb" has been taught to be mobile friendly. + + * "git apply --directory=./un/../normalized/path" now normalizes the + given path before using it. + + * "git maintenance" starts using the "geometric" strategy by default. + + * "git config list" is taught to show the values interpreted for + specific type with "--type=" option. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -110,6 +123,22 @@ Performance, Internal Implementation, Development Support etc. * The code to accept shallow "git push" has been optimized. + * Simplify build procedure for oxskeychain (in contrib/). + + * Fix dependency screw-up in meson-based builds. + + * Wean the mailmap code off of the_repository dependency. + + * API clean-up for the worktree subsystem. + + * The last uses of the_repository in "tree-diff.c" have been + eradicated. + + * Clean-up the code around "git repo info" command. + + * Mark the marge-ort codebase to prevent more uses of the_repository + from getting added. + Fixes since v2.53 ----------------- @@ -185,6 +214,17 @@ Fixes since v2.53 * An earlier attempt to optimize "git subtree" discarded too much relevant histories, which has been corrected. + * A prefetch call can be triggered to access a stale diff_queue entry + after diffcore-break breaks a filepair into two and freed the + original entry that is no longer used, leading to a segfault, which + has been corrected. + (merge 2d88ab078d hy/diff-lazy-fetch-with-break-fix later to maint). + + * "git fetch --deepen" that tries to go beyond merged branch used to + get confused where the updated shallow points are, which has been + corrected. + (merge 3ef68ff40e sp/shallow-deepen-relative-fix later to maint). + * Other code cleanup, docfix, build fix, etc. (merge d79fff4a11 jk/remote-tracking-ref-leakfix later to maint). (merge 7a747f972d dd/t5403-modernise later to maint). @@ -214,9 +254,4 @@ Fixes since v2.53 (merge b10e0cb1f3 kh/doc-am-xref later to maint). (merge ed84bc1c0d kh/doc-patch-id-4 later to maint). (merge 7451864bfa sc/pack-redundant-leakfix later to maint). - - * A prefetch call can be triggered to access a stale diff_queue entry - after diffcore-break breaks a filepair into two and freed the - original entry that is no longer used, leading to a segfault, which - has been corrected. - (merge 2d88ab078d hy/diff-lazy-fetch-with-break-fix later to maint). + (merge f87593ab1a cx/fetch-display-ubfix later to maint).