From 4b0a8b2506494323ae5c209ee7df6c08fec3831d Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Thu, 21 May 2026 14:06:07 +0000 Subject: [PATCH 01/95] remote: qualify "git pull" advice for non-upstream compareBranches Enable ENABLE_ADVICE_PULL for push-branch comparisons too, not just the upstream entry, so the "use git pull" hint prints when the local branch is behind its push branch. Spell out "git pull " so running the suggested command actually pulls the ref the user was told about; plain "git pull" would fetch the upstream instead. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- remote.c | 48 +++++++++++++++---- t/t6040-tracking-info.sh | 100 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 8 deletions(-) diff --git a/remote.c b/remote.c index a664cd166aa3b9..8870598d44a896 100644 --- a/remote.c +++ b/remote.c @@ -2267,6 +2267,8 @@ static void format_branch_comparison(struct strbuf *sb, bool up_to_date, int ours, int theirs, const char *branch_name, + const char *push_remote_name, + const char *push_branch_name, enum ahead_behind_flags abf, unsigned flags) { @@ -2302,9 +2304,15 @@ static void format_branch_comparison(struct strbuf *sb, "and can be fast-forwarded.\n", theirs), branch_name, theirs); - if (use_pull_advice && advice_enabled(ADVICE_STATUS_HINTS)) - strbuf_addstr(sb, - _(" (use \"git pull\" to update your local branch)\n")); + if (use_pull_advice && advice_enabled(ADVICE_STATUS_HINTS)) { + if (push_remote_name && push_branch_name) + strbuf_addf(sb, + _(" (use \"git pull %s %s\" to update your local branch)\n"), + push_remote_name, push_branch_name); + else + strbuf_addstr(sb, + _(" (use \"git pull\" to update your local branch)\n")); + } } else { strbuf_addf(sb, Q_("Your branch and '%s' have diverged,\n" @@ -2315,9 +2323,15 @@ static void format_branch_comparison(struct strbuf *sb, "respectively.\n", ours + theirs), branch_name, ours, theirs); - if (use_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS)) - strbuf_addstr(sb, - _(" (use \"git pull\" if you want to integrate the remote branch with yours)\n")); + if (use_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS)) { + if (push_remote_name && push_branch_name) + strbuf_addf(sb, + _(" (use \"git pull %s %s\" if you want to integrate the remote branch with yours)\n"), + push_remote_name, push_branch_name); + else + strbuf_addstr(sb, + _(" (use \"git pull\" if you want to integrate the remote branch with yours)\n")); + } } } @@ -2355,6 +2369,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb, int ours, theirs, cmp; int is_upstream, is_push; unsigned flags = 0; + const char *push_remote_name = NULL; + const char *push_branch_name = NULL; full_ref = resolve_compare_branch(branch, branches.items[i].string); @@ -2398,11 +2414,27 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb, if (is_upstream) flags |= ENABLE_ADVICE_PULL; - if (is_push) - flags |= ENABLE_ADVICE_PUSH; if (show_divergence_advice && is_upstream) flags |= ENABLE_ADVICE_DIVERGENCE; + if (is_push) { + flags |= ENABLE_ADVICE_PUSH; + if (!upstream_ref || strcmp(upstream_ref, full_ref)) { + push_remote_name = pushremote_for_branch(branch, NULL); + if (push_remote_name && + skip_prefix(full_ref, "refs/remotes/", &push_branch_name) && + skip_prefix(push_branch_name, push_remote_name, &push_branch_name) && + *push_branch_name == '/') { + push_branch_name++; + flags |= ENABLE_ADVICE_PULL; + } else { + push_remote_name = NULL; + } + } else { + flags |= ENABLE_ADVICE_PULL; + } + } format_branch_comparison(sb, !cmp, ours, theirs, short_ref, + push_remote_name, push_branch_name, abf, flags); reported = 1; diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh index 0242b5bf7abedb..91cbb8775d0a5a 100755 --- a/t/t6040-tracking-info.sh +++ b/t/t6040-tracking-info.sh @@ -646,4 +646,104 @@ test_expect_success 'status.compareBranches with remapped push and upstream remo test_cmp expect actual ' +test_expect_success 'status.compareBranches behind both upstream and push' ' + test_config -C test push.default current && + test_config -C test remote.pushDefault origin && + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test checkout -b feature13 upstream/main && + (cd test && advance work13) && + git -C test push origin && + git -C test branch --set-upstream-to upstream/ahead && + git -C test reset --hard HEAD^ && + git -C test status >actual && + cat >expect <<-EOF && + On branch feature13 + Your branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded. + (use "git pull" to update your local branch) + + Your branch is behind ${SQ}origin/feature13${SQ} by 1 commit, and can be fast-forwarded. + (use "git pull origin feature13" to update your local branch) + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'status.compareBranches with remapped push and behind push branch' ' + test_config -C test remote.pushDefault origin && + test_config -C test remote.origin.push refs/heads/feature14:refs/heads/remapped14 && + test_config -C test status.compareBranches "@{push}" && + git -C test checkout -b feature14 upstream/main && + (cd test && advance work14) && + git -C test push && + git -C test reset --hard HEAD^ && + git -C test status >actual && + cat >expect <<-EOF && + On branch feature14 + Your branch is behind ${SQ}origin/remapped14${SQ} by 1 commit, and can be fast-forwarded. + (use "git pull origin remapped14" to update your local branch) + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'status.compareBranches with behind push branch and no upstream' ' + test_config -C test push.default current && + test_config -C test remote.pushDefault origin && + test_config -C test status.compareBranches "@{push}" && + git -C test checkout --no-track -b feature15 upstream/main && + (cd test && advance work15) && + git -C test push origin && + git -C test reset --hard HEAD^ && + git -C test status >actual && + cat >expect <<-EOF && + On branch feature15 + Your branch is behind ${SQ}origin/feature15${SQ} by 1 commit, and can be fast-forwarded. + (use "git pull origin feature15" to update your local branch) + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'status.compareBranches behind upstream-equals-push suggests plain pull' ' + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test checkout -b feature16 origin/main && + (cd test && advance work16) && + git -C test push origin HEAD:main && + git -C test reset --hard HEAD^ && + git -C test status >actual && + cat >expect <<-EOF && + On branch feature16 + Your branch is behind ${SQ}origin/main${SQ} by 1 commit, and can be fast-forwarded. + (use "git pull" to update your local branch) + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'status.compareBranches suppresses advice when push tracking ref is unconventional' ' + test_config -C test push.default current && + test_config -C test remote.imported.url ../. && + test_config -C test remote.imported.fetch "+refs/heads/*:refs/imported/imported/*" && + test_config -C test branch.feature17.pushRemote imported && + test_config -C test status.compareBranches "@{push}" && + git -C test fetch imported && + git -C test checkout --no-track -b feature17 refs/imported/imported/main && + (cd test && advance work17) && + git -C test push imported HEAD:feature17 && + git -C test fetch imported && + git -C test reset --hard HEAD^ && + git -C test status >actual && + cat >expect <<-EOF && + On branch feature17 + Your branch is behind ${SQ}imported/imported/feature17${SQ} by 1 commit, and can be fast-forwarded. + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + test_done From 4ed1ffe680d1ad0fe7436c9816262b6abb518629 Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 27 May 2026 16:08:13 +0200 Subject: [PATCH 02/95] t5710: simplify 'mkdir X' followed by 'git -C X init' It's simpler and more efficient to just use `git init client` instead of `mkdir client && git -C client init`. So let's replace the latter with the former. Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- t/t5710-promisor-remote-capability.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh index b404ad9f0a9e3d..bf1cc54605394e 100755 --- a/t/t5710-promisor-remote-capability.sh +++ b/t/t5710-promisor-remote-capability.sh @@ -177,8 +177,7 @@ test_expect_success "init + fetch with promisor.advertise set to 'true'" ' git -C server config promisor.advertise true && test_when_finished "rm -rf client" && - mkdir client && - git -C client init && + git init client && git -C client config remote.lop.promisor true && git -C client config remote.lop.fetch "+refs/heads/*:refs/remotes/lop/*" && git -C client config remote.lop.url "$TRASH_DIRECTORY_URL/lop" && @@ -231,8 +230,7 @@ test_expect_success "init + fetch two promisors but only one advertised" ' # Create a promisor that will be configured but not be used git init --bare unused_lop && - mkdir client && - git -C client init && + git init client && git -C client config remote.unused_lop.promisor true && git -C client config remote.unused_lop.fetch "+refs/heads/*:refs/remotes/unused_lop/*" && git -C client config remote.unused_lop.url "$TRASH_DIRECTORY_URL/unused_lop" && From ee7ea4907ccef604f764df5e223640ad04192f6d Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 27 May 2026 16:08:14 +0200 Subject: [PATCH 03/95] urlmatch: change 'allow_globs' arg to bool The last argument of url_normalize_1() is `char allow_globs` but it is used as a boolean, not as a char. Let's convert it to a `bool`, and while at it convert the two calls to url_normalize_1() so they pass 'true' or 'false' instead of '1' or '0'. Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- urlmatch.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/urlmatch.c b/urlmatch.c index bf8cce6de9d8da..b2d88a5289df6c 100644 --- a/urlmatch.c +++ b/urlmatch.c @@ -112,7 +112,7 @@ static int match_host(const struct url_info *url_info, return (!url_len && !pat_len); } -static char *url_normalize_1(const char *url, struct url_info *out_info, char allow_globs) +static char *url_normalize_1(const char *url, struct url_info *out_info, bool allow_globs) { /* * Normalize NUL-terminated url using the following rules: @@ -438,7 +438,7 @@ static char *url_normalize_1(const char *url, struct url_info *out_info, char al char *url_normalize(const char *url, struct url_info *out_info) { - return url_normalize_1(url, out_info, 0); + return url_normalize_1(url, out_info, false); } char *url_parse(const char *url_orig, struct url_info *out_info) @@ -704,7 +704,7 @@ int urlmatch_config_entry(const char *var, const char *value, struct url_info norm_info; config_url = xmemdupz(key, dot - key); - norm_url = url_normalize_1(config_url, &norm_info, 1); + norm_url = url_normalize_1(config_url, &norm_info, true); if (norm_url) retval = match_urls(url, &norm_info, &matched); else if (collect->fallback_match_fn) From 58880c82feb460015153575dc02b9959e4d8a8a0 Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 27 May 2026 16:08:15 +0200 Subject: [PATCH 04/95] urlmatch: add url_normalize_pattern() helper In a following commit, we will need to normalize a URL glob pattern (which may contain '*' in the host portion) and extract its component offsets (host, path, etc.) for separate matching. Let's export a dedicated helper function url_normalize_pattern() for that purpose. It works like url_normalize(), but passes allow_globs=true to the internal url_normalize_1(), so that '*' characters in the host are accepted rather than rejected. Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- urlmatch.c | 5 +++++ urlmatch.h | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/urlmatch.c b/urlmatch.c index b2d88a5289df6c..20bc2d009cd0dd 100644 --- a/urlmatch.c +++ b/urlmatch.c @@ -441,6 +441,11 @@ char *url_normalize(const char *url, struct url_info *out_info) return url_normalize_1(url, out_info, false); } +char *url_normalize_pattern(const char *url, struct url_info *out_info) +{ + return url_normalize_1(url, out_info, true); +} + char *url_parse(const char *url_orig, struct url_info *out_info) { struct strbuf url; diff --git a/urlmatch.h b/urlmatch.h index 6b3ce428582da3..db1a335e728ea3 100644 --- a/urlmatch.h +++ b/urlmatch.h @@ -37,6 +37,18 @@ struct url_info { char *url_normalize(const char *, struct url_info *); char *url_parse(const char *, struct url_info *); +/* + * Like url_normalize(), but also allows '*' glob characters in the host + * portion. Use this when normalizing URL patterns from user configuration. + * + * Note that '*' is a valid path character per RFC 3986 (as a sub-delim), + * so glob patterns using '*' in the path are also accepted. + * + * Returns a newly allocated normalized string and fills out_info if + * non-NULL, or NULL if the pattern is invalid. + */ +char *url_normalize_pattern(const char *url, struct url_info *out_info); + struct urlmatch_item { size_t hostmatch_len; size_t pathmatch_len; From 53951298515ad26728175182c9103eea71885220 Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 27 May 2026 16:08:16 +0200 Subject: [PATCH 05/95] promisor-remote: add 'local_name' to 'struct promisor_info' In a following commit, we will store promisor remote information under a remote name different than the one the server advertised. To prepare for this change, let's add a new 'char *local_name' member to 'struct promisor_info', and let's update the related functions. While at it, let's also add a small promisor_info_local_name() helper that returns `local_name` when set, `name` otherwise, and let's use this small helper in promisor_store_advertised_fields() and in the post-loop of filter_promisor_remote() so that lookups against the local repo configuration use the right name. Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- promisor-remote.c | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/promisor-remote.c b/promisor-remote.c index 38fa05054227f6..138a41289350d6 100644 --- a/promisor-remote.c +++ b/promisor-remote.c @@ -434,13 +434,14 @@ static struct string_list *fields_stored(void) * Struct for promisor remotes involved in the "promisor-remote" * protocol capability. * - * Except for "name", each in this struct and its - * should correspond (either on the client side or on the server side) - * to a "remote.." config variable set to where - * "" is a promisor remote name. + * Except for "name" and "local_name", each in this struct + * and its should correspond (either on the client side or on + * the server side) to a "remote.." config variable set + * to where "" is a promisor remote name. */ struct promisor_info { - const char *name; + const char *name; /* name the server advertised */ + const char *local_name; /* name used locally (may be auto-generated) */ const char *url; const char *filter; const char *token; @@ -449,6 +450,7 @@ struct promisor_info { static void promisor_info_free(struct promisor_info *p) { free((char *)p->name); + free((char *)p->local_name); free((char *)p->url); free((char *)p->filter); free((char *)p->token); @@ -462,6 +464,11 @@ static void promisor_info_list_clear(struct string_list *list) string_list_clear(list, 0); } +static const char *promisor_info_local_name(struct promisor_info *p) +{ + return p->local_name ? p->local_name : p->name; +} + static void set_one_field(struct promisor_info *p, const char *field, const char *value) { @@ -829,7 +836,7 @@ static bool promisor_store_advertised_fields(struct promisor_info *advertised, { struct promisor_info *p; struct string_list_item *item; - const char *remote_name = advertised->name; + const char *remote_name = promisor_info_local_name(advertised); bool reload_config = false; if (!(store_info->store_filter || store_info->store_token)) @@ -937,7 +944,8 @@ static void filter_promisor_remote(struct repository *repo, /* Apply accepted remotes to the stable repo state */ for_each_string_list_item(item, accepted_remotes) { struct promisor_info *info = item->util; - struct promisor_remote *r = repo_promisor_remote_find(repo, info->name); + const char *remote_name = promisor_info_local_name(info); + struct promisor_remote *r = repo_promisor_remote_find(repo, remote_name); if (r) { r->accepted = 1; From 78e0d9b0e4649659cc38545450174d293cb1ec0c Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 27 May 2026 16:08:17 +0200 Subject: [PATCH 06/95] promisor-remote: introduce promisor.acceptFromServerUrl The "promisor-remote" protocol capability allows servers to advertise promisor remotes, but doesn't allow these remotes to be automatically configured on the client. Let's introduce a new `promisor.acceptFromServerUrl` config variable which contains a glob pattern, so that advertised remotes with a URL matching that pattern will be automatically configured. The glob pattern can optionally be prefixed with a remote name which will be used as the name of the new local remote. For now though, let's only introduce the functions to read and validate the glob patterns and the optional prefixes. Checking if the URLs of the advertised remotes match the glob patterns and taking the appropriate action is left for a following commit. Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- promisor-remote.c | 90 +++++++++++++++++++++++++++ t/t5710-promisor-remote-capability.sh | 21 +++++++ 2 files changed, 111 insertions(+) diff --git a/promisor-remote.c b/promisor-remote.c index 138a41289350d6..8d4f6e0a72251b 100644 --- a/promisor-remote.c +++ b/promisor-remote.c @@ -12,6 +12,7 @@ #include "packfile.h" #include "environment.h" #include "url.h" +#include "urlmatch.h" #include "version.h" struct promisor_remote_config { @@ -657,6 +658,90 @@ static bool has_control_char(const char *s) return false; } +struct allowed_url { + char *remote_name; + char *url_pattern; + struct url_info pattern_info; +}; + +static void allowed_url_free(void *util, const char *str UNUSED) +{ + struct allowed_url *allowed = util; + + if (!allowed) + return; + + /* Depending on prefix, free either remote_name or url_pattern */ + free(allowed->remote_name ? allowed->remote_name : allowed->url_pattern); + free(allowed->pattern_info.url); + free(allowed); +} + +static struct allowed_url *valid_accept_url(const char *url) +{ + char *dup, *p; + struct allowed_url *allowed; + + if (!url) + return NULL; + + dup = xstrdup(url); + p = strchr(dup, '='); + if (p) { + *p = '\0'; + if (!valid_remote_name(dup)) { + warning(_("invalid remote name '%s' before '=' sign " + "in '%s' from promisor.acceptFromServerUrl config"), + dup, url); + free(dup); + return NULL; + } + p++; + } else { + p = dup; + } + + if (has_control_char(p)) { + warning(_("invalid url pattern '%s' " + "in '%s' from promisor.acceptFromServerUrl config"), p, url); + free(dup); + return NULL; + } + + allowed = xmalloc(sizeof(*allowed)); + allowed->remote_name = (p == dup) ? NULL : dup; + allowed->url_pattern = p; + allowed->pattern_info.url = url_normalize_pattern(p, &allowed->pattern_info); + if (!allowed->pattern_info.url) { + warning(_("invalid url pattern '%s' " + "in '%s' from promisor.acceptFromServerUrl config"), p, url); + free(dup); + free(allowed); + return NULL; + } + + return allowed; +} + +static void load_accept_from_server_url(struct repository *repo, + struct string_list *accept_urls) +{ + const struct string_list *config_urls; + + if (!repo_config_get_string_multi(repo, "promisor.acceptfromserverurl", &config_urls)) { + struct string_list_item *item; + + for_each_string_list_item(item, config_urls) { + struct allowed_url *allowed = valid_accept_url(item->string); + if (allowed) { + struct string_list_item *new; + new = string_list_append(accept_urls, item->string); + new->util = allowed; + } + } + } +} + static int should_accept_remote(enum accept_promisor accept, struct promisor_info *advertised, struct string_list *config_info) @@ -901,6 +986,10 @@ static void filter_promisor_remote(struct repository *repo, struct string_list_item *item; bool reload_config = false; enum accept_promisor accept = accept_from_server(repo); + struct string_list accept_urls = STRING_LIST_INIT_DUP; + + /* Load and validate the acceptFromServerUrl config */ + load_accept_from_server_url(repo, &accept_urls); if (accept == ACCEPT_NONE) return; @@ -934,6 +1023,7 @@ static void filter_promisor_remote(struct repository *repo, } } + string_list_clear_func(&accept_urls, allowed_url_free); promisor_info_list_clear(&config_info); string_list_clear(&remote_info, 0); store_info_free(store_info); diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh index bf1cc54605394e..3b39505380caa7 100755 --- a/t/t5710-promisor-remote-capability.sh +++ b/t/t5710-promisor-remote-capability.sh @@ -387,6 +387,27 @@ test_expect_success "clone with 'KnownUrl' and empty url, so not advertised" ' check_missing_objects server 1 "$oid" ' +test_expect_success "clone with invalid promisor.acceptFromServerUrl" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + + # As "bad name" contains a space, which is not a valid remote name, + # the pattern should be rejected with a warning and no remote created. + GIT_NO_LAZY_FETCH=0 git clone \ + -c promisor.acceptfromserver=None \ + -c "promisor.acceptFromServerUrl=bad name=https://example.com/*" \ + --no-local --filter="blob:limit=5k" server client 2>err && + + # Check that a warning was emitted + test_grep "invalid remote name '\''bad name'\''" err && + + # Check that the largest object is not missing on the server + check_missing_objects server 0 "" && + + # Reinitialize server so that the largest object is missing again + initialize_server 1 "$oid" +' + test_expect_success "clone with promisor.sendFields" ' git -C server config promisor.advertise true && test_when_finished "rm -rf client" && From 5dd8043581ca331dcb59ab721aefb5881128e124 Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 27 May 2026 16:08:18 +0200 Subject: [PATCH 07/95] promisor-remote: trust known remotes matching acceptFromServerUrl A previous commit introduced the `promisor.acceptFromServerUrl` config variable along with the machinery to parse and validate the URL glob patterns and optional remote name prefixes it contains. However, these URL patterns are not yet tied into the client's acceptance logic. When a promisor remote is already configured locally, its fields (like authentication tokens) may occasionally need to be refreshed by the server. If `promisor.acceptFromServer` is set to the secure default ("None"), these updates are rejected, potentially causing future fetches to fail. To enable such targeted updates for trusted URLs, let's use the URL patterns from `promisor.acceptFromServerUrl` as an additional URL based allowlist. Concretely, let's check the advertised URLs against the URL glob patterns by introducing a new small helper function called url_matches_accept_list(), which iterates over the glob patterns and returns the first matching allowed_url entry (or NULL). The URL matching is done component by component: scheme and port are compared exactly, the host and path are matched with wildmatch(). Before matching, the advertised URL is passed through url_normalize() so that case variations in the scheme/host, percent-encoding tricks, and ".." path segments cannot bypass the allowlist. The username and password components of the URL are intentionally ignored during matching to allow servers to rotate them, though using the 'token' field of the capability is preferred over embedding credentials in the URL. Let's then use this helper in should_accept_remote() so that a known remote whose URL matches the allowlist is accepted. To prepare for this new logic, let's also: - Add an 'accept_urls' parameter to should_accept_remote(). - Replace the BUG() guard in the ACCEPT_KNOWN_URL case with an explicit 'if (accept == ACCEPT_KNOWN_URL) return' and a new BUG() guard in the ACCEPT_NONE case. - Call accept_from_server_url() from filter_promisor_remote() and relax its early return so that the function is entered when `accept_urls` has entries even if `accept == ACCEPT_NONE`. With this, many organizations may only need something like: git config set --global \ promisor.acceptFromServerUrl "https://my-org.com/*" to accept only their own remotes. And if they need to accept additional remotes in some specific repos, they can also set: git config set promisor.acceptFromServer knownUrl and configure the additional remote manually only in the repos where they are needed. Let's then properly document `promisor.acceptFromServerUrl` in "promisor.adoc" as an additive security allowlist for known remotes, including the URL normalization behavior and the component-wise matching, and let's mention it in "gitprotocol-v2.adoc". Also let's clarify in the documentation how `promisor.acceptFromServerUrl` interacts with `promisor.acceptFromServer`: - Precedence: when both options are set, `promisor.acceptFromServerUrl` is consulted first. If a matching pattern leads to acceptance, the remote is accepted regardless of `promisor.acceptFromServer`. Otherwise the decision is left to `promisor.acceptFromServer`. - URL-mismatch guard: even when the advertised URL matches the allowlist, an already-existing client-side remote whose configured URL differs from the advertised one is not accepted through `promisor.acceptFromServerUrl`. `promisor.acceptFromServer=all` and `=knownName` keep their pre-existing, looser semantics. The precedence paragraph is intentionally scoped here to known remotes only (field updates). A following commit that introduces auto-creation of unknown remotes will extend it to cover that case as well. Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- Documentation/config/promisor.adoc | 76 +++++++++++++++++++ Documentation/gitprotocol-v2.adoc | 9 ++- promisor-remote.c | 102 +++++++++++++++++++++++--- t/t5710-promisor-remote-capability.sh | 71 ++++++++++++++++++ 4 files changed, 244 insertions(+), 14 deletions(-) diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc index b0fa43b8393a53..605473c82f0409 100644 --- a/Documentation/config/promisor.adoc +++ b/Documentation/config/promisor.adoc @@ -51,6 +51,82 @@ promisor.acceptFromServer:: to "fetch" and "clone" requests from the client. Name and URL comparisons are case sensitive. See linkgit:gitprotocol-v2[5]. +promisor.acceptFromServerUrl:: + A glob pattern to specify which server-advertised URLs a + client is allowed to act on. When a URL matches, the client + will accept the advertised remote as a promisor remote and may + automatically accept field updates (such as authentication + tokens) from the server, even if `promisor.acceptFromServer` + is set to `none` (the default). ++ +This option can appear multiple times in config files. An advertised +URL will be accepted if it matches _ANY_ glob pattern specified by +this option in _ANY_ config file read by Git. ++ +When both `promisor.acceptFromServer` and `promisor.acceptFromServerUrl` +are set, `promisor.acceptFromServerUrl` is consulted first and takes +precedence: if a matching pattern leads to acceptance (by accepting +field updates for a known remote whose URL matches both the local +configuration and the allowlist), the advertised remote is accepted +regardless of the `promisor.acceptFromServer` setting. If no pattern +in `promisor.acceptFromServerUrl` triggers acceptance, the decision +is left to `promisor.acceptFromServer`. ++ +Note however that, even when an advertised URL matches a pattern in +`promisor.acceptFromServerUrl`, an already-existing remote on the +client whose name matches the advertised name but whose configured URL +differs from the advertised one will _NOT_ be accepted through +`promisor.acceptFromServerUrl`. This prevents a server from silently +re-pointing an existing client-side remote at a different URL. (Such a +remote may still be accepted through `promisor.acceptFromServer=all` +or `=knownName`, which have their own, looser semantics; see the +documentation of that option.) ++ +Be _VERY_ careful with these patterns: `*` matches any sequence of +characters within the 'host' and 'path' parts of a URL (but cannot +cross part boundaries). An overly broad pattern is a major security +risk, as a matching URL allows a server to update fields (such as +authentication tokens) on known remotes without further confirmation. +To minimize security risks, follow these guidelines: ++ +-- +1. Start with a secure protocol scheme, like `https://` or `ssh://`. ++ +2. Only allow domain names or paths where you control and trust _ALL_ + the content. Be especially careful with shared hosting platforms + like `github.com` or `gitlab.com`. A broad pattern like + `https://gitlab.com/*` is dangerous because it trusts every + repository on the entire platform. Always restrict such patterns to + your specific organization or namespace (e.g., + `https://gitlab.com/your-org/*`). ++ +3. Never use globs at the end of domain names. For example, + `https://cdn.your-org.com/*` might be safe, but + `https://cdn.your-org.com*/*` is a major security risk because + the latter matches `https://cdn.your-org.com.hacker.net/repo`. ++ +4. Be careful using globs at the beginning of domain names. While the + code ensures a `*` in the host cannot cross into the path, a + pattern like `https://*.example.com/*` will still match any + subdomain. This is extremely dangerous on shared hosting platforms + (e.g., `https://*.github.io/*` trusts every user's site on the + entire platform). +-- ++ +Before matching, both the advertised URL and the pattern are +normalized: the scheme and host are lowercased, percent-encoded +characters are decoded where possible, and path segments like `..` +are resolved. The port must also match exactly (e.g., +`https://example.com:8080/*` will not match a URL advertised on +port 9999). The username and password components of the URL are +ignored during matching. Note that embedding credentials in URLs is +discouraged. Passing authentication tokens via the `token` field of +the `promisor-remote` capability is strongly preferred. ++ +For the security implications of accepting a promisor remote, see the +documentation of `promisor.acceptFromServer`. For details on the +protocol, see linkgit:gitprotocol-v2[5]. + promisor.checkFields:: A comma or space separated list of additional remote related field names. A client checks if the values of these fields diff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc index befa697d21c281..2beb70595fc1e5 100644 --- a/Documentation/gitprotocol-v2.adoc +++ b/Documentation/gitprotocol-v2.adoc @@ -866,10 +866,11 @@ the server advertised, the client shouldn't advertise the On the server side, the "promisor.advertise" and "promisor.sendFields" configuration options can be used to control what it advertises. On -the client side, the "promisor.acceptFromServer" configuration option -can be used to control what it accepts, and the "promisor.storeFields" -option, to control what it stores. See the documentation of these -configuration options in linkgit:git-config[1] for more information. +the client side, the "promisor.acceptFromServer" and +"promisor.acceptFromServerUrl" configuration options can be used to +control what it accepts, and the "promisor.storeFields" option, to +control what it stores. See the documentation of these configuration +options in linkgit:git-config[1] for more information. Note that in the future it would be nice if the "promisor-remote" protocol capability could be used by the server, when responding to diff --git a/promisor-remote.c b/promisor-remote.c index 8d4f6e0a72251b..04a5bb9939928c 100644 --- a/promisor-remote.c +++ b/promisor-remote.c @@ -14,6 +14,7 @@ #include "url.h" #include "urlmatch.h" #include "version.h" +#include "wildmatch.h" struct promisor_remote_config { struct promisor_remote *promisors; @@ -742,8 +743,79 @@ static void load_accept_from_server_url(struct repository *repo, } } +static bool match_pattern_url(const char *pat, size_t pat_len, + const char *url, size_t url_len) +{ + char *p_str = xstrndup(pat, pat_len); + char *u_str = xstrndup(url, url_len); + bool res = !wildmatch(p_str, u_str, 0); + + free(p_str); + free(u_str); + + return res; +} + +static bool match_one_url(const struct url_info *pi, const struct url_info *ui) +{ + const char *pat = pi->url; + const char *url = ui->url; + + /* + * Schemes must match exactly. They are case-folded by + * url_normalize(), so strncmp() suffices. + */ + if (pi->scheme_len != ui->scheme_len || strncmp(pat, url, pi->scheme_len)) + return false; + + /* + * Ports must match exactly. url_normalize() strips default + * ports (like 443 for https), so length and content + * comparisons are sufficient. + */ + if (pi->port_len != ui->port_len || + strncmp(pat + pi->port_off, url + ui->port_off, pi->port_len)) + return false; + + /* + * Match host and path separately to prevent a '*' in the host + * portion of the pattern from matching across the '/' + * boundary into the path. + */ + + return match_pattern_url(pat + pi->host_off, pi->host_len, + url + ui->host_off, ui->host_len) && + match_pattern_url(pat + pi->path_off, pi->path_len, + url + ui->path_off, ui->path_len); +} + +static struct allowed_url *url_matches_accept_list( + struct string_list *accept_urls, const char *url) +{ + struct string_list_item *item; + struct url_info url_info; + + url_info.url = url_normalize(url, &url_info); + + if (!url_info.url) + return NULL; + + for_each_string_list_item(item, accept_urls) { + struct allowed_url *allowed = item->util; + + if (match_one_url(&allowed->pattern_info, &url_info)) { + free(url_info.url); + return allowed; + } + } + + free(url_info.url); + return NULL; +} + static int should_accept_remote(enum accept_promisor accept, struct promisor_info *advertised, + struct string_list *accept_urls, struct string_list *config_info) { struct promisor_info *p; @@ -756,23 +828,27 @@ static int should_accept_remote(enum accept_promisor accept, "this remote should have been rejected earlier", remote_name); - if (accept == ACCEPT_ALL) - return all_fields_match(advertised, config_info, NULL); - /* Get config info for that promisor remote */ item = string_list_lookup(config_info, remote_name); - if (!item) + if (!item) { /* We don't know about that remote */ + if (accept == ACCEPT_ALL) + return all_fields_match(advertised, config_info, NULL); return 0; + } p = item->util; - if (accept == ACCEPT_KNOWN_NAME) + /* Known remote in the allowlist? */ + if (!strcmp(p->url, remote_url) && url_matches_accept_list(accept_urls, remote_url)) return all_fields_match(advertised, config_info, p); - if (accept != ACCEPT_KNOWN_URL) - BUG("Unhandled 'enum accept_promisor' value '%d'", accept); + if (accept == ACCEPT_ALL) + return all_fields_match(advertised, config_info, NULL); + + if (accept == ACCEPT_KNOWN_NAME) + return all_fields_match(advertised, config_info, p); if (strcmp(p->url, remote_url)) { warning(_("known remote named '%s' but with URL '%s' instead of '%s', " @@ -781,7 +857,13 @@ static int should_accept_remote(enum accept_promisor accept, return 0; } - return all_fields_match(advertised, config_info, p); + if (accept == ACCEPT_KNOWN_URL) + return all_fields_match(advertised, config_info, p); + + if (accept != ACCEPT_NONE) + BUG("Unhandled 'enum accept_promisor' value '%d'", accept); + + return 0; } static int skip_field_name_prefix(const char *elem, const char *field_name, const char **value) @@ -991,7 +1073,7 @@ static void filter_promisor_remote(struct repository *repo, /* Load and validate the acceptFromServerUrl config */ load_accept_from_server_url(repo, &accept_urls); - if (accept == ACCEPT_NONE) + if (accept == ACCEPT_NONE && !accept_urls.nr) return; /* Parse remote info received */ @@ -1011,7 +1093,7 @@ static void filter_promisor_remote(struct repository *repo, string_list_sort(&config_info); } - if (should_accept_remote(accept, advertised, &config_info)) { + if (should_accept_remote(accept, advertised, &accept_urls, &config_info)) { if (!store_info) store_info = store_info_new(repo); if (promisor_store_advertised_fields(advertised, store_info)) diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh index 3b39505380caa7..0659b2ac157f3b 100755 --- a/t/t5710-promisor-remote-capability.sh +++ b/t/t5710-promisor-remote-capability.sh @@ -387,6 +387,77 @@ test_expect_success "clone with 'KnownUrl' and empty url, so not advertised" ' check_missing_objects server 1 "$oid" ' +test_expect_success "clone with 'None' but URL allowlisted" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + + GIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \ + -c remote.lop.fetch="+refs/heads/*:refs/remotes/lop/*" \ + -c remote.lop.url="$TRASH_DIRECTORY_URL/lop" \ + -c promisor.acceptfromserver=None \ + -c promisor.acceptFromServerUrl="$ENCODED_TRASH_DIRECTORY_URL/*" \ + --no-local --filter="blob:limit=5k" server client && + + # Check that the largest object is still missing on the server + check_missing_objects server 1 "$oid" +' + +test_expect_success "clone with 'None' but URL not in allowlist" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + + GIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \ + -c remote.lop.fetch="+refs/heads/*:refs/remotes/lop/*" \ + -c remote.lop.url="$TRASH_DIRECTORY_URL/lop" \ + -c promisor.acceptfromserver=None \ + -c promisor.acceptFromServerUrl="https://example.com/*" \ + --no-local --filter="blob:limit=5k" server client && + + # Check that the largest object is not missing on the server + check_missing_objects server 0 "" && + + # Reinitialize server so that the largest object is missing again + initialize_server 1 "$oid" +' + +test_expect_success "clone with 'None' but URL allowlisted in one pattern out of two" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + + GIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \ + -c remote.lop.fetch="+refs/heads/*:refs/remotes/lop/*" \ + -c remote.lop.url="$TRASH_DIRECTORY_URL/lop" \ + -c promisor.acceptfromserver=None \ + -c promisor.acceptFromServerUrl="https://example.com/*" \ + -c promisor.acceptFromServerUrl="$ENCODED_TRASH_DIRECTORY_URL/*" \ + --no-local --filter="blob:limit=5k" server client && + + # Check that the largest object is still missing on the server + check_missing_objects server 1 "$oid" +' + +test_expect_success "clone with 'None', URL allowlisted, but client has different URL" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + + # The client configures "lop" with a different URL (serverTwo) than + # what the server advertises (lop). Even though the advertised URL + # matches the allowlist, the remote is rejected because the + # configured URL does not match the advertised one. + GIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \ + -c remote.lop.fetch="+refs/heads/*:refs/remotes/lop/*" \ + -c remote.lop.url="$TRASH_DIRECTORY_URL/serverTwo" \ + -c promisor.acceptfromserver=None \ + -c promisor.acceptFromServerUrl="$ENCODED_TRASH_DIRECTORY_URL/*" \ + --no-local --filter="blob:limit=5k" server client && + + # Check that the largest object is not missing on the server + check_missing_objects server 0 "" && + + # Reinitialize server so that the largest object is missing again + initialize_server 1 "$oid" +' + test_expect_success "clone with invalid promisor.acceptFromServerUrl" ' git -C server config promisor.advertise true && test_when_finished "rm -rf client" && From 7a56394fc6c28572925b00c4fe3b1ff78b5f4322 Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 27 May 2026 16:08:19 +0200 Subject: [PATCH 08/95] promisor-remote: auto-configure unknown remotes Previous commits have introduced the `promisor.acceptFromServerUrl` config variable to allowlist some URLs advertised by a server through the "promisor-remote" protocol capability. However the new `promisor.acceptFromServerUrl` mechanism, like the old `promisor.acceptFromServer` mechanism, still requires a remote to already exist in the client's local configuration before it can be accepted. This places a significant manual burden on users to pre-configure these remotes, and creates friction for administrators who have to troubleshoot or manually provision these setups for their teams. To eliminate this burden, let's automatically create a new `[remote]` section in the client's config when a server advertises an unknown remote whose URL matches a `promisor.acceptFromServerUrl` glob pattern. Concretely, let's add four helpers: - sanitize_remote_name(): turn an arbitrary URL-derived string into a valid remote name by replacing non-alphanumeric characters, collapsing runs of '-', and prepending "promisor-auto-". - promisor_remote_name_from_url(): normalize the URL and extract host+port+path to build a human-readable base name, then pass it through sanitize_remote_name(). - configure_auto_promisor_remote(): write the remote.*.url, remote.*.promisor and remote.*.advertisedAs keys to the repo config. - handle_matching_allowed_url(): pick the final name (user-supplied alias or auto-generated), handle collisions by appending "-1", "-2", etc., then call configure_auto_promisor_remote(). Let's also add should_accept_new_remote_url() which reuses the url_matches_accept_list() helper introduced in a previous commit to find a matching pattern, then delegates to handle_matching_allowed_url() to create the remote. And then let's call should_accept_new_remote_url() from the '!item' (unknown remote) branch of should_accept_remote(), setting `reload_config` so that the newly-written config is picked up. Finally let's document all that by: - expanding the `promisor.acceptFromServerUrl` entry to describe auto-creation, the optional "name=" prefix syntax, the "promisor-auto-*" generation rules, and numeric-suffix collision handling, and by - adding a "remote..advertisedAs" entry to "remote.adoc". Also let's extend the precedence paragraph added by a previous commit to mention this new acceptance path: until now, the only way for `promisor.acceptFromServerUrl` to trigger acceptance was to allow field updates for a known remote. With this commit, it can also trigger auto-creation of a previously-unknown remote whose advertised URL matches the allowlist. Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- Documentation/config/promisor.adoc | 39 +++-- Documentation/config/remote.adoc | 9 ++ promisor-remote.c | 201 +++++++++++++++++++++++++- t/t5710-promisor-remote-capability.sh | 104 +++++++++++++ 4 files changed, 340 insertions(+), 13 deletions(-) diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc index 605473c82f0409..455ce40be892de 100644 --- a/Documentation/config/promisor.adoc +++ b/Documentation/config/promisor.adoc @@ -54,7 +54,8 @@ promisor.acceptFromServer:: promisor.acceptFromServerUrl:: A glob pattern to specify which server-advertised URLs a client is allowed to act on. When a URL matches, the client - will accept the advertised remote as a promisor remote and may + will accept the advertised remote as a promisor remote, may + automatically create a new remote configuration for it and may automatically accept field updates (such as authentication tokens) from the server, even if `promisor.acceptFromServer` is set to `none` (the default). @@ -65,12 +66,13 @@ this option in _ANY_ config file read by Git. + When both `promisor.acceptFromServer` and `promisor.acceptFromServerUrl` are set, `promisor.acceptFromServerUrl` is consulted first and takes -precedence: if a matching pattern leads to acceptance (by accepting -field updates for a known remote whose URL matches both the local -configuration and the allowlist), the advertised remote is accepted -regardless of the `promisor.acceptFromServer` setting. If no pattern -in `promisor.acceptFromServerUrl` triggers acceptance, the decision -is left to `promisor.acceptFromServer`. +precedence: if a matching pattern leads to acceptance (either by +auto-configuring an unknown remote or by accepting field updates for +a known remote whose URL matches both the local configuration and the +allowlist), the advertised remote is accepted regardless of the +`promisor.acceptFromServer` setting. If no pattern in +`promisor.acceptFromServerUrl` triggers acceptance, the decision is +left to `promisor.acceptFromServer`. + Note however that, even when an advertised URL matches a pattern in `promisor.acceptFromServerUrl`, an already-existing remote on the @@ -85,9 +87,10 @@ documentation of that option.) Be _VERY_ careful with these patterns: `*` matches any sequence of characters within the 'host' and 'path' parts of a URL (but cannot cross part boundaries). An overly broad pattern is a major security -risk, as a matching URL allows a server to update fields (such as -authentication tokens) on known remotes without further confirmation. -To minimize security risks, follow these guidelines: +risk, as a matching URL allows a server to auto-configure new remotes +and to update fields (such as authentication tokens) on known remotes +without further confirmation. To minimize security risks, follow these +guidelines: + -- 1. Start with a secure protocol scheme, like `https://` or `ssh://`. @@ -123,6 +126,22 @@ ignored during matching. Note that embedding credentials in URLs is discouraged. Passing authentication tokens via the `token` field of the `promisor-remote` capability is strongly preferred. + +The glob pattern can optionally be prefixed with a remote name and an +equals sign (e.g., `cdn=https://cdn.example.com/*`). If such a prefix +is provided, accepted remotes will be saved under that name. If no +such prefix is provided, a safe remote name will be automatically +generated by sanitizing the URL and prefixing it with +`promisor-auto-`. ++ +If a remote with the chosen name already exists but points to a +different URL, Git will append a numeric suffix (e.g., `-1`, `-2`) to +the name to prevent overwriting existing configurations. You should +make sure that this doesn't happen often though, as remotes will be +rejected if the numeric suffix increases too much. In all cases, the +original name advertised by the server is recorded in the +`remote..advertisedAs` configuration variable for tracing and +debugging purposes. ++ For the security implications of accepting a promisor remote, see the documentation of `promisor.acceptFromServer`. For details on the protocol, see linkgit:gitprotocol-v2[5]. diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc index 91e46f66f5dd1c..6e2bbdf4572db3 100644 --- a/Documentation/config/remote.adoc +++ b/Documentation/config/remote.adoc @@ -91,6 +91,15 @@ remote..promisor:: When set to true, this remote will be used to fetch promisor objects. +remote..advertisedAs:: + When a promisor remote is automatically configured using + information advertised by a server through the + `promisor-remote` protocol capability (see + `promisor.acceptFromServerUrl`), the server's originally + advertised name is saved in this variable. This is for + information, tracing and debugging purposes. Users should not + typically modify or create such configuration entries. + remote..partialclonefilter:: The filter that will be applied when fetching from this promisor remote. Changing or clearing this value will only affect fetches for new commits. diff --git a/promisor-remote.c b/promisor-remote.c index 04a5bb9939928c..8fb5e40f670207 100644 --- a/promisor-remote.c +++ b/promisor-remote.c @@ -813,10 +813,197 @@ static struct allowed_url *url_matches_accept_list( return NULL; } -static int should_accept_remote(enum accept_promisor accept, +/* + * Sanitize the buffer to make it a valid remote name coming from the + * server by: + * + * - replacing any non alphanumeric character with a '-' + * - stripping any leading '-', + * - condensing multiple '-' into one, + * - prepending "promisor-auto-", + * - validating the result. + */ +static int sanitize_remote_name(struct strbuf *buf, const char *url) +{ + char prev = '-'; + for (size_t i = 0; i < buf->len; ) { + if (!isalnum(buf->buf[i])) + buf->buf[i] = '-'; + if (prev == '-' && buf->buf[i] == '-') { + strbuf_remove(buf, i, 1); + } else { + prev = buf->buf[i]; + i++; + } + } + + strbuf_strip_suffix(buf, "-"); + + if (!buf->len) { + warning(_("couldn't generate a valid remote name from " + "advertised url '%s', ignoring this remote"), url); + return -1; + } + + strbuf_insertstr(buf, 0, "promisor-auto-"); + + if (!valid_remote_name(buf->buf)) { + warning(_("generated remote name '%s' from advertised url '%s' " + "is invalid, ignoring this remote"), buf->buf, url); + return -1; + } + + return 0; +} + +static char *promisor_remote_name_from_url(const char *url) +{ + struct url_info url_info = { 0 }; + char *normalized = url_normalize(url, &url_info); + struct strbuf buf = STRBUF_INIT; + + if (!normalized) { + warning(_("couldn't normalize advertised url '%s', " + "ignoring this remote"), url); + return NULL; + } + + if (url_info.host_len) { + strbuf_add(&buf, normalized + url_info.host_off, url_info.host_len); + strbuf_addch(&buf, '-'); + } + + if (url_info.port_len) { + strbuf_add(&buf, normalized + url_info.port_off, url_info.port_len); + strbuf_addch(&buf, '-'); + } + + if (url_info.path_len) { + strbuf_add(&buf, normalized + url_info.path_off, url_info.path_len); + strbuf_trim_trailing_dir_sep(&buf); + strbuf_strip_suffix(&buf, ".git"); + } + + free(normalized); + + if (sanitize_remote_name(&buf, url)) { + strbuf_release(&buf); + return NULL; + } + + return strbuf_detach(&buf, NULL); +} + +static void configure_auto_promisor_remote(struct repository *repo, + const char *name, + const char *url, + const char *advertised_as, + bool reuse) +{ + char *key; + + if (!reuse) { + fprintf(stderr, _("Auto-creating promisor remote '%s' for URL '%s'\n"), + name, url); + + key = xstrfmt("remote.%s.url", name); + repo_config_set_gently(repo, key, url); + free(key); + } + + /* NB: when reusing, this promotes an existing non-promisor remote */ + key = xstrfmt("remote.%s.promisor", name); + repo_config_set_gently(repo, key, "true"); + free(key); + + if (advertised_as) { + key = xstrfmt("remote.%s.advertisedAs", name); + repo_config_set_gently(repo, key, advertised_as); + free(key); + } +} + +#define MAX_REMOTES_WITH_SIMILAR_NAMES 20 + +/* Return the allocated local name, or NULL on failure */ +static char *handle_matching_allowed_url(struct repository *repo, + char *allowed_name, + const char *remote_url, + const char *remote_name) +{ + char *name; + char *basename = allowed_name ? + xstrdup(allowed_name) : + promisor_remote_name_from_url(remote_url); + int i = 0; + bool reuse = false; + + if (!basename) + return NULL; + + name = xstrdup(basename); + + while (i < MAX_REMOTES_WITH_SIMILAR_NAMES) { + char *url_key = xstrfmt("remote.%s.url", name); + const char *existing_url; + int exists = !repo_config_get_string_tmp(repo, url_key, &existing_url); + + free(url_key); + + if (!exists) + break; /* Free to use */ + + if (!strcmp(existing_url, remote_url)) { + reuse = true; + break; /* Same URL, so safe to reuse */ + } + + i++; + free(name); + name = xstrfmt("%s-%d", basename, i); + } + + if (i < MAX_REMOTES_WITH_SIMILAR_NAMES) { + configure_auto_promisor_remote(repo, name, + remote_url, remote_name, + reuse); + } else { + warning(_("too many remotes accepted with name like '%s-X', " + "ignoring this remote"), basename); + FREE_AND_NULL(name); + } + + free(basename); + return name; +} + +static int should_accept_new_remote_url(struct repository *repo, + struct string_list *accept_urls, + struct promisor_info *advertised) +{ + struct allowed_url *allowed = url_matches_accept_list(accept_urls, + advertised->url); + if (allowed) { + char *name = handle_matching_allowed_url(repo, + allowed->remote_name, + advertised->url, + advertised->name); + if (name) { + free((char *)advertised->local_name); + advertised->local_name = name; + return 1; + } + } + + return 0; +} + +static int should_accept_remote(struct repository *repo, + enum accept_promisor accept, struct promisor_info *advertised, struct string_list *accept_urls, - struct string_list *config_info) + struct string_list *config_info, + bool *reload_config) { struct promisor_info *p; struct string_list_item *item; @@ -833,6 +1020,13 @@ static int should_accept_remote(enum accept_promisor accept, if (!item) { /* We don't know about that remote */ + + int res = should_accept_new_remote_url(repo, accept_urls, advertised); + if (res) { + *reload_config = true; + return res; + } + if (accept == ACCEPT_ALL) return all_fields_match(advertised, config_info, NULL); return 0; @@ -1093,7 +1287,8 @@ static void filter_promisor_remote(struct repository *repo, string_list_sort(&config_info); } - if (should_accept_remote(accept, advertised, &accept_urls, &config_info)) { + if (should_accept_remote(repo, accept, advertised, &accept_urls, + &config_info, &reload_config)) { if (!store_info) store_info = store_info_new(repo); if (promisor_store_advertised_fields(advertised, store_info)) diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh index 0659b2ac157f3b..549acff23f1b22 100755 --- a/t/t5710-promisor-remote-capability.sh +++ b/t/t5710-promisor-remote-capability.sh @@ -458,6 +458,107 @@ test_expect_success "clone with 'None', URL allowlisted, but client has differen initialize_server 1 "$oid" ' +test_expect_success "clone with URL allowlisted and no remote already configured" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + test_when_finished "rm -f full_names" && + + GIT_NO_LAZY_FETCH=0 git clone \ + -c promisor.acceptfromserver=None \ + -c promisor.acceptFromServerUrl="$ENCODED_TRASH_DIRECTORY_URL/*" \ + --no-local --filter="blob:limit=5k" server client && + + # Check that exactly one remote has been auto-created, identified + # by "remote..advertisedAs" == "lop". + git -C client config get --all --show-names --regexp \ + "remote\..*\.advertisedas" >full_names && + test_line_count = 1 full_names && + REMOTE_NAME=$(sed "s/^remote\.\(.*\)\.advertisedas .*$/\1/" full_names) && + + # Check ".url" and ".promisor" values + printf "%s\n" "$TRASH_DIRECTORY_URL/lop" "true" >expect && + git -C client config "remote.$REMOTE_NAME.url" >actual && + git -C client config "remote.$REMOTE_NAME.promisor" >>actual && + test_cmp expect actual && + + # Check that the largest object is still missing on the server + check_missing_objects server 1 "$oid" +' + +test_expect_success "clone with named URL allowlisted and no pre-configured remote" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + + GIT_NO_LAZY_FETCH=0 git clone \ + -c promisor.acceptfromserver=None \ + -c promisor.acceptFromServerUrl="cdn=$ENCODED_TRASH_DIRECTORY_URL/*" \ + --no-local --filter="blob:limit=5k" server client && + + # Check that a remote has been auto-created with the right "cdn" name and fields. + printf "%s\n" "$TRASH_DIRECTORY_URL/lop" "true" "lop" >expect && + git -C client config "remote.cdn.url" >actual && + git -C client config "remote.cdn.promisor" >>actual && + git -C client config "remote.cdn.advertisedAs" >>actual && + test_cmp expect actual && + + # Check that the largest object is still missing on the server + check_missing_objects server 1 "$oid" +' + +test_expect_success "clone with URL allowlisted but colliding name" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + + GIT_NO_LAZY_FETCH=0 git clone -c remote.cdn.promisor=true \ + -c remote.cdn.fetch="+refs/heads/*:refs/remotes/lop/*" \ + -c remote.cdn.url="https://example.com/cdn" \ + -c promisor.acceptfromserver=None \ + -c promisor.acceptFromServerUrl="cdn=$ENCODED_TRASH_DIRECTORY_URL/*" \ + --no-local --filter="blob:limit=5k" server client && + + # Check that a remote has been auto-created with the right "cdn-1" name and fields. + printf "%s\n" "$TRASH_DIRECTORY_URL/lop" "true" "lop" >expect && + git -C client config "remote.cdn-1.url" >actual && + git -C client config "remote.cdn-1.promisor" >>actual && + git -C client config "remote.cdn-1.advertisedAs" >>actual && + test_cmp expect actual && + + # Check that the original "cdn" remote was not overwritten. + printf "%s\n" "https://example.com/cdn" "true" >expect && + git -C client config "remote.cdn.url" >actual && + git -C client config "remote.cdn.promisor" >>actual && + test_cmp expect actual && + + # Check that the largest object is still missing on the server + check_missing_objects server 1 "$oid" +' + +test_expect_success "clone with URL allowlisted and reusable remote" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + + GIT_NO_LAZY_FETCH=0 git clone \ + -c remote.cdn.fetch="+refs/heads/*:refs/remotes/lop/*" \ + -c remote.cdn.url="$TRASH_DIRECTORY_URL/lop" \ + -c promisor.acceptfromserver=None \ + -c promisor.acceptFromServerUrl="cdn=$ENCODED_TRASH_DIRECTORY_URL/*" \ + --no-local --filter="blob:limit=5k" server client && + + # Check that the existing "cdn" remote has been properly updated. + printf "%s\n" "$TRASH_DIRECTORY_URL/lop" "true" "lop" "+refs/heads/*:refs/remotes/lop/*" >expect && + git -C client config "remote.cdn.url" >actual && + git -C client config "remote.cdn.promisor" >>actual && + git -C client config "remote.cdn.advertisedAs" >>actual && + git -C client config "remote.cdn.fetch" >>actual && + test_cmp expect actual && + + # Check that no new "cdn-1" remote has been created. + test_must_fail git -C client config "remote.cdn-1.url" && + + # Check that the largest object is still missing on the server + check_missing_objects server 1 "$oid" +' + test_expect_success "clone with invalid promisor.acceptFromServerUrl" ' git -C server config promisor.advertise true && test_when_finished "rm -rf client" && @@ -472,6 +573,9 @@ test_expect_success "clone with invalid promisor.acceptFromServerUrl" ' # Check that a warning was emitted test_grep "invalid remote name '\''bad name'\''" err && + # Check that no remote was auto-created + test_must_fail git -C client config get --regexp "remote\..*\.advertisedas" && + # Check that the largest object is not missing on the server check_missing_objects server 0 "" && From 8f32e6f343b451f04e57dfda31bef04cb23f65a1 Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 27 May 2026 16:08:20 +0200 Subject: [PATCH 09/95] doc: promisor: improve acceptFromServer entry The entry for the `promisor.acceptFromServer` in "Documentation/config/promisor.adoc" has a number of issues: - it's not clear if new remotes and URLs can be created, - it looks like a big block of text, - it's not easy to see all the options, - it's not easy to see which option is the default one, - for "knownName", it says "advertised by the client" instead of "advertised by the server", - it doesn't refer to the new related `acceptFromServerUrl` option. Let's address all these issues by rewording large parts of it and using bullet points for the different options. Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- Documentation/config/promisor.adoc | 53 ++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc index 455ce40be892de..f07a2e883bd96c 100644 --- a/Documentation/config/promisor.adoc +++ b/Documentation/config/promisor.adoc @@ -32,24 +32,41 @@ variable is set to "true", and the "name" and "url" fields are always advertised regardless of this setting. promisor.acceptFromServer:: - If set to "all", a client will accept all the promisor remotes - a server might advertise using the "promisor-remote" - capability. If set to "knownName" the client will accept - promisor remotes which are already configured on the client - and have the same name as those advertised by the client. This - is not very secure, but could be used in a corporate setup - where servers and clients are trusted to not switch name and - URLs. If set to "knownUrl", the client will accept promisor - remotes which have both the same name and the same URL - configured on the client as the name and URL advertised by the - server. This is more secure than "all" or "knownName", so it - should be used if possible instead of those options. Default - is "none", which means no promisor remote advertised by a - server will be accepted. By accepting a promisor remote, the - client agrees that the server might omit objects that are - lazily fetchable from this promisor remote from its responses - to "fetch" and "clone" requests from the client. Name and URL - comparisons are case sensitive. See linkgit:gitprotocol-v2[5]. + Controls which promisor remotes advertised by a server (using the + "promisor-remote" protocol capability) a client will accept. By + accepting a promisor remote, the client agrees that the server + might omit objects that are lazily fetchable from this promisor + remote from its responses to "fetch" and "clone" requests. ++ +Note that this option does not cause new remotes to be automatically +created in the client's configuration. It only allows remotes which +are somehow already configured to be trusted for the current +operation, or their fields to be updated (if `promisor.storeFields` is +set and the remote already exists locally). To allow Git to +automatically create and persist new remotes from server +advertisements, use `promisor.acceptFromServerUrl`. ++ +The available options are: ++ +* `none` (default): No promisor remote advertised by a server will be + accepted. ++ +* `knownUrl`: The client will accept promisor remotes that are already + configured on the client and have both the same name and the same URL + as advertised by the server. This is more secure than `all` or + `knownName`, and should be used if possible instead of those options. ++ +* `knownName`: The client will accept promisor remotes that are already + configured on the client and have the same name as those advertised + by the server. This is not very secure, but could be used in a corporate + setup where servers and clients are trusted to not switch names and URLs. ++ +* `all`: The client will accept all the promisor remotes a server might + advertise. This is the least secure option and should only be used in + fully trusted environments. ++ +Name and URL comparisons are case-sensitive. See linkgit:gitprotocol-v2[5] +for protocol details. promisor.acceptFromServerUrl:: A glob pattern to specify which server-advertised URLs a From 9bef2cf33111dc844e46a8a19c266320774f4bd6 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 11 Jun 2026 08:44:39 +0200 Subject: [PATCH 10/95] builtin/init: stop modifying global `git_work_tree_cfg` variable When executing git-init(1) we need to figure out the final location of the worktree. This location can be configured in a couple of ways: via an environment variable, via the preexisting "core.worktree" config in case we're reinitializing, or implicitly when reinitializing a non-bare repository. When checking for the worktree location in "builtin/init-db.c" we populate any potentially-discovered value both by setting the global `git_work_tree_cfg` variable and via `set_git_work_tree()`, which ultimately ends up modifying `struct repository::worktree`. Modifying `git_work_tree_cfg` is unnecessary though: we configure the worktree in `create_default_files()`, and that function derives the worktree location via `repo_get_work_tree()`. Consequently, propagating the worktree via `set_git_work_tree()` is sufficient. Stop munging `git_work_tree_cfg` and make it file-local to "setup.c" and function-local to `cmd_init_db()`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/init-db.c | 4 ++++ environment.c | 3 --- environment.h | 1 - setup.c | 3 +++ 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/builtin/init-db.c b/builtin/init-db.c index c55517ad94d447..01bc27904e5fed 100644 --- a/builtin/init-db.c +++ b/builtin/init-db.c @@ -229,6 +229,8 @@ int cmd_init_db(int argc, if (!is_bare_repository_cfg) { const char *git_dir_parent = strrchr(git_dir, '/'); + char *git_work_tree_cfg = NULL; + if (git_dir_parent) { char *rel = xstrndup(git_dir, git_dir_parent - git_dir); git_work_tree_cfg = real_pathdup(rel, 1); @@ -243,6 +245,8 @@ int cmd_init_db(int argc, if (access(repo_get_work_tree(the_repository), X_OK)) die_errno (_("Cannot access work tree '%s'"), repo_get_work_tree(the_repository)); + + free(git_work_tree_cfg); } else { if (real_git_dir) diff --git a/environment.c b/environment.c index fc3ed8bb1c7a66..4e86335f250f2a 100644 --- a/environment.c +++ b/environment.c @@ -100,9 +100,6 @@ int auto_comment_line_char; bool warn_on_auto_comment_char; #endif /* !WITH_BREAKING_CHANGES */ -/* This is set by setup_git_directory_gently() and/or git_default_config() */ -char *git_work_tree_cfg; - /* * Repository-local GIT_* environment variables; see environment.h for details. */ diff --git a/environment.h b/environment.h index ccfcf37bfb9b99..5d6e4e6c1bc0cf 100644 --- a/environment.h +++ b/environment.h @@ -149,7 +149,6 @@ int have_git_dir(void); extern int is_bare_repository_cfg; int is_bare_repository(void); -extern char *git_work_tree_cfg; /* Environment bits from configuration mechanism */ extern int trust_executable_bit; diff --git a/setup.c b/setup.c index b4652651dfd454..52228b42a15235 100644 --- a/setup.c +++ b/setup.c @@ -31,6 +31,9 @@ enum allowed_bare_repo { ALLOWED_BARE_REPO_ALL, }; +/* This is set by setup_git_directory_gently() and/or git_default_config() */ +static char *git_work_tree_cfg; + static struct startup_info the_startup_info; struct startup_info *startup_info = &the_startup_info; const char *tmp_original_cwd; From 65eb5b989aa6d7f764d097c6759f6b6189eb0d27 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 11 Jun 2026 08:44:40 +0200 Subject: [PATCH 11/95] builtin/init: simplify logic to configure worktree In the preceding commit we have stopped modifying the global `git_work_tree_cfg` variable. With this change there's now some code paths where we end up setting the local `git_work_tree_cfg` variable, but without actually using the value for anything. Refactor the code a bit so that we only set the worktree configuration in case it's actually needed. Furthermore, reflow it a bit to make the code easier to follow. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/init-db.c | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/builtin/init-db.c b/builtin/init-db.c index 01bc27904e5fed..b4343c28044570 100644 --- a/builtin/init-db.c +++ b/builtin/init-db.c @@ -229,24 +229,29 @@ int cmd_init_db(int argc, if (!is_bare_repository_cfg) { const char *git_dir_parent = strrchr(git_dir, '/'); - char *git_work_tree_cfg = NULL; - if (git_dir_parent) { - char *rel = xstrndup(git_dir, git_dir_parent - git_dir); - git_work_tree_cfg = real_pathdup(rel, 1); - free(rel); - } - if (!git_work_tree_cfg) - git_work_tree_cfg = xgetcwd(); - if (work_tree) + if (work_tree) { set_git_work_tree(the_repository, work_tree); - else - set_git_work_tree(the_repository, git_work_tree_cfg); + } else { + char *work_tree_cfg = NULL; + + if (git_dir_parent) { + char *rel = xstrndup(git_dir, git_dir_parent - git_dir); + work_tree_cfg = real_pathdup(rel, 1); + free(rel); + } + + if (!work_tree_cfg) + work_tree_cfg = xgetcwd(); + + set_git_work_tree(the_repository, work_tree_cfg); + + free(work_tree_cfg); + } + if (access(repo_get_work_tree(the_repository), X_OK)) die_errno (_("Cannot access work tree '%s'"), repo_get_work_tree(the_repository)); - - free(git_work_tree_cfg); } else { if (real_git_dir) From 85f5f504f046bccf86b78ba02064a4b013d7264f Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 11 Jun 2026 08:44:41 +0200 Subject: [PATCH 12/95] setup: remove global `git_work_tree_cfg` variable The global `git_work_tree_cfg` variable used to be modified by both "setup.c" and by "builtin/init-db.c". We have refactored the latter user to not use that variable at all anymore in a preceding commit, which makes "setup.c" the only remaining user. Even for "setup.c" it is unnecessary though, as we only ever set it to the value we have stored in the discovered repository format. The consequence is that we only ever set it in case we already have it set to the same value in our discovered repository format, which makes it redundant. Refactor the code so that we instead use the worktree configuration as discovered via the repository format. Drop the global variable. Note that in `check_repository_format_gently()` we now have to free the candidate work tree variable. This change is required to retain previous semantics: before we essentially had an implicit `else` branch where we set `git_work_tree_cfg = NULL`, but we were able to elide that branch because we already knew that it would be `NULL` anyway. Now that we use the candidate work tree directly to populate the repository's work tree though we have to clear it to retain those semantics. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- setup.c | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/setup.c b/setup.c index 52228b42a15235..71fc6b33da142b 100644 --- a/setup.c +++ b/setup.c @@ -31,9 +31,6 @@ enum allowed_bare_repo { ALLOWED_BARE_REPO_ALL, }; -/* This is set by setup_git_directory_gently() and/or git_default_config() */ -static char *git_work_tree_cfg; - static struct startup_info the_startup_info; struct startup_info *startup_info = &the_startup_info; const char *tmp_original_cwd; @@ -799,13 +796,10 @@ static int check_repository_format_gently(const char *gitdir, } if (!has_common) { - if (candidate->is_bare != -1) { + if (candidate->is_bare != -1) is_bare_repository_cfg = candidate->is_bare; - } - if (candidate->work_tree) { - free(git_work_tree_cfg); - git_work_tree_cfg = xstrdup(candidate->work_tree); - } + } else { + FREE_AND_NULL(candidate->work_tree); } return 0; @@ -1145,7 +1139,7 @@ static const char *setup_explicit_git_dir(struct repository *repo, if (work_tree_env) set_git_work_tree(repo, work_tree_env); else if (is_bare_repository_cfg > 0) { - if (git_work_tree_cfg) { + if (repo_fmt->work_tree) { /* #22.2, #30 */ warning("core.bare and core.worktree do not make sense"); repo->worktree_config_is_bogus = true; @@ -1156,15 +1150,15 @@ static const char *setup_explicit_git_dir(struct repository *repo, free(gitfile); return NULL; } - else if (git_work_tree_cfg) { /* #6, #14 */ - if (is_absolute_path(git_work_tree_cfg)) - set_git_work_tree(repo, git_work_tree_cfg); + else if (repo_fmt->work_tree) { /* #6, #14 */ + if (is_absolute_path(repo_fmt->work_tree)) + set_git_work_tree(repo, repo_fmt->work_tree); else { char *core_worktree; if (chdir(gitdirenv)) die_errno(_("cannot chdir to '%s'"), gitdirenv); - if (chdir(git_work_tree_cfg)) - die_errno(_("cannot chdir to '%s'"), git_work_tree_cfg); + if (chdir(repo_fmt->work_tree)) + die_errno(_("cannot chdir to '%s'"), repo_fmt->work_tree); core_worktree = xgetcwd(); if (chdir(cwd->buf)) die_errno(_("cannot come back to cwd")); @@ -1217,7 +1211,7 @@ static const char *setup_discovered_git_dir(struct repository *repo, return NULL; /* --work-tree is set without --git-dir; use discovered one */ - if (getenv(GIT_WORK_TREE_ENVIRONMENT) || git_work_tree_cfg) { + if (getenv(GIT_WORK_TREE_ENVIRONMENT) || repo_fmt->work_tree) { char *to_free = NULL; const char *ret; @@ -1267,7 +1261,7 @@ static const char *setup_bare_git_dir(struct repository *repo, setenv(GIT_IMPLICIT_WORK_TREE_ENVIRONMENT, "0", 1); /* --work-tree is set without --git-dir; use discovered one */ - if (getenv(GIT_WORK_TREE_ENVIRONMENT) || git_work_tree_cfg) { + if (getenv(GIT_WORK_TREE_ENVIRONMENT) || repo_fmt->work_tree) { static const char *gitdir; gitdir = offset == cwd->len ? "." : xmemdupz(cwd->buf, offset); From f12d73132ea23788167a6b21fad63d85c76fa863 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 11 Jun 2026 08:44:42 +0200 Subject: [PATCH 13/95] builtin/init: stop modifying `is_bare_repository_cfg` We're modifying `is_bare_repository_cfg` in "builtin/init.c" to indicate whether the newly created repository is supposed to be a bare repository or not. This is ultimately unnecessary though: when initializing the repository in `init_db()` we eventually set `is_bare_repository_cfg = !work_tree`, so all that matters is whether or not we have a working tree configured, and the working tree is set up in the non-bare in "builtin/init.c". Stop modifying the global variable in "builtin/init.c" in favor of a local variable. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/init-db.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/builtin/init-db.c b/builtin/init-db.c index b4343c28044570..52aa92fb0a4c87 100644 --- a/builtin/init-db.c +++ b/builtin/init-db.c @@ -81,6 +81,7 @@ int cmd_init_db(int argc, const char *template_dir = NULL; char *template_dir_to_free = NULL; unsigned int flags = 0; + int bare = is_bare_repository_cfg; const char *object_format = NULL; const char *ref_format = NULL; const char *initial_branch = NULL; @@ -90,7 +91,7 @@ int cmd_init_db(int argc, const struct option init_db_options[] = { OPT_STRING(0, "template", &template_dir, N_("template-directory"), N_("directory from which templates will be used")), - OPT_SET_INT(0, "bare", &is_bare_repository_cfg, + OPT_SET_INT(0, "bare", &bare, N_("create a bare repository"), 1), { .type = OPTION_CALLBACK, @@ -116,7 +117,7 @@ int cmd_init_db(int argc, argc = parse_options(argc, argv, prefix, init_db_options, init_db_usage, 0); - if (real_git_dir && is_bare_repository_cfg == 1) + if (real_git_dir && bare == 1) die(_("options '%s' and '%s' cannot be used together"), "--separate-git-dir", "--bare"); if (real_git_dir && !is_absolute_path(real_git_dir)) @@ -160,7 +161,7 @@ int cmd_init_db(int argc, } else if (0 < argc) { usage(init_db_usage[0]); } - if (is_bare_repository_cfg == 1) { + if (bare == 1) { char *cwd = xgetcwd(); setenv(GIT_DIR_ENVIRONMENT, cwd, argc > 0); free(cwd); @@ -187,7 +188,7 @@ int cmd_init_db(int argc, */ git_dir = xstrdup_or_null(getenv(GIT_DIR_ENVIRONMENT)); work_tree = xstrdup_or_null(getenv(GIT_WORK_TREE_ENVIRONMENT)); - if ((!git_dir || is_bare_repository_cfg == 1) && work_tree) + if ((!git_dir || bare == 1) && work_tree) die(_("%s (or --work-tree=) not allowed without " "specifying %s (or --git-dir=)"), GIT_WORK_TREE_ENVIRONMENT, @@ -224,10 +225,10 @@ int cmd_init_db(int argc, strbuf_release(&sb); } - if (is_bare_repository_cfg < 0) - is_bare_repository_cfg = guess_repository_type(git_dir); + if (bare < 0) + bare = guess_repository_type(git_dir); - if (!is_bare_repository_cfg) { + if (!bare) { const char *git_dir_parent = strrchr(git_dir, '/'); if (work_tree) { From 7ff3a5895b6bf4c14d361e4c845d2de7e4006259 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 11 Jun 2026 08:44:43 +0200 Subject: [PATCH 14/95] environment: split up concerns of `is_bare_repository_cfg` The `is_bare_repository_cfg` variable tracks two different pieces of information: - It tracks whether the user has invoked git with the "--bare" flag, which makes us treat any discovered Git repository as if it was a bare repository. - Otherwise it tracks whether the discovered `the_repository` is bare. This makes the flag extremely confusing and creates a bit of a challenge when handling multiple repositories in the same process. Split up the concerns of this variable into two pieces: - `startup_info.force_bare_repository` tracks whether the user has passed the "--bare" flag. This is used as a hint to treat newly set up repositories as bare regardless of whether or not they have a worktree. - `struct repository::bare_cfg` tracks whether or not a repository is considered bare. This takes into account both whether the user has passed "--bare" and the discovered state of the repository itself. Whether or not a repository is bare is now resolved when checking the repository's format, and is then later applied to the repository itself via `apply_repository_format()`. This enables a subsequent change where we make `is_bare_repository()` not depend on global state anymore. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/init-db.c | 2 +- environment.c | 5 ++--- environment.h | 1 - git.c | 2 +- repository.c | 1 + repository.h | 7 +++++++ setup.c | 27 ++++++++++++++++++++------- setup.h | 6 ++++++ worktree.c | 2 +- 9 files changed, 39 insertions(+), 14 deletions(-) diff --git a/builtin/init-db.c b/builtin/init-db.c index 52aa92fb0a4c87..566732c9f4a8ee 100644 --- a/builtin/init-db.c +++ b/builtin/init-db.c @@ -81,7 +81,7 @@ int cmd_init_db(int argc, const char *template_dir = NULL; char *template_dir_to_free = NULL; unsigned int flags = 0; - int bare = is_bare_repository_cfg; + int bare = startup_info->force_bare_repository ? 1 : -1; const char *object_format = NULL; const char *ref_format = NULL; const char *initial_branch = NULL; diff --git a/environment.c b/environment.c index 4e86335f250f2a..9d7c908c55012a 100644 --- a/environment.c +++ b/environment.c @@ -48,7 +48,6 @@ int has_symlinks = 1; int minimum_abbrev = 4, default_abbrev = -1; int ignore_case; int assume_unchanged; -int is_bare_repository_cfg = -1; /* unspecified */ int warn_on_object_refname_ambiguity = 1; char *git_commit_encoding; char *git_log_output_encoding; @@ -136,7 +135,7 @@ const char *getenv_safe(struct strvec *argv, const char *name) int is_bare_repository(void) { /* if core.bare is not 'false', let's see if there is a work tree */ - return is_bare_repository_cfg && !repo_get_work_tree(the_repository); + return the_repository->bare_cfg && !repo_get_work_tree(the_repository); } int have_git_dir(void) @@ -342,7 +341,7 @@ int git_default_core_config(const char *var, const char *value, } if (!strcmp(var, "core.bare")) { - is_bare_repository_cfg = git_config_bool(var, value); + the_repository->bare_cfg = git_config_bool(var, value); return 0; } diff --git a/environment.h b/environment.h index 5d6e4e6c1bc0cf..afb5bcf197d674 100644 --- a/environment.h +++ b/environment.h @@ -147,7 +147,6 @@ void repo_config_values_init(struct repo_config_values *cfg); */ int have_git_dir(void); -extern int is_bare_repository_cfg; int is_bare_repository(void); /* Environment bits from configuration mechanism */ diff --git a/git.c b/git.c index 36f08891ef5476..387eabe38c1951 100644 --- a/git.c +++ b/git.c @@ -255,7 +255,7 @@ static int handle_options(const char ***argv, int *argc, int *envchanged) *envchanged = 1; } else if (!strcmp(cmd, "--bare")) { char *cwd = xgetcwd(); - is_bare_repository_cfg = 1; + startup_info->force_bare_repository = true; setenv(GIT_DIR_ENVIRONMENT, cwd, 0); free(cwd); setenv(GIT_IMPLICIT_WORK_TREE_ENVIRONMENT, "0", 1); diff --git a/repository.c b/repository.c index 187dd471c4e607..c1e91eb0da7b3a 100644 --- a/repository.c +++ b/repository.c @@ -73,6 +73,7 @@ void initialize_repository(struct repository *repo) ALLOC_ARRAY(repo->index, 1); index_state_init(repo->index, repo); repo->check_deprecated_config = true; + repo->bare_cfg = -1; repo_config_values_init(&repo->config_values_private_); /* diff --git a/repository.h b/repository.h index 36e2db26332c0e..7d649e32e7fad3 100644 --- a/repository.h +++ b/repository.h @@ -117,6 +117,13 @@ struct repository { bool worktree_initialized; bool worktree_config_is_bogus; + /* + * Whether the repository is bare, as set by "core.bare" config or + * inferred during repository discovery. -1 means unset/unknown, 0 + * means non-bare, 1 means bare. + */ + int bare_cfg; + /* * Path from the root of the top-level superproject down to this * repository. This is only non-NULL if the repository is initialized diff --git a/setup.c b/setup.c index 71fc6b33da142b..32f14a868860ea 100644 --- a/setup.c +++ b/setup.c @@ -795,10 +795,22 @@ static int check_repository_format_gently(const char *gitdir, has_common = 0; } - if (!has_common) { - if (candidate->is_bare != -1) - is_bare_repository_cfg = candidate->is_bare; - } else { + if (startup_info->force_bare_repository) { + candidate->is_bare = 1; + FREE_AND_NULL(candidate->work_tree); + } else if (has_common) { + /* + * When sharing a common dir with another repository (e.g. a + * linked worktree), do not let this repository's config + * dictate bareness; it is inherited from the main worktree. + */ + candidate->is_bare = -1; + + /* + * Furthermore, "core.worktree" is supposed to be ignored when + * we have a commondir configured, unless it comes from the + * per-worktree configuration. + */ FREE_AND_NULL(candidate->work_tree); } @@ -1138,7 +1150,7 @@ static const char *setup_explicit_git_dir(struct repository *repo, /* #3, #7, #11, #15, #19, #23, #27, #31 (see t1510) */ if (work_tree_env) set_git_work_tree(repo, work_tree_env); - else if (is_bare_repository_cfg > 0) { + else if (repo_fmt->is_bare > 0) { if (repo_fmt->work_tree) { /* #22.2, #30 */ warning("core.bare and core.worktree do not make sense"); @@ -1225,7 +1237,7 @@ static const char *setup_discovered_git_dir(struct repository *repo, } /* #16.2, #17.2, #20.2, #21.2, #24, #25, #28, #29 (see t1510) */ - if (is_bare_repository_cfg > 0) { + if (repo_fmt->is_bare > 0) { set_git_dir(repo, gitdir, (offset != cwd->len)); if (chdir(cwd->buf)) die_errno(_("cannot come back to cwd")); @@ -1762,6 +1774,7 @@ int apply_repository_format(struct repository *repo, alternate_object_directories = xstrdup_or_null(getenv(ALTERNATE_DB_ENVIRONMENT)); } + repo->bare_cfg = format->is_bare; repo_set_hash_algo(repo, format->hash_algo); repo->objects = odb_new(repo, object_directory, alternate_object_directories); @@ -2571,7 +2584,7 @@ static int create_default_files(struct repository *repo, repo_settings_set_shared_repository(repo, init_shared_repository); - is_bare_repository_cfg = !work_tree; + repo->bare_cfg = !work_tree; /* * We would have created the above under user's umask -- under diff --git a/setup.h b/setup.h index 705d1d6ff79685..b9fd96bea6e639 100644 --- a/setup.h +++ b/setup.h @@ -292,6 +292,12 @@ enum sharedrepo { int git_config_perm(const char *var, const char *value); struct startup_info { + /* + * Whether the user is asking us to treat the repository as bare via + * `git --bare`, even if it's not. + */ + bool force_bare_repository; + int have_repository; const char *prefix; const char *original_cwd; diff --git a/worktree.c b/worktree.c index 97eddc39166953..7d70f2c1dab15f 100644 --- a/worktree.c +++ b/worktree.c @@ -123,7 +123,7 @@ static struct worktree *get_main_worktree(int skip_reading_head) worktree->repo = the_repository; worktree->path = strbuf_detach(&worktree_path, NULL); worktree->is_current = is_current_worktree(worktree); - worktree->is_bare = (is_bare_repository_cfg == 1) || + worktree->is_bare = (the_repository->bare_cfg == 1) || is_bare_repository() || /* * When in a secondary worktree we have to also verify if the main From 2e1d55626f06be7b9374c0a6f579959d750f0cfb Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 11 Jun 2026 08:44:44 +0200 Subject: [PATCH 15/95] environment: stop using `the_repository` in `is_bare_repository()` Refactor `is_bare_repository()` to take in a repository parameter so that we no longer depend on `the_repository`. Adjust callers accordingly. Furthermore, move the function outside of the declarations that are only available when `USE_THE_REPOSITORY_VARIABLE` is set, as it no longer depends on that variable. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- attr.c | 4 ++-- builtin/bisect.c | 2 +- builtin/blame.c | 2 +- builtin/check-attr.c | 2 +- builtin/fetch.c | 2 +- builtin/gc.c | 2 +- builtin/history.c | 2 +- builtin/repack.c | 2 +- builtin/repo.c | 2 +- builtin/reset.c | 2 +- builtin/rev-parse.c | 2 +- environment.c | 4 ++-- environment.h | 4 ++-- mailmap.c | 4 ++-- refs/files-backend.c | 2 +- refs/reftable-backend.c | 2 +- setup.c | 2 +- transport.c | 4 ++-- worktree.c | 2 +- 19 files changed, 24 insertions(+), 24 deletions(-) diff --git a/attr.c b/attr.c index 75369547b306d6..04cb2849541d6f 100644 --- a/attr.c +++ b/attr.c @@ -681,7 +681,7 @@ static enum git_attr_direction direction; void git_attr_set_direction(enum git_attr_direction new_direction) { - if (is_bare_repository() && new_direction != GIT_ATTR_INDEX) + if (is_bare_repository(the_repository) && new_direction != GIT_ATTR_INDEX) BUG("non-INDEX attr direction in a bare repo"); if (new_direction != direction) @@ -848,7 +848,7 @@ static struct attr_stack *read_attr(struct index_state *istate, res = read_attr_from_index(istate, path, flags); } else if (tree_oid) { res = read_attr_from_blob(istate, tree_oid, path, flags); - } else if (!is_bare_repository()) { + } else if (!is_bare_repository(the_repository)) { if (direction == GIT_ATTR_CHECKOUT) { res = read_attr_from_index(istate, path, flags); if (!res) diff --git a/builtin/bisect.c b/builtin/bisect.c index e7c2d2f3bb0f4a..798e28f5012d31 100644 --- a/builtin/bisect.c +++ b/builtin/bisect.c @@ -724,7 +724,7 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc, struct object_id oid; const char *head; - if (is_bare_repository()) + if (is_bare_repository(the_repository)) no_checkout = 1; /* diff --git a/builtin/blame.c b/builtin/blame.c index ffbd3ce5c5a2e3..553f4cb78055eb 100644 --- a/builtin/blame.c +++ b/builtin/blame.c @@ -1163,7 +1163,7 @@ int cmd_blame(int argc, revs.disable_stdin = 1; setup_revisions(argc, argv, &revs, NULL); - if (!revs.pending.nr && is_bare_repository()) { + if (!revs.pending.nr && is_bare_repository(the_repository)) { struct commit *head_commit; struct object_id head_oid; diff --git a/builtin/check-attr.c b/builtin/check-attr.c index 98f64d5b922e6c..217d83ea7d5de0 100644 --- a/builtin/check-attr.c +++ b/builtin/check-attr.c @@ -116,7 +116,7 @@ int cmd_check_attr(int argc, struct object_id initialized_oid; int cnt, i, doubledash, filei; - if (!is_bare_repository()) + if (!is_bare_repository(the_repository)) setup_work_tree(the_repository); repo_config(the_repository, git_default_config, NULL); diff --git a/builtin/fetch.c b/builtin/fetch.c index c1d7c672f4e0d8..44b8c70da1bdbb 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -1764,7 +1764,7 @@ static int set_head(const struct ref *remote_refs, struct remote *remote) if (!head_name) goto cleanup; - baremirror = is_bare_repository() && remote->mirror; + baremirror = is_bare_repository(the_repository) && remote->mirror; create_only = follow_remote_head == FOLLOW_REMOTE_ALWAYS ? 0 : !baremirror; if (baremirror) { strbuf_addstr(&b_head, "HEAD"); diff --git a/builtin/gc.c b/builtin/gc.c index 84a66d32404e4d..61da30de9f3c8f 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -902,7 +902,7 @@ int cmd_gc(int argc, die(_("failed to parse gc.logExpiry value %s"), cfg.gc_log_expire); if (cfg.pack_refs < 0) - cfg.pack_refs = !is_bare_repository(); + cfg.pack_refs = !is_bare_repository(the_repository); argc = parse_options(argc, argv, prefix, builtin_gc_options, builtin_gc_usage, 0); diff --git a/builtin/history.c b/builtin/history.c index 091465a59e2f96..fd83de8265e817 100644 --- a/builtin/history.c +++ b/builtin/history.c @@ -525,7 +525,7 @@ static int cmd_history_fixup(int argc, if (action == REF_ACTION_DEFAULT) action = REF_ACTION_BRANCHES; - if (is_bare_repository()) { + if (is_bare_repository(repo)) { ret = error(_("cannot run fixup in a bare repository")); goto out; } diff --git a/builtin/repack.c b/builtin/repack.c index 1524a9c13ad5b8..bbc6f51639d89c 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -265,7 +265,7 @@ int cmd_repack(int argc, if (write_bitmaps < 0) { if (write_midx == REPACK_WRITE_MIDX_NONE && - (!(pack_everything & ALL_INTO_ONE) || !is_bare_repository())) + (!(pack_everything & ALL_INTO_ONE) || !is_bare_repository(the_repository))) write_bitmaps = 0; } if (po_args.pack_kept_objects < 0) diff --git a/builtin/repo.c b/builtin/repo.c index 71a5c1c29c05fe..34e96514bc2b77 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -58,7 +58,7 @@ struct repo_info_field { static int get_layout_bare(struct repository *repo UNUSED, struct strbuf *buf) { - strbuf_addstr(buf, is_bare_repository() ? "true" : "false"); + strbuf_addstr(buf, is_bare_repository(the_repository) ? "true" : "false"); return 0; } diff --git a/builtin/reset.c b/builtin/reset.c index 3be6bd0121afe5..78e69bd84ba2c3 100644 --- a/builtin/reset.c +++ b/builtin/reset.c @@ -470,7 +470,7 @@ int cmd_reset(int argc, if (reset_type != SOFT && (reset_type != MIXED || repo_get_work_tree(the_repository))) setup_work_tree(the_repository); - if (reset_type == MIXED && is_bare_repository()) + if (reset_type == MIXED && is_bare_repository(the_repository)) die(_("%s reset is not allowed in a bare repository"), _(reset_type_names[reset_type])); diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c index bb882678fe2a9e..090e5cfbb0b165 100644 --- a/builtin/rev-parse.c +++ b/builtin/rev-parse.c @@ -1084,7 +1084,7 @@ int cmd_rev_parse(int argc, continue; } if (!strcmp(arg, "--is-bare-repository")) { - printf("%s\n", is_bare_repository() ? "true" + printf("%s\n", is_bare_repository(the_repository) ? "true" : "false"); continue; } diff --git a/environment.c b/environment.c index 9d7c908c55012a..bf209534153b2b 100644 --- a/environment.c +++ b/environment.c @@ -132,10 +132,10 @@ const char *getenv_safe(struct strvec *argv, const char *name) return argv->v[argv->nr - 1]; } -int is_bare_repository(void) +int is_bare_repository(struct repository *repo) { /* if core.bare is not 'false', let's see if there is a work tree */ - return the_repository->bare_cfg && !repo_get_work_tree(the_repository); + return repo->bare_cfg && !repo_get_work_tree(repo); } int have_git_dir(void) diff --git a/environment.h b/environment.h index afb5bcf197d674..164a55df2ca225 100644 --- a/environment.h +++ b/environment.h @@ -125,6 +125,8 @@ int git_default_core_config(const char *var, const char *value, void repo_config_values_init(struct repo_config_values *cfg); +int is_bare_repository(struct repository *repo); + /* * TODO: All the below state either explicitly or implicitly relies on * `the_repository`. We should eventually get rid of these and make the @@ -147,8 +149,6 @@ void repo_config_values_init(struct repo_config_values *cfg); */ int have_git_dir(void); -int is_bare_repository(void); - /* Environment bits from configuration mechanism */ extern int trust_executable_bit; extern int trust_ctime; diff --git a/mailmap.c b/mailmap.c index 3b2691781d8ff1..7d8590cdd613fb 100644 --- a/mailmap.c +++ b/mailmap.c @@ -219,10 +219,10 @@ int read_mailmap(struct repository *repo, struct string_list *map) map->strdup_strings = 1; map->cmp = namemap_cmp; - if (!mailmap_blob && is_bare_repository()) + if (!mailmap_blob && is_bare_repository(the_repository)) mailmap_blob = xstrdup("HEAD:.mailmap"); - if (!startup_info->have_repository || !is_bare_repository()) + if (!startup_info->have_repository || !is_bare_repository(the_repository)) err |= read_mailmap_file(map, ".mailmap", startup_info->have_repository ? MAILMAP_NOFOLLOW : 0); diff --git a/refs/files-backend.c b/refs/files-backend.c index a4c7858787127d..2b270914840582 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -1865,7 +1865,7 @@ static int log_ref_setup(struct files_ref_store *refs, char *logfile; if (log_refs_cfg == LOG_REFS_UNSET) - log_refs_cfg = is_bare_repository() ? LOG_REFS_NONE : LOG_REFS_NORMAL; + log_refs_cfg = is_bare_repository(the_repository) ? LOG_REFS_NONE : LOG_REFS_NORMAL; files_reflog_path(refs, &logfile_sb, refname); logfile = strbuf_detach(&logfile_sb, NULL); diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index 4ae22922de558b..101ef29ac868d5 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -288,7 +288,7 @@ static int should_write_log(struct reftable_ref_store *refs, const char *refname { enum log_refs_config log_refs_cfg = refs->log_all_ref_updates; if (log_refs_cfg == LOG_REFS_UNSET) - log_refs_cfg = is_bare_repository() ? LOG_REFS_NONE : LOG_REFS_NORMAL; + log_refs_cfg = is_bare_repository(the_repository) ? LOG_REFS_NONE : LOG_REFS_NORMAL; switch (log_refs_cfg) { case LOG_REFS_NONE: diff --git a/setup.c b/setup.c index 32f14a868860ea..e6db80ab0728e6 100644 --- a/setup.c +++ b/setup.c @@ -2610,7 +2610,7 @@ static int create_default_files(struct repository *repo, } repo_config_set(repo, "core.filemode", filemode ? "true" : "false"); - if (is_bare_repository()) + if (is_bare_repository(the_repository)) repo_config_set(repo, "core.bare", "true"); else { repo_config_set(repo, "core.bare", "false"); diff --git a/transport.c b/transport.c index 0f5ec302472d53..fc144f0aedabd9 100644 --- a/transport.c +++ b/transport.c @@ -1482,7 +1482,7 @@ int transport_push(struct repository *r, if ((flags & (TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND | TRANSPORT_RECURSE_SUBMODULES_ONLY)) && - !is_bare_repository()) { + !is_bare_repository(the_repository)) { struct ref *ref = remote_refs; struct oid_array commits = OID_ARRAY_INIT; @@ -1509,7 +1509,7 @@ int transport_push(struct repository *r, if (((flags & TRANSPORT_RECURSE_SUBMODULES_CHECK) || ((flags & (TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND | TRANSPORT_RECURSE_SUBMODULES_ONLY)) && - !pretend)) && !is_bare_repository()) { + !pretend)) && !is_bare_repository(the_repository)) { struct ref *ref = remote_refs; struct string_list needs_pushing = STRING_LIST_INIT_DUP; struct oid_array commits = OID_ARRAY_INIT; diff --git a/worktree.c b/worktree.c index 7d70f2c1dab15f..30125827fd39ed 100644 --- a/worktree.c +++ b/worktree.c @@ -124,7 +124,7 @@ static struct worktree *get_main_worktree(int skip_reading_head) worktree->path = strbuf_detach(&worktree_path, NULL); worktree->is_current = is_current_worktree(worktree); worktree->is_bare = (the_repository->bare_cfg == 1) || - is_bare_repository() || + is_bare_repository(the_repository) || /* * When in a secondary worktree we have to also verify if the main * worktree is bare in $commondir/config.worktree. From 1ceee7431b40aba69707835b9b65b0ed7a9cb973 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 11 Jun 2026 08:44:45 +0200 Subject: [PATCH 16/95] treewide: drop USE_THE_REPOSITORY_VARIABLE Adapt a couple of trivial callers of `is_bare_repository()` to instead use a repository available via the caller's context so that we can drop the `USE_THE_REPOSITORY_VARIABLE` macro. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/repack.c | 3 +-- mailmap.c | 6 ++---- refs/reftable-backend.c | 4 +--- setup.c | 3 +-- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/builtin/repack.c b/builtin/repack.c index bbc6f51639d89c..d0465fb4f562e2 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -1,4 +1,3 @@ -#define USE_THE_REPOSITORY_VARIABLE #define DISABLE_SIGN_COMPARE_WARNINGS #include "builtin.h" @@ -265,7 +264,7 @@ int cmd_repack(int argc, if (write_bitmaps < 0) { if (write_midx == REPACK_WRITE_MIDX_NONE && - (!(pack_everything & ALL_INTO_ONE) || !is_bare_repository(the_repository))) + (!(pack_everything & ALL_INTO_ONE) || !is_bare_repository(repo))) write_bitmaps = 0; } if (po_args.pack_kept_objects < 0) diff --git a/mailmap.c b/mailmap.c index 7d8590cdd613fb..2d5514f833398b 100644 --- a/mailmap.c +++ b/mailmap.c @@ -1,5 +1,3 @@ -#define USE_THE_REPOSITORY_VARIABLE - #include "git-compat-util.h" #include "environment.h" #include "string-list.h" @@ -219,10 +217,10 @@ int read_mailmap(struct repository *repo, struct string_list *map) map->strdup_strings = 1; map->cmp = namemap_cmp; - if (!mailmap_blob && is_bare_repository(the_repository)) + if (!mailmap_blob && is_bare_repository(repo)) mailmap_blob = xstrdup("HEAD:.mailmap"); - if (!startup_info->have_repository || !is_bare_repository(the_repository)) + if (!startup_info->have_repository || !is_bare_repository(repo)) err |= read_mailmap_file(map, ".mailmap", startup_info->have_repository ? MAILMAP_NOFOLLOW : 0); diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index 101ef29ac868d5..c151d331e75836 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -1,5 +1,3 @@ -#define USE_THE_REPOSITORY_VARIABLE - #include "../git-compat-util.h" #include "../abspath.h" #include "../chdir-notify.h" @@ -288,7 +286,7 @@ static int should_write_log(struct reftable_ref_store *refs, const char *refname { enum log_refs_config log_refs_cfg = refs->log_all_ref_updates; if (log_refs_cfg == LOG_REFS_UNSET) - log_refs_cfg = is_bare_repository(the_repository) ? LOG_REFS_NONE : LOG_REFS_NORMAL; + log_refs_cfg = is_bare_repository(refs->base.repo) ? LOG_REFS_NONE : LOG_REFS_NORMAL; switch (log_refs_cfg) { case LOG_REFS_NONE: diff --git a/setup.c b/setup.c index e6db80ab0728e6..65f4ac95a8dd8c 100644 --- a/setup.c +++ b/setup.c @@ -1,4 +1,3 @@ -#define USE_THE_REPOSITORY_VARIABLE #define DISABLE_SIGN_COMPARE_WARNINGS #include "git-compat-util.h" @@ -2610,7 +2609,7 @@ static int create_default_files(struct repository *repo, } repo_config_set(repo, "core.filemode", filemode ? "true" : "false"); - if (is_bare_repository(the_repository)) + if (is_bare_repository(repo)) repo_config_set(repo, "core.bare", "true"); else { repo_config_set(repo, "core.bare", "false"); From 71386c21dfb7cea181df6707c34cd79b10fc0a2b Mon Sep 17 00:00:00 2001 From: Tian Yuchen Date: Wed, 10 Jun 2026 20:43:52 +0800 Subject: [PATCH 17/95] environment: move 'protect_hfs' and 'protect_ntfs' into 'repo_config_values' Move the global 'protect_hfs' and 'protect_ntfs' configurations into the repository-specific 'repo_config_values' struct. This will help with the elimination of 'the_repository' To ensure code readability, the getter functions 'repo_protect_hfs()' and 'repo_protect_ntfs()' have been introduced. For now, associated functions access this configuration by explicitly falling back to 'the_repository', which needs to be addressed in the future. Note: In 't/helper/test-path-utils.c', there is a function 'protect_ntfs_hfs_benchmark()' where these two global variables are used as loop iterators. New local variables have been created to replace them. Mentored-by: Christian Couder Mentored-by: Ayush Chandekar Mentored-by: Olamide Caleb Bello Signed-off-by: Tian Yuchen Signed-off-by: Junio C Hamano --- compat/mingw.c | 2 +- environment.c | 22 ++++++++++++++++++---- environment.h | 12 ++++++++++-- read-cache.c | 7 ++++--- t/helper/test-path-utils.c | 24 +++++++++++++++--------- 5 files changed, 48 insertions(+), 19 deletions(-) diff --git a/compat/mingw.c b/compat/mingw.c index aa7525f419cb64..af87df77fd082f 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -3392,7 +3392,7 @@ int is_valid_win32_path(const char *path, int allow_literal_nul) const char *p = path; int preceding_space_or_period = 0, i = 0, periods = 0; - if (!protect_ntfs) + if (!repo_protect_ntfs(the_repository)) return 1; skip_dos_drive_prefix((char **)&path); diff --git a/environment.c b/environment.c index fc3ed8bb1c7a66..683fe1b4d35257 100644 --- a/environment.c +++ b/environment.c @@ -82,12 +82,10 @@ unsigned long pack_size_limit_cfg; #ifndef PROTECT_HFS_DEFAULT #define PROTECT_HFS_DEFAULT 0 #endif -int protect_hfs = PROTECT_HFS_DEFAULT; #ifndef PROTECT_NTFS_DEFAULT #define PROTECT_NTFS_DEFAULT 1 #endif -int protect_ntfs = PROTECT_NTFS_DEFAULT; /* * The character that begins a commented line in user-editable file @@ -142,6 +140,20 @@ int is_bare_repository(void) return is_bare_repository_cfg && !repo_get_work_tree(the_repository); } +int repo_protect_ntfs(struct repository *repo) +{ + return repo->gitdir ? + repo_config_values(repo)->protect_ntfs : + PROTECT_NTFS_DEFAULT; +} + +int repo_protect_hfs(struct repository *repo) +{ + return repo->gitdir ? + repo_config_values(repo)->protect_hfs : + PROTECT_HFS_DEFAULT; +} + int have_git_dir(void) { return startup_info->have_repository @@ -541,12 +553,12 @@ int git_default_core_config(const char *var, const char *value, } if (!strcmp(var, "core.protecthfs")) { - protect_hfs = git_config_bool(var, value); + cfg->protect_hfs = git_config_bool(var, value); return 0; } if (!strcmp(var, "core.protectntfs")) { - protect_ntfs = git_config_bool(var, value); + cfg->protect_ntfs = git_config_bool(var, value); return 0; } @@ -720,5 +732,7 @@ void repo_config_values_init(struct repo_config_values *cfg) { cfg->attributes_file = NULL; cfg->apply_sparse_checkout = 0; + cfg->protect_hfs = PROTECT_HFS_DEFAULT; + cfg->protect_ntfs = PROTECT_NTFS_DEFAULT; cfg->branch_track = BRANCH_TRACK_REMOTE; } diff --git a/environment.h b/environment.h index 9eb97b3869c9b1..fdd9775900701d 100644 --- a/environment.h +++ b/environment.h @@ -91,6 +91,8 @@ struct repo_config_values { /* section "core" config values */ char *attributes_file; int apply_sparse_checkout; + int protect_hfs; + int protect_ntfs; /* section "branch" config values */ enum branch_track branch_track; @@ -123,6 +125,14 @@ int git_default_config(const char *, const char *, int git_default_core_config(const char *var, const char *value, const struct config_context *ctx, void *cb); +/* + * Getters for the `protect_hfs` and `protect_ntfs` fields of `struct repo_config_values`. + * They check `repo->gitdir` to prevent calling repo_config_values() + * before the configuration is loaded or in bare environments. + */ +int repo_protect_hfs(struct repository *repo); +int repo_protect_ntfs(struct repository *repo); + void repo_config_values_init(struct repo_config_values *cfg); /* @@ -173,8 +183,6 @@ extern int pack_compression_level; extern unsigned long pack_size_limit_cfg; extern int precomposed_unicode; -extern int protect_hfs; -extern int protect_ntfs; extern int core_sparse_checkout_cone; extern int sparse_expect_files_outside_of_patterns; diff --git a/read-cache.c b/read-cache.c index 21829102ae275e..2c6a60c7564a5b 100644 --- a/read-cache.c +++ b/read-cache.c @@ -1002,7 +1002,7 @@ static enum verify_path_result verify_path_internal(const char *path, return PATH_OK; if (is_dir_sep(c)) { inside: - if (protect_hfs) { + if (repo_protect_hfs(the_repository)) { if (is_hfs_dotgit(path)) return PATH_INVALID; @@ -1011,7 +1011,7 @@ static enum verify_path_result verify_path_internal(const char *path, return PATH_INVALID; } } - if (protect_ntfs) { + if (repo_protect_ntfs(the_repository)) { #if defined GIT_WINDOWS_NATIVE || defined __CYGWIN__ if (c == '\\') return PATH_INVALID; @@ -1035,7 +1035,8 @@ static enum verify_path_result verify_path_internal(const char *path, if (c == '\0') return S_ISDIR(mode) ? PATH_DIR_WITH_SEP : PATH_INVALID; - } else if (c == '\\' && protect_ntfs) { + } else if (c == '\\' && + repo_protect_ntfs(the_repository)) { if (is_ntfs_dotgit(path)) return PATH_INVALID; if (S_ISLNK(mode)) { diff --git a/t/helper/test-path-utils.c b/t/helper/test-path-utils.c index 15eb44485cda3d..f77b3f9d70fe0d 100644 --- a/t/helper/test-path-utils.c +++ b/t/helper/test-path-utils.c @@ -250,6 +250,7 @@ static int protect_ntfs_hfs_benchmark(int argc, const char **argv) double m[3][2], v[3][2]; uint64_t cumul; double cumul2; + int ntfs, hfs; if (argc > 1 && !strcmp(argv[1], "--with-symlink-mode")) { file_mode = 0120000; @@ -276,8 +277,13 @@ static int protect_ntfs_hfs_benchmark(int argc, const char **argv) names[i][--len] = (char)(' ' + (my_random() % ('\x7f' - ' '))); } - for (protect_ntfs = 0; protect_ntfs < 2; protect_ntfs++) - for (protect_hfs = 0; protect_hfs < 2; protect_hfs++) { + if (!the_repository->gitdir) + the_repository->gitdir = xstrdup(".git"); + + for (ntfs = 0; ntfs < 2; ntfs++) + for (hfs = 0; hfs < 2; hfs++) { + repo_config_values(the_repository)->protect_ntfs = ntfs; + repo_config_values(the_repository)->protect_hfs = hfs; cumul = 0; cumul2 = 0; for (i = 0; i < repetitions; i++) { @@ -285,18 +291,18 @@ static int protect_ntfs_hfs_benchmark(int argc, const char **argv) for (j = 0; j < nr; j++) verify_path(names[j], file_mode); end = getnanotime(); - printf("protect_ntfs = %d, protect_hfs = %d: %lfms\n", protect_ntfs, protect_hfs, (end-begin) / (double)1e6); + printf("protect_ntfs = %d, protect_hfs = %d: %lfms\n", ntfs, hfs, (end-begin) / (double)1e6); cumul += end - begin; cumul2 += (end - begin) * (end - begin); } - m[protect_ntfs][protect_hfs] = cumul / (double)repetitions; - v[protect_ntfs][protect_hfs] = my_sqrt(cumul2 / (double)repetitions - m[protect_ntfs][protect_hfs] * m[protect_ntfs][protect_hfs]); - printf("mean: %lfms, stddev: %lfms\n", m[protect_ntfs][protect_hfs] / (double)1e6, v[protect_ntfs][protect_hfs] / (double)1e6); + m[ntfs][hfs] = cumul / (double)repetitions; + v[ntfs][hfs] = my_sqrt(cumul2 / (double)repetitions - m[ntfs][hfs] * m[ntfs][hfs]); + printf("mean: %lfms, stddev: %lfms\n", m[ntfs][hfs] / (double)1e6, v[ntfs][hfs] / (double)1e6); } - for (protect_ntfs = 0; protect_ntfs < 2; protect_ntfs++) - for (protect_hfs = 0; protect_hfs < 2; protect_hfs++) - printf("ntfs=%d/hfs=%d: %lf%% slower\n", protect_ntfs, protect_hfs, (m[protect_ntfs][protect_hfs] - m[0][0]) * 100 / m[0][0]); + for (ntfs = 0; ntfs < 2; ntfs++) + for (hfs = 0; hfs < 2; hfs++) + printf("ntfs=%d/hfs=%d: %lf%% slower\n", ntfs, hfs, (m[ntfs][hfs] - m[0][0]) * 100 / m[0][0]); return 0; } From ad2e20d8aab0d4d43fbac407736f05a94e65c49b Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Fri, 12 Jun 2026 17:27:44 -0400 Subject: [PATCH 18/95] ref-filter: restore prefix-scoped iteration dabecb9db2 (for-each-ref: introduce a '--start-after' option, 2025-07-15) changed branch, remote-tracking branch, and tag enumeration from constructing an iterator with the namespace prefix to constructing an unscoped iterator and seeking to the prefix. Review of --start-after noted that the construction prefix and seek position represent different state and are easy to conflate [1]. It also noted that future branch or tag support would need to retain the namespace prefix while moving the cursor [2]. The files backend constructs its loose-ref iterator with cache priming enabled. cache_ref_iterator_begin() immediately applies the construction prefix through cache_ref_iterator_set_prefix(), reading loose refs beneath it before packed refs are opened. An empty prefix therefore reads every loose ref, and a later seek cannot undo that I/O. For the current single-kind filters, construct the iterator with the namespace prefix when start_after is not set. Leave the existing start_after path unchanged; no current command combines it with these filters, and future support must carry the prefix separately from the cursor. With 10,000 unrelated loose refs in the files backend, the p6300 tests improve as follows: before after branch 2.74 s 0.11 s branch --remotes 2.81 s 0.12 s tag 3.01 s 0.11 s [1] https://lore.kernel.org/r/aGZidwwlToWThkn8@pks.im/ [2] https://lore.kernel.org/r/xmqqikjq7s16.fsf@gitster.g/ Fixes: dabecb9db2b2 ("for-each-ref: introduce a '--start-after' option") Suggested-by: Karthik Nayak Signed-off-by: Tamir Duberstein Signed-off-by: Junio C Hamano --- ref-filter.c | 13 ++++++------ t/perf/p6300-for-each-ref.sh | 39 +++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/ref-filter.c b/ref-filter.c index 1da4c0e60df3fa..9b04e3af85ef1c 100644 --- a/ref-filter.c +++ b/ref-filter.c @@ -3316,15 +3316,14 @@ static int do_filter_refs(struct ref_filter *filter, unsigned int type, refs_for if (prefix) { struct ref_iterator *iter; + struct ref_store *store = get_main_ref_store(the_repository); - iter = refs_ref_iterator_begin(get_main_ref_store(the_repository), - "", NULL, 0, 0); - - if (filter->start_after) + if (filter->start_after) { + iter = refs_ref_iterator_begin(store, "", NULL, 0, 0); ret = start_ref_iterator_after(iter, filter->start_after); - else - ret = ref_iterator_seek(iter, prefix, - REF_ITERATOR_SEEK_SET_PREFIX); + } else { + iter = refs_ref_iterator_begin(store, prefix, NULL, 0, 0); + } if (!ret) ret = do_for_each_ref_iterator(iter, fn, cb_data); diff --git a/t/perf/p6300-for-each-ref.sh b/t/perf/p6300-for-each-ref.sh index fa7289c7522b10..25ffa5e84cf8b7 100755 --- a/t/perf/p6300-for-each-ref.sh +++ b/t/perf/p6300-for-each-ref.sh @@ -1,6 +1,6 @@ #!/bin/sh -test_description='performance of for-each-ref' +test_description='performance of ref-filter users' . ./perf-lib.sh test_perf_fresh_repo @@ -84,4 +84,41 @@ test_expect_success 'pack refs' ' ' run_tests "packed" +test_expect_success 'setup many unrelated refs' ' + git init scoped && + test_commit -C scoped --no-tag base && + test_seq $ref_count_per_type | + sed "s,.*,update refs/custom/unrelated_& HEAD," | + git -C scoped update-ref --stdin && + git -C scoped update-ref refs/remotes/origin/main HEAD && + git -C scoped update-ref refs/tags/only HEAD +' + +test_perf "branch (many unrelated refs)" " + ( + cd scoped && + for i in \$(test_seq $test_iteration_count); do + git branch --format='%(refname)' >/dev/null + done + ) +" + +test_perf "branch --remotes (many unrelated refs)" " + ( + cd scoped && + for i in \$(test_seq $test_iteration_count); do + git branch --remotes --format='%(refname)' >/dev/null + done + ) +" + +test_perf "tag (many unrelated refs)" " + ( + cd scoped && + for i in \$(test_seq $test_iteration_count); do + git tag --format='%(refname)' >/dev/null + done + ) +" + test_done From da80feb5be9076bb56af0681d684c36028f92ae6 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sun, 14 Jun 2026 06:37:22 +0000 Subject: [PATCH 19/95] merge-ort: propagate callback errors from traverse_trees_wrapper() traverse_trees_wrapper() saves entries from a first pass through traverse_trees() and then replays them through the real callback (collect_merge_info_callback). However, the replay loop silently discards the callback return value. This is not a deferred error; it is an ignored error. Today the only originator of a negative return in this entire call graph is traverse_trees()'s "exceeded maximum allowed tree depth" check; everything else (collect_merge_info_callback, traverse_trees_wrapper, the inner traverse_trees recursion) only relays that. So in current Git, the visible effect of dropping the replay callback's return value is narrow but bad: a tree nested past core.maxTreeDepth has its -1 swallowed, the subtree below the limit is silently pruned, and the merge completes as if that were the correct result. A later patch in this series will teach collect_merge_info_callback() to return -1 on an additional path -- detecting duplicate entries in malformed trees -- which is similarly handled today by just ignoring the problem (resulting in mostly a "last one wins" rule, though the non-last entry can mutate various state flags). Capture the return value, stop the loop on negative returns, and propagate the error to the caller. The callback returns a positive mask value on success, so normalize non-negative returns to 0 for the caller. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-ort.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/merge-ort.c b/merge-ort.c index 00923ce3cd749b..4b8e32209d9b3a 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -1008,18 +1008,20 @@ static int traverse_trees_wrapper(struct index_state *istate, info->traverse_path = renames->callback_data_traverse_path; info->fn = old_fn; for (i = old_offset; i < renames->callback_data_nr; ++i) { - info->fn(n, - renames->callback_data[i].mask, - renames->callback_data[i].dirmask, - renames->callback_data[i].names, - info); + ret = info->fn(n, + renames->callback_data[i].mask, + renames->callback_data[i].dirmask, + renames->callback_data[i].names, + info); + if (ret < 0) + break; } renames->callback_data_nr = old_offset; free(renames->callback_data_traverse_path); renames->callback_data_traverse_path = old_callback_data_traverse_path; info->traverse_path = NULL; - return 0; + return ret < 0 ? ret : 0; } static void setup_path_info(struct merge_options *opt, From 159e4d903458ac3ec0aa944aefeadbaf9e83b73c Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sun, 14 Jun 2026 06:37:23 +0000 Subject: [PATCH 20/95] merge-ort: drop unnecessary show_all_errors from collect_merge_info() collect_merge_info() has set info.show_all_errors = 1 since d2bc1994f363 (merge-ort: implement a very basic collect_merge_info(), 2020-12-13). This setting was copied from unpack-trees.c where it controls batching of error messages for porcelain display, but merge-ort has no such error-batching logic and never needed it. With show_all_errors set, traverse_trees() captures a negative callback return but continues processing remaining entries rather than stopping immediately. Removing the setting restores the default behavior where a negative return from collect_merge_info_callback() breaks out of the traversal loop right away, allowing a future commit to exit early when a corrupt tree is detected. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-ort.c | 1 - 1 file changed, 1 deletion(-) diff --git a/merge-ort.c b/merge-ort.c index 4b8e32209d9b3a..74e9636020fe40 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -1740,7 +1740,6 @@ static int collect_merge_info(struct merge_options *opt, setup_traverse_info(&info, opt->priv->toplevel_dir); info.fn = collect_merge_info_callback; info.data = opt; - info.show_all_errors = 1; if (repo_parse_tree(opt->repo, merge_base) < 0 || repo_parse_tree(opt->repo, side1) < 0 || From 83ae606c9838b06bde36479756de2ab75b6fbb96 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sun, 14 Jun 2026 06:37:24 +0000 Subject: [PATCH 21/95] merge-ort: free diff pairs queue in clear_or_reinit_internal_opts() clear_or_reinit_internal_opts() is responsible for cleaning up the various data structures in merge_options_internal. It already handles many renames-related structures (dirs_removed, dir_renames, relevant_sources, cached_pairs, deferred, etc.) but does not free renames->pairs[].queue. In the normal code path, resolve_and_process_renames() frees pairs[s].queue and reinitializes it with diff_queue_init() before clear_or_reinit_internal_opts() runs, so the omission is harmless. However, if collect_merge_info() encounters an error and returns early (before resolve_and_process_renames() is ever called), any diff pairs already queued by collect_rename_info()/add_pair() will have their backing array leaked. Fix this by freeing renames->pairs[].queue in the cleanup function. In the normal path the pointer is already NULL (from the earlier diff_queue_init() in resolve_and_process_renames()), so free(NULL) is a safe no-op. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-ort.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/merge-ort.c b/merge-ort.c index 74e9636020fe40..8f911cb63979eb 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -728,6 +728,8 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti, strintmap_clear_func(&renames->deferred[i].possible_trivial_merges); strset_clear_func(&renames->deferred[i].target_dirs); renames->deferred[i].trivial_merges_okay = 1; /* 1 == maybe */ + free(renames->pairs[i].queue); + diff_queue_init(&renames->pairs[i]); } renames->cached_pairs_valid_side = 0; renames->dir_rename_mask = 0; From 43a5fa7f5a9b7c44dd958a21368d690fa55d4f50 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sun, 14 Jun 2026 06:37:25 +0000 Subject: [PATCH 22/95] merge-ort: abort merge when trees have duplicate entries Trees with duplicate entries are malformed; fsck reports "contains duplicate file entries" for them. merge-ort has from the beginning assumed that we would never hit such trees. It was written with the assumption that traverse_trees() calls collect_merge_info_callback() at most once per path. The "sanity checks" in that callback (added in d2bc1994f363 (merge-ort: implement a very basic collect_merge_info(), 2020-12-13)) verify properties of each individual call but not that invariant. The strmap_put() in setup_path_info() silently overwrites the entry from any prior call for the same path, because it assumed there would be no other path. Unfortunately, supplemental data structures for various optimizations could still be tweaked before the extra paths were overwritten, and those data structures not matching expected state could trip various assertions. Change the return type of setup_path_info() from void to int to allow us to detect this case, and abort the merge with a clear error message when it occurs. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-ort.c | 61 ++++++++++++++++------------ t/t6422-merge-rename-corner-cases.sh | 54 ++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 27 deletions(-) diff --git a/merge-ort.c b/merge-ort.c index 8f911cb63979eb..be0829bbb781ef 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -1026,18 +1026,18 @@ static int traverse_trees_wrapper(struct index_state *istate, return ret < 0 ? ret : 0; } -static void setup_path_info(struct merge_options *opt, - struct string_list_item *result, - const char *current_dir_name, - int current_dir_name_len, - char *fullpath, /* we'll take over ownership */ - struct name_entry *names, - struct name_entry *merged_version, - unsigned is_null, /* boolean */ - unsigned df_conflict, /* boolean */ - unsigned filemask, - unsigned dirmask, - int resolved /* boolean */) +static int setup_path_info(struct merge_options *opt, + struct string_list_item *result, + const char *current_dir_name, + int current_dir_name_len, + char *fullpath, /* we'll take over ownership */ + struct name_entry *names, + struct name_entry *merged_version, + unsigned is_null, /* boolean */ + unsigned df_conflict, /* boolean */ + unsigned filemask, + unsigned dirmask, + int resolved /* boolean */) { /* result->util is void*, so mi is a convenience typed variable */ struct merged_info *mi; @@ -1081,9 +1081,11 @@ static void setup_path_info(struct merge_options *opt, */ mi->is_null = 1; } - strmap_put(&opt->priv->paths, fullpath, mi); + if (strmap_put(&opt->priv->paths, fullpath, mi)) + return error(_("tree has duplicate entries for '%s'"), fullpath); result->string = fullpath; result->util = mi; + return 0; } static void add_pair(struct merge_options *opt, @@ -1350,9 +1352,10 @@ static int collect_merge_info_callback(int n, */ if (side1_matches_mbase && side2_matches_mbase) { /* mbase, side1, & side2 all match; use mbase as resolution */ - setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, - names, names+0, mbase_null, 0 /* df_conflict */, - filemask, dirmask, 1 /* resolved */); + if (setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, + names, names+0, mbase_null, 0 /* df_conflict */, + filemask, dirmask, 1 /* resolved */)) + return -1; /* Quit traversing */ return mask; } @@ -1364,9 +1367,10 @@ static int collect_merge_info_callback(int n, */ if (sides_match && filemask == 0x07) { /* use side1 (== side2) version as resolution */ - setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, - names, names+1, side1_null, 0, - filemask, dirmask, 1); + if (setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, + names, names+1, side1_null, 0, + filemask, dirmask, 1)) + return -1; /* Quit traversing */ return mask; } @@ -1378,18 +1382,20 @@ static int collect_merge_info_callback(int n, */ if (side1_matches_mbase && filemask == 0x07) { /* use side2 version as resolution */ - setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, - names, names+2, side2_null, 0, - filemask, dirmask, 1); + if (setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, + names, names+2, side2_null, 0, + filemask, dirmask, 1)) + return -1; /* Quit traversing */ return mask; } /* Similar to above but swapping sides 1 and 2 */ if (side2_matches_mbase && filemask == 0x07) { /* use side1 version as resolution */ - setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, - names, names+1, side1_null, 0, - filemask, dirmask, 1); + if (setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, + names, names+1, side1_null, 0, + filemask, dirmask, 1)) + return -1; /* Quit traversing */ return mask; } @@ -1413,8 +1419,9 @@ static int collect_merge_info_callback(int n, * unconflict some more cases, but that comes later so all we can * do now is record the different non-null file hashes.) */ - setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, - names, NULL, 0, df_conflict, filemask, dirmask, 0); + if (setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, + names, NULL, 0, df_conflict, filemask, dirmask, 0)) + return -1; /* Quit traversing */ ci = pi.util; VERIFY_CI(ci); diff --git a/t/t6422-merge-rename-corner-cases.sh b/t/t6422-merge-rename-corner-cases.sh index e18d5a227d54f7..81b645bb3bdc5b 100755 --- a/t/t6422-merge-rename-corner-cases.sh +++ b/t/t6422-merge-rename-corner-cases.sh @@ -1525,4 +1525,58 @@ test_expect_success 'submodule/directory preliminary conflict' ' ) ' +# Testcase: submodule/directory conflict with duplicate tree entries +# One side has a path as a gitlink (submodule). The other side replaces +# the gitlink with a directory. A third-party tool creates a tree on the +# submodule side that has *both* a gitlink and a tree entry for the same +# path (adding a file inside the submodule path ignoring that there's a +# gitlink there). collect_merge_info_callback() should detect the +# duplicate and abort rather than silently corrupting its bookkeeping. + +test_expect_success 'duplicate tree entries trigger an error' ' + test_when_finished "rm -rf duplicate-entry" && + git init duplicate-entry && + ( + cd duplicate-entry && + + # Base commit: "docs" is a gitlink (submodule) + empty_tree=$(git mktree file.txt && + git add file.txt && + git commit -m base && + + # side1: remove the gitlink, replace with a directory + git checkout -b side1 && + git rm --cached docs && + mkdir -p docs && + echo hello >docs/requirements.txt && + git add docs/requirements.txt && + git commit -m "side1: submodule to directory" && + + # side2: keep the gitlink but craft a tree that also + # contains a tree entry for "docs" (simulating a tool + # that adds files inside a submodule path without + # removing the gitlink first). + git checkout main && + git checkout -b side2 && + blob_oid=$(echo world | git hash-object -w --stdin) && + docs_tree=$(printf "100644 blob %s\trequirements.txt\n" \ + "$blob_oid" | git mktree) && + cur_tree=$(git rev-parse HEAD^{tree}) && + git cat-file -p $cur_tree >tree-listing && + printf "040000 tree %s\tdocs\n" "$docs_tree" >>tree-listing && + new_tree=$(git mktree err && + test_grep "duplicate entries" err + ) +' + test_done From 0c8eb46fbbfc48065bc93dcc4c9901031615516c Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sun, 14 Jun 2026 06:37:26 +0000 Subject: [PATCH 23/95] cache-tree: fix verify_cache() to catch non-adjacent D/F conflicts verify_cache() checks that the index does not contain both "path" and "path/file" before writing a tree. It does this by comparing only adjacent entries, relying on the assumption that "path/file" would immediately follow "path" in sorted order. Unfortunately, this assumption does not always hold. For example: docs <-- submodule entry docs-internal/README.md <-- intervening entry docs/requirements.txt <-- D/F conflict, NOT adjacent to "docs" When this happens, verify_cache() silently misses the D/F conflict and write-tree produces a corrupt tree object containing duplicate entries (one for the submodule "docs" and one for the tree "docs"). I could not find any caller in current git that both allows the index to get into this state and then tries to write it out without doing other checks beyond the verify_cache() call in cache_tree_update(), but verify_cache() is documented as a safety net for preventing corrupt trees and should actually provide that guarantee. A downstream consumer that relied solely on cache_tree_update()'s internal checking via verify_cache() to prevent duplicate tree entries was bitten by the gap. Add a test that constructs a corrupt index directly (bypassing the D/F checks in add_index_entry) and verifies that write-tree now rejects it. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- cache-tree.c | 64 ++++++++++++++++++++++++++++------ t/meson.build | 1 + t/t0093-direct-index-write.pl | 38 ++++++++++++++++++++ t/t0093-verify-cache-df-gap.sh | 59 +++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 11 deletions(-) create mode 100644 t/t0093-direct-index-write.pl create mode 100755 t/t0093-verify-cache-df-gap.sh diff --git a/cache-tree.c b/cache-tree.c index 7881b42aa24c80..4d2669b312417a 100644 --- a/cache-tree.c +++ b/cache-tree.c @@ -161,6 +161,54 @@ void cache_tree_invalidate_path(struct index_state *istate, const char *path) istate->cache_changed |= CACHE_TREE_CHANGED; } +/* + * Check whether this_ce and the next entry in the index form a D/F + * conflict ("path" vs "path/file"). Returns the conflicting "path/..." + * name when one is found, or NULL otherwise. + * + * The cache is sorted, so "path/file" sorts after "path" and the + * conflict is usually visible as adjacent entries. But other entries + * can sort between them -- e.g. "path-internal" sits between "path" + * and "path/file" because '-' (0x2D) precedes '/' (0x2F) -- so when + * the immediately following entry shares our prefix but starts with a + * character that sorts before '/', binary search for "path/" instead. + */ +static const char *find_df_conflict(struct index_state *istate, + const struct cache_entry *this_ce, + const struct cache_entry *next_ce) +{ + const char *this_name = this_ce->name; + const char *next_name = next_ce->name; + int this_len = ce_namelen(this_ce); + const struct cache_entry *other; + struct strbuf probe = STRBUF_INIT; + int pos; + + if (this_len >= ce_namelen(next_ce) || + next_name[this_len] > '/' || + strncmp(this_name, next_name, this_len)) + return NULL; + + if (next_name[this_len] == '/') + return next_name; + + strbuf_add(&probe, this_name, this_len); + strbuf_addch(&probe, '/'); + pos = index_name_pos_sparse(istate, probe.buf, probe.len); + strbuf_release(&probe); + + if (pos < 0) + pos = -pos - 1; + if (pos >= (int)istate->cache_nr) + return NULL; + other = istate->cache[pos]; + if (ce_namelen(other) > this_len && + other->name[this_len] == '/' && + !strncmp(this_name, other->name, this_len)) + return other->name; + return NULL; +} + static int verify_cache(struct index_state *istate, int flags) { unsigned i, funny; @@ -190,24 +238,18 @@ static int verify_cache(struct index_state *istate, int flags) */ funny = 0; for (i = 0; i + 1 < istate->cache_nr; i++) { - /* path/file always comes after path because of the way - * the cache is sorted. Also path can appear only once, - * which means conflicting one would immediately follow. - */ const struct cache_entry *this_ce = istate->cache[i]; const struct cache_entry *next_ce = istate->cache[i + 1]; - const char *this_name = this_ce->name; - const char *next_name = next_ce->name; - int this_len = ce_namelen(this_ce); - if (this_len < ce_namelen(next_ce) && - next_name[this_len] == '/' && - strncmp(this_name, next_name, this_len) == 0) { + const char *conflict_name; + + conflict_name = find_df_conflict(istate, this_ce, next_ce); + if (conflict_name) { if (10 < ++funny) { fprintf(stderr, "...\n"); break; } fprintf(stderr, "You have both %s and %s\n", - this_name, next_name); + this_ce->name, conflict_name); } } if (funny) diff --git a/t/meson.build b/t/meson.build index 7528e5cda5fef0..362177999bd342 100644 --- a/t/meson.build +++ b/t/meson.build @@ -124,6 +124,7 @@ integration_tests = [ 't0090-cache-tree.sh', 't0091-bugreport.sh', 't0092-diagnose.sh', + 't0093-verify-cache-df-gap.sh', 't0095-bloom.sh', 't0100-previous.sh', 't0101-at-syntax.sh', diff --git a/t/t0093-direct-index-write.pl b/t/t0093-direct-index-write.pl new file mode 100644 index 00000000000000..2881a3ebb21dcd --- /dev/null +++ b/t/t0093-direct-index-write.pl @@ -0,0 +1,38 @@ +#!/usr/bin/perl +# +# Build a v2 index file from entries listed on stdin. +# Each line: "octalmode hex-oid name" +# Output: binary index written to stdout. +# +# This bypasses all D/F safety checks in add_index_entry(), simulating +# what happens when code uses ADD_CACHE_JUST_APPEND to bulk-load entries. +use strict; +use warnings; +use Digest::SHA qw(sha1 sha256); + +my $hash_algo = $ENV{'GIT_DEFAULT_HASH'} || 'sha1'; +my $hash_func = $hash_algo eq 'sha256' ? \&sha256 : \&sha1; + +my @entries; +while (my $line = ) { + chomp $line; + my ($mode, $oid_hex, $name) = split(/ /, $line, 3); + push @entries, [$mode, $oid_hex, $name]; +} + +my $body = "DIRC" . pack("NN", 2, scalar @entries); + +for my $ent (@entries) { + my ($mode, $oid_hex, $name) = @{$ent}; + # 10 x 32-bit stat fields (zeroed), with mode in position 7 + my $stat = pack("N10", 0, 0, 0, 0, 0, 0, oct($mode), 0, 0, 0); + my $oid = pack("H*", $oid_hex); + my $flags = pack("n", length($name) & 0xFFF); + my $entry = $stat . $oid . $flags . $name . "\0"; + # Pad to 8-byte boundary + while (length($entry) % 8) { $entry .= "\0"; } + $body .= $entry; +} + +binmode STDOUT; +print $body . $hash_func->($body); diff --git a/t/t0093-verify-cache-df-gap.sh b/t/t0093-verify-cache-df-gap.sh new file mode 100755 index 00000000000000..0b6829d805269d --- /dev/null +++ b/t/t0093-verify-cache-df-gap.sh @@ -0,0 +1,59 @@ +#!/bin/sh + +test_description='verify_cache() must catch non-adjacent D/F conflicts + +Ensure that verify_cache() can complain about bad entries like: + + docs <-- submodule + docs-internal/... <-- sorts here because "-" < "/" + docs/... <-- D/F conflict with "docs" above, not adjacent + +In order to test verify_cache, we directly construct a corrupt index +(bypassing the D/F safety checks in add_index_entry) and verify that +write-tree rejects it. +' + +. ./test-lib.sh + +if ! test_have_prereq PERL +then + skip_all='skipping verify_cache D/F tests; Perl not available' + test_done +fi + +# Build a v2 index from entries on stdin, bypassing D/F checks. +# Each line: "octalmode hex-oid name" (entries must be pre-sorted). +build_corrupt_index () { + perl "$TEST_DIRECTORY/t0093-direct-index-write.pl" >"$1" +} + +test_expect_success 'setup objects' ' + test_commit base && + BLOB=$(git rev-parse HEAD:base.t) && + SUB_COMMIT=$(git rev-parse HEAD) +' + +test_expect_success 'adjacent D/F conflict is caught by verify_cache' ' + cat >index-entries <<-EOF && + 0160000 $SUB_COMMIT docs + 0100644 $BLOB docs/requirements.txt + EOF + build_corrupt_index .git/index err && + test_grep "You have both docs and docs/requirements.txt" err +' + +test_expect_success 'non-adjacent D/F conflict is caught by verify_cache' ' + cat >index-entries <<-EOF && + 0160000 $SUB_COMMIT docs + 0100644 $BLOB docs-internal/README.md + 0100644 $BLOB docs/requirements.txt + EOF + build_corrupt_index .git/index err && + test_grep "You have both docs and docs/requirements.txt" err +' + +test_done From 800581cd44e1d164e53f624257fcc0ceb937d154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Sun, 14 Jun 2026 18:28:34 +0200 Subject: [PATCH 24/95] cat-file: speed up default format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit eb54a3391b (cat-file: skip expanding default format, 2022-03-15) added special handling for the default batch format. In the meantime it has fallen behind the code path for handling arbitrary formats. Bring it up to speed by using the new and more efficient strbuf_add_oid_hex() and strbuf_add_uint() instead of strbuf_addf(): Benchmark 1: ./git_main cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype) %(objectsize)' Time (mean ± σ): 1.051 s ± 0.003 s [User: 1.027 s, System: 0.023 s] Range (min … max): 1.049 s … 1.058 s 10 runs Benchmark 2: ./git_main cat-file --batch-all-objects --batch-check='%(objectname)-%(objecttype)-%(objectsize)' Time (mean ± σ): 1.012 s ± 0.002 s [User: 0.988 s, System: 0.023 s] Range (min … max): 1.010 s … 1.018 s 10 runs Benchmark 3: ./git cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype) %(objectsize)' Time (mean ± σ): 979.0 ms ± 1.1 ms [User: 954.1 ms, System: 23.2 ms] Range (min … max): 977.7 ms … 980.8 ms 10 runs Summary ./git cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype) %(objectsize)' ran 1.03 ± 0.00 times faster than ./git_main cat-file --batch-all-objects --batch-check='%(objectname)-%(objecttype)-%(objectsize)' 1.07 ± 0.00 times faster than ./git_main cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype) %(objectsize)' Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- builtin/cat-file.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/builtin/cat-file.c b/builtin/cat-file.c index 446d649904beed..ec157eff13906d 100644 --- a/builtin/cat-file.c +++ b/builtin/cat-file.c @@ -461,9 +461,12 @@ static void print_object_or_die(struct batch_options *opt, struct expand_data *d static void print_default_format(struct strbuf *scratch, struct expand_data *data, struct batch_options *opt) { - strbuf_addf(scratch, "%s %s %"PRIuMAX"%c", oid_to_hex(&data->oid), - type_name(data->type), - (uintmax_t)data->size, opt->output_delim); + strbuf_add_oid_hex(scratch, &data->oid); + strbuf_addch(scratch, ' '); + strbuf_addstr(scratch, type_name(data->type)); + strbuf_addch(scratch, ' '); + strbuf_add_uint(scratch, data->size); + strbuf_addch(scratch, opt->output_delim); } static void report_object_status(struct batch_options *opt, From 16b2924aab52141bedd6612dfad16280181321c7 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 15 Jun 2026 14:59:41 +0200 Subject: [PATCH 25/95] MyFirstContribution: recommend shallow threading of cover letters The "MyFirstContribution" document recommends the use of deep threading of cover letters: every cover letter of subsequent iterations shall be linked to the cover letter of the preceding version. The result of this is that eventually, threads with many versions are getting nested so deep that it becomes hard to follow. Adapt the recommendation to instead propose shallow threading of cover letters: instead of linking the cover letter to the previous cover letter, the user is supposed to always link it to the first cover letter. This still makes it easy to follow the iterations, but has the benefit of nesting to a much shallower level. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/MyFirstContribution.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Documentation/MyFirstContribution.adoc b/Documentation/MyFirstContribution.adoc index b9fdefce0224c9..984b7f5aa8bfd3 100644 --- a/Documentation/MyFirstContribution.adoc +++ b/Documentation/MyFirstContribution.adoc @@ -790,7 +790,7 @@ We can note a few things: v3", etc. in place of "PATCH". For example, "[PATCH v2 1/3]" would be the first of three patches in the second iteration. Each iteration is sent with a new cover letter (like "[PATCH v2 0/3]" above), itself a reply to the cover letter of the - previous iteration (more on that below). + first iteration (more on that below). NOTE: A single-patch topic is sent with "[PATCH]", "[PATCH v2]", etc. without _i_/_n_ numbering (in the above thread overview, no single-patch topic appears, @@ -1214,7 +1214,7 @@ between your last version and now, if it's something significant. You do not need the exact same body in your second cover letter; focus on explaining to reviewers the changes you've made that may not be as visible. -You will also need to go and find the Message-ID of your previous cover letter. +You will also need to go and find the Message-ID of your first cover letter. You can either note it when you send the first series, from the output of `git send-email`, or you can look it up on the https://lore.kernel.org/git[mailing list]. Find your cover letter in the @@ -1227,8 +1227,8 @@ Message-ID: Your Message-ID is ``. This example will be used below as well; make sure to replace it with the correct Message-ID for your -**previous cover letter** - that is, if you're sending v2, use the Message-ID -from v1; if you're sending v3, use the Message-ID from v2. +**first cover letter** - that is, for any subsequent version that you send, +always use the Message-ID from v1. While you're looking at the email, you should also note who is CC'd, as it's common practice in the mailing list to keep all CCs on a thread. You can add From efe4ac70647c62152c37f2f247404c9dfd494ed5 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 15 Jun 2026 14:59:42 +0200 Subject: [PATCH 26/95] MyFirstContribution: recommend the use of b4 The b4 tool originates from the Linux kernel community and is intended to help mailing-list based workflows. It automates a lot of the annoying bookkeeping tasks that contributors typically need to do: tracking the list of recipients, Message-IDs, range-diffs and the like. In addition to that, b4 also has many other subcommands that help the maintainer and reviewers. The Git project uses the same infrastructure as the kernel, so this tool is also a very good fit for us. Adapt "MyFirstContribution" to explicitly recommend its use. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/MyFirstContribution.adoc | 92 +++++++++++++++++++++++++- Documentation/SubmittingPatches | 6 +- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/Documentation/MyFirstContribution.adoc b/Documentation/MyFirstContribution.adoc index 984b7f5aa8bfd3..607876f3d89c08 100644 --- a/Documentation/MyFirstContribution.adoc +++ b/Documentation/MyFirstContribution.adoc @@ -833,7 +833,7 @@ This patchset is part of the MyFirstContribution tutorial and should not be merged. ---- -At this point the tutorial diverges, in order to demonstrate two +At this point the tutorial diverges, in order to demonstrate three different methods of formatting your patchset and getting it reviewed. The first method to be covered is GitGitGadget, which is useful for those @@ -845,9 +845,14 @@ more fine-grained control over the emails to be sent. This method requires some setup which can change depending on your system and will not be covered in this tutorial. +The third method to be covered is `b4`, which builds on top of `git +format-patch` and `git send-email`. This method is the recommended way to +submit patches via mail as it automates a lot of the bookkeeping required by +`git send-email`. + Regardless of which method you choose, your engagement with reviewers will be -the same; the review process will be covered after the sections on GitGitGadget -and `git send-email`. +the same; the review process will be covered after the sections on GitGitGadget, +`git send-email` and `b4`. [[howto-ggg]] == Sending Patches via GitGitGadget @@ -1296,6 +1301,87 @@ index 88f126184c..38da593a60 100644 2.21.0.392.gf8f6787159e-goog ---- +[[howto-b4]] +== Sending Patches with `b4` + +`b4` is a tool that builds on top of `git format-patch` and `git send-email`. +It automates much of the bookkeeping involved in sending a patch series to a +mailing-list-based project. + +Refer to the https://b4.docs.kernel.org/[b4 documentation] for a full reference. + +[[prep-b4]] +=== Preparing a Patch Series + +`b4` tracks your patch series as a branch. To start tracking the `psuh` branch +you have been working on, run: + +---- +$ b4 prep --enroll master +---- + +This enrolls the current branch, using `master` as the base of the topic. `b4` +manages the cover letter as part of the branch, so you can edit it at any time +with: + +---- +$ b4 prep --edit-cover +---- + +The cover letter not only tracks the content of the top-level mail, but also +the set of recipients. You can add recipients by adding `To:` and `Cc:` +trailer lines. + +[[send-b4]] +=== Sending the Patches + +Before sending the series out for real, you can inspect what `b4` would send by +passing `--dry-run`: + +---- +$ b4 send --dry-run +---- + +Once you are happy with the result, send the series with: + +---- +$ b4 send +---- + +[[v2-b4]] +=== Sending v2 + +When you are ready to send a new iteration of your series, refine your +patches as usual using linkgit:git-rebase[1]. Note that you typically want to +rebase on top of the cover letter. You can configure an alias to enable easy +rebases going forward: + +--- +$ git config set alias.b4-rebase 'rebase "HEAD^{/--- b4-submit-tracking ---}"' +$ git b4-rebase -i +--- + +Before sending out the new version you should also update the cover letter with +`b4 prep --edit-cover` to note the relevant changes compared to the previous +version. You can inspect the changes between the two versions with `b4 prep +--compare-to=v1`. + +Same as with the first version, you can use `b4 send` to send out the second +version. `b4` automatically bumps the version to `v2`, generates the range-diff +against the previous iteration, and threads the new series as a reply to the +cover letter of the first version. + +[[configure-b4]] +=== Configure b4 + +`b4` can be configured via linkgit:git-config[1]. In addition to that, projects +can have their own set of defaults in `.b4-config` in the root tree, which also +uses Git's config format. The user's configuration always takes precedence over +the per-project defaults. + +Refer to the https://b4.docs.kernel.org/en/latest/config.html[b4 config documentation] +for more information on the available options. + [[now-what]] == My Patch Got Emailed - Now What? diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index d570184ec84998..99427e1ee1efff 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -573,8 +573,10 @@ your existing e-mail client (often optimized for "multipart/*" MIME type e-mails) might render your patches unusable. NOTE: Here we outline the procedure using `format-patch` and -`send-email`, but you can instead use GitGitGadget to send in your -patches (see link:MyFirstContribution.html[MyFirstContribution]). +`send-email`, but you can instead use GitGitGadget or `b4` to send in +your patches (see link:MyFirstContribution.html[MyFirstContribution]). +Contributors are encouraged to use `b4`, which automates much of the +bookkeeping that is otherwise done by hand. People on the Git mailing list need to be able to read and comment on the changes you are submitting. It is important for From 84b2ff7f2f708b8343d684ab57c72c1367de01e6 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 15 Jun 2026 14:59:43 +0200 Subject: [PATCH 27/95] b4: introduce configuration for the Git project In the preceding commit we have extended our documentation to recommend b4 for sending patch series to the mailing list. Introduce configuration so that it knows to honor preferences of the Git project by default. For now, this configuration does two things: - It configures "send-same-thread = shallow", which tells b4 to always send subsequent versions of the same patch series as a reply to the cover letter of the first version. - It configures "prep-cover-template", which tells b4 to use a custom template for the cover letter. The most important change compared to the default template is that our custom template also includes a range-diff. There's potentially more things that we may want to configure going forward, like for example auto-configuration of folks to Cc on certain patches. But these two tweaks feel like a good place to start. Note that these values only serve as defaults, and users may want to tweak those defaults based on their own preference. Luckily, users can do that without having to touch `.b4-config` at all, as b4 allows them to override values via Git configuration: ``` $ git config set b4.prep-cover-template /does/not/exist $ b4 send --dry-run ERROR: prep-cover-template says to use x, but it does not exist ``` So this gives users an easy way to override our defaults without having to touch ".b4-config", which would dirty the tree. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- .b4-config | 6 ++++++ .b4-cover-template | 11 +++++++++++ 2 files changed, 17 insertions(+) create mode 100644 .b4-config create mode 100644 .b4-cover-template diff --git a/.b4-config b/.b4-config new file mode 100644 index 00000000000000..fd4fb56b6d5678 --- /dev/null +++ b/.b4-config @@ -0,0 +1,6 @@ +# Note that these are default values that you can tweak via the typical +# git-config(1) machinery. You thus shouldn't ever have to change this file. +# See also https://b4.docs.kernel.org/en/latest/config.html. +[b4] +send-same-thread = shallow +prep-cover-template = ./.b4-cover-template diff --git a/.b4-cover-template b/.b4-cover-template new file mode 100644 index 00000000000000..ab864933b5c846 --- /dev/null +++ b/.b4-cover-template @@ -0,0 +1,11 @@ +${cover} + +--- +${shortlog} + +${diffstat} + +${range_diff} +--- +base-commit: ${base_commit} +${prerequisites} From 54a441bcea885c13c7a8a80bc3bbcfb1ace9f8a9 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Tue, 16 Jun 2026 08:35:16 -0400 Subject: [PATCH 28/95] read_gitfile(): simplify NOT_A_REPO error message If a .git file is well-formed but points to a directory that is not itself a valid repository, then we say: fatal: not a git repository: without mentioning the .git file that pointed us there in the first place. Doing so could better help the user understand the source of the problem. In theory the most helpful thing we could do is mention both paths, like: gitfile '' points to invalid repository: But there's another catch: when we generate the error, we don't always know the pointed-to repository! This leads to a potential segfault. The message comes from read_gitfile_error_die(). Originally we only called that function from inside read_gitfile_gently(), passing in both the gitfile path and the pointed-to path. But that changed in 1dd27bfbfd (setup: improve error diagnosis for invalid .git files, 2026-03-04). Since then, the caller in setup_git_directory_gently(), even if it wants to die on error, always passes in the "return_error_code" flag, asking the function to instead return a numeric error code. And then it calls read_gitfile_error_die() itself, passing NULL for the pointed-to path. If we get the READ_GITFILE_ERR_NOT_A_REPO code, we form a message using that NULL pointer, and either segfault or get garbage like "not a git repository: (null)", depending on the platform. We could fix this by having the function pass out both the numeric error code and the pointed-to path. But that creates a new headache: we have to allocate that string on the heap and pass ownership back to the caller. So now every caller has to be aware of it (and either free the result, or signal that they are not interested by using an extra parameter). Instead, let's just drop the pointed-to path from the error message entirely, and mention only the gitfile. This fixes the NULL dereference without introducing any more complexity. The user-facing error message is not as detailed as it could be, but is better than the original. Since it mentions the gitfile, a user investigating the situation can look there to find the pointed-to path (whereas you could not go the other way from the original message). There's an existing test in t0002 which triggers this case, but we didn't notice the problem because it checks only that we said "not a repository", and not the full string. So if we print "(null)" it is happy. It will probably crash on some non-glibc platforms, but nobody seems to have reported it yet (the breakage is recent-ish as of v2.54). I'm also somewhat surprised that building with ASan/UBSan doesn't catch this, but it doesn't seem to (and I found an open issue with somebody asking for NULL printf checks to be implemented in the sanitizers). We'll tweak the test to match the new error, but there's no need to beef it up further, since we're not showing the pointed-to path at all. We also racily trigger this in t7450. During parallel cloning we might see one of several errors, including this one. And so we must update that message, too (you can otherwise find the failure pretty quickly by running t7450 with --stress). Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- setup.c | 9 +++++---- setup.h | 2 +- submodule.c | 2 +- t/t0002-gitfile.sh | 2 +- t/t7450-bad-git-dotfiles.sh | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/setup.c b/setup.c index 7ec4427368a2a7..ae352f572028f9 100644 --- a/setup.c +++ b/setup.c @@ -917,7 +917,7 @@ int verify_repository_format(const struct repository_format *format, return 0; } -void read_gitfile_error_die(int error_code, const char *path, const char *dir) +void read_gitfile_error_die(int error_code, const char *path) { switch (error_code) { case READ_GITFILE_ERR_NOT_A_FILE: @@ -937,7 +937,8 @@ void read_gitfile_error_die(int error_code, const char *path, const char *dir) case READ_GITFILE_ERR_NO_PATH: die(_("no path in gitfile: %s"), path); case READ_GITFILE_ERR_NOT_A_REPO: - die(_("not a git repository: %s"), dir); + die(_("gitfile does not point to a valid repository: %s"), + path); default: BUG("unknown error code"); } @@ -1028,7 +1029,7 @@ const char *read_gitfile_gently(const char *path, int *return_error_code) if (return_error_code) *return_error_code = error_code; else if (error_code) - read_gitfile_error_die(error_code, path, dir); + read_gitfile_error_die(error_code, path); free(buf); return error_code ? NULL : path; @@ -1633,7 +1634,7 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir, return GIT_DIR_INVALID_GITFILE; default: if (die_on_error) - read_gitfile_error_die(error_code, dir->buf, NULL); + read_gitfile_error_die(error_code, dir->buf); else return GIT_DIR_INVALID_GITFILE; } diff --git a/setup.h b/setup.h index 80bc6e5f078af8..2630ed196b68bd 100644 --- a/setup.h +++ b/setup.h @@ -38,7 +38,7 @@ int is_nonbare_repository_dir(struct strbuf *path); #define READ_GITFILE_ERR_TOO_LARGE 8 #define READ_GITFILE_ERR_MISSING 9 #define READ_GITFILE_ERR_IS_A_DIR 10 -void read_gitfile_error_die(int error_code, const char *path, const char *dir); +void read_gitfile_error_die(int error_code, const char *path); const char *read_gitfile_gently(const char *path, int *return_error_code); #define read_gitfile(path) read_gitfile_gently((path), NULL) const char *resolve_gitdir_gently(const char *suspect, int *return_error_code); diff --git a/submodule.c b/submodule.c index b1a0363f9d2a96..9ab7fec8dc7abc 100644 --- a/submodule.c +++ b/submodule.c @@ -2578,7 +2578,7 @@ void absorb_git_dir_into_superproject(const char *path, if (err_code != READ_GITFILE_ERR_NOT_A_REPO) /* We don't know what broke here. */ - read_gitfile_error_die(err_code, path, NULL); + read_gitfile_error_die(err_code, path); /* * Maybe populated, but no git directory was found? diff --git a/t/t0002-gitfile.sh b/t/t0002-gitfile.sh index dfbcdddbcc1f0f..6356e9ec7272a2 100755 --- a/t/t0002-gitfile.sh +++ b/t/t0002-gitfile.sh @@ -27,7 +27,7 @@ test_expect_success 'bad setup: invalid .git file format' ' test_expect_success 'bad setup: invalid .git file path' ' echo "gitdir: $REAL.not" >.git && test_must_fail git rev-parse 2>.err && - test_grep "not a git repository" .err + test_grep "gitfile does not point to a valid repository" .err ' test_expect_success 'final setup + check rev-parse --git-dir' ' diff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh index f512eed278c46b..7fdbf085c91f11 100755 --- a/t/t7450-bad-git-dotfiles.sh +++ b/t/t7450-bad-git-dotfiles.sh @@ -348,7 +348,7 @@ test_expect_success 'git dirs of sibling submodules must not be nested' ' test_expect_success 'submodule git dir nesting detection must work with parallel cloning' ' test_must_fail git clone --recurse-submodules --jobs=2 nested clone_parallel 2>err && cat err && - grep -E "(already exists|is inside git dir|not a git repository)" err && + grep -E "(already exists|is inside git dir|does not point to a valid repository)" err && { test_path_is_missing .git/modules/hippo/HEAD || test_path_is_missing .git/modules/hippo/hooks/HEAD From d1b74b6b988e71d78d05a3fd6f186025e3f692ba Mon Sep 17 00:00:00 2001 From: Philip Oakley Date: Tue, 16 Jun 2026 14:49:52 +0000 Subject: [PATCH 29/95] hash-object: demonstrate a >4GB/LLP64 problem On LLP64 systems, such as Windows, the size of `long`, `int`, etc. is only 32 bits (for backward compatibility). Git's use of `unsigned long` for file memory sizes in many places, rather than size_t, limits the handling of large files on LLP64 systems (commonly given as `>4GB`). Provide a minimum test for handling a >4GB file. The `hash-object` command, with the `--literally` and without `-w` option avoids writing the object, either loose or packed. This avoids the code paths hitting the `bigFileThreshold` config test code, the zlib code, and the pack code. Subsequent patches will walk the test's call chain, converting types to `size_t` (which is larger in LLP64 data models) where appropriate. Signed-off-by: Philip Oakley Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t1007-hash-object.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh index de076293b62a76..7867fd1dbf940c 100755 --- a/t/t1007-hash-object.sh +++ b/t/t1007-hash-object.sh @@ -49,6 +49,9 @@ test_expect_success 'setup' ' example sha1:ddd3f836d3e3fbb7ae289aa9ae83536f76956399 example sha256:b44fe1fe65589848253737db859bd490453510719d7424daab03daf0767b85ae + + large5GB sha1:0be2be10a4c8764f32c4bf372a98edc731a4b204 + large5GB sha256:dc18ca621300c8d3cfa505a275641ebab00de189859e022a975056882d313e64 EOF ' @@ -258,4 +261,12 @@ test_expect_success '--stdin outside of repository (uses default hash)' ' test_cmp expect actual ' +test_expect_failure EXPENSIVE,SIZE_T_IS_64BIT,!LONG_IS_64BIT \ + 'files over 4GB hash literally' ' + test-tool genzeros $((5*1024*1024*1024)) >big && + test_oid large5GB >expect && + git hash-object --stdin --literally actual && + test_cmp expect actual +' + test_done From a39fda4fca37c1b8700ccfe0f9d0194445373b97 Mon Sep 17 00:00:00 2001 From: Philip Oakley Date: Tue, 16 Jun 2026 14:49:53 +0000 Subject: [PATCH 30/95] object-file.c: use size_t for header lengths Continue walking the code path for the >4GB `hash-object --literally` test. The `hash_object_file_literally()` function internally uses both `hash_object_file()` and `write_object_file_prepare()`. Both function signatures use `unsigned long` rather than `size_t` for the mem buffer sizes. Use `size_t` instead, for LLP64 compatibility. While at it, convert those function's object's header buffer length to `size_t` for consistency. The value is already upcast to `uintmax_t` for print format compatibility. Note: The hash-object test still does not pass. A subsequent commit continues to walk the call tree's lower level hash functions to identify further fixes. Signed-off-by: Philip Oakley Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- object-file.c | 10 +++++----- object-file.h | 6 +++--- odb/source-files.c | 2 +- odb/source-inmemory.c | 2 +- odb/source-loose.c | 4 ++-- odb/source.h | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/object-file.c b/object-file.c index 9afa842da2dc8e..dccbe0fc3ea8b9 100644 --- a/object-file.c +++ b/object-file.c @@ -318,7 +318,7 @@ int parse_loose_header(const char *hdr, struct object_info *oi) static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_ctx *c, const void *buf, unsigned long len, struct object_id *oid, - char *hdr, int *hdrlen) + char *hdr, size_t *hdrlen) { algo->init_fn(c); git_hash_update(c, hdr, *hdrlen); @@ -327,9 +327,9 @@ static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_c } void write_object_file_prepare(const struct git_hash_algo *algo, - const void *buf, unsigned long len, + const void *buf, size_t len, enum object_type type, struct object_id *oid, - char *hdr, int *hdrlen) + char *hdr, size_t *hdrlen) { struct git_hash_ctx c; @@ -472,11 +472,11 @@ int finalize_object_file_flags(struct repository *repo, } void hash_object_file(const struct git_hash_algo *algo, const void *buf, - unsigned long len, enum object_type type, + size_t len, enum object_type type, struct object_id *oid) { char hdr[MAX_HEADER_LEN]; - int hdrlen = sizeof(hdr); + size_t hdrlen = sizeof(hdr); write_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen); } diff --git a/object-file.h b/object-file.h index 528c4e6e697f87..4c87cd160bb58a 100644 --- a/object-file.h +++ b/object-file.h @@ -131,12 +131,12 @@ int finalize_object_file_flags(struct repository *repo, enum finalize_object_file_flags flags); void hash_object_file(const struct git_hash_algo *algo, const void *buf, - unsigned long len, enum object_type type, + size_t len, enum object_type type, struct object_id *oid); void write_object_file_prepare(const struct git_hash_algo *algo, - const void *buf, unsigned long len, + const void *buf, size_t len, enum object_type type, struct object_id *oid, - char *hdr, int *hdrlen); + char *hdr, size_t *hdrlen); int write_loose_object(struct odb_source_loose *loose, const struct object_id *oid, char *hdr, int hdrlen, const void *buf, unsigned long len, diff --git a/odb/source-files.c b/odb/source-files.c index 5bdd0429225397..3b1261eba9700e 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -159,7 +159,7 @@ static int odb_source_files_freshen_object(struct odb_source *source, } static int odb_source_files_write_object(struct odb_source *source, - const void *buf, unsigned long len, + const void *buf, size_t len, enum object_type type, struct object_id *oid, struct object_id *compat_oid, diff --git a/odb/source-inmemory.c b/odb/source-inmemory.c index e004566d768b01..7f1b6f46363f2e 100644 --- a/odb/source-inmemory.c +++ b/odb/source-inmemory.c @@ -227,7 +227,7 @@ static int odb_source_inmemory_count_objects(struct odb_source *source, } static int odb_source_inmemory_write_object(struct odb_source *source, - const void *buf, unsigned long len, + const void *buf, size_t len, enum object_type type, struct object_id *oid, struct object_id *compat_oid UNUSED, diff --git a/odb/source-loose.c b/odb/source-loose.c index 7d7ea2fb842537..4cfa5b23fc1719 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -591,7 +591,7 @@ static int odb_source_loose_freshen_object(struct odb_source *source, } static int odb_source_loose_write_object(struct odb_source *source, - const void *buf, unsigned long len, + const void *buf, size_t len, enum object_type type, struct object_id *oid, struct object_id *compat_oid_in, enum odb_write_object_flags flags) @@ -601,7 +601,7 @@ static int odb_source_loose_write_object(struct odb_source *source, const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; struct object_id compat_oid; char hdr[MAX_HEADER_LEN]; - int hdrlen = sizeof(hdr); + size_t hdrlen = sizeof(hdr); /* Generate compat_oid */ if (compat) { diff --git a/odb/source.h b/odb/source.h index 2192a101b8ab08..1c65a05e2c4c6b 100644 --- a/odb/source.h +++ b/odb/source.h @@ -199,7 +199,7 @@ struct odb_source { * return 0 on success, a negative error code otherwise. */ int (*write_object)(struct odb_source *source, - const void *buf, unsigned long len, + const void *buf, size_t len, enum object_type type, struct object_id *oid, struct object_id *compat_oid, From 58823d431061b82a9cd02a1623aff6fe93a51446 Mon Sep 17 00:00:00 2001 From: Philip Oakley Date: Tue, 16 Jun 2026 14:49:54 +0000 Subject: [PATCH 31/95] hash algorithms: use size_t for section lengths Continue walking the code path for the >4GB `hash-object --literally` test to the hash algorithm step for LLP64 systems. This patch lets the SHA1DC code use `size_t`, making it compatible with LLP64 data models (as used e.g. by Windows). The interested reader of this patch will note that we adjust the signature of the `git_SHA1DCUpdate()` function without updating _any_ call site. This certainly puzzled at least one reviewer already, so here is an explanation: This function is never called directly, but always via the macro `platform_SHA1_Update`, which is usually called via the macro `git_SHA1_Update`. However, we never call `git_SHA1_Update()` directly in `struct git_hash_algo`. Instead, we call `git_hash_sha1_update()`, which is defined thusly: static void git_hash_sha1_update(git_hash_ctx *ctx, const void *data, size_t len) { git_SHA1_Update(&ctx->sha1, data, len); } i.e. it contains an implicit downcast from `size_t` to `unsigned long` (before this here patch). With this patch, there is no downcast anymore. With this patch, finally, the t1007-hash-object.sh "files over 4GB hash literally" test case is fixed. Signed-off-by: Philip Oakley Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- object-file.c | 4 ++-- sha1dc_git.c | 3 +-- sha1dc_git.h | 2 +- t/t1007-hash-object.sh | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/object-file.c b/object-file.c index dccbe0fc3ea8b9..0056c369cee036 100644 --- a/object-file.c +++ b/object-file.c @@ -316,7 +316,7 @@ int parse_loose_header(const char *hdr, struct object_info *oi) } static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_ctx *c, - const void *buf, unsigned long len, + const void *buf, size_t len, struct object_id *oid, char *hdr, size_t *hdrlen) { @@ -336,7 +336,7 @@ void write_object_file_prepare(const struct git_hash_algo *algo, /* Generate the header */ *hdrlen = format_object_header(hdr, *hdrlen, type, len); - /* Sha1.. */ + /* Hash (function pointers) computation */ hash_object_body(algo, &c, buf, len, oid, hdr, hdrlen); } diff --git a/sha1dc_git.c b/sha1dc_git.c index 9b675a046ee699..fe58d7962a30c9 100644 --- a/sha1dc_git.c +++ b/sha1dc_git.c @@ -27,10 +27,9 @@ void git_SHA1DCFinal(unsigned char hash[20], SHA1_CTX *ctx) /* * Same as SHA1DCUpdate, but adjust types to match git's usual interface. */ -void git_SHA1DCUpdate(SHA1_CTX *ctx, const void *vdata, unsigned long len) +void git_SHA1DCUpdate(SHA1_CTX *ctx, const void *vdata, size_t len) { const char *data = vdata; - /* We expect an unsigned long, but sha1dc only takes an int */ while (len > INT_MAX) { SHA1DCUpdate(ctx, data, INT_MAX); data += INT_MAX; diff --git a/sha1dc_git.h b/sha1dc_git.h index f6f880cabea382..0bcf1aa84b7241 100644 --- a/sha1dc_git.h +++ b/sha1dc_git.h @@ -15,7 +15,7 @@ void git_SHA1DCInit(SHA1_CTX *); #endif void git_SHA1DCFinal(unsigned char [20], SHA1_CTX *); -void git_SHA1DCUpdate(SHA1_CTX *ctx, const void *data, unsigned long len); +void git_SHA1DCUpdate(SHA1_CTX *ctx, const void *data, size_t len); #define platform_SHA_IS_SHA1DC /* used by "test-tool sha1-is-sha1dc" */ diff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh index 7867fd1dbf940c..f028a1cbcc4ae9 100755 --- a/t/t1007-hash-object.sh +++ b/t/t1007-hash-object.sh @@ -261,7 +261,7 @@ test_expect_success '--stdin outside of repository (uses default hash)' ' test_cmp expect actual ' -test_expect_failure EXPENSIVE,SIZE_T_IS_64BIT,!LONG_IS_64BIT \ +test_expect_success EXPENSIVE,SIZE_T_IS_64BIT \ 'files over 4GB hash literally' ' test-tool genzeros $((5*1024*1024*1024)) >big && test_oid large5GB >expect && From 9729b5e2f209ff982cf536773bc223d149910968 Mon Sep 17 00:00:00 2001 From: Philip Oakley Date: Tue, 16 Jun 2026 14:49:55 +0000 Subject: [PATCH 32/95] hash-object --stdin: verify that it works with >4GB/LLP64 Just like the `hash-object --literally` code path, the `--stdin` code path also needs to use `size_t` instead of `unsigned long` to represent memory sizes, otherwise it would cause problems on platforms using the LLP64 data model (such as Windows). To limit the scope of the test case, the object is explicitly not written to the object store, nor are any filters applied. The `big` file from the previous test case is reused to save setup time; To avoid relying on that side effect, it is generated if it does not exist (e.g. when running via `sh t1007-*.sh --long --run=1,41`). Signed-off-by: Philip Oakley Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t1007-hash-object.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh index f028a1cbcc4ae9..bcae3fc54c5be2 100755 --- a/t/t1007-hash-object.sh +++ b/t/t1007-hash-object.sh @@ -269,4 +269,12 @@ test_expect_success EXPENSIVE,SIZE_T_IS_64BIT \ test_cmp expect actual ' +test_expect_success EXPENSIVE,SIZE_T_IS_64BIT \ + 'files over 4GB hash correctly via --stdin' ' + { test -f big || test-tool genzeros $((5*1024*1024*1024)) >big; } && + test_oid large5GB >expect && + git hash-object --stdin actual && + test_cmp expect actual +' + test_done From 2fefea368287f225c1b050453fe16149ab4eae8f Mon Sep 17 00:00:00 2001 From: Philip Oakley Date: Tue, 16 Jun 2026 14:49:56 +0000 Subject: [PATCH 33/95] hash-object: add another >4GB/LLP64 test case To complement the `--stdin` and `--literally` test cases that verify that we can hash files larger than 4GB on 64-bit platforms using the LLP64 data model, here is a test case that exercises `hash-object` _without_ any options. Just as before, we use the `big` file from the previous test case if it exists to save on setup time, otherwise generate it. Signed-off-by: Philip Oakley Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t1007-hash-object.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh index bcae3fc54c5be2..f96c29ce68ea49 100755 --- a/t/t1007-hash-object.sh +++ b/t/t1007-hash-object.sh @@ -277,4 +277,12 @@ test_expect_success EXPENSIVE,SIZE_T_IS_64BIT \ test_cmp expect actual ' +test_expect_success EXPENSIVE,SIZE_T_IS_64BIT \ + 'files over 4GB hash correctly' ' + { test -f big || test-tool genzeros $((5*1024*1024*1024)) >big; } && + test_oid large5GB >expect && + git hash-object -- big >actual && + test_cmp expect actual +' + test_done From d99e13d0bec75820f48924d3489394172b7a4be2 Mon Sep 17 00:00:00 2001 From: Philip Oakley Date: Tue, 16 Jun 2026 14:49:57 +0000 Subject: [PATCH 34/95] hash-object: add a >4GB/LLP64 test case using filtered input To verify that the `clean` side of the `clean`/`smudge` filter code is correct with regards to LLP64 (read: to ensure that `size_t` is used instead of `unsigned long`), here is a test case using a trivial filter, specifically _not_ writing anything to the object store to limit the scope of the test case. As in previous commits, the `big` file from previous test cases is reused if available, to save setup time, otherwise re-generated. Signed-off-by: Philip Oakley Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t1007-hash-object.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh index f96c29ce68ea49..4bc82dd9682954 100755 --- a/t/t1007-hash-object.sh +++ b/t/t1007-hash-object.sh @@ -285,4 +285,16 @@ test_expect_success EXPENSIVE,SIZE_T_IS_64BIT \ test_cmp expect actual ' +# This clean filter does nothing, other than excercising the interface. +# We ensure that cleaning doesn't mangle large files on 64-bit Windows. +test_expect_success EXPENSIVE,SIZE_T_IS_64BIT \ + 'hash filtered files over 4GB correctly' ' + { test -f big || test-tool genzeros $((5*1024*1024*1024)) >big; } && + test_oid large5GB >expect && + test_config filter.null-filter.clean "cat" && + echo "big filter=null-filter" >.gitattributes && + git hash-object -- big >actual && + test_cmp expect actual +' + test_done From e2fb4ba003dac9c71b78b997fb90aac10c11c2df Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:39:44 +0200 Subject: [PATCH 35/95] packfile: rename `struct packfile_store` to `odb_source_packed` Not too long ago, we have introduced the packfile store in b7983adb51 (packfile: introduce a new `struct packfile_store`, 2025-09-23). This struct is responsible for managing all of our access to packfiles and is used as one of the two sources of objects for the "files" source. Back when I introduced this structure I didn't have the clear vision yet that it will eventually also turn into a proper object database source, and how exactly that infrastructure will look like. Now though it's becoming increasingly clear that it does make sense to treat it just the same as any of our other ODB sources. The consequence is that the naming is now a bit out-of-date: it's just another source and will be turned into a proper `struct odb_source` over the next couple of commits, but it's not named accordingly. Rename the structure to `odb_source_packed` to align it with this goal and to bring it in line with the other sources we already have. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-files.h | 4 ++-- packfile.c | 56 +++++++++++++++++++++++----------------------- packfile.h | 32 +++++++++++++------------- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/odb/source-files.h b/odb/source-files.h index 23a3b4e04b1218..d7ac3c1c81d892 100644 --- a/odb/source-files.h +++ b/odb/source-files.h @@ -4,7 +4,7 @@ #include "odb/source.h" struct odb_source_loose; -struct packfile_store; +struct odb_source_packed; /* * The files object database source uses a combination of loose objects and @@ -13,7 +13,7 @@ struct packfile_store; struct odb_source_files { struct odb_source base; struct odb_source_loose *loose; - struct packfile_store *packed; + struct odb_source_packed *packed; }; /* Allocate and initialize a new object source. */ diff --git a/packfile.c b/packfile.c index 89366abfe32386..a2d768d0ae4bdd 100644 --- a/packfile.c +++ b/packfile.c @@ -859,7 +859,7 @@ struct packed_git *add_packed_git(struct repository *r, const char *path, return p; } -void packfile_store_add_pack(struct packfile_store *store, +void packfile_store_add_pack(struct odb_source_packed *store, struct packed_git *pack) { if (pack->pack_fd != -1) @@ -869,7 +869,7 @@ void packfile_store_add_pack(struct packfile_store *store, strmap_put(&store->packs_by_path, pack->pack_name, pack); } -struct packed_git *packfile_store_load_pack(struct packfile_store *store, +struct packed_git *packfile_store_load_pack(struct odb_source_packed *store, const char *idx_path, int local) { struct strbuf key = STRBUF_INIT; @@ -1068,7 +1068,7 @@ static int sort_pack(const struct packfile_list_entry *a, return -1; } -void packfile_store_prepare(struct packfile_store *store) +void packfile_store_prepare(struct odb_source_packed *store) { if (store->initialized) return; @@ -1084,13 +1084,13 @@ void packfile_store_prepare(struct packfile_store *store) store->initialized = true; } -void packfile_store_reprepare(struct packfile_store *store) +void packfile_store_reprepare(struct odb_source_packed *store) { store->initialized = false; packfile_store_prepare(store); } -struct packfile_list_entry *packfile_store_get_packs(struct packfile_store *store) +struct packfile_list_entry *packfile_store_get_packs(struct odb_source_packed *store) { packfile_store_prepare(store); @@ -1103,7 +1103,7 @@ struct packfile_list_entry *packfile_store_get_packs(struct packfile_store *stor return store->packs.head; } -int packfile_store_count_objects(struct packfile_store *store, +int packfile_store_count_objects(struct odb_source_packed *store, enum odb_count_objects_flags flags UNUSED, unsigned long *out) { @@ -2160,7 +2160,7 @@ static int fill_pack_entry(const struct object_id *oid, return 1; } -static int find_pack_entry(struct packfile_store *store, +static int find_pack_entry(struct odb_source_packed *store, const struct object_id *oid, struct pack_entry *e) { @@ -2183,7 +2183,7 @@ static int find_pack_entry(struct packfile_store *store, return 0; } -int packfile_store_freshen_object(struct packfile_store *store, +int packfile_store_freshen_object(struct odb_source_packed *store, const struct object_id *oid) { struct pack_entry e; @@ -2199,7 +2199,7 @@ int packfile_store_freshen_object(struct packfile_store *store, return 1; } -int packfile_store_read_object_info(struct packfile_store *store, +int packfile_store_read_object_info(struct odb_source_packed *store, const struct object_id *oid, struct object_info *oi, enum object_info_flags flags) @@ -2234,7 +2234,7 @@ int packfile_store_read_object_info(struct packfile_store *store, return 0; } -static void maybe_invalidate_kept_pack_cache(struct packfile_store *store, +static void maybe_invalidate_kept_pack_cache(struct odb_source_packed *store, unsigned flags) { if (!store->kept_cache.packs) @@ -2245,7 +2245,7 @@ static void maybe_invalidate_kept_pack_cache(struct packfile_store *store, store->kept_cache.flags = 0; } -struct packed_git **packfile_store_get_kept_pack_cache(struct packfile_store *store, +struct packed_git **packfile_store_get_kept_pack_cache(struct odb_source_packed *store, unsigned flags) { maybe_invalidate_kept_pack_cache(store, flags); @@ -2365,8 +2365,8 @@ int for_each_object_in_pack(struct packed_git *p, return r; } -struct packfile_store_for_each_object_wrapper_data { - struct packfile_store *store; +struct odb_source_packed_for_each_object_wrapper_data { + struct odb_source_packed *store; const struct object_info *request; odb_for_each_object_cb cb; void *cb_data; @@ -2377,7 +2377,7 @@ static int packfile_store_for_each_object_wrapper(const struct object_id *oid, uint32_t index_pos, void *cb_data) { - struct packfile_store_for_each_object_wrapper_data *data = cb_data; + struct odb_source_packed_for_each_object_wrapper_data *data = cb_data; if (data->request) { off_t offset = nth_packed_object_offset(pack, index_pos); @@ -2411,10 +2411,10 @@ static int match_hash(unsigned len, const unsigned char *a, const unsigned char } static int for_each_prefixed_object_in_midx( - struct packfile_store *store, + struct odb_source_packed *store, struct multi_pack_index *m, const struct odb_for_each_object_options *opts, - struct packfile_store_for_each_object_wrapper_data *data) + struct odb_source_packed_for_each_object_wrapper_data *data) { int ret; @@ -2470,10 +2470,10 @@ static int for_each_prefixed_object_in_midx( } static int for_each_prefixed_object_in_pack( - struct packfile_store *store, + struct odb_source_packed *store, struct packed_git *p, const struct odb_for_each_object_options *opts, - struct packfile_store_for_each_object_wrapper_data *data) + struct odb_source_packed_for_each_object_wrapper_data *data) { uint32_t num, i, first = 0; int len = opts->prefix_hex_len > p->repo->hash_algo->hexsz ? @@ -2519,9 +2519,9 @@ static int for_each_prefixed_object_in_pack( } static int packfile_store_for_each_prefixed_object( - struct packfile_store *store, + struct odb_source_packed *store, const struct odb_for_each_object_options *opts, - struct packfile_store_for_each_object_wrapper_data *data) + struct odb_source_packed_for_each_object_wrapper_data *data) { struct packfile_list_entry *e; struct multi_pack_index *m; @@ -2566,13 +2566,13 @@ static int packfile_store_for_each_prefixed_object( return ret; } -int packfile_store_for_each_object(struct packfile_store *store, +int packfile_store_for_each_object(struct odb_source_packed *store, const struct object_info *request, odb_for_each_object_cb cb, void *cb_data, const struct odb_for_each_object_options *opts) { - struct packfile_store_for_each_object_wrapper_data data = { + struct odb_source_packed_for_each_object_wrapper_data data = { .store = store, .request = request, .cb = cb, @@ -2707,7 +2707,7 @@ static void find_abbrev_len_for_pack(struct packed_git *p, *out = len; } -int packfile_store_find_abbrev_len(struct packfile_store *store, +int packfile_store_find_abbrev_len(struct odb_source_packed *store, const struct object_id *oid, unsigned min_len, unsigned *out) @@ -2832,16 +2832,16 @@ int parse_pack_header_option(const char *in, unsigned char *out, unsigned int *l return 0; } -struct packfile_store *packfile_store_new(struct odb_source *source) +struct odb_source_packed *packfile_store_new(struct odb_source *source) { - struct packfile_store *store; + struct odb_source_packed *store; CALLOC_ARRAY(store, 1); store->source = source; strmap_init(&store->packs_by_path); return store; } -void packfile_store_free(struct packfile_store *store) +void packfile_store_free(struct odb_source_packed *store) { for (struct packfile_list_entry *e = store->packs.head; e; e = e->next) free(e->pack); @@ -2851,7 +2851,7 @@ void packfile_store_free(struct packfile_store *store) free(store); } -void packfile_store_close(struct packfile_store *store) +void packfile_store_close(struct odb_source_packed *store) { for (struct packfile_list_entry *e = store->packs.head; e; e = e->next) { if (e->pack->do_not_close) @@ -2988,7 +2988,7 @@ int packfile_read_object_stream(struct odb_read_stream **out, } int packfile_store_read_object_stream(struct odb_read_stream **out, - struct packfile_store *store, + struct odb_source_packed *store, const struct object_id *oid) { struct pack_entry e; diff --git a/packfile.h b/packfile.h index 49d6bdecf6ea18..9cec15bc50a223 100644 --- a/packfile.h +++ b/packfile.h @@ -79,7 +79,7 @@ struct packed_git *packfile_list_find_oid(struct packfile_list_entry *packs, /* * A store that manages packfiles for a given object database. */ -struct packfile_store { +struct odb_source_packed { struct odb_source *source; /* @@ -138,19 +138,19 @@ struct packfile_store { * Allocate and initialize a new empty packfile store for the given object * database source. */ -struct packfile_store *packfile_store_new(struct odb_source *source); +struct odb_source_packed *packfile_store_new(struct odb_source *source); /* * Free the packfile store and all its associated state. All packfiles * tracked by the store will be closed. */ -void packfile_store_free(struct packfile_store *store); +void packfile_store_free(struct odb_source_packed *store); /* * Close all packfiles associated with this store. The packfiles won't be * free'd, so they can be re-opened at a later point in time. */ -void packfile_store_close(struct packfile_store *store); +void packfile_store_close(struct odb_source_packed *store); /* * Prepare the packfile store by loading packfiles and multi-pack indices for @@ -159,7 +159,7 @@ void packfile_store_close(struct packfile_store *store); * It shouldn't typically be necessary to call this function directly, as * functions that access the store know to prepare it. */ -void packfile_store_prepare(struct packfile_store *store); +void packfile_store_prepare(struct odb_source_packed *store); /* * Clear the packfile caches and try to look up any new packfiles that have @@ -167,20 +167,20 @@ void packfile_store_prepare(struct packfile_store *store); * * This function must be called under the `odb_read_lock()`. */ -void packfile_store_reprepare(struct packfile_store *store); +void packfile_store_reprepare(struct odb_source_packed *store); /* * Add the pack to the store so that contained objects become accessible via * the store. This moves ownership into the store. */ -void packfile_store_add_pack(struct packfile_store *store, +void packfile_store_add_pack(struct odb_source_packed *store, struct packed_git *pack); /* * Get all packs managed by the given store, including packfiles that are * referenced by multi-pack indices. */ -struct packfile_list_entry *packfile_store_get_packs(struct packfile_store *store); +struct packfile_list_entry *packfile_store_get_packs(struct odb_source_packed *store); struct repo_for_each_pack_data { struct odb_source *source; @@ -239,7 +239,7 @@ static inline void repo_for_each_pack_data_next(struct repo_for_each_pack_data * repo_for_each_pack_data_next(&eack_pack_data)) int packfile_store_read_object_stream(struct odb_read_stream **out, - struct packfile_store *store, + struct odb_source_packed *store, const struct object_id *oid); /* @@ -248,7 +248,7 @@ int packfile_store_read_object_stream(struct odb_read_stream **out, * not found, 0 if it was and read successfully, and a negative error code in * case the object was corrupted. */ -int packfile_store_read_object_info(struct packfile_store *store, +int packfile_store_read_object_info(struct odb_source_packed *store, const struct object_id *oid, struct object_info *oi, enum object_info_flags flags); @@ -258,10 +258,10 @@ int packfile_store_read_object_info(struct packfile_store *store, * either the newly opened packfile or the preexisting packfile. Returns a * `NULL` pointer in case the packfile could not be opened. */ -struct packed_git *packfile_store_load_pack(struct packfile_store *store, +struct packed_git *packfile_store_load_pack(struct odb_source_packed *store, const char *idx_path, int local); -int packfile_store_freshen_object(struct packfile_store *store, +int packfile_store_freshen_object(struct odb_source_packed *store, const struct object_id *oid); enum kept_pack_type { @@ -276,7 +276,7 @@ enum kept_pack_type { * * Return 0 on success, a negative error code otherwise. */ -int packfile_store_count_objects(struct packfile_store *store, +int packfile_store_count_objects(struct odb_source_packed *store, enum odb_count_objects_flags flags, unsigned long *out); @@ -285,7 +285,7 @@ int packfile_store_count_objects(struct packfile_store *store, * combination of `kept_pack_type` flags. The cache is computed on demand and * will be recomputed whenever the flags change. */ -struct packed_git **packfile_store_get_kept_pack_cache(struct packfile_store *store, +struct packed_git **packfile_store_get_kept_pack_cache(struct odb_source_packed *store, unsigned flags); struct pack_window { @@ -365,13 +365,13 @@ int for_each_object_in_pack(struct packed_git *p, * * The flags parameter is a combination of `odb_for_each_object_flags`. */ -int packfile_store_for_each_object(struct packfile_store *store, +int packfile_store_for_each_object(struct odb_source_packed *store, const struct object_info *request, odb_for_each_object_cb cb, void *cb_data, const struct odb_for_each_object_options *opts); -int packfile_store_find_abbrev_len(struct packfile_store *store, +int packfile_store_find_abbrev_len(struct odb_source_packed *store, const struct object_id *oid, unsigned min_len, unsigned *out); From cf86a18ac3b4e49f49c2c19351de109064999827 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:39:45 +0200 Subject: [PATCH 36/95] packfile: split out packfile list logic In the next commit we're about to introduce the "packed" object database source. This source will embed a packfile list, and consequently we'll have to include "packfile.h" to make the struct definition available. This will unfortunately lead to a cyclic dependency that we cannot resolve with a forward declaration. Split out the code that relates to the packfile list into a separate compilation unit so that both "packfile.h" and "odb/source-packed.h" can include it. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Makefile | 1 + meson.build | 1 + packfile-list.c | 86 +++++++++++++++++++++++++++++++++++++++++++++++++ packfile-list.h | 28 ++++++++++++++++ packfile.c | 83 ----------------------------------------------- packfile.h | 23 +------------ 6 files changed, 117 insertions(+), 105 deletions(-) create mode 100644 packfile-list.c create mode 100644 packfile-list.h diff --git a/Makefile b/Makefile index 0976a69b4ca424..ed1731548e903f 100644 --- a/Makefile +++ b/Makefile @@ -1233,6 +1233,7 @@ LIB_OBJS += pack-refs.o LIB_OBJS += pack-revindex.o LIB_OBJS += pack-write.o LIB_OBJS += packfile.o +LIB_OBJS += packfile-list.o LIB_OBJS += pager.o LIB_OBJS += parallel-checkout.o LIB_OBJS += parse.o diff --git a/meson.build b/meson.build index 3247697f74aae1..12913fc9489ec3 100644 --- a/meson.build +++ b/meson.build @@ -421,6 +421,7 @@ libgit_sources = [ 'pack-revindex.c', 'pack-write.c', 'packfile.c', + 'packfile-list.c', 'pager.c', 'parallel-checkout.c', 'parse.c', diff --git a/packfile-list.c b/packfile-list.c new file mode 100644 index 00000000000000..01fb913abf78fc --- /dev/null +++ b/packfile-list.c @@ -0,0 +1,86 @@ +#include "git-compat-util.h" +#include "packfile.h" +#include "packfile-list.h" + +void packfile_list_clear(struct packfile_list *list) +{ + struct packfile_list_entry *e, *next; + + for (e = list->head; e; e = next) { + next = e->next; + free(e); + } + + list->head = list->tail = NULL; +} + +static struct packfile_list_entry *packfile_list_remove_internal(struct packfile_list *list, + struct packed_git *pack) +{ + struct packfile_list_entry *e, *prev; + + for (e = list->head, prev = NULL; e; prev = e, e = e->next) { + if (e->pack != pack) + continue; + + if (prev) + prev->next = e->next; + if (list->head == e) + list->head = e->next; + if (list->tail == e) + list->tail = prev; + + return e; + } + + return NULL; +} + +void packfile_list_remove(struct packfile_list *list, struct packed_git *pack) +{ + free(packfile_list_remove_internal(list, pack)); +} + +void packfile_list_prepend(struct packfile_list *list, struct packed_git *pack) +{ + struct packfile_list_entry *entry; + + entry = packfile_list_remove_internal(list, pack); + if (!entry) { + entry = xmalloc(sizeof(*entry)); + entry->pack = pack; + } + entry->next = list->head; + + list->head = entry; + if (!list->tail) + list->tail = entry; +} + +void packfile_list_append(struct packfile_list *list, struct packed_git *pack) +{ + struct packfile_list_entry *entry; + + entry = packfile_list_remove_internal(list, pack); + if (!entry) { + entry = xmalloc(sizeof(*entry)); + entry->pack = pack; + } + entry->next = NULL; + + if (list->tail) { + list->tail->next = entry; + list->tail = entry; + } else { + list->head = list->tail = entry; + } +} + +struct packed_git *packfile_list_find_oid(struct packfile_list_entry *packs, + const struct object_id *oid) +{ + for (; packs; packs = packs->next) + if (find_pack_entry_one(oid, packs->pack)) + return packs->pack; + return NULL; +} diff --git a/packfile-list.h b/packfile-list.h new file mode 100644 index 00000000000000..1b05e2aa36de08 --- /dev/null +++ b/packfile-list.h @@ -0,0 +1,28 @@ +#ifndef PACKFILE_LIST_H +#define PACKFILE_LIST_H + +struct object_id; + +struct packfile_list { + struct packfile_list_entry *head, *tail; +}; + +struct packfile_list_entry { + struct packfile_list_entry *next; + struct packed_git *pack; +}; + +void packfile_list_clear(struct packfile_list *list); +void packfile_list_remove(struct packfile_list *list, struct packed_git *pack); +void packfile_list_prepend(struct packfile_list *list, struct packed_git *pack); +void packfile_list_append(struct packfile_list *list, struct packed_git *pack); + +/* + * Find the pack within the "packs" list whose index contains the object + * "oid". For general object lookups, you probably don't want this; use + * find_pack_entry() instead. + */ +struct packed_git *packfile_list_find_oid(struct packfile_list_entry *packs, + const struct object_id *oid); + +#endif diff --git a/packfile.c b/packfile.c index a2d768d0ae4bdd..27ea4a8436a535 100644 --- a/packfile.c +++ b/packfile.c @@ -48,89 +48,6 @@ static size_t pack_mapped; #define SZ_FMT PRIuMAX static inline uintmax_t sz_fmt(size_t s) { return s; } -void packfile_list_clear(struct packfile_list *list) -{ - struct packfile_list_entry *e, *next; - - for (e = list->head; e; e = next) { - next = e->next; - free(e); - } - - list->head = list->tail = NULL; -} - -static struct packfile_list_entry *packfile_list_remove_internal(struct packfile_list *list, - struct packed_git *pack) -{ - struct packfile_list_entry *e, *prev; - - for (e = list->head, prev = NULL; e; prev = e, e = e->next) { - if (e->pack != pack) - continue; - - if (prev) - prev->next = e->next; - if (list->head == e) - list->head = e->next; - if (list->tail == e) - list->tail = prev; - - return e; - } - - return NULL; -} - -void packfile_list_remove(struct packfile_list *list, struct packed_git *pack) -{ - free(packfile_list_remove_internal(list, pack)); -} - -void packfile_list_prepend(struct packfile_list *list, struct packed_git *pack) -{ - struct packfile_list_entry *entry; - - entry = packfile_list_remove_internal(list, pack); - if (!entry) { - entry = xmalloc(sizeof(*entry)); - entry->pack = pack; - } - entry->next = list->head; - - list->head = entry; - if (!list->tail) - list->tail = entry; -} - -void packfile_list_append(struct packfile_list *list, struct packed_git *pack) -{ - struct packfile_list_entry *entry; - - entry = packfile_list_remove_internal(list, pack); - if (!entry) { - entry = xmalloc(sizeof(*entry)); - entry->pack = pack; - } - entry->next = NULL; - - if (list->tail) { - list->tail->next = entry; - list->tail = entry; - } else { - list->head = list->tail = entry; - } -} - -struct packed_git *packfile_list_find_oid(struct packfile_list_entry *packs, - const struct object_id *oid) -{ - for (; packs; packs = packs->next) - if (find_pack_entry_one(oid, packs->pack)) - return packs->pack; - return NULL; -} - void pack_report(struct repository *repo) { fprintf(stderr, diff --git a/packfile.h b/packfile.h index 9cec15bc50a223..4e3d701a3a2504 100644 --- a/packfile.h +++ b/packfile.h @@ -6,6 +6,7 @@ #include "odb.h" #include "odb/source-files.h" #include "oidset.h" +#include "packfile-list.h" #include "repository.h" #include "strmap.h" @@ -54,28 +55,6 @@ struct packed_git { char pack_name[FLEX_ARRAY]; /* more */ }; -struct packfile_list { - struct packfile_list_entry *head, *tail; -}; - -struct packfile_list_entry { - struct packfile_list_entry *next; - struct packed_git *pack; -}; - -void packfile_list_clear(struct packfile_list *list); -void packfile_list_remove(struct packfile_list *list, struct packed_git *pack); -void packfile_list_prepend(struct packfile_list *list, struct packed_git *pack); -void packfile_list_append(struct packfile_list *list, struct packed_git *pack); - -/* - * Find the pack within the "packs" list whose index contains the object - * "oid". For general object lookups, you probably don't want this; use - * find_pack_entry() instead. - */ -struct packed_git *packfile_list_find_oid(struct packfile_list_entry *packs, - const struct object_id *oid); - /* * A store that manages packfiles for a given object database. */ From c71a1214156374b7b8c3c9158aa27e94e70df6c9 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:39:46 +0200 Subject: [PATCH 37/95] packfile: move packed source into "odb/" subsystem In subsequent patches we'll be turning `struct odb_source_packed` into a proper `struct odb_source`. As a first step towards this goal, move its struct out of "packfile.{c,h}" and into "odb/source-packed.{c,h}". This detaches the implementation of the packfile object source from the generic packfile code, following the same convention already used by the "files" and "in-memory" sources. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Makefile | 1 + meson.build | 1 + odb/source-files.c | 2 +- odb/source-packed.c | 11 +++++++ odb/source-packed.h | 72 +++++++++++++++++++++++++++++++++++++++++++++ packfile.c | 9 ------ packfile.h | 66 +---------------------------------------- 7 files changed, 87 insertions(+), 75 deletions(-) create mode 100644 odb/source-packed.c create mode 100644 odb/source-packed.h diff --git a/Makefile b/Makefile index ed1731548e903f..113fa459939cae 100644 --- a/Makefile +++ b/Makefile @@ -1218,6 +1218,7 @@ LIB_OBJS += odb/source.o LIB_OBJS += odb/source-files.o LIB_OBJS += odb/source-inmemory.o LIB_OBJS += odb/source-loose.o +LIB_OBJS += odb/source-packed.o LIB_OBJS += odb/streaming.o LIB_OBJS += odb/transaction.o LIB_OBJS += oid-array.o diff --git a/meson.build b/meson.build index 12913fc9489ec3..ca235801cf4848 100644 --- a/meson.build +++ b/meson.build @@ -406,6 +406,7 @@ libgit_sources = [ 'odb/source-files.c', 'odb/source-inmemory.c', 'odb/source-loose.c', + 'odb/source-packed.c', 'odb/streaming.c', 'odb/transaction.c', 'oid-array.c', diff --git a/odb/source-files.c b/odb/source-files.c index 5bdd0429225397..191562f316743e 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -269,7 +269,7 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb, CALLOC_ARRAY(files, 1); odb_source_init(&files->base, odb, ODB_SOURCE_FILES, path, local); files->loose = odb_source_loose_new(odb, path, local); - files->packed = packfile_store_new(&files->base); + files->packed = odb_source_packed_new(&files->base); files->base.free = odb_source_files_free; files->base.close = odb_source_files_close; diff --git a/odb/source-packed.c b/odb/source-packed.c new file mode 100644 index 00000000000000..1e94b47ea0bf2c --- /dev/null +++ b/odb/source-packed.c @@ -0,0 +1,11 @@ +#include "git-compat-util.h" +#include "odb/source-packed.h" + +struct odb_source_packed *odb_source_packed_new(struct odb_source *source) +{ + struct odb_source_packed *store; + CALLOC_ARRAY(store, 1); + store->source = source; + strmap_init(&store->packs_by_path); + return store; +} diff --git a/odb/source-packed.h b/odb/source-packed.h new file mode 100644 index 00000000000000..327be4ad659b54 --- /dev/null +++ b/odb/source-packed.h @@ -0,0 +1,72 @@ +#ifndef ODB_SOURCE_PACKED_H +#define ODB_SOURCE_PACKED_H + +#include "odb/source.h" +#include "packfile-list.h" +#include "strmap.h" + +/* + * A store that manages packfiles for a given object database. + */ +struct odb_source_packed { + struct odb_source *source; + + /* + * The list of packfiles in the order in which they have been most + * recently used. + */ + struct packfile_list packs; + + /* + * Cache of packfiles which are marked as "kept", either because there + * is an on-disk ".keep" file or because they are marked as "kept" in + * memory. + * + * Should not be accessed directly, but via + * `packfile_store_get_kept_pack_cache()`. The list of packs gets + * invalidated when the stored flags and the flags passed to + * `packfile_store_get_kept_pack_cache()` mismatch. + */ + struct { + struct packed_git **packs; + unsigned flags; + } kept_cache; + + /* The multi-pack index that belongs to this specific packfile store. */ + struct multi_pack_index *midx; + + /* + * A map of packfile names to packed_git structs for tracking which + * packs have been loaded already. + */ + struct strmap packs_by_path; + + /* + * Whether packfiles have already been populated with this store's + * packs. + */ + bool initialized; + + /* + * Usually, packfiles will be reordered to the front of the `packs` + * list whenever an object is looked up via them. This has the effect + * that packs that contain a lot of accessed objects will be located + * towards the front. + * + * This is usually desireable, but there are exceptions. One exception + * is when the looking up multiple objects in a loop for each packfile. + * In that case, we may easily end up with an infinite loop as the + * packfiles get reordered to the front repeatedly. + * + * Setting this field to `true` thus disables these reorderings. + */ + bool skip_mru_updates; +}; + +/* + * Allocate and initialize a new empty packfile store for the given object + * database source. + */ +struct odb_source_packed *odb_source_packed_new(struct odb_source *source); + +#endif diff --git a/packfile.c b/packfile.c index 27ea4a8436a535..99be5789efbad4 100644 --- a/packfile.c +++ b/packfile.c @@ -2749,15 +2749,6 @@ int parse_pack_header_option(const char *in, unsigned char *out, unsigned int *l return 0; } -struct odb_source_packed *packfile_store_new(struct odb_source *source) -{ - struct odb_source_packed *store; - CALLOC_ARRAY(store, 1); - store->source = source; - strmap_init(&store->packs_by_path); - return store; -} - void packfile_store_free(struct odb_source_packed *store) { for (struct packfile_list_entry *e = store->packs.head; e; e = e->next) diff --git a/packfile.h b/packfile.h index 4e3d701a3a2504..2d0bb7adbe83c6 100644 --- a/packfile.h +++ b/packfile.h @@ -5,10 +5,10 @@ #include "object.h" #include "odb.h" #include "odb/source-files.h" +#include "odb/source-packed.h" #include "oidset.h" #include "packfile-list.h" #include "repository.h" -#include "strmap.h" /* in odb.h */ struct object_info; @@ -55,70 +55,6 @@ struct packed_git { char pack_name[FLEX_ARRAY]; /* more */ }; -/* - * A store that manages packfiles for a given object database. - */ -struct odb_source_packed { - struct odb_source *source; - - /* - * The list of packfiles in the order in which they have been most - * recently used. - */ - struct packfile_list packs; - - /* - * Cache of packfiles which are marked as "kept", either because there - * is an on-disk ".keep" file or because they are marked as "kept" in - * memory. - * - * Should not be accessed directly, but via - * `packfile_store_get_kept_pack_cache()`. The list of packs gets - * invalidated when the stored flags and the flags passed to - * `packfile_store_get_kept_pack_cache()` mismatch. - */ - struct { - struct packed_git **packs; - unsigned flags; - } kept_cache; - - /* The multi-pack index that belongs to this specific packfile store. */ - struct multi_pack_index *midx; - - /* - * A map of packfile names to packed_git structs for tracking which - * packs have been loaded already. - */ - struct strmap packs_by_path; - - /* - * Whether packfiles have already been populated with this store's - * packs. - */ - bool initialized; - - /* - * Usually, packfiles will be reordered to the front of the `packs` - * list whenever an object is looked up via them. This has the effect - * that packs that contain a lot of accessed objects will be located - * towards the front. - * - * This is usually desireable, but there are exceptions. One exception - * is when the looking up multiple objects in a loop for each packfile. - * In that case, we may easily end up with an infinite loop as the - * packfiles get reordered to the front repeatedly. - * - * Setting this field to `true` thus disables these reorderings. - */ - bool skip_mru_updates; -}; - -/* - * Allocate and initialize a new empty packfile store for the given object - * database source. - */ -struct odb_source_packed *packfile_store_new(struct odb_source *source); - /* * Free the packfile store and all its associated state. All packfiles * tracked by the store will be closed. From 3ac21e6d825a642dcab826772b495f492148299c Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:39:47 +0200 Subject: [PATCH 38/95] odb/source-packed: store pointer to "files" instead of generic source The `struct odb_source_packed` holds a pointer to its owning parent source. The way that Git is currently structured, this parent is always the "files" source. In subsequent commits we're going to detangle that so that the "packed" source doesn't have any owning parent source at all, which makes it usable as a completely standalone source. Detangling this mess is somewhat intricate though, and is made even more intricate because it's not always clear which kind of source one is holding at a specific point in time -- either the parent "files" source, or the child "packed" source. Make this relationship more explicit by storing a pointer to the "files" source instead of storing a pointer to a generic `struct odb_source`. This will help make subsequent steps a bit clearer by making it more obvious whether we're using the generic "base" source or the owning "files" source. Note that this is a temporary step, only. At the end of this series we will have dropped the "files" pointer completely. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-files.c | 2 +- odb/source-packed.c | 4 ++-- odb/source-packed.h | 4 ++-- packfile.c | 12 ++++++------ 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/odb/source-files.c b/odb/source-files.c index 191562f316743e..e04525fb08be97 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -269,7 +269,7 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb, CALLOC_ARRAY(files, 1); odb_source_init(&files->base, odb, ODB_SOURCE_FILES, path, local); files->loose = odb_source_loose_new(odb, path, local); - files->packed = odb_source_packed_new(&files->base); + files->packed = odb_source_packed_new(files); files->base.free = odb_source_files_free; files->base.close = odb_source_files_close; diff --git a/odb/source-packed.c b/odb/source-packed.c index 1e94b47ea0bf2c..12e785be48498a 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -1,11 +1,11 @@ #include "git-compat-util.h" #include "odb/source-packed.h" -struct odb_source_packed *odb_source_packed_new(struct odb_source *source) +struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent) { struct odb_source_packed *store; CALLOC_ARRAY(store, 1); - store->source = source; + store->files = parent; strmap_init(&store->packs_by_path); return store; } diff --git a/odb/source-packed.h b/odb/source-packed.h index 327be4ad659b54..3c2d229a174749 100644 --- a/odb/source-packed.h +++ b/odb/source-packed.h @@ -9,7 +9,7 @@ * A store that manages packfiles for a given object database. */ struct odb_source_packed { - struct odb_source *source; + struct odb_source_files *files; /* * The list of packfiles in the order in which they have been most @@ -67,6 +67,6 @@ struct odb_source_packed { * Allocate and initialize a new empty packfile store for the given object * database source. */ -struct odb_source_packed *odb_source_packed_new(struct odb_source *source); +struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent); #endif diff --git a/packfile.c b/packfile.c index 99be5789efbad4..862a24ad49bd78 100644 --- a/packfile.c +++ b/packfile.c @@ -802,7 +802,7 @@ struct packed_git *packfile_store_load_pack(struct odb_source_packed *store, p = strmap_get(&store->packs_by_path, key.buf); if (!p) { - p = add_packed_git(store->source->odb->repo, idx_path, + p = add_packed_git(store->files->base.odb->repo, idx_path, strlen(idx_path), local); if (p) packfile_store_add_pack(store, p); @@ -990,8 +990,8 @@ void packfile_store_prepare(struct odb_source_packed *store) if (store->initialized) return; - prepare_multi_pack_index_one(store->source); - prepare_packed_git_one(store->source); + prepare_multi_pack_index_one(&store->files->base); + prepare_packed_git_one(&store->files->base); sort_packs(&store->packs.head, sort_pack); for (struct packfile_list_entry *e = store->packs.head; e; e = e->next) @@ -1029,7 +1029,7 @@ int packfile_store_count_objects(struct odb_source_packed *store, unsigned long count = 0; int ret; - m = get_multi_pack_index(store->source); + m = get_multi_pack_index(&store->files->base); if (m) count += m->num_objects + m->num_objects_in_base; @@ -2450,7 +2450,7 @@ static int packfile_store_for_each_prefixed_object( store->skip_mru_updates = true; - m = get_multi_pack_index(store->source); + m = get_multi_pack_index(&store->files->base); if (m) { ret = for_each_prefixed_object_in_midx(store, m, opts, data); if (ret) @@ -2632,7 +2632,7 @@ int packfile_store_find_abbrev_len(struct odb_source_packed *store, struct packfile_list_entry *e; struct multi_pack_index *m; - m = get_multi_pack_index(store->source); + m = get_multi_pack_index(&store->files->base); if (m) find_abbrev_len_for_midx(m, oid, min_len, &min_len); From 0de2467e6c35930cf2530a213082829c8b86970d Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:39:48 +0200 Subject: [PATCH 39/95] odb/source-packed: start converting to a proper `struct odb_source` Start converting `struct odb_source_packed` into a proper pluggable `struct odb_source` by embedding the base struct and assigning it the new `ODB_SOURCE_PACKED` type. Furthermore, wire up lifecycle management of this source by implementing the `free` callback and taking ownership of the chdir notifications. Note that the packed source is not yet functional as a standalone `struct odb_source`, as it's missing all of the callback implementations. These will be wired up in subsequent commits. Further note that we're also registering a `chdir_notify` callback to reparent our path. This wasn't previously necessary (and still isn't at this point in time) because all paths are taken from the owning "files" source, and that source already handles the reparenting for us. But a subsequent commit will change that so that we're using the path of the "packed" source, and once that happens we'll need it to be updated when changing the working directory. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-files.c | 2 +- odb/source-packed.c | 49 ++++++++++++++++++++++++++++++++++++++++----- odb/source-packed.h | 12 +++++++++++ odb/source.h | 3 +++ packfile.c | 10 --------- packfile.h | 6 ------ 6 files changed, 60 insertions(+), 22 deletions(-) diff --git a/odb/source-files.c b/odb/source-files.c index e04525fb08be97..3608808e7c6f3a 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -29,7 +29,7 @@ static void odb_source_files_free(struct odb_source *source) struct odb_source_files *files = odb_source_files_downcast(source); chdir_notify_unregister(NULL, odb_source_files_reparent, files); odb_source_free(&files->loose->base); - packfile_store_free(files->packed); + odb_source_free(&files->packed->base); odb_source_release(&files->base); free(files); } diff --git a/odb/source-packed.c b/odb/source-packed.c index 12e785be48498a..f81a990cbde757 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -1,11 +1,50 @@ #include "git-compat-util.h" +#include "abspath.h" +#include "chdir-notify.h" #include "odb/source-packed.h" +#include "packfile.h" + +static void odb_source_packed_reparent(const char *name UNUSED, + const char *old_cwd, + const char *new_cwd, + void *cb_data) +{ + struct odb_source_packed *packed = cb_data; + char *path = reparent_relative_path(old_cwd, new_cwd, + packed->base.path); + free(packed->base.path); + packed->base.path = path; +} + +static void odb_source_packed_free(struct odb_source *source) +{ + struct odb_source_packed *packed = odb_source_packed_downcast(source); + + chdir_notify_unregister(NULL, odb_source_packed_reparent, packed); + + for (struct packfile_list_entry *e = packed->packs.head; e; e = e->next) + free(e->pack); + packfile_list_clear(&packed->packs); + + strmap_clear(&packed->packs_by_path, 0); + odb_source_release(&packed->base); + free(packed); +} struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent) { - struct odb_source_packed *store; - CALLOC_ARRAY(store, 1); - store->files = parent; - strmap_init(&store->packs_by_path); - return store; + struct odb_source_packed *packed; + + CALLOC_ARRAY(packed, 1); + odb_source_init(&packed->base, parent->base.odb, ODB_SOURCE_PACKED, + parent->base.path, parent->base.local); + packed->files = parent; + strmap_init(&packed->packs_by_path); + + packed->base.free = odb_source_packed_free; + + if (!is_absolute_path(parent->base.path)) + chdir_notify_register(NULL, odb_source_packed_reparent, packed); + + return packed; } diff --git a/odb/source-packed.h b/odb/source-packed.h index 3c2d229a174749..68e64cabab5186 100644 --- a/odb/source-packed.h +++ b/odb/source-packed.h @@ -9,6 +9,7 @@ * A store that manages packfiles for a given object database. */ struct odb_source_packed { + struct odb_source base; struct odb_source_files *files; /* @@ -69,4 +70,15 @@ struct odb_source_packed { */ struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent); +/* + * Cast the given object database source to the packed backend. This will cause + * a BUG in case the source doesn't use this backend. + */ +static inline struct odb_source_packed *odb_source_packed_downcast(struct odb_source *source) +{ + if (source->type != ODB_SOURCE_PACKED) + BUG("trying to downcast source of type '%d' to packed", source->type); + return container_of(source, struct odb_source_packed, base); +} + #endif diff --git a/odb/source.h b/odb/source.h index 8bcb67787ebafd..6865e1f71acfdc 100644 --- a/odb/source.h +++ b/odb/source.h @@ -17,6 +17,9 @@ enum odb_source_type { /* The "loose" backend that uses loose objects, only. */ ODB_SOURCE_LOOSE, + /* The "packed" backend that uses packfiles. */ + ODB_SOURCE_PACKED, + /* The "in-memory" backend that stores objects in memory. */ ODB_SOURCE_INMEMORY, }; diff --git a/packfile.c b/packfile.c index 862a24ad49bd78..6d492216de356a 100644 --- a/packfile.c +++ b/packfile.c @@ -2749,16 +2749,6 @@ int parse_pack_header_option(const char *in, unsigned char *out, unsigned int *l return 0; } -void packfile_store_free(struct odb_source_packed *store) -{ - for (struct packfile_list_entry *e = store->packs.head; e; e = e->next) - free(e->pack); - packfile_list_clear(&store->packs); - - strmap_clear(&store->packs_by_path, 0); - free(store); -} - void packfile_store_close(struct odb_source_packed *store) { for (struct packfile_list_entry *e = store->packs.head; e; e = e->next) { diff --git a/packfile.h b/packfile.h index 2d0bb7adbe83c6..e8bc9349f8d0dc 100644 --- a/packfile.h +++ b/packfile.h @@ -55,12 +55,6 @@ struct packed_git { char pack_name[FLEX_ARRAY]; /* more */ }; -/* - * Free the packfile store and all its associated state. All packfiles - * tracked by the store will be closed. - */ -void packfile_store_free(struct odb_source_packed *store); - /* * Close all packfiles associated with this store. The packfiles won't be * free'd, so they can be re-opened at a later point in time. From 4f35c8b060085835150ab605207f6669e58a8d94 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:39:49 +0200 Subject: [PATCH 40/95] odb/source-packed: wire up `close()` callback Wire up a new `close()` callback for the packed source and call it from the "files" source via the generic `odb_source_close()` interface. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-files.c | 2 +- odb/source-packed.c | 16 ++++++++++++++++ packfile.c | 12 ------------ packfile.h | 6 ------ 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/odb/source-files.c b/odb/source-files.c index 3608808e7c6f3a..9b0fa9ccdce226 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -38,7 +38,7 @@ static void odb_source_files_close(struct odb_source *source) { struct odb_source_files *files = odb_source_files_downcast(source); odb_source_close(&files->loose->base); - packfile_store_close(files->packed); + odb_source_close(&files->packed->base); } static void odb_source_files_reprepare(struct odb_source *source) diff --git a/odb/source-packed.c b/odb/source-packed.c index f81a990cbde757..74805be1ddce5e 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -1,6 +1,7 @@ #include "git-compat-util.h" #include "abspath.h" #include "chdir-notify.h" +#include "midx.h" #include "odb/source-packed.h" #include "packfile.h" @@ -16,6 +17,20 @@ static void odb_source_packed_reparent(const char *name UNUSED, packed->base.path = path; } +static void odb_source_packed_close(struct odb_source *source) +{ + struct odb_source_packed *packed = odb_source_packed_downcast(source); + + for (struct packfile_list_entry *e = packed->packs.head; e; e = e->next) { + if (e->pack->do_not_close) + BUG("want to close pack marked 'do-not-close'"); + close_pack(e->pack); + } + if (packed->midx) + close_midx(packed->midx); + packed->midx = NULL; +} + static void odb_source_packed_free(struct odb_source *source) { struct odb_source_packed *packed = odb_source_packed_downcast(source); @@ -42,6 +57,7 @@ struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent) strmap_init(&packed->packs_by_path); packed->base.free = odb_source_packed_free; + packed->base.close = odb_source_packed_close; if (!is_absolute_path(parent->base.path)) chdir_notify_register(NULL, odb_source_packed_reparent, packed); diff --git a/packfile.c b/packfile.c index 6d492216de356a..e5386145a7835b 100644 --- a/packfile.c +++ b/packfile.c @@ -2749,18 +2749,6 @@ int parse_pack_header_option(const char *in, unsigned char *out, unsigned int *l return 0; } -void packfile_store_close(struct odb_source_packed *store) -{ - for (struct packfile_list_entry *e = store->packs.head; e; e = e->next) { - if (e->pack->do_not_close) - BUG("want to close pack marked 'do-not-close'"); - close_pack(e->pack); - } - if (store->midx) - close_midx(store->midx); - store->midx = NULL; -} - struct odb_packed_read_stream { struct odb_read_stream base; struct packed_git *pack; diff --git a/packfile.h b/packfile.h index e8bc9349f8d0dc..9dc3a131127d6a 100644 --- a/packfile.h +++ b/packfile.h @@ -55,12 +55,6 @@ struct packed_git { char pack_name[FLEX_ARRAY]; /* more */ }; -/* - * Close all packfiles associated with this store. The packfiles won't be - * free'd, so they can be re-opened at a later point in time. - */ -void packfile_store_close(struct odb_source_packed *store); - /* * Prepare the packfile store by loading packfiles and multi-pack indices for * all alternates. This becomes a no-op if the store is already prepared. From 9ea4ef8586d8fa74bf45c1dd8da2256099abebbd Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:39:50 +0200 Subject: [PATCH 41/95] odb/source-packed: wire up `reprepare()` callback Move the logic to prepare and reprepare the "packed" source into "odb/source-packed.c" and wire it up as the `reprepare()` callback. Note that "preparing" a source is not yet generic. Eventually, it would probably make sense to turn the existing `reprepare()` callback into a `prepare()` callback with an optional flag to force re-preparing. But this step will be handled in a separate patch series. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/grep.c | 2 +- midx.c | 2 +- odb/source-files.c | 2 +- odb/source-packed.c | 157 +++++++++++++++++++++++++++++++++++++++++++ odb/source-packed.h | 9 +++ packfile.c | 160 +------------------------------------------- packfile.h | 17 ----- 7 files changed, 172 insertions(+), 177 deletions(-) diff --git a/builtin/grep.c b/builtin/grep.c index 6a09571903cd26..8080d1bf5ec2b4 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -1363,7 +1363,7 @@ int cmd_grep(int argc, odb_prepare_alternates(the_repository->objects); for (source = the_repository->objects->sources; source; source = source->next) { struct odb_source_files *files = odb_source_files_downcast(source); - packfile_store_prepare(files->packed); + odb_source_packed_prepare(files->packed); } } diff --git a/midx.c b/midx.c index efbfbb13f4106a..00bbd137b27a73 100644 --- a/midx.c +++ b/midx.c @@ -102,7 +102,7 @@ static int midx_read_object_offsets(const unsigned char *chunk_start, struct multi_pack_index *get_multi_pack_index(struct odb_source *source) { struct odb_source_files *files = odb_source_files_downcast(source); - packfile_store_prepare(files->packed); + odb_source_packed_prepare(files->packed); return files->packed->midx; } diff --git a/odb/source-files.c b/odb/source-files.c index 9b0fa9ccdce226..7b1e0ac565b17d 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -45,7 +45,7 @@ static void odb_source_files_reprepare(struct odb_source *source) { struct odb_source_files *files = odb_source_files_downcast(source); odb_source_reprepare(&files->loose->base); - packfile_store_reprepare(files->packed); + odb_source_reprepare(&files->packed->base); } static int odb_source_files_read_object_info(struct odb_source *source, diff --git a/odb/source-packed.c b/odb/source-packed.c index 74805be1ddce5e..e8e2e5bb4879c4 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -1,10 +1,166 @@ #include "git-compat-util.h" #include "abspath.h" #include "chdir-notify.h" +#include "dir.h" +#include "mergesort.h" #include "midx.h" #include "odb/source-packed.h" #include "packfile.h" +void (*report_garbage)(unsigned seen_bits, const char *path); + +static void report_helper(const struct string_list *list, + int seen_bits, int first, int last) +{ + if (seen_bits == (PACKDIR_FILE_PACK|PACKDIR_FILE_IDX)) + return; + + for (; first < last; first++) + report_garbage(seen_bits, list->items[first].string); +} + +static void report_pack_garbage(struct string_list *list) +{ + int baselen = -1, first = 0, seen_bits = 0; + + if (!report_garbage) + return; + + string_list_sort(list); + + for (size_t i = 0; i < list->nr; i++) { + const char *path = list->items[i].string; + if (baselen != -1 && + strncmp(path, list->items[first].string, baselen)) { + report_helper(list, seen_bits, first, i); + baselen = -1; + seen_bits = 0; + } + if (baselen == -1) { + const char *dot = strrchr(path, '.'); + if (!dot) { + report_garbage(PACKDIR_FILE_GARBAGE, path); + continue; + } + baselen = dot - path + 1; + first = i; + } + if (!strcmp(path + baselen, "pack")) + seen_bits |= 1; + else if (!strcmp(path + baselen, "idx")) + seen_bits |= 2; + } + report_helper(list, seen_bits, first, list->nr); +} + +struct prepare_pack_data { + struct odb_source *source; + struct string_list *garbage; +}; + +static void prepare_pack(const char *full_name, size_t full_name_len, + const char *file_name, void *_data) +{ + struct prepare_pack_data *data = (struct prepare_pack_data *)_data; + struct odb_source_files *files = odb_source_files_downcast(data->source); + size_t base_len = full_name_len; + + if (strip_suffix_mem(full_name, &base_len, ".idx") && + !(files->packed->midx && + midx_contains_pack(files->packed->midx, file_name))) { + char *trimmed_path = xstrndup(full_name, full_name_len); + packfile_store_load_pack(files->packed, + trimmed_path, data->source->local); + free(trimmed_path); + } + + if (!report_garbage) + return; + + if (!strcmp(file_name, "multi-pack-index") || + !strcmp(file_name, "multi-pack-index.d")) + return; + if (starts_with(file_name, "multi-pack-index") && + (ends_with(file_name, ".bitmap") || ends_with(file_name, ".rev"))) + return; + if (ends_with(file_name, ".idx") || + ends_with(file_name, ".rev") || + ends_with(file_name, ".pack") || + ends_with(file_name, ".bitmap") || + ends_with(file_name, ".keep") || + ends_with(file_name, ".promisor") || + ends_with(file_name, ".mtimes")) + string_list_append(data->garbage, full_name); + else + report_garbage(PACKDIR_FILE_GARBAGE, full_name); +} + +static void prepare_packed_git_one(struct odb_source *source) +{ + struct string_list garbage = STRING_LIST_INIT_DUP; + struct prepare_pack_data data = { + .source = source, + .garbage = &garbage, + }; + + for_each_file_in_pack_dir(source->path, prepare_pack, &data); + + report_pack_garbage(data.garbage); + string_list_clear(data.garbage, 0); +} + +DEFINE_LIST_SORT(static, sort_packs, struct packfile_list_entry, next); + +static int sort_pack(const struct packfile_list_entry *a, + const struct packfile_list_entry *b) +{ + int st; + + /* + * Local packs tend to contain objects specific to our + * variant of the project than remote ones. In addition, + * remote ones could be on a network mounted filesystem. + * Favor local ones for these reasons. + */ + st = a->pack->pack_local - b->pack->pack_local; + if (st) + return -st; + + /* + * Younger packs tend to contain more recent objects, + * and more recent objects tend to get accessed more + * often. + */ + if (a->pack->mtime < b->pack->mtime) + return 1; + else if (a->pack->mtime == b->pack->mtime) + return 0; + return -1; +} + +void odb_source_packed_prepare(struct odb_source_packed *source) +{ + if (source->initialized) + return; + + prepare_multi_pack_index_one(&source->files->base); + prepare_packed_git_one(&source->files->base); + + sort_packs(&source->packs.head, sort_pack); + for (struct packfile_list_entry *e = source->packs.head; e; e = e->next) + if (!e->next) + source->packs.tail = e; + + source->initialized = true; +} + +static void odb_source_packed_reprepare(struct odb_source *source) +{ + struct odb_source_packed *packed = odb_source_packed_downcast(source); + packed->initialized = false; + odb_source_packed_prepare(packed); +} + static void odb_source_packed_reparent(const char *name UNUSED, const char *old_cwd, const char *new_cwd, @@ -58,6 +214,7 @@ struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent) packed->base.free = odb_source_packed_free; packed->base.close = odb_source_packed_close; + packed->base.reprepare = odb_source_packed_reprepare; if (!is_absolute_path(parent->base.path)) chdir_notify_register(NULL, odb_source_packed_reparent, packed); diff --git a/odb/source-packed.h b/odb/source-packed.h index 68e64cabab5186..9d4796261a72d3 100644 --- a/odb/source-packed.h +++ b/odb/source-packed.h @@ -81,4 +81,13 @@ static inline struct odb_source_packed *odb_source_packed_downcast(struct odb_so return container_of(source, struct odb_source_packed, base); } +/* + * Prepare the source by loading packfiles and multi-pack indices for + * all alternates. This becomes a no-op if the source is already prepared. + * + * It shouldn't typically be necessary to call this function directly, as + * functions that access the source know to prepare it. + */ +void odb_source_packed_prepare(struct odb_source_packed *source); + #endif diff --git a/packfile.c b/packfile.c index e5386145a7835b..65631f674f1e1c 100644 --- a/packfile.c +++ b/packfile.c @@ -8,7 +8,6 @@ #include "pack.h" #include "repository.h" #include "dir.h" -#include "mergesort.h" #include "packfile.h" #include "delta.h" #include "hash-lookup.h" @@ -812,52 +811,6 @@ struct packed_git *packfile_store_load_pack(struct odb_source_packed *store, return p; } -void (*report_garbage)(unsigned seen_bits, const char *path); - -static void report_helper(const struct string_list *list, - int seen_bits, int first, int last) -{ - if (seen_bits == (PACKDIR_FILE_PACK|PACKDIR_FILE_IDX)) - return; - - for (; first < last; first++) - report_garbage(seen_bits, list->items[first].string); -} - -static void report_pack_garbage(struct string_list *list) -{ - int i, baselen = -1, first = 0, seen_bits = 0; - - if (!report_garbage) - return; - - string_list_sort(list); - - for (i = 0; i < list->nr; i++) { - const char *path = list->items[i].string; - if (baselen != -1 && - strncmp(path, list->items[first].string, baselen)) { - report_helper(list, seen_bits, first, i); - baselen = -1; - seen_bits = 0; - } - if (baselen == -1) { - const char *dot = strrchr(path, '.'); - if (!dot) { - report_garbage(PACKDIR_FILE_GARBAGE, path); - continue; - } - baselen = dot - path + 1; - first = i; - } - if (!strcmp(path + baselen, "pack")) - seen_bits |= 1; - else if (!strcmp(path + baselen, "idx")) - seen_bits |= 2; - } - report_helper(list, seen_bits, first, list->nr); -} - void for_each_file_in_pack_subdir(const char *objdir, const char *subdir, each_file_in_pack_dir_fn fn, @@ -900,116 +853,9 @@ void for_each_file_in_pack_dir(const char *objdir, for_each_file_in_pack_subdir(objdir, NULL, fn, data); } -struct prepare_pack_data { - struct odb_source *source; - struct string_list *garbage; -}; - -static void prepare_pack(const char *full_name, size_t full_name_len, - const char *file_name, void *_data) -{ - struct prepare_pack_data *data = (struct prepare_pack_data *)_data; - struct odb_source_files *files = odb_source_files_downcast(data->source); - size_t base_len = full_name_len; - - if (strip_suffix_mem(full_name, &base_len, ".idx") && - !(files->packed->midx && - midx_contains_pack(files->packed->midx, file_name))) { - char *trimmed_path = xstrndup(full_name, full_name_len); - packfile_store_load_pack(files->packed, - trimmed_path, data->source->local); - free(trimmed_path); - } - - if (!report_garbage) - return; - - if (!strcmp(file_name, "multi-pack-index") || - !strcmp(file_name, "multi-pack-index.d")) - return; - if (starts_with(file_name, "multi-pack-index") && - (ends_with(file_name, ".bitmap") || ends_with(file_name, ".rev"))) - return; - if (ends_with(file_name, ".idx") || - ends_with(file_name, ".rev") || - ends_with(file_name, ".pack") || - ends_with(file_name, ".bitmap") || - ends_with(file_name, ".keep") || - ends_with(file_name, ".promisor") || - ends_with(file_name, ".mtimes")) - string_list_append(data->garbage, full_name); - else - report_garbage(PACKDIR_FILE_GARBAGE, full_name); -} - -static void prepare_packed_git_one(struct odb_source *source) -{ - struct string_list garbage = STRING_LIST_INIT_DUP; - struct prepare_pack_data data = { - .source = source, - .garbage = &garbage, - }; - - for_each_file_in_pack_dir(source->path, prepare_pack, &data); - - report_pack_garbage(data.garbage); - string_list_clear(data.garbage, 0); -} - -DEFINE_LIST_SORT(static, sort_packs, struct packfile_list_entry, next); - -static int sort_pack(const struct packfile_list_entry *a, - const struct packfile_list_entry *b) -{ - int st; - - /* - * Local packs tend to contain objects specific to our - * variant of the project than remote ones. In addition, - * remote ones could be on a network mounted filesystem. - * Favor local ones for these reasons. - */ - st = a->pack->pack_local - b->pack->pack_local; - if (st) - return -st; - - /* - * Younger packs tend to contain more recent objects, - * and more recent objects tend to get accessed more - * often. - */ - if (a->pack->mtime < b->pack->mtime) - return 1; - else if (a->pack->mtime == b->pack->mtime) - return 0; - return -1; -} - -void packfile_store_prepare(struct odb_source_packed *store) -{ - if (store->initialized) - return; - - prepare_multi_pack_index_one(&store->files->base); - prepare_packed_git_one(&store->files->base); - - sort_packs(&store->packs.head, sort_pack); - for (struct packfile_list_entry *e = store->packs.head; e; e = e->next) - if (!e->next) - store->packs.tail = e; - - store->initialized = true; -} - -void packfile_store_reprepare(struct odb_source_packed *store) -{ - store->initialized = false; - packfile_store_prepare(store); -} - struct packfile_list_entry *packfile_store_get_packs(struct odb_source_packed *store) { - packfile_store_prepare(store); + odb_source_packed_prepare(store); if (store->midx) { struct multi_pack_index *m = store->midx; @@ -2083,7 +1929,7 @@ static int find_pack_entry(struct odb_source_packed *store, { struct packfile_list_entry *l; - packfile_store_prepare(store); + odb_source_packed_prepare(store); if (store->midx && fill_midx_entry(store->midx, oid, e)) return 1; @@ -2130,7 +1976,7 @@ int packfile_store_read_object_info(struct odb_source_packed *store, * been added since the last time we have prepared the packfile store. */ if (flags & OBJECT_INFO_SECOND_READ) - packfile_store_reprepare(store); + odb_source_reprepare(&store->base); if (!find_pack_entry(store, oid, &e)) return 1; diff --git a/packfile.h b/packfile.h index 9dc3a131127d6a..9674e573aece64 100644 --- a/packfile.h +++ b/packfile.h @@ -55,23 +55,6 @@ struct packed_git { char pack_name[FLEX_ARRAY]; /* more */ }; -/* - * Prepare the packfile store by loading packfiles and multi-pack indices for - * all alternates. This becomes a no-op if the store is already prepared. - * - * It shouldn't typically be necessary to call this function directly, as - * functions that access the store know to prepare it. - */ -void packfile_store_prepare(struct odb_source_packed *store); - -/* - * Clear the packfile caches and try to look up any new packfiles that have - * appeared since last preparing the packfiles store. - * - * This function must be called under the `odb_read_lock()`. - */ -void packfile_store_reprepare(struct odb_source_packed *store); - /* * Add the pack to the store so that contained objects become accessible via * the store. This moves ownership into the store. From 77e175c6d03a26e97deb2a7bc707894eb45ced26 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:39:51 +0200 Subject: [PATCH 42/95] packfile: use higher-level interface to implement `has_object_pack()` In `has_object_pack()` we're checking whether a specific object exists as part of a packfile. This is done by calling the low-level function `find_pack_entry()`, but this function will eventually be moved into "odb/source-packed.c" and made file-local. Refactor the code to use `packfile_store_read_object_info()` instead. This refactoring is functionally equivalent as that function will call `find_pack_entry()` itself and then return immediately when it ain't got no object info pointer as parameter. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- packfile.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packfile.c b/packfile.c index 65631f674f1e1c..b35afd77979fc9 100644 --- a/packfile.c +++ b/packfile.c @@ -2049,14 +2049,12 @@ struct packed_git **packfile_store_get_kept_pack_cache(struct odb_source_packed int has_object_pack(struct repository *r, const struct object_id *oid) { struct odb_source *source; - struct pack_entry e; odb_prepare_alternates(r->objects); for (source = r->objects->sources; source; source = source->next) { struct odb_source_files *files = odb_source_files_downcast(source); - int ret = find_pack_entry(files->packed, oid, &e); - if (ret) - return ret; + if (!packfile_store_read_object_info(files->packed, oid, NULL, 0)) + return 1; } return 0; From 64136a82075331d98fd1315fa957f69acb49885c Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:39:52 +0200 Subject: [PATCH 43/95] odb/source-packed: wire up `read_object_info()` callback Move the logic to read object info from a "packed" source into "odb/source-packed.c" and wire it up as the `read_object_info()` callback. Note that we also move around the supporting `find_pack_entry()`, but we still have to expose it to other callers that exist in "packfile.c". This will be fixed in subsequent commits though, where all callers in "packfile.c" will have been moved into "odb/source-packed.c", and at that point we'll be able to make `find_pack_entry()` file-local again. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-files.c | 2 +- odb/source-packed.c | 60 ++++++++++++++++++++++++++++++++++++ odb/source-packed.h | 6 ++++ packfile.c | 74 +++++---------------------------------------- packfile.h | 15 +++------ 5 files changed, 79 insertions(+), 78 deletions(-) diff --git a/odb/source-files.c b/odb/source-files.c index 7b1e0ac565b17d..8cae35d25e73f9 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -55,7 +55,7 @@ static int odb_source_files_read_object_info(struct odb_source *source, { struct odb_source_files *files = odb_source_files_downcast(source); - if (!packfile_store_read_object_info(files->packed, oid, oi, flags) || + if (!odb_source_read_object_info(&files->packed->base, oid, oi, flags) || !odb_source_read_object_info(&files->loose->base, oid, oi, flags)) return 0; diff --git a/odb/source-packed.c b/odb/source-packed.c index e8e2e5bb4879c4..f71a19473991d1 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -7,6 +7,65 @@ #include "odb/source-packed.h" #include "packfile.h" +int find_pack_entry(struct odb_source_packed *store, + const struct object_id *oid, + struct pack_entry *e) +{ + struct packfile_list_entry *l; + + odb_source_packed_prepare(store); + if (store->midx && fill_midx_entry(store->midx, oid, e)) + return 1; + + for (l = store->packs.head; l; l = l->next) { + struct packed_git *p = l->pack; + + if (!p->multi_pack_index && packfile_fill_entry(p, oid, e)) { + if (!store->skip_mru_updates) + packfile_list_prepend(&store->packs, p); + return 1; + } + } + + return 0; +} + +static int odb_source_packed_read_object_info(struct odb_source *source, + const struct object_id *oid, + struct object_info *oi, + enum object_info_flags flags) +{ + struct odb_source_packed *packed = odb_source_packed_downcast(source); + struct pack_entry e; + int ret; + + /* + * In case the first read didn't surface the object, we have to reload + * packfiles. This may cause us to discover new packfiles that have + * been added since the last time we have prepared the packfile store. + */ + if (flags & OBJECT_INFO_SECOND_READ) + odb_source_reprepare(source); + + if (!find_pack_entry(packed, oid, &e)) + return 1; + + /* + * We know that the caller doesn't actually need the + * information below, so return early. + */ + if (!oi) + return 0; + + ret = packed_object_info(e.p, e.offset, oi); + if (ret < 0) { + mark_bad_packed_object(e.p, oid); + return -1; + } + + return 0; +} + void (*report_garbage)(unsigned seen_bits, const char *path); static void report_helper(const struct string_list *list, @@ -215,6 +274,7 @@ struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent) packed->base.free = odb_source_packed_free; packed->base.close = odb_source_packed_close; packed->base.reprepare = odb_source_packed_reprepare; + packed->base.read_object_info = odb_source_packed_read_object_info; if (!is_absolute_path(parent->base.path)) chdir_notify_register(NULL, odb_source_packed_reparent, packed); diff --git a/odb/source-packed.h b/odb/source-packed.h index 9d4796261a72d3..f430ee0b9466bd 100644 --- a/odb/source-packed.h +++ b/odb/source-packed.h @@ -90,4 +90,10 @@ static inline struct odb_source_packed *odb_source_packed_downcast(struct odb_so */ void odb_source_packed_prepare(struct odb_source_packed *source); +struct pack_entry; + +int find_pack_entry(struct odb_source_packed *store, + const struct object_id *oid, + struct pack_entry *e); + #endif diff --git a/packfile.c b/packfile.c index b35afd77979fc9..29530532bac514 100644 --- a/packfile.c +++ b/packfile.c @@ -1895,9 +1895,9 @@ int is_pack_valid(struct packed_git *p) return !open_packed_git(p); } -static int fill_pack_entry(const struct object_id *oid, - struct pack_entry *e, - struct packed_git *p) +int packfile_fill_entry(struct packed_git *p, + const struct object_id *oid, + struct pack_entry *e) { off_t offset; @@ -1923,29 +1923,6 @@ static int fill_pack_entry(const struct object_id *oid, return 1; } -static int find_pack_entry(struct odb_source_packed *store, - const struct object_id *oid, - struct pack_entry *e) -{ - struct packfile_list_entry *l; - - odb_source_packed_prepare(store); - if (store->midx && fill_midx_entry(store->midx, oid, e)) - return 1; - - for (l = store->packs.head; l; l = l->next) { - struct packed_git *p = l->pack; - - if (!p->multi_pack_index && fill_pack_entry(oid, e, p)) { - if (!store->skip_mru_updates) - packfile_list_prepend(&store->packs, p); - return 1; - } - } - - return 0; -} - int packfile_store_freshen_object(struct odb_source_packed *store, const struct object_id *oid) { @@ -1962,41 +1939,6 @@ int packfile_store_freshen_object(struct odb_source_packed *store, return 1; } -int packfile_store_read_object_info(struct odb_source_packed *store, - const struct object_id *oid, - struct object_info *oi, - enum object_info_flags flags) -{ - struct pack_entry e; - int ret; - - /* - * In case the first read didn't surface the object, we have to reload - * packfiles. This may cause us to discover new packfiles that have - * been added since the last time we have prepared the packfile store. - */ - if (flags & OBJECT_INFO_SECOND_READ) - odb_source_reprepare(&store->base); - - if (!find_pack_entry(store, oid, &e)) - return 1; - - /* - * We know that the caller doesn't actually need the - * information below, so return early. - */ - if (!oi) - return 0; - - ret = packed_object_info(e.p, e.offset, oi); - if (ret < 0) { - mark_bad_packed_object(e.p, oid); - return -1; - } - - return 0; -} - static void maybe_invalidate_kept_pack_cache(struct odb_source_packed *store, unsigned flags) { @@ -2053,7 +1995,7 @@ int has_object_pack(struct repository *r, const struct object_id *oid) odb_prepare_alternates(r->objects); for (source = r->objects->sources; source; source = source->next) { struct odb_source_files *files = odb_source_files_downcast(source); - if (!packfile_store_read_object_info(files->packed, oid, NULL, 0)) + if (!odb_source_read_object_info(&files->packed->base, oid, NULL, 0)) return 1; } @@ -2074,7 +2016,7 @@ int has_object_kept_pack(struct repository *r, const struct object_id *oid, for (; *cache; cache++) { struct packed_git *p = *cache; - if (fill_pack_entry(oid, &e, p)) + if (packfile_fill_entry(p, oid, &e)) return 1; } } @@ -2208,8 +2150,8 @@ static int for_each_prefixed_object_in_midx( if (data->request) { struct object_info oi = *data->request; - ret = packfile_store_read_object_info(store, current, - &oi, 0); + ret = odb_source_read_object_info(&store->base, current, + &oi, 0); if (ret) goto out; @@ -2259,7 +2201,7 @@ static int for_each_prefixed_object_in_pack( if (data->request) { struct object_info oi = *data->request; - ret = packfile_store_read_object_info(store, &oid, &oi, 0); + ret = odb_source_read_object_info(&store->base, &oid, &oi, 0); if (ret) goto out; diff --git a/packfile.h b/packfile.h index 9674e573aece64..25d458beb09f42 100644 --- a/packfile.h +++ b/packfile.h @@ -128,17 +128,6 @@ int packfile_store_read_object_stream(struct odb_read_stream **out, struct odb_source_packed *store, const struct object_id *oid); -/* - * Try to read the object identified by its ID from the object store and - * populate the object info with its data. Returns 1 in case the object was - * not found, 0 if it was and read successfully, and a negative error code in - * case the object was corrupted. - */ -int packfile_store_read_object_info(struct odb_source_packed *store, - const struct object_id *oid, - struct object_info *oi, - enum object_info_flags flags); - /* * Open the packfile and add it to the store if it isn't yet known. Returns * either the newly opened packfile or the preexisting packfile. Returns a @@ -340,6 +329,10 @@ off_t nth_packed_object_offset(const struct packed_git *, uint32_t n); */ off_t find_pack_entry_one(const struct object_id *oid, struct packed_git *); +int packfile_fill_entry(struct packed_git *p, + const struct object_id *oid, + struct pack_entry *e); + int is_pack_valid(struct packed_git *); void *unpack_entry(struct repository *r, struct packed_git *, off_t, enum object_type *, unsigned long *); unsigned long unpack_object_header_buffer(const unsigned char *buf, unsigned long len, enum object_type *type, size_t *sizep); From 83f3a2b91b44d4a39bce371a7580b0ad853df8a2 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:39:53 +0200 Subject: [PATCH 44/95] odb/source-packed: wire up `read_object_stream()` callback Wire up the `read_object_stream()` callback for the packed source and call it in the "files" source via the `odb_source_read_object_stream()` interface. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-files.c | 2 +- odb/source-packed.c | 16 ++++++++++++++++ packfile.c | 12 ------------ packfile.h | 4 ---- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/odb/source-files.c b/odb/source-files.c index 8cae35d25e73f9..dff69d0e4e08c9 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -67,7 +67,7 @@ static int odb_source_files_read_object_stream(struct odb_read_stream **out, const struct object_id *oid) { struct odb_source_files *files = odb_source_files_downcast(source); - if (!packfile_store_read_object_stream(out, files->packed, oid) || + if (!odb_source_read_object_stream(out, &files->packed->base, oid) || !odb_source_read_object_stream(out, &files->loose->base, oid)) return 0; return -1; diff --git a/odb/source-packed.c b/odb/source-packed.c index f71a19473991d1..23d7149fe389ef 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -2,9 +2,11 @@ #include "abspath.h" #include "chdir-notify.h" #include "dir.h" +#include "git-zlib.h" #include "mergesort.h" #include "midx.h" #include "odb/source-packed.h" +#include "odb/streaming.h" #include "packfile.h" int find_pack_entry(struct odb_source_packed *store, @@ -66,6 +68,19 @@ static int odb_source_packed_read_object_info(struct odb_source *source, return 0; } +static int odb_source_packed_read_object_stream(struct odb_read_stream **out, + struct odb_source *source, + const struct object_id *oid) +{ + struct odb_source_packed *packed = odb_source_packed_downcast(source); + struct pack_entry e; + + if (!find_pack_entry(packed, oid, &e)) + return -1; + + return packfile_read_object_stream(out, oid, e.p, e.offset); +} + void (*report_garbage)(unsigned seen_bits, const char *path); static void report_helper(const struct string_list *list, @@ -275,6 +290,7 @@ struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent) packed->base.close = odb_source_packed_close; packed->base.reprepare = odb_source_packed_reprepare; packed->base.read_object_info = odb_source_packed_read_object_info; + packed->base.read_object_stream = odb_source_packed_read_object_stream; if (!is_absolute_path(parent->base.path)) chdir_notify_register(NULL, odb_source_packed_reparent, packed); diff --git a/packfile.c b/packfile.c index 29530532bac514..42c84397eb2667 100644 --- a/packfile.c +++ b/packfile.c @@ -2658,15 +2658,3 @@ int packfile_read_object_stream(struct odb_read_stream **out, return 0; } - -int packfile_store_read_object_stream(struct odb_read_stream **out, - struct odb_source_packed *store, - const struct object_id *oid) -{ - struct pack_entry e; - - if (!find_pack_entry(store, oid, &e)) - return -1; - - return packfile_read_object_stream(out, oid, e.p, e.offset); -} diff --git a/packfile.h b/packfile.h index 25d458beb09f42..dd97684e7098dd 100644 --- a/packfile.h +++ b/packfile.h @@ -124,10 +124,6 @@ static inline void repo_for_each_pack_data_next(struct repo_for_each_pack_data * ((p) = (eack_pack_data.entry ? eack_pack_data.entry->pack : NULL)); \ repo_for_each_pack_data_next(&eack_pack_data)) -int packfile_store_read_object_stream(struct odb_read_stream **out, - struct odb_source_packed *store, - const struct object_id *oid); - /* * Open the packfile and add it to the store if it isn't yet known. Returns * either the newly opened packfile or the preexisting packfile. Returns a From 7ed53cde288a1e5558acac33f2d89f17b81618e2 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:39:54 +0200 Subject: [PATCH 45/95] odb/source-packed: wire up `for_each_object()` callback Move `packfile_store_for_each_object()` and its associated helpers from "packfile.c" into "odb/source-packed.c" and wire it up as the `for_each_object()` callback of the "packed" source. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/cat-file.c | 4 +- builtin/pack-objects.c | 4 +- commit-graph.c | 4 +- odb/source-files.c | 2 +- odb/source-packed.c | 258 ++++++++++++++++++++++++++++++++++++++++ packfile.c | 260 +---------------------------------------- packfile.h | 17 +-- 7 files changed, 269 insertions(+), 280 deletions(-) diff --git a/builtin/cat-file.c b/builtin/cat-file.c index 04b64006a54477..d997011531faf7 100644 --- a/builtin/cat-file.c +++ b/builtin/cat-file.c @@ -916,8 +916,8 @@ static void batch_each_object(struct batch_options *opt, for (source = the_repository->objects->sources; source; source = source->next) { struct odb_source_files *files = odb_source_files_downcast(source); - int ret = packfile_store_for_each_object(files->packed, &oi, - batch_one_object_oi, &payload, &opts); + int ret = odb_source_for_each_object(&files->packed->base, &oi, + batch_one_object_oi, &payload, &opts); if (ret) break; } diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 50675481e14caa..5e948054789a53 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -4503,8 +4503,8 @@ static void add_objects_in_unpacked_packs(void) if (!source->local) continue; - if (packfile_store_for_each_object(files->packed, &oi, - add_object_in_unpacked_pack, NULL, &opts)) + if (odb_source_for_each_object(&files->packed->base, &oi, + add_object_in_unpacked_pack, NULL, &opts)) die(_("cannot open pack index")); } } diff --git a/commit-graph.c b/commit-graph.c index 9abe62bd5a278a..1e4038baf340a1 100644 --- a/commit-graph.c +++ b/commit-graph.c @@ -2016,8 +2016,8 @@ static void fill_oids_from_all_packs(struct write_commit_graph_context *ctx) odb_prepare_alternates(ctx->r->objects); for (source = ctx->r->objects->sources; source; source = source->next) { struct odb_source_files *files = odb_source_files_downcast(source); - packfile_store_for_each_object(files->packed, &oi, add_packed_commits_oi, - ctx, &opts); + odb_source_for_each_object(&files->packed->base, &oi, add_packed_commits_oi, + ctx, &opts); } if (ctx->progress_done < ctx->approx_nr_objects) diff --git a/odb/source-files.c b/odb/source-files.c index dff69d0e4e08c9..c73a7e5f9096ad 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -88,7 +88,7 @@ static int odb_source_files_for_each_object(struct odb_source *source, return ret; } - ret = packfile_store_for_each_object(files->packed, request, cb, cb_data, opts); + ret = odb_source_for_each_object(&files->packed->base, request, cb, cb_data, opts); if (ret) return ret; diff --git a/odb/source-packed.c b/odb/source-packed.c index 23d7149fe389ef..a61c809c8c8fa6 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -81,6 +81,263 @@ static int odb_source_packed_read_object_stream(struct odb_read_stream **out, return packfile_read_object_stream(out, oid, e.p, e.offset); } +struct odb_source_packed_for_each_object_wrapper_data { + struct odb_source_packed *store; + const struct object_info *request; + odb_for_each_object_cb cb; + void *cb_data; +}; + +static int odb_source_packed_for_each_object_wrapper(const struct object_id *oid, + struct packed_git *pack, + uint32_t index_pos, + void *cb_data) +{ + struct odb_source_packed_for_each_object_wrapper_data *data = cb_data; + + if (data->request) { + off_t offset = nth_packed_object_offset(pack, index_pos); + struct object_info oi = *data->request; + + if (packed_object_info_with_index_pos(pack, offset, + &index_pos, &oi) < 0) { + mark_bad_packed_object(pack, oid); + return -1; + } + + return data->cb(oid, &oi, data->cb_data); + } else { + return data->cb(oid, NULL, data->cb_data); + } +} + +static int match_hash(unsigned len, const unsigned char *a, const unsigned char *b) +{ + do { + if (*a != *b) + return 0; + a++; + b++; + len -= 2; + } while (len > 1); + if (len) + if ((*a ^ *b) & 0xf0) + return 0; + return 1; +} + +static int for_each_prefixed_object_in_midx( + struct odb_source_packed *store, + struct multi_pack_index *m, + const struct odb_for_each_object_options *opts, + struct odb_source_packed_for_each_object_wrapper_data *data) +{ + int ret; + + for (; m; m = m->base_midx) { + uint32_t num, i, first = 0; + int len = opts->prefix_hex_len > m->source->odb->repo->hash_algo->hexsz ? + m->source->odb->repo->hash_algo->hexsz : opts->prefix_hex_len; + + if (!m->num_objects) + continue; + + num = m->num_objects + m->num_objects_in_base; + + bsearch_one_midx(opts->prefix, m, &first); + + /* + * At this point, "first" is the location of the lowest + * object with an object name that could match "opts->prefix". + * See if we have 0, 1 or more objects that actually match(es). + */ + for (i = first; i < num; i++) { + const struct object_id *current = NULL; + struct object_id oid; + + current = nth_midxed_object_oid(&oid, m, i); + + if (!match_hash(len, opts->prefix->hash, current->hash)) + break; + + if (data->request) { + struct object_info oi = *data->request; + + ret = odb_source_read_object_info(&store->base, current, + &oi, 0); + if (ret) + goto out; + + ret = data->cb(&oid, &oi, data->cb_data); + if (ret) + goto out; + } else { + ret = data->cb(&oid, NULL, data->cb_data); + if (ret) + goto out; + } + } + } + + ret = 0; + +out: + return ret; +} + +static int for_each_prefixed_object_in_pack( + struct odb_source_packed *store, + struct packed_git *p, + const struct odb_for_each_object_options *opts, + struct odb_source_packed_for_each_object_wrapper_data *data) +{ + uint32_t num, i, first = 0; + int len = opts->prefix_hex_len > p->repo->hash_algo->hexsz ? + p->repo->hash_algo->hexsz : opts->prefix_hex_len; + int ret; + + num = p->num_objects; + bsearch_pack(opts->prefix, p, &first); + + /* + * At this point, "first" is the location of the lowest object + * with an object name that could match "bin_pfx". See if we have + * 0, 1 or more objects that actually match(es). + */ + for (i = first; i < num; i++) { + struct object_id oid; + + nth_packed_object_id(&oid, p, i); + if (!match_hash(len, opts->prefix->hash, oid.hash)) + break; + + if (data->request) { + struct object_info oi = *data->request; + + ret = odb_source_read_object_info(&store->base, &oid, &oi, 0); + if (ret) + goto out; + + ret = data->cb(&oid, &oi, data->cb_data); + if (ret) + goto out; + } else { + ret = data->cb(&oid, NULL, data->cb_data); + if (ret) + goto out; + } + } + + ret = 0; + +out: + return ret; +} + +static int odb_source_packed_for_each_prefixed_object( + struct odb_source_packed *store, + const struct odb_for_each_object_options *opts, + struct odb_source_packed_for_each_object_wrapper_data *data) +{ + struct packfile_list_entry *e; + struct multi_pack_index *m; + bool pack_errors = false; + int ret; + + if (opts->flags) + BUG("flags unsupported"); + + store->skip_mru_updates = true; + + m = get_multi_pack_index(&store->files->base); + if (m) { + ret = for_each_prefixed_object_in_midx(store, m, opts, data); + if (ret) + goto out; + } + + for (e = packfile_store_get_packs(store); e; e = e->next) { + if (e->pack->multi_pack_index) + continue; + + if (open_pack_index(e->pack)) { + pack_errors = true; + continue; + } + + if (!e->pack->num_objects) + continue; + + ret = for_each_prefixed_object_in_pack(store, e->pack, opts, data); + if (ret) + goto out; + } + + ret = 0; + +out: + store->skip_mru_updates = false; + if (!ret && pack_errors) + ret = -1; + return ret; +} + +static int odb_source_packed_for_each_object(struct odb_source *source, + const struct object_info *request, + odb_for_each_object_cb cb, + void *cb_data, + const struct odb_for_each_object_options *opts) +{ + struct odb_source_packed *packed = odb_source_packed_downcast(source); + struct odb_source_packed_for_each_object_wrapper_data data = { + .store = packed, + .request = request, + .cb = cb, + .cb_data = cb_data, + }; + struct packfile_list_entry *e; + int pack_errors = 0, ret; + + if (opts->prefix) + return odb_source_packed_for_each_prefixed_object(packed, opts, &data); + + packed->skip_mru_updates = true; + + for (e = packfile_store_get_packs(packed); e; e = e->next) { + struct packed_git *p = e->pack; + + if ((opts->flags & ODB_FOR_EACH_OBJECT_LOCAL_ONLY) && !p->pack_local) + continue; + if ((opts->flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY) && + !p->pack_promisor) + continue; + if ((opts->flags & ODB_FOR_EACH_OBJECT_SKIP_IN_CORE_KEPT_PACKS) && + p->pack_keep_in_core) + continue; + if ((opts->flags & ODB_FOR_EACH_OBJECT_SKIP_ON_DISK_KEPT_PACKS) && + p->pack_keep) + continue; + if (open_pack_index(p)) { + pack_errors = 1; + continue; + } + + ret = for_each_object_in_pack(p, odb_source_packed_for_each_object_wrapper, + &data, opts->flags); + if (ret) + goto out; + } + + ret = 0; + +out: + packed->skip_mru_updates = false; + + if (!ret && pack_errors) + ret = -1; + return ret; +} + void (*report_garbage)(unsigned seen_bits, const char *path); static void report_helper(const struct string_list *list, @@ -291,6 +548,7 @@ struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent) packed->base.reprepare = odb_source_packed_reprepare; packed->base.read_object_info = odb_source_packed_read_object_info; packed->base.read_object_stream = odb_source_packed_read_object_stream; + packed->base.for_each_object = odb_source_packed_for_each_object; if (!is_absolute_path(parent->base.path)) chdir_notify_register(NULL, odb_source_packed_reparent, packed); diff --git a/packfile.c b/packfile.c index 42c84397eb2667..b8d6054c16aa72 100644 --- a/packfile.c +++ b/packfile.c @@ -1362,8 +1362,8 @@ static void add_delta_base_cache(struct packed_git *p, off_t base_offset, hashmap_add(&delta_base_cache, &ent->ent); } -static int packed_object_info_with_index_pos(struct packed_git *p, off_t obj_offset, - uint32_t *maybe_index_pos, struct object_info *oi) +int packed_object_info_with_index_pos(struct packed_git *p, off_t obj_offset, + uint32_t *maybe_index_pos, struct object_info *oi) { struct pack_window *w_curs = NULL; size_t size; @@ -2068,262 +2068,6 @@ int for_each_object_in_pack(struct packed_git *p, return r; } -struct odb_source_packed_for_each_object_wrapper_data { - struct odb_source_packed *store; - const struct object_info *request; - odb_for_each_object_cb cb; - void *cb_data; -}; - -static int packfile_store_for_each_object_wrapper(const struct object_id *oid, - struct packed_git *pack, - uint32_t index_pos, - void *cb_data) -{ - struct odb_source_packed_for_each_object_wrapper_data *data = cb_data; - - if (data->request) { - off_t offset = nth_packed_object_offset(pack, index_pos); - struct object_info oi = *data->request; - - if (packed_object_info_with_index_pos(pack, offset, - &index_pos, &oi) < 0) { - mark_bad_packed_object(pack, oid); - return -1; - } - - return data->cb(oid, &oi, data->cb_data); - } else { - return data->cb(oid, NULL, data->cb_data); - } -} - -static int match_hash(unsigned len, const unsigned char *a, const unsigned char *b) -{ - do { - if (*a != *b) - return 0; - a++; - b++; - len -= 2; - } while (len > 1); - if (len) - if ((*a ^ *b) & 0xf0) - return 0; - return 1; -} - -static int for_each_prefixed_object_in_midx( - struct odb_source_packed *store, - struct multi_pack_index *m, - const struct odb_for_each_object_options *opts, - struct odb_source_packed_for_each_object_wrapper_data *data) -{ - int ret; - - for (; m; m = m->base_midx) { - uint32_t num, i, first = 0; - int len = opts->prefix_hex_len > m->source->odb->repo->hash_algo->hexsz ? - m->source->odb->repo->hash_algo->hexsz : opts->prefix_hex_len; - - if (!m->num_objects) - continue; - - num = m->num_objects + m->num_objects_in_base; - - bsearch_one_midx(opts->prefix, m, &first); - - /* - * At this point, "first" is the location of the lowest - * object with an object name that could match "opts->prefix". - * See if we have 0, 1 or more objects that actually match(es). - */ - for (i = first; i < num; i++) { - const struct object_id *current = NULL; - struct object_id oid; - - current = nth_midxed_object_oid(&oid, m, i); - - if (!match_hash(len, opts->prefix->hash, current->hash)) - break; - - if (data->request) { - struct object_info oi = *data->request; - - ret = odb_source_read_object_info(&store->base, current, - &oi, 0); - if (ret) - goto out; - - ret = data->cb(&oid, &oi, data->cb_data); - if (ret) - goto out; - } else { - ret = data->cb(&oid, NULL, data->cb_data); - if (ret) - goto out; - } - } - } - - ret = 0; - -out: - return ret; -} - -static int for_each_prefixed_object_in_pack( - struct odb_source_packed *store, - struct packed_git *p, - const struct odb_for_each_object_options *opts, - struct odb_source_packed_for_each_object_wrapper_data *data) -{ - uint32_t num, i, first = 0; - int len = opts->prefix_hex_len > p->repo->hash_algo->hexsz ? - p->repo->hash_algo->hexsz : opts->prefix_hex_len; - int ret; - - num = p->num_objects; - bsearch_pack(opts->prefix, p, &first); - - /* - * At this point, "first" is the location of the lowest object - * with an object name that could match "bin_pfx". See if we have - * 0, 1 or more objects that actually match(es). - */ - for (i = first; i < num; i++) { - struct object_id oid; - - nth_packed_object_id(&oid, p, i); - if (!match_hash(len, opts->prefix->hash, oid.hash)) - break; - - if (data->request) { - struct object_info oi = *data->request; - - ret = odb_source_read_object_info(&store->base, &oid, &oi, 0); - if (ret) - goto out; - - ret = data->cb(&oid, &oi, data->cb_data); - if (ret) - goto out; - } else { - ret = data->cb(&oid, NULL, data->cb_data); - if (ret) - goto out; - } - } - - ret = 0; - -out: - return ret; -} - -static int packfile_store_for_each_prefixed_object( - struct odb_source_packed *store, - const struct odb_for_each_object_options *opts, - struct odb_source_packed_for_each_object_wrapper_data *data) -{ - struct packfile_list_entry *e; - struct multi_pack_index *m; - bool pack_errors = false; - int ret; - - if (opts->flags) - BUG("flags unsupported"); - - store->skip_mru_updates = true; - - m = get_multi_pack_index(&store->files->base); - if (m) { - ret = for_each_prefixed_object_in_midx(store, m, opts, data); - if (ret) - goto out; - } - - for (e = packfile_store_get_packs(store); e; e = e->next) { - if (e->pack->multi_pack_index) - continue; - - if (open_pack_index(e->pack)) { - pack_errors = true; - continue; - } - - if (!e->pack->num_objects) - continue; - - ret = for_each_prefixed_object_in_pack(store, e->pack, opts, data); - if (ret) - goto out; - } - - ret = 0; - -out: - store->skip_mru_updates = false; - if (!ret && pack_errors) - ret = -1; - return ret; -} - -int packfile_store_for_each_object(struct odb_source_packed *store, - const struct object_info *request, - odb_for_each_object_cb cb, - void *cb_data, - const struct odb_for_each_object_options *opts) -{ - struct odb_source_packed_for_each_object_wrapper_data data = { - .store = store, - .request = request, - .cb = cb, - .cb_data = cb_data, - }; - struct packfile_list_entry *e; - int pack_errors = 0, ret; - - if (opts->prefix) - return packfile_store_for_each_prefixed_object(store, opts, &data); - - store->skip_mru_updates = true; - - for (e = packfile_store_get_packs(store); e; e = e->next) { - struct packed_git *p = e->pack; - - if ((opts->flags & ODB_FOR_EACH_OBJECT_LOCAL_ONLY) && !p->pack_local) - continue; - if ((opts->flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY) && - !p->pack_promisor) - continue; - if ((opts->flags & ODB_FOR_EACH_OBJECT_SKIP_IN_CORE_KEPT_PACKS) && - p->pack_keep_in_core) - continue; - if ((opts->flags & ODB_FOR_EACH_OBJECT_SKIP_ON_DISK_KEPT_PACKS) && - p->pack_keep) - continue; - if (open_pack_index(p)) { - pack_errors = 1; - continue; - } - - ret = for_each_object_in_pack(p, packfile_store_for_each_object_wrapper, - &data, opts->flags); - if (ret) - goto out; - } - - ret = 0; - -out: - store->skip_mru_updates = false; - - if (!ret && pack_errors) - ret = -1; - return ret; -} - static int extend_abbrev_len(const struct object_id *a, const struct object_id *b, unsigned *out) diff --git a/packfile.h b/packfile.h index dd97684e7098dd..0097de0b274ff9 100644 --- a/packfile.h +++ b/packfile.h @@ -227,21 +227,6 @@ int for_each_object_in_pack(struct packed_git *p, each_packed_object_fn, void *data, enum odb_for_each_object_flags flags); -/* - * Iterate through all packed objects in the given packfile store and invoke - * the callback function for each of them. If an object info request is given, - * then the object info will be read for every individual object and passed to - * the callback as if `packfile_store_read_object_info()` was called for the - * object. - * - * The flags parameter is a combination of `odb_for_each_object_flags`. - */ -int packfile_store_for_each_object(struct odb_source_packed *store, - const struct object_info *request, - odb_for_each_object_cb cb, - void *cb_data, - const struct odb_for_each_object_options *opts); - int packfile_store_find_abbrev_len(struct odb_source_packed *store, const struct object_id *oid, unsigned min_len, @@ -354,6 +339,8 @@ extern int do_check_packed_object_crc; */ int packed_object_info(struct packed_git *pack, off_t offset, struct object_info *); +int packed_object_info_with_index_pos(struct packed_git *p, off_t obj_offset, + uint32_t *maybe_index_pos, struct object_info *oi); void mark_bad_packed_object(struct packed_git *, const struct object_id *); const struct packed_git *has_packed_and_bad(struct repository *, const struct object_id *); From 6b7484fcb683211eafd82a7ba4b4470d553a62b6 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:39:55 +0200 Subject: [PATCH 46/95] odb/source-packed: wire up `count_objects()` callback Move `packfile_store_count_objects()` from "packfile.c" into "odb/source-packed.c" and wire it up as the `count_objects()` callback of the "packed" source. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-files.c | 2 +- odb/source-packed.c | 33 +++++++++++++++++++++++++++++++++ packfile.c | 31 ------------------------------- packfile.h | 10 ---------- 4 files changed, 34 insertions(+), 42 deletions(-) diff --git a/odb/source-files.c b/odb/source-files.c index c73a7e5f9096ad..274923e0ba690a 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -103,7 +103,7 @@ static int odb_source_files_count_objects(struct odb_source *source, unsigned long count; int ret; - ret = packfile_store_count_objects(files->packed, flags, &count); + ret = odb_source_count_objects(&files->packed->base, flags, &count); if (ret < 0) goto out; diff --git a/odb/source-packed.c b/odb/source-packed.c index a61c809c8c8fa6..070a4e39585b30 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -338,6 +338,38 @@ static int odb_source_packed_for_each_object(struct odb_source *source, return ret; } +static int odb_source_packed_count_objects(struct odb_source *source, + enum odb_count_objects_flags flags UNUSED, + unsigned long *out) +{ + struct odb_source_packed *packed = odb_source_packed_downcast(source); + struct packfile_list_entry *e; + struct multi_pack_index *m; + unsigned long count = 0; + int ret; + + m = get_multi_pack_index(&packed->files->base); + if (m) + count += m->num_objects + m->num_objects_in_base; + + for (e = packfile_store_get_packs(packed); e; e = e->next) { + if (e->pack->multi_pack_index) + continue; + if (open_pack_index(e->pack)) { + ret = -1; + goto out; + } + + count += e->pack->num_objects; + } + + *out = count; + ret = 0; + +out: + return ret; +} + void (*report_garbage)(unsigned seen_bits, const char *path); static void report_helper(const struct string_list *list, @@ -549,6 +581,7 @@ struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent) packed->base.read_object_info = odb_source_packed_read_object_info; packed->base.read_object_stream = odb_source_packed_read_object_stream; packed->base.for_each_object = odb_source_packed_for_each_object; + packed->base.count_objects = odb_source_packed_count_objects; if (!is_absolute_path(parent->base.path)) chdir_notify_register(NULL, odb_source_packed_reparent, packed); diff --git a/packfile.c b/packfile.c index b8d6054c16aa72..2da6bbe2b510c7 100644 --- a/packfile.c +++ b/packfile.c @@ -866,37 +866,6 @@ struct packfile_list_entry *packfile_store_get_packs(struct odb_source_packed *s return store->packs.head; } -int packfile_store_count_objects(struct odb_source_packed *store, - enum odb_count_objects_flags flags UNUSED, - unsigned long *out) -{ - struct packfile_list_entry *e; - struct multi_pack_index *m; - unsigned long count = 0; - int ret; - - m = get_multi_pack_index(&store->files->base); - if (m) - count += m->num_objects + m->num_objects_in_base; - - for (e = packfile_store_get_packs(store); e; e = e->next) { - if (e->pack->multi_pack_index) - continue; - if (open_pack_index(e->pack)) { - ret = -1; - goto out; - } - - count += e->pack->num_objects; - } - - *out = count; - ret = 0; - -out: - return ret; -} - unsigned long unpack_object_header_buffer(const unsigned char *buf, unsigned long len, enum object_type *type, size_t *sizep) { diff --git a/packfile.h b/packfile.h index 0097de0b274ff9..0613fd3c639dc8 100644 --- a/packfile.h +++ b/packfile.h @@ -141,16 +141,6 @@ enum kept_pack_type { KEPT_PACK_IN_CORE_OPEN = (1 << 2), }; -/* - * Count the number objects contained in the given packfile store. If - * successful, the number of objects will be written to the `out` pointer. - * - * Return 0 on success, a negative error code otherwise. - */ -int packfile_store_count_objects(struct odb_source_packed *store, - enum odb_count_objects_flags flags, - unsigned long *out); - /* * Retrieve the cache of kept packs from the given packfile store. Accepts a * combination of `kept_pack_type` flags. The cache is computed on demand and From 3c0f7b732e37b885009cdc64a13541591ebab00e Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:39:56 +0200 Subject: [PATCH 47/95] odb/source-packed: wire up `find_abbrev_len()` callback Move `packfile_store_find_abbrev_len()` and its associated helpers from "packfile.c" into "odb/source-packed.c" and wire it up as the `find_abbrev_len()` callback of the "packed" source. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-files.c | 2 +- odb/source-packed.c | 113 ++++++++++++++++++++++++++++++++++++++++++++ packfile.c | 111 ------------------------------------------- packfile.h | 5 -- 4 files changed, 114 insertions(+), 117 deletions(-) diff --git a/odb/source-files.c b/odb/source-files.c index 274923e0ba690a..8ad782dc7be454 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -133,7 +133,7 @@ static int odb_source_files_find_abbrev_len(struct odb_source *source, unsigned len = min_len; int ret; - ret = packfile_store_find_abbrev_len(files->packed, oid, len, &len); + ret = odb_source_find_abbrev_len(&files->packed->base, oid, len, &len); if (ret < 0) goto out; diff --git a/odb/source-packed.c b/odb/source-packed.c index 070a4e39585b30..b801b620236f9e 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -370,6 +370,118 @@ static int odb_source_packed_count_objects(struct odb_source *source, return ret; } +static int extend_abbrev_len(const struct object_id *a, + const struct object_id *b, + unsigned *out) +{ + unsigned len = oid_common_prefix_hexlen(a, b); + if (len != hash_algos[a->algo].hexsz && len >= *out) + *out = len + 1; + return 0; +} + +static void find_abbrev_len_for_midx(struct multi_pack_index *m, + const struct object_id *oid, + unsigned min_len, + unsigned *out) +{ + unsigned len = min_len; + + for (; m; m = m->base_midx) { + int match = 0; + uint32_t num, first = 0; + struct object_id found_oid; + + if (!m->num_objects) + continue; + + num = m->num_objects + m->num_objects_in_base; + match = bsearch_one_midx(oid, m, &first); + + /* + * first is now the position in the packfile where we + * would insert the object ID if it does not exist (or the + * position of the object ID if it does exist). Hence, we + * consider a maximum of two objects nearby for the + * abbreviation length. + */ + + if (!match) { + if (nth_midxed_object_oid(&found_oid, m, first)) + extend_abbrev_len(&found_oid, oid, &len); + } else if (first < num - 1) { + if (nth_midxed_object_oid(&found_oid, m, first + 1)) + extend_abbrev_len(&found_oid, oid, &len); + } + if (first > 0) { + if (nth_midxed_object_oid(&found_oid, m, first - 1)) + extend_abbrev_len(&found_oid, oid, &len); + } + } + + *out = len; +} + +static void find_abbrev_len_for_pack(struct packed_git *p, + const struct object_id *oid, + unsigned min_len, + unsigned *out) +{ + int match; + uint32_t num, first = 0; + struct object_id found_oid; + unsigned len = min_len; + + num = p->num_objects; + match = bsearch_pack(oid, p, &first); + + /* + * first is now the position in the packfile where we would insert + * the object ID if it does not exist (or the position of mad->hash if + * it does exist). Hence, we consider a maximum of two objects + * nearby for the abbreviation length. + */ + if (!match) { + if (!nth_packed_object_id(&found_oid, p, first)) + extend_abbrev_len(&found_oid, oid, &len); + } else if (first < num - 1) { + if (!nth_packed_object_id(&found_oid, p, first + 1)) + extend_abbrev_len(&found_oid, oid, &len); + } + if (first > 0) { + if (!nth_packed_object_id(&found_oid, p, first - 1)) + extend_abbrev_len(&found_oid, oid, &len); + } + + *out = len; +} + +static int odb_source_packed_find_abbrev_len(struct odb_source *source, + const struct object_id *oid, + unsigned min_len, + unsigned *out) +{ + struct odb_source_packed *packed = odb_source_packed_downcast(source); + struct packfile_list_entry *e; + struct multi_pack_index *m; + + m = get_multi_pack_index(&packed->files->base); + if (m) + find_abbrev_len_for_midx(m, oid, min_len, &min_len); + + for (e = packfile_store_get_packs(packed); e; e = e->next) { + if (e->pack->multi_pack_index) + continue; + if (open_pack_index(e->pack) || !e->pack->num_objects) + continue; + + find_abbrev_len_for_pack(e->pack, oid, min_len, &min_len); + } + + *out = min_len; + return 0; +} + void (*report_garbage)(unsigned seen_bits, const char *path); static void report_helper(const struct string_list *list, @@ -582,6 +694,7 @@ struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent) packed->base.read_object_stream = odb_source_packed_read_object_stream; packed->base.for_each_object = odb_source_packed_for_each_object; packed->base.count_objects = odb_source_packed_count_objects; + packed->base.find_abbrev_len = odb_source_packed_find_abbrev_len; if (!is_absolute_path(parent->base.path)) chdir_notify_register(NULL, odb_source_packed_reparent, packed); diff --git a/packfile.c b/packfile.c index 2da6bbe2b510c7..7f84094e536886 100644 --- a/packfile.c +++ b/packfile.c @@ -2037,117 +2037,6 @@ int for_each_object_in_pack(struct packed_git *p, return r; } -static int extend_abbrev_len(const struct object_id *a, - const struct object_id *b, - unsigned *out) -{ - unsigned len = oid_common_prefix_hexlen(a, b); - if (len != hash_algos[a->algo].hexsz && len >= *out) - *out = len + 1; - return 0; -} - -static void find_abbrev_len_for_midx(struct multi_pack_index *m, - const struct object_id *oid, - unsigned min_len, - unsigned *out) -{ - unsigned len = min_len; - - for (; m; m = m->base_midx) { - int match = 0; - uint32_t num, first = 0; - struct object_id found_oid; - - if (!m->num_objects) - continue; - - num = m->num_objects + m->num_objects_in_base; - match = bsearch_one_midx(oid, m, &first); - - /* - * first is now the position in the packfile where we - * would insert the object ID if it does not exist (or the - * position of the object ID if it does exist). Hence, we - * consider a maximum of two objects nearby for the - * abbreviation length. - */ - - if (!match) { - if (nth_midxed_object_oid(&found_oid, m, first)) - extend_abbrev_len(&found_oid, oid, &len); - } else if (first < num - 1) { - if (nth_midxed_object_oid(&found_oid, m, first + 1)) - extend_abbrev_len(&found_oid, oid, &len); - } - if (first > 0) { - if (nth_midxed_object_oid(&found_oid, m, first - 1)) - extend_abbrev_len(&found_oid, oid, &len); - } - } - - *out = len; -} - -static void find_abbrev_len_for_pack(struct packed_git *p, - const struct object_id *oid, - unsigned min_len, - unsigned *out) -{ - int match; - uint32_t num, first = 0; - struct object_id found_oid; - unsigned len = min_len; - - num = p->num_objects; - match = bsearch_pack(oid, p, &first); - - /* - * first is now the position in the packfile where we would insert - * the object ID if it does not exist (or the position of mad->hash if - * it does exist). Hence, we consider a maximum of two objects - * nearby for the abbreviation length. - */ - if (!match) { - if (!nth_packed_object_id(&found_oid, p, first)) - extend_abbrev_len(&found_oid, oid, &len); - } else if (first < num - 1) { - if (!nth_packed_object_id(&found_oid, p, first + 1)) - extend_abbrev_len(&found_oid, oid, &len); - } - if (first > 0) { - if (!nth_packed_object_id(&found_oid, p, first - 1)) - extend_abbrev_len(&found_oid, oid, &len); - } - - *out = len; -} - -int packfile_store_find_abbrev_len(struct odb_source_packed *store, - const struct object_id *oid, - unsigned min_len, - unsigned *out) -{ - struct packfile_list_entry *e; - struct multi_pack_index *m; - - m = get_multi_pack_index(&store->files->base); - if (m) - find_abbrev_len_for_midx(m, oid, min_len, &min_len); - - for (e = packfile_store_get_packs(store); e; e = e->next) { - if (e->pack->multi_pack_index) - continue; - if (open_pack_index(e->pack) || !e->pack->num_objects) - continue; - - find_abbrev_len_for_pack(e->pack, oid, min_len, &min_len); - } - - *out = min_len; - return 0; -} - struct add_promisor_object_data { struct repository *repo; struct oidset *set; diff --git a/packfile.h b/packfile.h index 0613fd3c639dc8..79324e4010632b 100644 --- a/packfile.h +++ b/packfile.h @@ -217,11 +217,6 @@ int for_each_object_in_pack(struct packed_git *p, each_packed_object_fn, void *data, enum odb_for_each_object_flags flags); -int packfile_store_find_abbrev_len(struct odb_source_packed *store, - const struct object_id *oid, - unsigned min_len, - unsigned *out); - /* A hook to report invalid files in pack directory */ #define PACKDIR_FILE_PACK 1 #define PACKDIR_FILE_IDX 2 From 98189bdd0a789115e3bb5ec5d5312ce71d6f59b0 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:39:57 +0200 Subject: [PATCH 48/95] odb/source-packed: wire up `freshen_object()` callback Move `packfile_store_freshen_object()` and from "packfile.c" into "odb/source-packed.c" and wire it up as the `freshen_object()` callback of the "packed" source. Note that this removes the last external caller of `find_pack_entry()` from "packfile.c", which means that we can now make this function static. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-files.c | 2 +- odb/source-packed.c | 26 +++++++++++++++++++++++--- odb/source-packed.h | 6 ------ packfile.c | 16 ---------------- packfile.h | 3 --- 5 files changed, 24 insertions(+), 29 deletions(-) diff --git a/odb/source-files.c b/odb/source-files.c index 8ad782dc7be454..fa2e18e71b3cf6 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -152,7 +152,7 @@ static int odb_source_files_freshen_object(struct odb_source *source, const struct object_id *oid) { struct odb_source_files *files = odb_source_files_downcast(source); - if (packfile_store_freshen_object(files->packed, oid) || + if (odb_source_freshen_object(&files->packed->base, oid) || odb_source_freshen_object(&files->loose->base, oid)) return 1; return 0; diff --git a/odb/source-packed.c b/odb/source-packed.c index b801b620236f9e..e40b52e445c57c 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -9,9 +9,9 @@ #include "odb/streaming.h" #include "packfile.h" -int find_pack_entry(struct odb_source_packed *store, - const struct object_id *oid, - struct pack_entry *e) +static int find_pack_entry(struct odb_source_packed *store, + const struct object_id *oid, + struct pack_entry *e) { struct packfile_list_entry *l; @@ -482,6 +482,25 @@ static int odb_source_packed_find_abbrev_len(struct odb_source *source, return 0; } +static int odb_source_packed_freshen_object(struct odb_source *source, + const struct object_id *oid) +{ + struct odb_source_packed *packed = odb_source_packed_downcast(source); + struct pack_entry e; + + if (!find_pack_entry(packed, oid, &e)) + return 0; + if (e.p->is_cruft) + return 0; + if (e.p->freshened) + return 1; + if (utime(e.p->pack_name, NULL)) + return 0; + e.p->freshened = 1; + + return 1; +} + void (*report_garbage)(unsigned seen_bits, const char *path); static void report_helper(const struct string_list *list, @@ -695,6 +714,7 @@ struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent) packed->base.for_each_object = odb_source_packed_for_each_object; packed->base.count_objects = odb_source_packed_count_objects; packed->base.find_abbrev_len = odb_source_packed_find_abbrev_len; + packed->base.freshen_object = odb_source_packed_freshen_object; if (!is_absolute_path(parent->base.path)) chdir_notify_register(NULL, odb_source_packed_reparent, packed); diff --git a/odb/source-packed.h b/odb/source-packed.h index f430ee0b9466bd..9d4796261a72d3 100644 --- a/odb/source-packed.h +++ b/odb/source-packed.h @@ -90,10 +90,4 @@ static inline struct odb_source_packed *odb_source_packed_downcast(struct odb_so */ void odb_source_packed_prepare(struct odb_source_packed *source); -struct pack_entry; - -int find_pack_entry(struct odb_source_packed *store, - const struct object_id *oid, - struct pack_entry *e); - #endif diff --git a/packfile.c b/packfile.c index 7f84094e536886..a577275d4f334e 100644 --- a/packfile.c +++ b/packfile.c @@ -1892,22 +1892,6 @@ int packfile_fill_entry(struct packed_git *p, return 1; } -int packfile_store_freshen_object(struct odb_source_packed *store, - const struct object_id *oid) -{ - struct pack_entry e; - if (!find_pack_entry(store, oid, &e)) - return 0; - if (e.p->is_cruft) - return 0; - if (e.p->freshened) - return 1; - if (utime(e.p->pack_name, NULL)) - return 0; - e.p->freshened = 1; - return 1; -} - static void maybe_invalidate_kept_pack_cache(struct odb_source_packed *store, unsigned flags) { diff --git a/packfile.h b/packfile.h index 79324e4010632b..71a71017ee1058 100644 --- a/packfile.h +++ b/packfile.h @@ -132,9 +132,6 @@ static inline void repo_for_each_pack_data_next(struct repo_for_each_pack_data * struct packed_git *packfile_store_load_pack(struct odb_source_packed *store, const char *idx_path, int local); -int packfile_store_freshen_object(struct odb_source_packed *store, - const struct object_id *oid); - enum kept_pack_type { KEPT_PACK_ON_DISK = (1 << 0), KEPT_PACK_IN_CORE = (1 << 1), From f236b0c38d11b97930608384eaddd210f74fc7ab Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:39:58 +0200 Subject: [PATCH 49/95] odb/source-packed: stub out remaining functions Stub out remaining functions that we either don't need or that are basically no-ops. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-packed.c | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/odb/source-packed.c b/odb/source-packed.c index e40b52e445c57c..08a2de9bc51127 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -501,6 +501,43 @@ static int odb_source_packed_freshen_object(struct odb_source *source, return 1; } +static int odb_source_packed_write_object(struct odb_source *source UNUSED, + const void *buf UNUSED, + unsigned long len UNUSED, + enum object_type type UNUSED, + struct object_id *oid UNUSED, + struct object_id *compat_oid UNUSED, + unsigned flags UNUSED) +{ + return error("packed backend cannot write objects"); +} + +static int odb_source_packed_write_object_stream(struct odb_source *source UNUSED, + struct odb_write_stream *stream UNUSED, + size_t len UNUSED, + struct object_id *oid UNUSED) +{ + return error("packed backend cannot write object streams"); +} + +static int odb_source_packed_begin_transaction(struct odb_source *source UNUSED, + struct odb_transaction **out UNUSED) +{ + return error("packed backend cannot begin transactions"); +} + +static int odb_source_packed_read_alternates(struct odb_source *source UNUSED, + struct strvec *out UNUSED) +{ + return 0; +} + +static int odb_source_packed_write_alternate(struct odb_source *source UNUSED, + const char *alternate UNUSED) +{ + return error("packed backend cannot write alternates"); +} + void (*report_garbage)(unsigned seen_bits, const char *path); static void report_helper(const struct string_list *list, @@ -715,6 +752,11 @@ struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent) packed->base.count_objects = odb_source_packed_count_objects; packed->base.find_abbrev_len = odb_source_packed_find_abbrev_len; packed->base.freshen_object = odb_source_packed_freshen_object; + packed->base.write_object = odb_source_packed_write_object; + packed->base.write_object_stream = odb_source_packed_write_object_stream; + packed->base.begin_transaction = odb_source_packed_begin_transaction; + packed->base.read_alternates = odb_source_packed_read_alternates; + packed->base.write_alternate = odb_source_packed_write_alternate; if (!is_absolute_path(parent->base.path)) chdir_notify_register(NULL, odb_source_packed_reparent, packed); From 7fa8c61afe29bfb82066c81a82ed6393f34dd704 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:39:59 +0200 Subject: [PATCH 50/95] midx: refactor interfaces to work on "packed" source Our interfaces used to interact with MIDXs all work on top of the generic `struct odb_source`. This doesn't make much sense though: a MIDX is strictly tied to the "packed" source, so passing in a generic source gives the false sense that it may also work with a different type of source. Fix this conceptual weirdness and instead require the caller to pass in a "packed" source explicitly. This also makes the next commit easier to implement, where we drop the pointer to the "files" source in the "packed" source. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/multi-pack-index.c | 29 ++++----- builtin/pack-objects.c | 3 +- builtin/repack.c | 8 ++- midx-write.c | 34 +++++------ midx.c | 118 ++++++++++++++++++------------------- midx.h | 30 +++++----- odb/source-packed.c | 12 ++-- pack-bitmap.c | 8 ++- pack-revindex.c | 6 +- repack-geometry.c | 3 +- repack-midx.c | 9 ++- repack.c | 6 +- t/helper/test-read-midx.c | 7 ++- 13 files changed, 144 insertions(+), 129 deletions(-) diff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c index 00ffb36394d08c..6e73c85cde324f 100644 --- a/builtin/multi-pack-index.c +++ b/builtin/multi-pack-index.c @@ -10,6 +10,7 @@ #include "trace2.h" #include "odb.h" #include "odb/source.h" +#include "odb/source-files.h" #include "replace-object.h" #include "repository.h" @@ -85,12 +86,12 @@ static int parse_object_dir(const struct option *opt, const char *arg, return 0; } -static struct odb_source *handle_object_dir_option(struct repository *repo) +static struct odb_source_files *handle_object_dir_option(struct repository *repo) { struct odb_source *source = odb_find_source(repo->objects, opts.object_dir); if (!source) source = odb_add_to_alternates_memory(repo->objects, opts.object_dir); - return source; + return odb_source_files_downcast(source); } static struct option common_opts[] = { @@ -167,7 +168,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv, N_("refs snapshot for selecting bitmap commits")), OPT_END(), }; - struct odb_source *source; + struct odb_source_files *source; int ret; opts.flags |= MIDX_WRITE_BITMAP_HASH_CACHE; @@ -211,7 +212,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv, read_packs_from_stdin(&packs); - ret = write_midx_file_only(source, &packs, + ret = write_midx_file_only(source->packed, &packs, opts.preferred_pack, opts.refs_snapshot, opts.incremental_base, opts.flags); @@ -223,7 +224,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv, } - ret = write_midx_file(source, opts.preferred_pack, + ret = write_midx_file(source->packed, opts.preferred_pack, opts.refs_snapshot, opts.flags); free(opts.refs_snapshot); @@ -237,7 +238,7 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv, struct multi_pack_index *m, *cur; struct multi_pack_index *from_midx = NULL; struct multi_pack_index *to_midx = NULL; - struct odb_source *source; + struct odb_source_files *source; int ret; struct option *options; @@ -282,7 +283,7 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv, FREE_AND_NULL(options); - m = get_multi_pack_index(source); + m = get_multi_pack_index(source->packed); for (cur = m; cur && !(from_midx && to_midx); cur = cur->base_midx) { const char *midx_csum = midx_get_checksum_hex(cur); @@ -305,7 +306,7 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv, die(_("MIDX %s must be an ancestor of %s"), argv[0], argv[1]); } - ret = write_midx_file_compact(source, from_midx, to_midx, + ret = write_midx_file_compact(source->packed, from_midx, to_midx, opts.incremental_base, opts.flags); return ret; @@ -319,7 +320,7 @@ static int cmd_multi_pack_index_verify(int argc, const char **argv, static struct option builtin_multi_pack_index_verify_options[] = { OPT_END(), }; - struct odb_source *source; + struct odb_source_files *source; options = add_common_options(builtin_multi_pack_index_verify_options); @@ -337,7 +338,7 @@ static int cmd_multi_pack_index_verify(int argc, const char **argv, FREE_AND_NULL(options); - return verify_midx_file(source, opts.flags); + return verify_midx_file(source->packed, opts.flags); } static int cmd_multi_pack_index_expire(int argc, const char **argv, @@ -348,7 +349,7 @@ static int cmd_multi_pack_index_expire(int argc, const char **argv, static struct option builtin_multi_pack_index_expire_options[] = { OPT_END(), }; - struct odb_source *source; + struct odb_source_files *source; options = add_common_options(builtin_multi_pack_index_expire_options); @@ -366,7 +367,7 @@ static int cmd_multi_pack_index_expire(int argc, const char **argv, FREE_AND_NULL(options); - return expire_midx_packs(source, opts.flags); + return expire_midx_packs(source->packed, opts.flags); } static int cmd_multi_pack_index_repack(int argc, const char **argv, @@ -379,7 +380,7 @@ static int cmd_multi_pack_index_repack(int argc, const char **argv, N_("during repack, collect pack-files of smaller size into a batch that is larger than this size")), OPT_END(), }; - struct odb_source *source; + struct odb_source_files *source; options = add_common_options(builtin_multi_pack_index_repack_options); @@ -398,7 +399,7 @@ static int cmd_multi_pack_index_repack(int argc, const char **argv, FREE_AND_NULL(options); - return midx_repack(source, (size_t)opts.batch_size, opts.flags); + return midx_repack(source->packed, (size_t)opts.batch_size, opts.flags); } int cmd_multi_pack_index(int argc, diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 5e948054789a53..424c92cc29679b 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -1775,7 +1775,8 @@ static int want_object_in_pack_mtime(const struct object_id *oid, odb_prepare_alternates(the_repository->objects); for (source = the_repository->objects->sources; source; source = source->next) { - struct multi_pack_index *m = get_multi_pack_index(source); + struct odb_source_files *files = odb_source_files_downcast(source); + struct multi_pack_index *m = get_multi_pack_index(files->packed); struct pack_entry e; if (m && fill_midx_entry(m, oid, &e)) { diff --git a/builtin/repack.c b/builtin/repack.c index 1524a9c13ad5b8..47966a686b9ac9 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -458,6 +458,8 @@ int cmd_repack(int argc, } if (!names.nr) { + struct odb_source_files *files = odb_source_files_downcast(existing.source); + if (!po_args.quiet) printf_ln(_("Nothing new to pack.")); /* @@ -473,7 +475,7 @@ int cmd_repack(int argc, * midx_has_unknown_packs() will make the decision for * us. */ - if (!get_multi_pack_index(existing.source)) + if (!get_multi_pack_index(files->packed)) midx_must_contain_cruft = 1; } @@ -626,10 +628,12 @@ int cmd_repack(int argc, update_server_info(repo, 0); if (git_env_bool(GIT_TEST_MULTI_PACK_INDEX, 0)) { + struct odb_source_files *files = odb_source_files_downcast(existing.source); unsigned flags = 0; + if (git_env_bool(GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL, 0)) flags |= MIDX_WRITE_INCREMENTAL; - write_midx_file(existing.source, NULL, NULL, flags); + write_midx_file(files->packed, NULL, NULL, flags); } cleanup: diff --git a/midx-write.c b/midx-write.c index 561e9eedc0e6ef..7cafc49fb8a7cd 100644 --- a/midx-write.c +++ b/midx-write.c @@ -25,9 +25,9 @@ #define NO_PREFERRED_PACK (~((uint32_t)0)) extern int midx_checksum_valid(struct multi_pack_index *m); -extern void clear_midx_files_ext(struct odb_source *source, const char *ext, +extern void clear_midx_files_ext(struct odb_source_packed *source, const char *ext, const char *keep_hash); -extern void clear_incremental_midx_files_ext(struct odb_source *source, +extern void clear_incremental_midx_files_ext(struct odb_source_packed *source, const char *ext, const struct strvec *keep_hashes); extern int cmp_idx_or_pack_name(const char *idx_or_pack_name, @@ -119,7 +119,7 @@ struct write_midx_context { struct string_list *to_include; struct repository *repo; - struct odb_source *source; + struct odb_source_packed *source; }; static uint32_t midx_pack_perm(struct write_midx_context *ctx, @@ -1107,7 +1107,7 @@ static int link_midx_to_chain(struct multi_pack_index *m) return ret; } -static void clear_midx_files(struct odb_source *source, +static void clear_midx_files(struct odb_source_packed *source, const struct strvec *hashes, unsigned incremental) { /* @@ -1237,7 +1237,7 @@ static int midx_hashcmp(const struct multi_pack_index *a, } struct write_midx_opts { - struct odb_source *source; /* non-optional */ + struct odb_source_packed *source; /* non-optional */ struct string_list *packs_to_include; struct string_list *packs_to_drop; @@ -1253,7 +1253,7 @@ struct write_midx_opts { static int write_midx_internal(struct write_midx_opts *opts) { - struct repository *r = opts->source->odb->repo; + struct repository *r = opts->source->base.odb->repo; struct strbuf midx_name = STRBUF_INIT; unsigned char midx_hash[GIT_MAX_RAWSZ]; uint32_t start_pack; @@ -1301,7 +1301,7 @@ static int write_midx_internal(struct write_midx_opts *opts) if (ctx.incremental) strbuf_addf(&midx_name, "%s/pack/multi-pack-index.d/tmp_midx_XXXXXX", - opts->source->path); + opts->source->base.path); else get_midx_filename(opts->source, &midx_name); if (safe_create_leading_directories(r, midx_name.buf)) @@ -1396,7 +1396,7 @@ static int write_midx_internal(struct write_midx_opts *opts) fill_packs_from_midx_range(&ctx, bitmap_order); } else { ctx.to_include = opts->packs_to_include; - for_each_file_in_pack_dir(opts->source->path, add_pack_to_midx, &ctx); + for_each_file_in_pack_dir(opts->source->base.path, add_pack_to_midx, &ctx); } stop_progress(&ctx.progress); @@ -1847,7 +1847,7 @@ static int write_midx_internal(struct write_midx_opts *opts) return result; } -int write_midx_file(struct odb_source *source, +int write_midx_file(struct odb_source_packed *source, const char *preferred_pack_name, const char *refs_snapshot, unsigned flags) @@ -1862,7 +1862,7 @@ int write_midx_file(struct odb_source *source, return write_midx_internal(&opts); } -int write_midx_file_only(struct odb_source *source, +int write_midx_file_only(struct odb_source_packed *source, struct string_list *packs_to_include, const char *preferred_pack_name, const char *refs_snapshot, @@ -1881,7 +1881,7 @@ int write_midx_file_only(struct odb_source *source, return write_midx_internal(&opts); } -int write_midx_file_compact(struct odb_source *source, +int write_midx_file_compact(struct odb_source_packed *source, struct multi_pack_index *from, struct multi_pack_index *to, const char *incremental_base, @@ -1898,7 +1898,7 @@ int write_midx_file_compact(struct odb_source *source, return write_midx_internal(&opts); } -int expire_midx_packs(struct odb_source *source, unsigned flags) +int expire_midx_packs(struct odb_source_packed *source, unsigned flags) { uint32_t i, *count, result = 0; struct string_list packs_to_drop = STRING_LIST_INIT_DUP; @@ -1915,7 +1915,7 @@ int expire_midx_packs(struct odb_source *source, unsigned flags) if (flags & MIDX_PROGRESS) progress = start_delayed_progress( - source->odb->repo, + source->base.odb->repo, _("Counting referenced objects"), m->num_objects); for (i = 0; i < m->num_objects; i++) { @@ -1927,7 +1927,7 @@ int expire_midx_packs(struct odb_source *source, unsigned flags) if (flags & MIDX_PROGRESS) progress = start_delayed_progress( - source->odb->repo, + source->base.odb->repo, _("Finding and deleting unreferenced packfiles"), m->num_packs); for (i = 0; i < m->num_packs; i++) { @@ -2085,9 +2085,9 @@ static void fill_included_packs_batch(struct repository *r, free(pack_info); } -int midx_repack(struct odb_source *source, size_t batch_size, unsigned flags) +int midx_repack(struct odb_source_packed *source, size_t batch_size, unsigned flags) { - struct repository *r = source->odb->repo; + struct repository *r = source->base.odb->repo; int result = 0; uint32_t i, packs_to_repack = 0; unsigned char *include_pack; @@ -2131,7 +2131,7 @@ int midx_repack(struct odb_source *source, size_t batch_size, unsigned flags) strvec_push(&cmd.args, "pack-objects"); - strvec_pushf(&cmd.args, "%s/pack/pack", source->path); + strvec_pushf(&cmd.args, "%s/pack/pack", source->base.path); if (delta_base_offset) strvec_push(&cmd.args, "--delta-base-offset"); diff --git a/midx.c b/midx.c index 00bbd137b27a73..cc6b94f9dd4fba 100644 --- a/midx.c +++ b/midx.c @@ -17,9 +17,9 @@ #define MIDX_PACK_ERROR ((void *)(intptr_t)-1) int midx_checksum_valid(struct multi_pack_index *m); -void clear_midx_files_ext(struct odb_source *source, const char *ext, +void clear_midx_files_ext(struct odb_source_packed *source, const char *ext, const char *keep_hash); -void clear_incremental_midx_files_ext(struct odb_source *source, const char *ext, +void clear_incremental_midx_files_ext(struct odb_source_packed *source, const char *ext, const struct strvec *keep_hashes); int cmp_idx_or_pack_name(const char *idx_or_pack_name, const char *idx_name); @@ -27,25 +27,25 @@ int cmp_idx_or_pack_name(const char *idx_or_pack_name, const char *midx_get_checksum_hex(const struct multi_pack_index *m) { return hash_to_hex_algop(midx_get_checksum_hash(m), - m->source->odb->repo->hash_algo); + m->source->base.odb->repo->hash_algo); } const unsigned char *midx_get_checksum_hash(const struct multi_pack_index *m) { - return m->data + m->data_len - m->source->odb->repo->hash_algo->rawsz; + return m->data + m->data_len - m->source->base.odb->repo->hash_algo->rawsz; } -void get_midx_filename(struct odb_source *source, struct strbuf *out) +void get_midx_filename(struct odb_source_packed *source, struct strbuf *out) { get_midx_filename_ext(source, out, NULL, NULL); } -void get_midx_filename_ext(struct odb_source *source, struct strbuf *out, +void get_midx_filename_ext(struct odb_source_packed *source, struct strbuf *out, const unsigned char *hash, const char *ext) { - strbuf_addf(out, "%s/pack/multi-pack-index", source->path); + strbuf_addf(out, "%s/pack/multi-pack-index", source->base.path); if (ext) - strbuf_addf(out, "-%s.%s", hash_to_hex_algop(hash, source->odb->repo->hash_algo), ext); + strbuf_addf(out, "-%s.%s", hash_to_hex_algop(hash, source->base.odb->repo->hash_algo), ext); } static int midx_read_oid_fanout(const unsigned char *chunk_start, @@ -99,17 +99,16 @@ static int midx_read_object_offsets(const unsigned char *chunk_start, return 0; } -struct multi_pack_index *get_multi_pack_index(struct odb_source *source) +struct multi_pack_index *get_multi_pack_index(struct odb_source_packed *source) { - struct odb_source_files *files = odb_source_files_downcast(source); - odb_source_packed_prepare(files->packed); - return files->packed->midx; + odb_source_packed_prepare(source); + return source->midx; } -static struct multi_pack_index *load_multi_pack_index_one(struct odb_source *source, +static struct multi_pack_index *load_multi_pack_index_one(struct odb_source_packed *source, const char *midx_name) { - struct repository *r = source->odb->repo; + struct repository *r = source->base.odb->repo; struct multi_pack_index *m = NULL; int fd; struct stat st; @@ -234,23 +233,23 @@ static struct multi_pack_index *load_multi_pack_index_one(struct odb_source *sou return NULL; } -void get_midx_chain_dirname(struct odb_source *source, struct strbuf *buf) +void get_midx_chain_dirname(struct odb_source_packed *source, struct strbuf *buf) { - strbuf_addf(buf, "%s/pack/multi-pack-index.d", source->path); + strbuf_addf(buf, "%s/pack/multi-pack-index.d", source->base.path); } -void get_midx_chain_filename(struct odb_source *source, struct strbuf *buf) +void get_midx_chain_filename(struct odb_source_packed *source, struct strbuf *buf) { get_midx_chain_dirname(source, buf); strbuf_addstr(buf, "/multi-pack-index-chain"); } -void get_split_midx_filename_ext(struct odb_source *source, struct strbuf *buf, +void get_split_midx_filename_ext(struct odb_source_packed *source, struct strbuf *buf, const unsigned char *hash, const char *ext) { get_midx_chain_dirname(source, buf); strbuf_addf(buf, "/multi-pack-index-%s.%s", - hash_to_hex_algop(hash, source->odb->repo->hash_algo), ext); + hash_to_hex_algop(hash, source->base.odb->repo->hash_algo), ext); } static int open_multi_pack_index_chain(const struct git_hash_algo *hash_algo, @@ -306,11 +305,11 @@ static int add_midx_to_chain(struct multi_pack_index *midx, return 1; } -static struct multi_pack_index *load_midx_chain_fd_st(struct odb_source *source, +static struct multi_pack_index *load_midx_chain_fd_st(struct odb_source_packed *source, int fd, struct stat *st, int *incomplete_chain) { - const struct git_hash_algo *hash_algo = source->odb->repo->hash_algo; + const struct git_hash_algo *hash_algo = source->base.odb->repo->hash_algo; struct multi_pack_index *midx_chain = NULL; struct strbuf buf = STRBUF_INIT; int valid = 1; @@ -362,7 +361,7 @@ static struct multi_pack_index *load_midx_chain_fd_st(struct odb_source *source, return midx_chain; } -static struct multi_pack_index *load_multi_pack_index_chain(struct odb_source *source) +static struct multi_pack_index *load_multi_pack_index_chain(struct odb_source_packed *source) { struct strbuf chain_file = STRBUF_INIT; struct stat st; @@ -370,7 +369,8 @@ static struct multi_pack_index *load_multi_pack_index_chain(struct odb_source *s struct multi_pack_index *m = NULL; get_midx_chain_filename(source, &chain_file); - if (open_multi_pack_index_chain(source->odb->repo->hash_algo, chain_file.buf, &fd, &st)) { + if (open_multi_pack_index_chain(source->base.odb->repo->hash_algo, + chain_file.buf, &fd, &st)) { int incomplete; /* ownership of fd is taken over by load function */ m = load_midx_chain_fd_st(source, fd, &st, &incomplete); @@ -380,7 +380,7 @@ static struct multi_pack_index *load_multi_pack_index_chain(struct odb_source *s return m; } -struct multi_pack_index *load_multi_pack_index(struct odb_source *source) +struct multi_pack_index *load_multi_pack_index(struct odb_source_packed *source) { struct strbuf midx_name = STRBUF_INIT; struct multi_pack_index *m; @@ -456,7 +456,7 @@ static uint32_t midx_for_pack(struct multi_pack_index **_m, int prepare_midx_pack(struct multi_pack_index *m, uint32_t pack_int_id) { - struct odb_source_files *files = odb_source_files_downcast(m->source); + struct odb_source_packed *packed = m->source; struct strbuf pack_name = STRBUF_INIT; struct packed_git *p; @@ -467,10 +467,10 @@ int prepare_midx_pack(struct multi_pack_index *m, if (m->packs[pack_int_id]) return 0; - strbuf_addf(&pack_name, "%s/pack/%s", files->base.path, + strbuf_addf(&pack_name, "%s/pack/%s", packed->base.path, m->pack_names[pack_int_id]); - p = packfile_store_load_pack(files->packed, - pack_name.buf, files->base.local); + p = packfile_store_load_pack(packed, + pack_name.buf, packed->base.local); strbuf_release(&pack_name); if (!p) { @@ -523,7 +523,7 @@ int bsearch_one_midx(const struct object_id *oid, struct multi_pack_index *m, { int ret = bsearch_hash(oid->hash, m->chunk_oid_fanout, m->chunk_oid_lookup, - m->source->odb->repo->hash_algo->rawsz, + m->source->base.odb->repo->hash_algo->rawsz, result); if (result) *result += m->num_objects_in_base; @@ -554,7 +554,7 @@ struct object_id *nth_midxed_object_oid(struct object_id *oid, n = midx_for_object(&m, n); oidread(oid, m->chunk_oid_lookup + st_mult(m->hash_len, n), - m->source->odb->repo->hash_algo); + m->source->base.odb->repo->hash_algo); return oid; } @@ -734,26 +734,25 @@ int midx_preferred_pack(struct multi_pack_index *m, uint32_t *pack_int_id) return 0; } -int prepare_multi_pack_index_one(struct odb_source *source) +int prepare_multi_pack_index_one(struct odb_source_packed *source) { - struct odb_source_files *files = odb_source_files_downcast(source); - struct repository *r = source->odb->repo; + struct repository *r = source->base.odb->repo; prepare_repo_settings(r); if (!r->settings.core_multi_pack_index) return 0; - if (files->packed->midx) + if (source->midx) return 1; - files->packed->midx = load_multi_pack_index(source); + source->midx = load_multi_pack_index(source); - return !!files->packed->midx; + return !!source->midx; } int midx_checksum_valid(struct multi_pack_index *m) { - return hashfile_checksum_valid(m->source->odb->repo->hash_algo, + return hashfile_checksum_valid(m->source->base.odb->repo->hash_algo, m->data, m->data_len); } @@ -776,7 +775,7 @@ static void clear_midx_file_ext(const char *full_path, size_t full_path_len UNUS die_errno(_("failed to remove %s"), full_path); } -void clear_midx_files_ext(struct odb_source *source, const char *ext, +void clear_midx_files_ext(struct odb_source_packed *source, const char *ext, const char *keep_hash) { struct clear_midx_data data = { @@ -793,12 +792,12 @@ void clear_midx_files_ext(struct odb_source *source, const char *ext, strbuf_release(&buf); } - for_each_file_in_pack_dir(source->path, clear_midx_file_ext, &data); + for_each_file_in_pack_dir(source->base.path, clear_midx_file_ext, &data); strset_clear(&data.keep); } -void clear_incremental_midx_files_ext(struct odb_source *source, const char *ext, +void clear_incremental_midx_files_ext(struct odb_source_packed *source, const char *ext, const struct strvec *keep_hashes) { struct clear_midx_data data = { @@ -817,7 +816,7 @@ void clear_incremental_midx_files_ext(struct odb_source *source, const char *ext } } - for_each_file_in_pack_subdir(source->path, "multi-pack-index.d", + for_each_file_in_pack_subdir(source->base.path, "multi-pack-index.d", clear_midx_file_ext, &data); strbuf_release(&buf); @@ -826,26 +825,28 @@ void clear_incremental_midx_files_ext(struct odb_source *source, const char *ext void clear_midx_file(struct repository *r) { + struct odb_source_files *files; struct strbuf midx = STRBUF_INIT; - get_midx_filename(r->objects->sources, &midx); - if (r->objects) { struct odb_source *source; for (source = r->objects->sources; source; source = source->next) { - struct odb_source_files *files = odb_source_files_downcast(source); + files = odb_source_files_downcast(source); if (files->packed->midx) close_midx(files->packed->midx); files->packed->midx = NULL; } } + files = odb_source_files_downcast(r->objects->sources); + get_midx_filename(files->packed, &midx); + if (remove_path(midx.buf)) die(_("failed to clear multi-pack-index at %s"), midx.buf); - clear_midx_files_ext(r->objects->sources, MIDX_EXT_BITMAP, NULL); - clear_midx_files_ext(r->objects->sources, MIDX_EXT_REV, NULL); + clear_midx_files_ext(files->packed, MIDX_EXT_BITMAP, NULL); + clear_midx_files_ext(files->packed, MIDX_EXT_REV, NULL); strbuf_release(&midx); } @@ -853,28 +854,27 @@ void clear_midx_file(struct repository *r) void clear_incremental_midx_files(struct repository *r, const struct strvec *keep_hashes) { - struct odb_source *source = r->objects->sources; + struct odb_source_files *files; + struct odb_source *source; struct strbuf chain = STRBUF_INIT; - get_midx_chain_filename(source, &chain); - - for (; source; source = source->next) { - struct odb_source_files *files = odb_source_files_downcast(source); + for (source = r->objects->sources; source; source = source->next) { + files = odb_source_files_downcast(source); if (files->packed->midx) close_midx(files->packed->midx); files->packed->midx = NULL; } + files = odb_source_files_downcast(r->objects->sources); + get_midx_chain_filename(files->packed, &chain); + if (!keep_hashes && remove_path(chain.buf)) die(_("failed to clear multi-pack-index chain at %s"), chain.buf); - clear_incremental_midx_files_ext(r->objects->sources, MIDX_EXT_BITMAP, - keep_hashes); - clear_incremental_midx_files_ext(r->objects->sources, MIDX_EXT_REV, - keep_hashes); - clear_incremental_midx_files_ext(r->objects->sources, MIDX_EXT_MIDX, - keep_hashes); + clear_incremental_midx_files_ext(files->packed, MIDX_EXT_BITMAP, keep_hashes); + clear_incremental_midx_files_ext(files->packed, MIDX_EXT_REV, keep_hashes); + clear_incremental_midx_files_ext(files->packed, MIDX_EXT_MIDX, keep_hashes); strbuf_release(&chain); } @@ -918,9 +918,9 @@ static int compare_pair_pos_vs_id(const void *_a, const void *_b) display_progress(progress, _n); \ } while (0) -int verify_midx_file(struct odb_source *source, unsigned flags) +int verify_midx_file(struct odb_source_packed *source, unsigned flags) { - struct repository *r = source->odb->repo; + struct repository *r = source->base.odb->repo; struct pair_pos_vs_id *pairs = NULL; uint32_t i; struct progress *progress = NULL; diff --git a/midx.h b/midx.h index 63853a03a47fd1..939c18e5885e6f 100644 --- a/midx.h +++ b/midx.h @@ -37,7 +37,7 @@ struct strvec; "GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL" struct multi_pack_index { - struct odb_source *source; + struct odb_source_packed *source; const unsigned char *data; size_t data_len; @@ -92,16 +92,16 @@ struct multi_pack_index { const char *midx_get_checksum_hex(const struct multi_pack_index *m) /* static buffer */; const unsigned char *midx_get_checksum_hash(const struct multi_pack_index *m); -void get_midx_filename(struct odb_source *source, struct strbuf *out); -void get_midx_filename_ext(struct odb_source *source, struct strbuf *out, +void get_midx_filename(struct odb_source_packed *source, struct strbuf *out); +void get_midx_filename_ext(struct odb_source_packed *source, struct strbuf *out, const unsigned char *hash, const char *ext); -void get_midx_chain_dirname(struct odb_source *source, struct strbuf *out); -void get_midx_chain_filename(struct odb_source *source, struct strbuf *out); -void get_split_midx_filename_ext(struct odb_source *source, struct strbuf *buf, +void get_midx_chain_dirname(struct odb_source_packed *source, struct strbuf *out); +void get_midx_chain_filename(struct odb_source_packed *source, struct strbuf *out); +void get_split_midx_filename_ext(struct odb_source_packed *source, struct strbuf *buf, const unsigned char *hash, const char *ext); -struct multi_pack_index *get_multi_pack_index(struct odb_source *source); -struct multi_pack_index *load_multi_pack_index(struct odb_source *source); +struct multi_pack_index *get_multi_pack_index(struct odb_source_packed *source); +struct multi_pack_index *load_multi_pack_index(struct odb_source_packed *source); int prepare_midx_pack(struct multi_pack_index *m, uint32_t pack_int_id); struct packed_git *nth_midxed_pack(struct multi_pack_index *m, uint32_t pack_int_id); @@ -123,22 +123,22 @@ int midx_contains_pack(struct multi_pack_index *m, int midx_layer_contains_pack(struct multi_pack_index *m, const char *idx_or_pack_name); int midx_preferred_pack(struct multi_pack_index *m, uint32_t *pack_int_id); -int prepare_multi_pack_index_one(struct odb_source *source); +int prepare_multi_pack_index_one(struct odb_source_packed *source); /* * Variant of write_midx_file which writes a MIDX containing only the packs * specified in packs_to_include. */ -int write_midx_file(struct odb_source *source, +int write_midx_file(struct odb_source_packed *source, const char *preferred_pack_name, const char *refs_snapshot, unsigned flags); -int write_midx_file_only(struct odb_source *source, +int write_midx_file_only(struct odb_source_packed *source, struct string_list *packs_to_include, const char *preferred_pack_name, const char *refs_snapshot, const char *incremental_base, unsigned flags); -int write_midx_file_compact(struct odb_source *source, +int write_midx_file_compact(struct odb_source_packed *source, struct multi_pack_index *from, struct multi_pack_index *to, const char *incremental_base, @@ -146,9 +146,9 @@ int write_midx_file_compact(struct odb_source *source, void clear_midx_file(struct repository *r); void clear_incremental_midx_files(struct repository *r, const struct strvec *keep_hashes); -int verify_midx_file(struct odb_source *source, unsigned flags); -int expire_midx_packs(struct odb_source *source, unsigned flags); -int midx_repack(struct odb_source *source, size_t batch_size, unsigned flags); +int verify_midx_file(struct odb_source_packed *source, unsigned flags); +int expire_midx_packs(struct odb_source_packed *source, unsigned flags); +int midx_repack(struct odb_source_packed *source, size_t batch_size, unsigned flags); void close_midx(struct multi_pack_index *m); diff --git a/odb/source-packed.c b/odb/source-packed.c index 08a2de9bc51127..d513b3efc367fd 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -136,8 +136,8 @@ static int for_each_prefixed_object_in_midx( for (; m; m = m->base_midx) { uint32_t num, i, first = 0; - int len = opts->prefix_hex_len > m->source->odb->repo->hash_algo->hexsz ? - m->source->odb->repo->hash_algo->hexsz : opts->prefix_hex_len; + int len = opts->prefix_hex_len > m->source->base.odb->repo->hash_algo->hexsz ? + m->source->base.odb->repo->hash_algo->hexsz : opts->prefix_hex_len; if (!m->num_objects) continue; @@ -249,7 +249,7 @@ static int odb_source_packed_for_each_prefixed_object( store->skip_mru_updates = true; - m = get_multi_pack_index(&store->files->base); + m = get_multi_pack_index(store); if (m) { ret = for_each_prefixed_object_in_midx(store, m, opts, data); if (ret) @@ -348,7 +348,7 @@ static int odb_source_packed_count_objects(struct odb_source *source, unsigned long count = 0; int ret; - m = get_multi_pack_index(&packed->files->base); + m = get_multi_pack_index(packed); if (m) count += m->num_objects + m->num_objects_in_base; @@ -465,7 +465,7 @@ static int odb_source_packed_find_abbrev_len(struct odb_source *source, struct packfile_list_entry *e; struct multi_pack_index *m; - m = get_multi_pack_index(&packed->files->base); + m = get_multi_pack_index(packed); if (m) find_abbrev_len_for_midx(m, oid, min_len, &min_len); @@ -674,7 +674,7 @@ void odb_source_packed_prepare(struct odb_source_packed *source) if (source->initialized) return; - prepare_multi_pack_index_one(&source->files->base); + prepare_multi_pack_index_one(source); prepare_packed_git_one(&source->files->base); sort_packs(&source->packs.head, sort_pack); diff --git a/pack-bitmap.c b/pack-bitmap.c index f9af8a96bdf4ee..6bfcbc8ce6a0d2 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -238,7 +238,7 @@ static uint32_t bitmap_name_hash(struct bitmap_index *index, uint32_t pos) static struct repository *bitmap_repo(struct bitmap_index *bitmap_git) { if (bitmap_is_midx(bitmap_git)) - return bitmap_git->midx->source->odb->repo; + return bitmap_git->midx->source->base.odb->repo; return bitmap_git->pack->repo; } @@ -711,7 +711,8 @@ static int open_midx_bitmap(struct repository *r, odb_prepare_alternates(r->objects); for (source = r->objects->sources; source; source = source->next) { - struct multi_pack_index *midx = get_multi_pack_index(source); + struct odb_source_files *files = odb_source_files_downcast(source); + struct multi_pack_index *midx = get_multi_pack_index(files->packed); if (midx && !open_midx_bitmap_1(bitmap_git, midx)) ret = 0; } @@ -3399,7 +3400,8 @@ int verify_bitmap_files(struct repository *r) odb_prepare_alternates(r->objects); for (source = r->objects->sources; source; source = source->next) { - struct multi_pack_index *m = get_multi_pack_index(source); + struct odb_source_files *files = odb_source_files_downcast(source); + struct multi_pack_index *m = get_multi_pack_index(files->packed); char *midx_bitmap_name; if (!m) diff --git a/pack-revindex.c b/pack-revindex.c index 1b67863606a75f..62387ae6320181 100644 --- a/pack-revindex.c +++ b/pack-revindex.c @@ -383,13 +383,13 @@ int load_midx_revindex(struct multi_pack_index *m) * not want to accidentally call munmap() in the middle of the * MIDX. */ - trace2_data_string("load_midx_revindex", m->source->odb->repo, + trace2_data_string("load_midx_revindex", m->source->base.odb->repo, "source", "midx"); m->revindex_data = (const uint32_t *)m->chunk_revindex; return 0; } - trace2_data_string("load_midx_revindex", m->source->odb->repo, + trace2_data_string("load_midx_revindex", m->source->base.odb->repo, "source", "rev"); if (m->has_chain) @@ -401,7 +401,7 @@ int load_midx_revindex(struct multi_pack_index *m) midx_get_checksum_hash(m), MIDX_EXT_REV); - ret = load_revindex_from_disk(m->source->odb->repo->hash_algo, + ret = load_revindex_from_disk(m->source->base.odb->repo->hash_algo, revindex_name.buf, m->num_objects, &m->revindex_map, diff --git a/repack-geometry.c b/repack-geometry.c index 2064683dcfe1e1..15b34129506d2a 100644 --- a/repack-geometry.c +++ b/repack-geometry.c @@ -32,7 +32,8 @@ void pack_geometry_init(struct pack_geometry *geometry, { struct packed_git *p; struct strbuf buf = STRBUF_INIT; - struct multi_pack_index *m = get_multi_pack_index(existing->source); + struct odb_source_files *files = odb_source_files_downcast(existing->source); + struct multi_pack_index *m = get_multi_pack_index(files->packed); repo_for_each_pack(existing->repo, p) { if (geometry->midx_layer_threshold_set && m && diff --git a/repack-midx.c b/repack-midx.c index b6b1de718058da..7c7c3620e50b7d 100644 --- a/repack-midx.c +++ b/repack-midx.c @@ -557,13 +557,14 @@ static void repack_make_midx_append_plan(struct repack_write_midx_opts *opts, struct midx_compaction_step **steps_p, size_t *steps_nr_p) { + struct odb_source_files *files = odb_source_files_downcast(opts->existing->source); struct multi_pack_index *m; struct midx_compaction_step *steps = NULL; struct midx_compaction_step *step; size_t steps_nr = 0, steps_alloc = 0; odb_reprepare(opts->existing->repo->objects); - m = get_multi_pack_index(opts->existing->source); + m = get_multi_pack_index(files->packed); if (opts->names->nr) { struct strbuf buf = STRBUF_INIT; @@ -606,6 +607,7 @@ static int repack_make_midx_compaction_plan(struct repack_write_midx_opts *opts, struct midx_compaction_step **steps_p, size_t *steps_nr_p) { + struct odb_source_files *files = odb_source_files_downcast(opts->existing->source); struct multi_pack_index *m; struct midx_compaction_step *steps = NULL; struct midx_compaction_step step = { 0 }; @@ -618,7 +620,7 @@ static int repack_make_midx_compaction_plan(struct repack_write_midx_opts *opts, opts->existing->repo); odb_reprepare(opts->existing->repo->objects); - m = get_multi_pack_index(opts->existing->source); + m = get_multi_pack_index(files->packed); for (i = 0; m && i < m->num_packs + m->num_packs_in_base; i++) { if (prepare_midx_pack(m, i)) { @@ -938,6 +940,7 @@ static int repack_make_midx_compaction_plan(struct repack_write_midx_opts *opts, static int write_midx_incremental(struct repack_write_midx_opts *opts) { + struct odb_source_files *files = odb_source_files_downcast(opts->existing->source); struct midx_compaction_step *steps = NULL; struct strbuf lock_name = STRBUF_INIT; struct lock_file lf; @@ -946,7 +949,7 @@ static int write_midx_incremental(struct repack_write_midx_opts *opts) size_t i; int ret = 0; - get_midx_chain_filename(opts->existing->source, &lock_name); + get_midx_chain_filename(files->packed, &lock_name); if (safe_create_leading_directories(opts->existing->repo, lock_name.buf)) die_errno(_("unable to create leading directories of %s"), diff --git a/repack.c b/repack.c index 571dabb665ee9b..d2aa58e13484fd 100644 --- a/repack.c +++ b/repack.c @@ -59,10 +59,10 @@ void repack_remove_redundant_pack(struct repository *repo, const char *dir_name, bool wrote_incremental_midx) { struct strbuf buf = STRBUF_INIT; - struct odb_source *source = repo->objects->sources; - struct multi_pack_index *m = get_multi_pack_index(source); + struct odb_source_files *files = odb_source_files_downcast(repo->objects->sources); + struct multi_pack_index *m = get_multi_pack_index(files->packed); strbuf_addf(&buf, "%s.pack", base_name); - if (m && source->local && midx_contains_pack(m, buf.buf)) { + if (m && files->base.local && midx_contains_pack(m, buf.buf)) { clear_midx_file(repo); if (!wrote_incremental_midx) clear_incremental_midx_files(repo, NULL); diff --git a/t/helper/test-read-midx.c b/t/helper/test-read-midx.c index 790000fb26c270..fb16ec0176d7b4 100644 --- a/t/helper/test-read-midx.c +++ b/t/helper/test-read-midx.c @@ -13,13 +13,16 @@ static struct multi_pack_index *setup_midx(const char *object_dir) { + struct odb_source_files *files; struct odb_source *source; setup_git_directory(the_repository); source = odb_find_source(the_repository->objects, object_dir); if (!source) source = odb_add_to_alternates_memory(the_repository->objects, object_dir); - return load_multi_pack_index(source); + files = odb_source_files_downcast(source); + + return load_multi_pack_index(files->packed); } static int read_midx_file(const char *object_dir, const char *checksum, @@ -70,7 +73,7 @@ static int read_midx_file(const char *object_dir, const char *checksum, for (i = 0; i < m->num_packs; i++) printf("%s\n", m->pack_names[i]); - printf("object-dir: %s\n", m->source->path); + printf("object-dir: %s\n", m->source->base.path); if (show_objects) { struct object_id oid; From 1bba3c035d2283456edcb159e965b8be2015e114 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 08:40:00 +0200 Subject: [PATCH 51/95] odb/source-packed: drop pointer to "files" parent source Over the last commits we have turned the packfile store into a proper object database source that can be used as a standalone backend. As such, it is no longer necessary to have it coupled to the "files" parent source. Remove the pointer to the owning "files" source so that the "packed" source can be used as a standalone entity. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-files.c | 2 +- odb/source-packed.c | 27 +++++++++++++-------------- odb/source-packed.h | 7 ++++--- packfile.c | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/odb/source-files.c b/odb/source-files.c index fa2e18e71b3cf6..3bc6419dd7e2f9 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -269,7 +269,7 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb, CALLOC_ARRAY(files, 1); odb_source_init(&files->base, odb, ODB_SOURCE_FILES, path, local); files->loose = odb_source_loose_new(odb, path, local); - files->packed = odb_source_packed_new(files); + files->packed = odb_source_packed_new(odb, path, local); files->base.free = odb_source_files_free; files->base.close = odb_source_files_close; diff --git a/odb/source-packed.c b/odb/source-packed.c index d513b3efc367fd..42c28fba0e34b2 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -585,7 +585,7 @@ static void report_pack_garbage(struct string_list *list) } struct prepare_pack_data { - struct odb_source *source; + struct odb_source_packed *source; struct string_list *garbage; }; @@ -593,15 +593,14 @@ static void prepare_pack(const char *full_name, size_t full_name_len, const char *file_name, void *_data) { struct prepare_pack_data *data = (struct prepare_pack_data *)_data; - struct odb_source_files *files = odb_source_files_downcast(data->source); size_t base_len = full_name_len; if (strip_suffix_mem(full_name, &base_len, ".idx") && - !(files->packed->midx && - midx_contains_pack(files->packed->midx, file_name))) { + !(data->source->midx && + midx_contains_pack(data->source->midx, file_name))) { char *trimmed_path = xstrndup(full_name, full_name_len); - packfile_store_load_pack(files->packed, - trimmed_path, data->source->local); + packfile_store_load_pack(data->source, + trimmed_path, data->source->base.local); free(trimmed_path); } @@ -626,7 +625,7 @@ static void prepare_pack(const char *full_name, size_t full_name_len, report_garbage(PACKDIR_FILE_GARBAGE, full_name); } -static void prepare_packed_git_one(struct odb_source *source) +static void prepare_packed_git_one(struct odb_source_packed *source) { struct string_list garbage = STRING_LIST_INIT_DUP; struct prepare_pack_data data = { @@ -634,7 +633,7 @@ static void prepare_packed_git_one(struct odb_source *source) .garbage = &garbage, }; - for_each_file_in_pack_dir(source->path, prepare_pack, &data); + for_each_file_in_pack_dir(source->base.path, prepare_pack, &data); report_pack_garbage(data.garbage); string_list_clear(data.garbage, 0); @@ -675,7 +674,7 @@ void odb_source_packed_prepare(struct odb_source_packed *source) return; prepare_multi_pack_index_one(source); - prepare_packed_git_one(&source->files->base); + prepare_packed_git_one(source); sort_packs(&source->packs.head, sort_pack); for (struct packfile_list_entry *e = source->packs.head; e; e = e->next) @@ -733,14 +732,14 @@ static void odb_source_packed_free(struct odb_source *source) free(packed); } -struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent) +struct odb_source_packed *odb_source_packed_new(struct object_database *odb, + const char *path, + bool local) { struct odb_source_packed *packed; CALLOC_ARRAY(packed, 1); - odb_source_init(&packed->base, parent->base.odb, ODB_SOURCE_PACKED, - parent->base.path, parent->base.local); - packed->files = parent; + odb_source_init(&packed->base, odb, ODB_SOURCE_PACKED, path, local); strmap_init(&packed->packs_by_path); packed->base.free = odb_source_packed_free; @@ -758,7 +757,7 @@ struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent) packed->base.read_alternates = odb_source_packed_read_alternates; packed->base.write_alternate = odb_source_packed_write_alternate; - if (!is_absolute_path(parent->base.path)) + if (!is_absolute_path(path)) chdir_notify_register(NULL, odb_source_packed_reparent, packed); return packed; diff --git a/odb/source-packed.h b/odb/source-packed.h index 9d4796261a72d3..88994098c18e7d 100644 --- a/odb/source-packed.h +++ b/odb/source-packed.h @@ -10,7 +10,6 @@ */ struct odb_source_packed { struct odb_source base; - struct odb_source_files *files; /* * The list of packfiles in the order in which they have been most @@ -66,9 +65,11 @@ struct odb_source_packed { /* * Allocate and initialize a new empty packfile store for the given object - * database source. + * database. */ -struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent); +struct odb_source_packed *odb_source_packed_new(struct object_database *odb, + const char *path, + bool local); /* * Cast the given object database source to the packed backend. This will cause diff --git a/packfile.c b/packfile.c index a577275d4f334e..59cee7925daf0b 100644 --- a/packfile.c +++ b/packfile.c @@ -801,7 +801,7 @@ struct packed_git *packfile_store_load_pack(struct odb_source_packed *store, p = strmap_get(&store->packs_by_path, key.buf); if (!p) { - p = add_packed_git(store->files->base.odb->repo, idx_path, + p = add_packed_git(store->base.odb->repo, idx_path, strlen(idx_path), local); if (p) packfile_store_add_pack(store, p); From d0c6ecea230aed04ad703502b43c9b313ccfc95d Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Fri, 19 Jun 2026 07:44:50 +0200 Subject: [PATCH 52/95] SubmittingPatches: encourage trailer use for substantial help MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trailers beyond the mandatory s-o-b are regularly used based on my last two years of reading the mailing list. Moreover, reviewers might encourage it.[1] This is also in line with the project crediting both commit authors and people mentioned in trailers each release; “Nobody is THE one making contribution”.[2] Adding trailers is already encouraged, but in the section `send-patches`. Let’s replace “If you like” with outright encouragement in this section so that all trailer discussion (except s-o-b; see `sign-off` section) is contained in this section; a link to from `send-patches` makes this information equally visible. Now we need to make a heading for `commit-trailers` in order for the HTML output to make sense. At the same time, it is important to temper this recommendation to a significant enough contribution; in my experience beginners can be eager to add a trailer for everyone who replies with an action point that is followed up on. Let’s also spell out that these trailers should follow the Git author/ committer format. One might naturally just write the name, but in that case it will not be picked up by: git shortlog --group=trailer: and normalization via `.mailmap` will not work. Also introduce the list of common trailers as such. Granted, this is already implied by the later paragraph about “create your own trailer”, so this just frontloads this information. † 1: https://lore.kernel.org/git/CAP8UFD0POvYDgGtEx8GBhvKkd8XzzWQsy8XxAKL9M3+uz3ka+w@mail.gmail.com/#:~:text=for%20at%20least † 2: https://lore.kernel.org/git/xmqqzh248sy0.fsf@gitster.c.googlers.com/ Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/SubmittingPatches | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index 176567738d47df..4e8dea4eaa6080 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -444,7 +444,15 @@ The goal of this policy is to allow us to have sufficient information to contact you if questions arise about your contribution. [[commit-trailers]] -If you like, you can put extra trailers at the end: +=== Commit trailers +It is polite to credit people who have helped with your work to a +substantial enough degree. This project uses commit trailers for that, +where the credited person is written out like a Git author, i.e. with +both their name and their email address. Note that the threshold to +credit someone is a judgement call, and crediting someone for simple +review work is certainly not necessary. + +These are the common trailers in use: . `Reported-by:` is used to credit someone who found the bug that the patch attempts to fix. @@ -562,8 +570,8 @@ when the maintainer did not heavily participate in the discussion and instead left the review to trusted others. Do not forget to add trailers such as `Acked-by:`, `Reviewed-by:` and -`Tested-by:` lines as necessary to credit people who helped your -patch, and "cc:" them when sending such a final version for inclusion. +`Tested-by:` (see <>), and "cc:" them +when sending such a final version for inclusion. ==== `format-patch` and `send-email` From b7f68ff5c92e40157da2afeecba13bf6f4fdd6db Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Fri, 19 Jun 2026 07:44:51 +0200 Subject: [PATCH 53/95] SubmittingPatches: discourage common Linux trailers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Linux Kernel regularly uses trailers (or “tags”) `Fixes` and `Link`. Sometimes people submit patches to this project with them. They have their use in that project but it is not clear what purpose they would serve here. For `Fixes`: Linux has many trees, and applying patches with cherry-picks is common. A `Fixes` trailer in commit C2 pointing to commit C1 helps the cherry-picker figure out that she probably needs C2 if she wants to apply C1. See linux/d5d6281a (checkpatch: check for missing Fixes tags, 2024-06-11):[1] Why are stable patches encouraged to have a fixes tag? Some people mark their stable patches as "# 5.10" etc. This is useful but a Fixes tag is still a good idea. For example, the Fixes tag helps in review. It helps people to not cherry-pick buggy patches without also cherry-picking the fix. In contrast the Git project has few trees (to my knowledge), and there is much less need to cherry-pick fixes as opposed to either using backmerges or rebasing all of the downstream tree’s commits on top of git.git `master` from time to time. This project does regularly mention what commits a patch/commit fixes, but that is done inline in the commit message proper (cf. the trailer block of the message). For `Link`: These are used both to link back to the patch submission as well as with footnotes. In contrast this project has `refs/notes/amlog` for linking back to the patch submissions, and footnotes are only used in the commit message proper. † 1: Commit linux/d5d6281a has “linux” in front of it since this commit is from the Linux Kernel, not Git. Example of a Linux tree—as well as an example of `Link`—is [2]. Link: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/ [2] Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/SubmittingPatches | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index 4e8dea4eaa6080..8d946e9acb3143 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -476,6 +476,10 @@ While you can also create your own trailer if the situation warrants it, we encourage you to instead use one of the common trailers in this project highlighted above. +Other projects might regularly refer to other kinds of data, like +`Fixes:` and `Link:` in the Linux Kernel project, but these ones in +particular are not used in this project. + Only capitalize the very first letter of the trailer, i.e. favor "Signed-off-by" over "Signed-Off-By" and "Acked-by:" over "Acked-By". From 6d2665156748bde12274cfb3aea24bca68d7ac0c Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Fri, 19 Jun 2026 07:44:52 +0200 Subject: [PATCH 54/95] SubmittingPatches: document Based-on-patch-by trailer This trailer comes up often enough and the use case is not fully covered by the other trailers here. For example, it is sometimes better to use this trailer instead of `Co-authored-by:`. Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/SubmittingPatches | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index 8d946e9acb3143..5b4ab93543c86d 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -465,6 +465,10 @@ These are the common trailers in use: and found it to have the desired effect. . `Co-authored-by:` is used to indicate that people exchanged drafts of a patch before submitting it. +. `Based-on-patch-by:` is used when someone else authored parts of the + patch that you are submitting. This might be relevant if someone sent + a patch to the mailing list with their sign-off. (Be mindful and ask + them to sign off on it if they did not.) . `Helped-by:` is used to credit someone who suggested ideas for changes without providing the precise changes in patch form. . `Mentored-by:` is used to credit someone with helping develop a From 89d86442441d354abddf53d06d440bc06500c738 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Fri, 19 Jun 2026 07:44:53 +0200 Subject: [PATCH 55/95] SubmittingPatches: be consistent with trailer markup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rest of this section and (most importantly) the list has decided to use `:`. So let’s use backticks (`) and a colon (:) throughout the document. Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/SubmittingPatches | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index 5b4ab93543c86d..125bc0a2d63566 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -374,7 +374,7 @@ or, on an older version of Git without support for --pretty=reference: .... [[sign-off]] -=== Certify your work by adding your `Signed-off-by` trailer +=== Certify your work by adding your `Signed-off-by:` trailer To improve tracking of who did what, we ask you to certify that you wrote the patch or have the right to pass it on under the same license @@ -411,7 +411,7 @@ d. I understand and agree that this project and the contribution this project or the open source license(s) involved. ____ -you add a "Signed-off-by" trailer to your commit, that looks like +you add a `Signed-off-by:` trailer to your commit, that looks like this: .... @@ -421,7 +421,7 @@ this: This line can be added by Git if you run the git-commit command with the -s option. -Notice that you can place your own `Signed-off-by` trailer when +Notice that you can place your own `Signed-off-by:` trailer when forwarding somebody else's patch with the above rules for D-C-O. Indeed you are encouraged to do so. Do not forget to place an in-body "From: " line at the beginning to properly attribute @@ -433,7 +433,7 @@ your patch differs from project to project, so it may be different from that of the project you are accustomed to. [[real-name]] -Please use a known identity in the `Signed-off-by` trailer, since we cannot +Please use a known identity in the `Signed-off-by:` trailer, since we cannot accept anonymous contributions. It is common, but not required, to use some form of your real name. We realize that some contributors are not comfortable doing so or prefer to contribute under a pseudonym or preferred name and we can accept @@ -485,7 +485,7 @@ Other projects might regularly refer to other kinds of data, like particular are not used in this project. Only capitalize the very first letter of the trailer, i.e. favor -"Signed-off-by" over "Signed-Off-By" and "Acked-by:" over "Acked-By". +`Signed-off-by:` over `Signed-Off-By:` and `Acked-by:` over `Acked-By:`. [[ai]] === Use of Artificial Intelligence (AI) @@ -607,7 +607,7 @@ Here is a link:MyFirstContribution.html#v2-git-send-email[step-by-step guide] on how to submit updated versions of a patch series. If your log message (including your name on the -`Signed-off-by` trailer) is not writable in ASCII, make sure that +`Signed-off-by:` trailer) is not writable in ASCII, make sure that you send off a message in the correct encoding. WARNING: Be wary of your MUAs word-wrap @@ -627,7 +627,7 @@ previously sent. The `git format-patch` command follows the best current practice to format the body of an e-mail message. At the beginning of the patch should come your commit message, ending with the -`Signed-off-by` trailers, and a line that consists of three dashes, +`Signed-off-by:` trailers, and a line that consists of three dashes, followed by the diffstat information and the patch itself. If you are forwarding a patch from somebody else, optionally, at the beginning of the e-mail message just before the commit From 55113123f5a830e5d4f1b728668a90a0e6ca7193 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Fri, 19 Jun 2026 07:44:54 +0200 Subject: [PATCH 56/95] SubmittingPatches: note that trailer order matters It matters where you put new trailers: they should be added in chronological order, and each person who passes on a patch should add their s-o-b last. You are signing off on the patch as well as the whole message up to that point. This also makes it clear who added what: Acked-by: The Reviewer Signed-off-by: The Contributor Acked-by: The (Late) Reviewer Signed-off-by: The Maintainer The first ack was added by the contributor and the second one was added by the maintainer. Helped-by: Junio C Hamano Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/SubmittingPatches | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index 125bc0a2d63566..56706e55ea16b8 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -427,6 +427,10 @@ D-C-O. Indeed you are encouraged to do so. Do not forget to place an in-body "From: " line at the beginning to properly attribute the change to its true author (see (2) above). +Place this `Signed-off-by:` trailer at the end, after trailers added by +others and after other trailers added by you; see +<> below ("chronological order"). + This procedure originally came from the Linux kernel project, so our rule is quite similar to theirs, but what exactly it means to sign-off your patch differs from project to project, so it may be different @@ -487,6 +491,12 @@ particular are not used in this project. Only capitalize the very first letter of the trailer, i.e. favor `Signed-off-by:` over `Signed-Off-By:` and `Acked-by:` over `Acked-By:`. +As mentioned under <> above, trailers are added in +chronological order; one person might sign-off on a patch and send it to +someone else, who then in turn adds her own sign-off. Further, any +trailers that you add beyond your sign-off should come before that +sign-off. That makes it clear what trailers which person added. + [[ai]] === Use of Artificial Intelligence (AI) From 3e09c9e8207eb425ce45863da6bbfe11cd49a765 Mon Sep 17 00:00:00 2001 From: Matt Hunter Date: Fri, 19 Jun 2026 05:44:20 -0400 Subject: [PATCH 57/95] fetch: fixup set_head advice for warn-if-not-branch Specifying the word 'branch' in the command is not correct - a mismatch with both the implementation in remote.c and the documentation. Signed-off-by: Matt Hunter 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 c1d7c672f4e0d8..82969e230f5adb 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -1700,7 +1700,7 @@ static void set_head_advice_msg(const char *remote, const char *head_name) N_("Run 'git remote set-head %s %s' to follow the change, or set\n" "'remote.%s.followRemoteHEAD' configuration option to a different value\n" "if you do not want to see this message. Specifically running\n" - "'git config set remote.%s.followRemoteHEAD warn-if-not-branch-%s'\n" + "'git config set remote.%s.followRemoteHEAD warn-if-not-%s'\n" "will disable the warning until the remote changes HEAD to something else."); advise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head), From 54c9d481011b095561099393ac3ef4828de4a329 Mon Sep 17 00:00:00 2001 From: Matt Hunter Date: Fri, 19 Jun 2026 05:44:21 -0400 Subject: [PATCH 58/95] doc: explain fetchRemoteHEADWarn advice When the user sets 'remote..followRemoteHEAD' to 'warn[-if-not-$branch]', git-fetch will report when a fetched HEAD disagrees with the locally-configured remote's HEAD. This additional advice instructs the user how to deal with these warnings, but was previously undocumented in git-config. Signed-off-by: Matt Hunter Signed-off-by: Junio C Hamano --- Documentation/config/advice.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc index 257db58918179a..c3c190ba6a4fb8 100644 --- a/Documentation/config/advice.adoc +++ b/Documentation/config/advice.adoc @@ -48,6 +48,10 @@ all advice messages. to create a local branch after the fact. diverging:: Shown when a fast-forward is not possible. + fetchRemoteHEADWarn:: + Shown when linkgit:git-fetch[1] reveals that a remote `HEAD` + differs from what is set locally and the user has opted into + receiving a warning in this situation. fetchShowForcedUpdates:: Shown when linkgit:git-fetch[1] takes a long time to calculate forced updates after ref updates, or to warn From f8a7bbf09d4d8b4f29f689afd77e7c75b0f0cb37 Mon Sep 17 00:00:00 2001 From: Matt Hunter Date: Fri, 19 Jun 2026 05:44:22 -0400 Subject: [PATCH 59/95] t5510: cleanup remote in followRemoteHEAD dangling ref test A later patch will introduce a new test which closely mirrors this one. Update this test to remove the 'custom-head' remote it creates. Otherwise, the two tests will conflict with each other, as the second one to execute will fail to create this remote (which already exists, thanks to the first test). Signed-off-by: Matt Hunter 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 eca9a973b5cb16..43190630e714d6 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -251,6 +251,7 @@ test_expect_success 'followRemoteHEAD does not kick in with refspecs' ' ' test_expect_success 'followRemoteHEAD create does not overwrite dangling symref' ' + test_when_finished "git -C two remote remove custom-head" && git -C two remote add -m does-not-exist custom-head ../one && test_config -C two remote.custom-head.followRemoteHEAD create && git -C two fetch custom-head && From b4154d85de799e8bcebae9850bb892da59b78e86 Mon Sep 17 00:00:00 2001 From: Matt Hunter Date: Fri, 19 Jun 2026 05:44:23 -0400 Subject: [PATCH 60/95] fetch: rename function report_set_head Update to the slightly more obvious name 'warn_set_head', which matches the verbiage of the followRemoteHEAD options. Signed-off-by: Matt Hunter Signed-off-by: Junio C Hamano --- builtin/fetch.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/fetch.c b/builtin/fetch.c index 82969e230f5adb..9a45e1e7a44de4 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -1707,7 +1707,7 @@ static void set_head_advice_msg(const char *remote, const char *head_name) remote, head_name, remote, remote, head_name); } -static void report_set_head(const char *remote, const char *head_name, +static void warn_set_head(const char *remote, const char *head_name, struct strbuf *buf_prev, int updateres) { struct strbuf buf_prefix = STRBUF_INIT; const char *prev_head = NULL; @@ -1787,7 +1787,7 @@ static int set_head(const struct ref *remote_refs, struct remote *remote) if (verbosity >= 0 && follow_remote_head == FOLLOW_REMOTE_WARN && (!no_warn_branch || strcmp(no_warn_branch, head_name))) - report_set_head(remote->name, head_name, &b_local_head, was_detached); + warn_set_head(remote->name, head_name, &b_local_head, was_detached); cleanup: free(head_name); From c6ac26e0137c0443c5840d6485216a60e3d3c5a2 Mon Sep 17 00:00:00 2001 From: Matt Hunter Date: Fri, 19 Jun 2026 05:44:24 -0400 Subject: [PATCH 61/95] fetch: return 0 on known git_fetch_config The git config callback for git-fetch should only forward calls to git_default_config when an unknown key is given. Prevent this in the case of 'fetch.output' by returning '0', as the other known keys do. Signed-off-by: Matt Hunter Signed-off-by: Junio C Hamano --- builtin/fetch.c | 1 + 1 file changed, 1 insertion(+) diff --git a/builtin/fetch.c b/builtin/fetch.c index 9a45e1e7a44de4..1036e8edbc59ae 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -171,6 +171,7 @@ static int git_fetch_config(const char *k, const char *v, else die(_("invalid value for '%s': '%s'"), "fetch.output", v); + return 0; } return git_default_config(k, v, ctx, cb); From 85ef88dc5511750218588dcc56b76b1b4ab67e7d Mon Sep 17 00:00:00 2001 From: Matt Hunter Date: Fri, 19 Jun 2026 05:44:25 -0400 Subject: [PATCH 62/95] fetch: refactor do_fetch handling of followRemoteHEAD Update enum follow_remote_head_settings to include the value FOLLOW_REMOTE_UNCONFIGURED as the new zero-initialized value for followRemoteHEAD. This will allow us to distinguish between the variable being unset vs. explicitly set to 'create', which is ultimately the system default. The unnecessary indentation is removed. The do_fetch function is likewise updated to perform its own decision making to determine the effective followRemoteHEAD mode, falling back to the system default if necessary. This will enable the next patch to introduce a user-configurable default. Function set_head now accepts the mode as an argument rather than only considering the value defined by the remote. The use of the 'warn-if-not-$branch' value is awkward in the context of a global default, since the branches will differ between individual remotes. For this reason, it's left out of this scheme and handling of the no_warn_branch variable is untouched. Since a remote-specific value for followRemoteHEAD takes priority, we can assume that if remote->no_warn_branch is set, then the remote is also asserting FOLLOW_REMOTE_WARN as the effective operating mode, and it will be honored by do_fetch. Signed-off-by: Matt Hunter Signed-off-by: Junio C Hamano --- builtin/fetch.c | 14 ++++++++++---- remote.h | 14 ++++++++------ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/builtin/fetch.c b/builtin/fetch.c index 1036e8edbc59ae..ad63ca943c33d7 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -1730,12 +1730,12 @@ static void warn_set_head(const char *remote, const char *head_name, strbuf_release(&buf_prefix); } -static int set_head(const struct ref *remote_refs, struct remote *remote) +static int set_head(const struct ref *remote_refs, struct remote *remote, + int follow_remote_head) { int result = 0, create_only, baremirror, was_detached; struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT, b_local_head = STRBUF_INIT; - int follow_remote_head = remote->follow_remote_head; const char *no_warn_branch = remote->no_warn_branch; char *head_name = NULL; struct ref *ref, *matches; @@ -1902,6 +1902,7 @@ static int do_fetch(struct transport *transport, struct ref_update_display_info_array display_array = { 0 }; struct strmap rejected_refs = STRMAP_INIT; int summary_width = 0; + int follow_remote_head; if (tags == TAGS_DEFAULT) { if (transport->remote->fetch_tags == 2) @@ -1917,6 +1918,11 @@ static int do_fetch(struct transport *transport, goto cleanup; } + if (transport->remote->follow_remote_head) + follow_remote_head = transport->remote->follow_remote_head; + else + follow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT; + if (rs->nr) { refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes); } else { @@ -1925,7 +1931,7 @@ static int do_fetch(struct transport *transport, if (transport->remote->fetch.nr) { refspec_ref_prefixes(&transport->remote->fetch, &transport_ls_refs_options.ref_prefixes); - if (transport->remote->follow_remote_head != FOLLOW_REMOTE_NEVER) + if (follow_remote_head != FOLLOW_REMOTE_NEVER) do_set_head = 1; } if (branch && branch_has_merge_config(branch) && @@ -2132,7 +2138,7 @@ static int do_fetch(struct transport *transport, * Way too many cases where this can go wrong so let's just * ignore errors and fail silently for now. */ - set_head(remote_refs, transport->remote); + set_head(remote_refs, transport->remote, follow_remote_head); } cleanup: diff --git a/remote.h b/remote.h index 54b17e4b028b5b..72a54d84ad511d 100644 --- a/remote.h +++ b/remote.h @@ -62,12 +62,14 @@ struct remote_state { void remote_state_clear(struct remote_state *remote_state); struct remote_state *remote_state_new(void); - enum follow_remote_head_settings { - FOLLOW_REMOTE_NEVER = -1, - FOLLOW_REMOTE_CREATE = 0, - FOLLOW_REMOTE_WARN = 1, - FOLLOW_REMOTE_ALWAYS = 2, - }; +#define BUILTIN_FOLLOW_REMOTE_HEAD_DFLT FOLLOW_REMOTE_CREATE +enum follow_remote_head_settings { + FOLLOW_REMOTE_UNCONFIGURED = 0, + FOLLOW_REMOTE_NEVER, + FOLLOW_REMOTE_CREATE, + FOLLOW_REMOTE_WARN, + FOLLOW_REMOTE_ALWAYS, +}; struct remote { struct hashmap_entry ent; From 7d00999b10579708fc3c2512cb0a55eff4e7565f Mon Sep 17 00:00:00 2001 From: Matt Hunter Date: Fri, 19 Jun 2026 05:44:26 -0400 Subject: [PATCH 63/95] fetch: add configuration variable fetch.followRemoteHEAD 'fetch.followRemoteHEAD' is added as a generic setting used by all remotes for which 'remote..followRemoteHEAD' is undefined. If both variables are undefined, a builtin default of "create" is in effect, matching the previous behavior. As mentioned in the previous patch, 'fetch.followRemoteHEAD' supports all of the values that its 'remote' counterpart does _except_ warn-if-not-$branch, due to its tighter coupling to individual remote repositories. This setting interacts with the do_fetch mechanism in the same way as the previous does, but there are opportunities for improved user-experience discussed in [1]. See the included NEEDSWORK comment as well. Documentation and advice messages for both of the followRemoteHEAD variables are reworded to better capture the relationship between the two. The added tests assert feature parity between the two followRemoteHEAD variables, as well as the fact that 'remote..followRemoteHEAD' always supersedes this new configurable default. [1]: https://lore.kernel.org/git/xmqqh5n213bw.fsf@gitster.g/ Helped-by: Junio C Hamano Signed-off-by: Matt Hunter Signed-off-by: Junio C Hamano --- Documentation/config/fetch.adoc | 19 ++++++ Documentation/config/remote.adoc | 21 +++---- builtin/fetch.c | 41 ++++++++++-- t/t5510-fetch.sh | 105 +++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 17 deletions(-) diff --git a/Documentation/config/fetch.adoc b/Documentation/config/fetch.adoc index 04ac90912d3a58..00435e9a16d9f9 100644 --- a/Documentation/config/fetch.adoc +++ b/Documentation/config/fetch.adoc @@ -126,3 +126,22 @@ the new bundle URI. The creation token values are chosen by the provider serving the specific bundle URI. If you modify the URI at `fetch.bundleURI`, then be sure to remove the value for the `fetch.bundleCreationToken` value before fetching. + +`fetch.followRemoteHEAD`:: + When fetching using a default refspec, this setting determines how to handle + differences between a fetched remote's `HEAD` and the local + `remotes//HEAD` symbolic-ref. Its value is one of ++ +-- +`create`;; + Create `remotes//HEAD` if a ref exists on the remote, but not locally. + An existing symbolic-ref will not be touched. This is the default value. +`warn`;; + Display a warning if the remote advertises a different `HEAD` than what is + set locally. Behaves like "create" if the local symbolic-ref doesn't exist. +`always`;; + Silently update `remotes//HEAD` whenever the remote advertises a new + value. +`never`;; + Never create or modify the `remotes//HEAD` symbolic-ref. +-- diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc index eb9c8a3c488448..04724bc5162883 100644 --- a/Documentation/config/remote.adoc +++ b/Documentation/config/remote.adoc @@ -157,15 +157,12 @@ Blank values signal to ignore all previous values, allowing a reset of the list from broader config scenarios. remote..followRemoteHEAD:: - How linkgit:git-fetch[1] should handle updates to `remotes//HEAD` - when fetching using the configured refspecs of a remote. - The default value is "create", which will create `remotes//HEAD` - if it exists on the remote, but not locally; this will not touch an - already existing local reference. Setting it to "warn" will print - a message if the remote has a different value than the local one; - in case there is no local reference, it behaves like "create". - A variant on "warn" is "warn-if-not-$branch", which behaves like - "warn", but if `HEAD` on the remote is `$branch` it will be silent. - Setting it to "always" will silently update `remotes//HEAD` to - the value on the remote. Finally, setting it to "never" will never - change or create the local reference. + When fetching this remote using its default refspec, this setting determines + how to handle differences between the remote's `HEAD` and the local + `remotes//HEAD` symbolic-ref. Overrides the value of + `fetch.followRemoteHEAD`. See `fetch.followRemoteHEAD` for a description of + accepted values. ++ +In addition to the values supported by `fetch.followRemoteHEAD`, this setting +may also take on the value "warn-if-not-`$branch`", which behaves like "warn", +but ignores the warning if the remote's `HEAD` is `remotes//$branch`. diff --git a/builtin/fetch.c b/builtin/fetch.c index ad63ca943c33d7..3c8210d1776f27 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -103,6 +103,7 @@ static struct string_list negotiation_include = STRING_LIST_INIT_NODUP; struct fetch_config { enum display_format display_format; + enum follow_remote_head_settings follow_remote_head; int all; int prune; int prune_tags; @@ -174,6 +175,22 @@ static int git_fetch_config(const char *k, const char *v, return 0; } + if (!strcmp(k, "fetch.followremotehead")) { + if (!v) + return config_error_nonbool(k); + else if (!strcmp(v, "never")) + fetch_config->follow_remote_head = FOLLOW_REMOTE_NEVER; + else if (!strcmp(v, "create")) + fetch_config->follow_remote_head = FOLLOW_REMOTE_CREATE; + else if (!strcmp(v, "warn")) + fetch_config->follow_remote_head = FOLLOW_REMOTE_WARN; + else if (!strcmp(v, "always")) + fetch_config->follow_remote_head = FOLLOW_REMOTE_ALWAYS; + else + warning(_("unrecognized fetch.followRemoteHEAD value '%s' ignored"), v); + return 0; + } + return git_default_config(k, v, ctx, cb); } @@ -1698,11 +1715,13 @@ static const char *strip_refshead(const char *name){ static void set_head_advice_msg(const char *remote, const char *head_name) { const char message_advice_set_head[] = - N_("Run 'git remote set-head %s %s' to follow the change, or set\n" - "'remote.%s.followRemoteHEAD' configuration option to a different value\n" - "if you do not want to see this message. Specifically running\n" - "'git config set remote.%s.followRemoteHEAD warn-if-not-%s'\n" - "will disable the warning until the remote changes HEAD to something else."); + N_("Run 'git remote set-head %s %s' to follow the change, or modify\n" + "either of the 'remote.%s.followRemoteHEAD' or 'fetch.followRemoteHEAD'\n" + "configuration variables to handle the situation differently.\n\n" + + "Using this specific setting\n\n" + " git config set remote.%s.followRemoteHEAD warn-if-not-%s\n\n" + "will suppress the warning until the remote changes HEAD to something else."); advise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head), remote, head_name, remote, remote, head_name); @@ -1918,8 +1937,19 @@ static int do_fetch(struct transport *transport, goto cleanup; } + /* + * NEEDSWORK: By the time this function executes, we have already parsed + * all such followRemoteHEAD values from the external configuration, + * potentially emitting warning messages for bogus values. Ideally, if + * this fetch ends up not needing to consult these values, then git would + * not ever output a value warning. (eg: when pulling from a URL directly - + * rather than a configured remote, or when a remote's followRemoteHEAD + * overrides the fallback fetch setting) + */ if (transport->remote->follow_remote_head) follow_remote_head = transport->remote->follow_remote_head; + else if (config->follow_remote_head) + follow_remote_head = config->follow_remote_head; else follow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT; @@ -2478,6 +2508,7 @@ int cmd_fetch(int argc, { struct fetch_config config = { .display_format = DISPLAY_FORMAT_FULL, + .follow_remote_head = FOLLOW_REMOTE_UNCONFIGURED, .prune = -1, .prune_tags = -1, .show_forced_updates = 1, diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index 43190630e714d6..6f0ae1bdd7981b 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -140,6 +140,16 @@ test_expect_success "fetch test remote HEAD change" ' ) ' +test_expect_success "fetch test default followRemoteHEAD never" ' + git -C two update-ref --no-deref -d refs/remotes/origin/HEAD && + test_config -C two fetch.followRemoteHEAD "never" && + GIT_TRACE_PACKET=$PWD/trace.out git -C two fetch && + # Confirm that we do not even ask for HEAD when we are + # not going to act on it. + test_grep ! "ref-prefix HEAD" trace.out && + test_must_fail git -C two rev-parse --verify refs/remotes/origin/HEAD +' + test_expect_success "fetch test followRemoteHEAD never" ' git -C two update-ref --no-deref -d refs/remotes/origin/HEAD && test_config -C two remote.origin.followRemoteHEAD "never" && @@ -150,6 +160,21 @@ test_expect_success "fetch test followRemoteHEAD never" ' test_must_fail git -C two rev-parse --verify refs/remotes/origin/HEAD ' +test_expect_success "fetch test default followRemoteHEAD warn no change" ' + git -C two rev-parse --verify refs/remotes/origin/other && + git -C two remote set-head origin other && + git -C two rev-parse --verify refs/remotes/origin/HEAD && + git -C two rev-parse --verify refs/remotes/origin/main && + test_config -C two fetch.followRemoteHEAD "warn" && + git -C two fetch >output && + echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \ + "but we have ${SQ}other${SQ} locally." >expect && + test_cmp expect output && + head=$(git -C two rev-parse refs/remotes/origin/HEAD) && + branch=$(git -C two rev-parse refs/remotes/origin/other) && + test "z$head" = "z$branch" +' + test_expect_success "fetch test followRemoteHEAD warn no change" ' git -C two rev-parse --verify refs/remotes/origin/other && git -C two remote set-head origin other && @@ -165,6 +190,17 @@ test_expect_success "fetch test followRemoteHEAD warn no change" ' test "z$head" = "z$branch" ' +test_expect_success "fetch test default followRemoteHEAD warn create" ' + git -C two update-ref --no-deref -d refs/remotes/origin/HEAD && + test_config -C two fetch.followRemoteHEAD "warn" && + git -C two rev-parse --verify refs/remotes/origin/main && + output=$(git -C two fetch) && + test "z" = "z$output" && + head=$(git -C two rev-parse refs/remotes/origin/HEAD) && + branch=$(git -C two rev-parse refs/remotes/origin/main) && + test "z$head" = "z$branch" +' + test_expect_success "fetch test followRemoteHEAD warn create" ' git -C two update-ref --no-deref -d refs/remotes/origin/HEAD && test_config -C two remote.origin.followRemoteHEAD "warn" && @@ -176,6 +212,18 @@ test_expect_success "fetch test followRemoteHEAD warn create" ' test "z$head" = "z$branch" ' +test_expect_success "fetch test default followRemoteHEAD warn detached" ' + git -C two update-ref --no-deref -d refs/remotes/origin/HEAD && + git -C two update-ref refs/remotes/origin/HEAD HEAD && + HEAD=$(git -C two log --pretty="%H") && + test_config -C two fetch.followRemoteHEAD "warn" && + git -C two fetch >output && + echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \ + "but we have a detached HEAD pointing to" \ + "${SQ}${HEAD}${SQ} locally." >expect && + test_cmp expect output +' + test_expect_success "fetch test followRemoteHEAD warn detached" ' git -C two update-ref --no-deref -d refs/remotes/origin/HEAD && git -C two update-ref refs/remotes/origin/HEAD HEAD && @@ -188,6 +236,19 @@ test_expect_success "fetch test followRemoteHEAD warn detached" ' test_cmp expect output ' +test_expect_success "fetch test default followRemoteHEAD warn quiet" ' + git -C two rev-parse --verify refs/remotes/origin/other && + git -C two remote set-head origin other && + git -C two rev-parse --verify refs/remotes/origin/HEAD && + git -C two rev-parse --verify refs/remotes/origin/main && + test_config -C two fetch.followRemoteHEAD "warn" && + output=$(git -C two fetch --quiet) && + test "z" = "z$output" && + head=$(git -C two rev-parse refs/remotes/origin/HEAD) && + branch=$(git -C two rev-parse refs/remotes/origin/other) && + test "z$head" = "z$branch" +' + test_expect_success "fetch test followRemoteHEAD warn quiet" ' git -C two rev-parse --verify refs/remotes/origin/other && git -C two remote set-head origin other && @@ -229,6 +290,18 @@ test_expect_success "fetch test followRemoteHEAD warn-if-not-branch branch is di test "z$head" = "z$branch" ' +test_expect_success "fetch test default followRemoteHEAD always" ' + git -C two rev-parse --verify refs/remotes/origin/other && + git -C two remote set-head origin other && + git -C two rev-parse --verify refs/remotes/origin/HEAD && + git -C two rev-parse --verify refs/remotes/origin/main && + test_config -C two fetch.followRemoteHEAD "always" && + git -C two fetch && + head=$(git -C two rev-parse refs/remotes/origin/HEAD) && + branch=$(git -C two rev-parse refs/remotes/origin/main) && + test "z$head" = "z$branch" +' + test_expect_success "fetch test followRemoteHEAD always" ' git -C two rev-parse --verify refs/remotes/origin/other && git -C two remote set-head origin other && @@ -241,6 +314,28 @@ test_expect_success "fetch test followRemoteHEAD always" ' test "z$head" = "z$branch" ' +test_expect_success 'per-remote followRemoteHEAD takes priority over fetch default' ' + git -C two rev-parse --verify refs/remotes/origin/other && + git -C two remote set-head origin other && + git -C two rev-parse --verify refs/remotes/origin/HEAD && + git -C two rev-parse --verify refs/remotes/origin/main && + test_config -C two fetch.followRemoteHEAD "never" && + test_config -C two remote.origin.followRemoteHEAD "always" && + git -C two fetch && + head=$(git -C two rev-parse refs/remotes/origin/HEAD) && + branch=$(git -C two rev-parse refs/remotes/origin/main) && + test "z$head" = "z$branch" +' + +test_expect_success 'default followRemoteHEAD does not kick in with refspecs' ' + git -C two remote set-head origin other && + test_config -C two fetch.followRemoteHEAD always && + git -C two fetch origin refs/heads/main:refs/remotes/origin/main && + echo refs/remotes/origin/other >expect && + git -C two symbolic-ref refs/remotes/origin/HEAD >actual && + test_cmp expect actual +' + test_expect_success 'followRemoteHEAD does not kick in with refspecs' ' git -C two remote set-head origin other && test_config -C two remote.origin.followRemoteHEAD always && @@ -250,6 +345,16 @@ test_expect_success 'followRemoteHEAD does not kick in with refspecs' ' test_cmp expect actual ' +test_expect_success 'default followRemoteHEAD create does not overwrite dangling symref' ' + test_when_finished "git -C two remote remove custom-head" && + git -C two remote add -m does-not-exist custom-head ../one && + test_config -C two fetch.followRemoteHEAD create && + git -C two fetch custom-head && + echo refs/remotes/custom-head/does-not-exist >expect && + git -C two symbolic-ref refs/remotes/custom-head/HEAD >actual && + test_cmp expect actual +' + test_expect_success 'followRemoteHEAD create does not overwrite dangling symref' ' test_when_finished "git -C two remote remove custom-head" && git -C two remote add -m does-not-exist custom-head ../one && From 1a81aa364af949ea16127417926a7e1595ecc2b9 Mon Sep 17 00:00:00 2001 From: Matt Hunter Date: Fri, 19 Jun 2026 05:44:27 -0400 Subject: [PATCH 64/95] fetch: fixup a misaligned comment Signed-off-by: Matt Hunter 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 3c8210d1776f27..25ab8803a819d8 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -1793,7 +1793,7 @@ static int set_head(const struct ref *remote_refs, struct remote *remote, strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote->name); strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote->name, head_name); } - /* make sure it's valid */ + /* make sure it's valid */ if (!baremirror && !refs_ref_exists(refs, b_remote_head.buf)) { result = 1; goto cleanup; From a472af8bfdebe98e4e48a76b83e5424708cb42ef Mon Sep 17 00:00:00 2001 From: Tian Yuchen Date: Sat, 20 Jun 2026 22:09:57 +0800 Subject: [PATCH 65/95] environment: use 'repo->initialized' for repo_protect_hfs() and repo_protect_ntfs() To match how we refrain from calling repo_config_values() on an uninitialized instance of a repository object in other two topics that deal with ignore_case and trust_executable_bit, check the repo->initialized bit instead of the repo->gitdir member. Mentored-by: Christian Couder Mentored-by: Ayush Chandekar Mentored-by: Olamide Caleb Bello Signed-off-by: Tian Yuchen Signed-off-by: Junio C Hamano --- environment.c | 4 ++-- environment.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/environment.c b/environment.c index 683fe1b4d35257..f34f6fc7507f6b 100644 --- a/environment.c +++ b/environment.c @@ -142,14 +142,14 @@ int is_bare_repository(void) int repo_protect_ntfs(struct repository *repo) { - return repo->gitdir ? + return (repo && repo->initialized) ? repo_config_values(repo)->protect_ntfs : PROTECT_NTFS_DEFAULT; } int repo_protect_hfs(struct repository *repo) { - return repo->gitdir ? + return (repo && repo->initialized) ? repo_config_values(repo)->protect_hfs : PROTECT_HFS_DEFAULT; } diff --git a/environment.h b/environment.h index fdd9775900701d..b1ae4a70decc7c 100644 --- a/environment.h +++ b/environment.h @@ -127,8 +127,8 @@ int git_default_core_config(const char *var, const char *value, /* * Getters for the `protect_hfs` and `protect_ntfs` fields of `struct repo_config_values`. - * They check `repo->gitdir` to prevent calling repo_config_values() - * before the configuration is loaded or in bare environments. + * They check `repo->initialized` to prevent calling `repo_config_values()` + * before the repository setup is fully complete or in non-git environments. */ int repo_protect_hfs(struct repository *repo); int repo_protect_ntfs(struct repository *repo); From 5fe19d285c832b12de1479c78231d53842b44583 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sat, 20 Jun 2026 16:43:00 -0700 Subject: [PATCH 66/95] SubmittingPatches: address design critiques Contributors sometimes fail to answer fundamental design or viability comments from reviewers and submit subsequent rounds without addressing them. When design decisions are resolved on the mailing list, the final justification should be recorded in the commit messages. Instruct authors to be particularly mindful of critiques regarding high-level design or viability, to defend their choices on the list, and to accompany new iterations with clearer explanations in the cover letter, responses, and revised commit messages. Also instruct them to explicitly document the resolution of these concerns in the commit message body to keep the historical record complete. Signed-off-by: Junio C Hamano --- Documentation/SubmittingPatches | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index f042bb5aaf4a45..28b4f2f7957987 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -51,6 +51,22 @@ area. respond to them with "Reply-All" on the mailing list, while taking them into account while preparing an updated set of patches. + +Be particularly mindful of critiques regarding the high-level design +or viability of your proposal (e.g., questioning if the feature is +worth implementing, or if the chosen approach is appropriate). Defend +your design decisions on the list first and work with reviewers and +other members to improve the design before revising the implementation. +This will avoid wasting effort on an implementation before its design is +solid. ++ +Make sure that any new version explains and justifies those design +decisions more clearly, in the cover letter and in the revised commit +messages. Aim to make the reviewers say "it is now clear why we may +want to do this with the updated version". ++ +Topics with unresolved fundamental design critiques will not be +considered ready for merging. ++ It is often beneficial to allow some time for reviewers to provide feedback before sending a new version, rather than sending an updated series immediately after receiving a review. This helps collect broader @@ -323,6 +339,10 @@ The body should provide a meaningful commit message, which: . alternate solutions considered but discarded, if any. +. records the resolution of design or viability concerns raised by the + community during the review, if any, ensuring the historical record + explains why the chosen approach was accepted over alternatives. + [[present-tense]] The problem statement that describes the status quo is written in the present tense. Write "The code does X when it is given input Y", From 7bcc64be776baf9b8b22e8352b7c1e0d5da66cfe Mon Sep 17 00:00:00 2001 From: Weijie Yuan Date: Sun, 21 Jun 2026 16:05:06 +0800 Subject: [PATCH 67/95] doc: encourage review replies before rerolling Review feedback should not be answered only by sending a new patch version. Encourage contributors to discuss their planned response in the mailing-list thread before rerolling. This makes the author's reasoning explicit before the next version is prepared, instead of forcing reviewers to infer it from the rerolled patches. It also encourages more direct social interaction between contributors and helps foster a more collaborative review process. Signed-off-by: Weijie Yuan Signed-off-by: Junio C Hamano --- Documentation/MyFirstContribution.adoc | 12 +++++++----- Documentation/SubmittingPatches | 12 +++++++++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Documentation/MyFirstContribution.adoc b/Documentation/MyFirstContribution.adoc index b9fdefce0224c9..00704ab91e334c 100644 --- a/Documentation/MyFirstContribution.adoc +++ b/Documentation/MyFirstContribution.adoc @@ -1337,11 +1337,13 @@ fewer mistakes were the only one they would need to review. After a few days, you will hopefully receive a reply to your patchset with some comments. Woohoo! Now you can get back to work. -It's good manners to reply to each comment, notifying the reviewer that you have -made the change suggested, feel the original is better, or that the comment -inspired you to do something a new way which is superior to both the original -and the suggested change. This way reviewers don't need to inspect your v2 to -figure out whether you implemented their comment or not. +It's good manners to reply to each comment in the mailing list discussion +instead of letting the next version of your patch be your only response. Tell +the reviewer whether you plan to make the suggested change, keep the original, +or pursue a different approach. This way reviewers can respond to your reasoning +before you spend time preparing a version they may not agree with, and later do +not need to inspect your v2 to figure out whether you implemented their comment +or not. Reviewers may ask you about what you wrote in the patchset, either in the proposed commit log message or in the changes themselves. You diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index f042bb5aaf4a45..6c1e1f6423978c 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -48,8 +48,12 @@ area. . You get comments and suggestions for improvements. You may even get them in an "on top of your change" patch form. You are expected to - respond to them with "Reply-All" on the mailing list, while taking - them into account while preparing an updated set of patches. + respond to them with "Reply-All" on the mailing list, instead of + letting an updated patch series be your only response. Tell + reviewers which suggestions you plan to use, which ones you disagree + with, and when a comment leads you to consider a different approach. + Use these replies and any follow-up discussion as input when + preparing an updated set of patches. + It is often beneficial to allow some time for reviewers to provide feedback before sending a new version, rather than sending an updated @@ -613,7 +617,9 @@ grouped into their own e-mail thread to help readers find all parts of the series. To that end, send them as replies to either an additional "cover letter" message (see below), the first patch, or the respective preceding patch. Here is a link:MyFirstContribution.html#v2-git-send-email[step-by-step guide] on -how to submit updated versions of a patch series. +how to submit updated versions of a patch series. Before sending another +version, make sure you have answered meaningful review comments in the existing +discussion. If your log message (including your name on the `Signed-off-by` trailer) is not writable in ASCII, make sure that From 477172636160c9729a2c4ae4aca4b3bcf0d4d83a Mon Sep 17 00:00:00 2001 From: Weijie Yuan Date: Sun, 21 Jun 2026 16:05:34 +0800 Subject: [PATCH 68/95] doc: advise batching patch rerolls Contributors often need guidance on how quickly to send later iterations of a patch series. Add a rough default of no more than one new version of the same series per day so feedback can be batched and reviewers have time to comment regardless of their time zones. Mention factors that can affect the timing, such as series size, review depth, and substantial rework. Also point out that avoiding rapid rerolls encourages authors to polish each version before sending it, so reviewers can focus on substantial issues. Helped-by: Patrick Steinhardt Signed-off-by: Weijie Yuan Signed-off-by: Junio C Hamano --- Documentation/MyFirstContribution.adoc | 22 ++++++++++++++++++++++ Documentation/SubmittingPatches | 13 +++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/Documentation/MyFirstContribution.adoc b/Documentation/MyFirstContribution.adoc index 00704ab91e334c..35105bc3b41601 100644 --- a/Documentation/MyFirstContribution.adoc +++ b/Documentation/MyFirstContribution.adoc @@ -1330,6 +1330,28 @@ previous one" patches over 2 days), reviewers would strongly prefer if a single polished version came 2 days later instead, and that version with fewer mistakes were the only one they would need to review. +This consideration applies not only when going from the initial patch to v2, +but also to later iterations of the same series. There is no fixed rule for how +long to wait before sending a new version. A useful default is to send at most +one new version of the same patch series per day. This gives multiple reviewers +time to comment, gives reviewers across time zones a fair chance to +participate, lets you batch feedback together, and gives you time to think +through the comments you received. Knowing that you should not immediately send +another version also encourages you to review the patches more carefully before +sending them, catch small mistakes such as typos and off-by-one errors +yourself, and let reviewers spend more of their attention on design, +algorithms, and other substantial issues. + +The right timing depends on the topic and the feedback. Larger series usually +need more review time. If the only comments so far are minor, such as typo +fixes, it often makes sense to wait a little longer in case deeper reviews are +still coming. If the comments call for substantial rework, do not rush out an +updated version before you have reviewed the larger changes carefully. Instead, +reply to the review that prompted the rewrite, say that you are preparing a +substantial rework, and mention which parts of the current series will become +obsolete so reviewers can avoid spending time on them until the updated series +is ready. + [[reviewing]] === Responding to Reviews diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index 6c1e1f6423978c..d89efe07079d13 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -58,7 +58,15 @@ area. It is often beneficial to allow some time for reviewers to provide feedback before sending a new version, rather than sending an updated series immediately after receiving a review. This helps collect broader -input and avoids unnecessary churn from many rapid iterations. +input, gives reviewers in different time zones a fair chance to comment, +and avoids unnecessary churn from many rapid iterations. Waiting also +encourages you to polish each version before sending it, so reviewers +can focus on substantial issues rather than typos or other small +mistakes. ++ +As a rough default, avoid sending more than one new version of the same +series per day, while considering the size of the series and the depth +of review. . These early update iterations are expected to be full replacements, not incremental updates on top of what you posted already. If you @@ -619,7 +627,8 @@ letter" message (see below), the first patch, or the respective preceding patch. Here is a link:MyFirstContribution.html#v2-git-send-email[step-by-step guide] on how to submit updated versions of a patch series. Before sending another version, make sure you have answered meaningful review comments in the existing -discussion. +discussion. Also give reviewers enough time to comment before sending another +version. If your log message (including your name on the `Signed-off-by` trailer) is not writable in ASCII, make sure that From 00f7a12211f7ef4f5d308efe7648a688fe6b13f1 Mon Sep 17 00:00:00 2001 From: Taylor Blau Date: Sun, 21 Jun 2026 19:02:59 -0400 Subject: [PATCH 69/95] t/perf: drop p5311's lookup-table permutation p5311 measures the cost of serving a fetch from a bitmapped pack and indexing the resulting pack on the client. Since 761416ef91d (bitmap-lookup-table: add performance tests for lookup table, 2022-08-14), p5311 effectively runs itself twice: once with the bitmap's lookup table extension enabled, and again with it disabled. This comparison has served its useful purpose, as the lookup table is almost four years old, and the de-facto default in server-side Git deployments. A following commit will want to test a different combination (repacking with and without '--path-walk' instead of the lookup table). Instead of multiplying the current test count by two again to produce four variations of `test_fetch_bitmaps()`, drop the lookup table option to reduce the number of perf tests we run. Retain `test_fetch_bitmaps()` itself, since we will use this in the future for the new parameterization. (As an aside, a future commit outside of this series will adjust the default value of 'pack.writeBitmapLookupTable' to "true", matching the de-facto norm for deployments where the existence of bitmap lookup tables is meaningful. Punt on that to a later series and instead make the minimal change for now.) Suggested-by: Derrick Stolee Signed-off-by: Taylor Blau Signed-off-by: Junio C Hamano --- t/perf/p5311-pack-bitmaps-fetch.sh | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/t/perf/p5311-pack-bitmaps-fetch.sh b/t/perf/p5311-pack-bitmaps-fetch.sh index 047efb995d647b..5bea5c64e7b44c 100755 --- a/t/perf/p5311-pack-bitmaps-fetch.sh +++ b/t/perf/p5311-pack-bitmaps-fetch.sh @@ -12,7 +12,6 @@ test_fetch_bitmaps () { test_expect_success 'create bitmapped server repo' ' git config pack.writebitmaps true && - git config pack.writeBitmapLookupTable '"$1"' && git repack -ad ' @@ -32,7 +31,7 @@ test_fetch_bitmaps () { } >revs ' - test_perf "server $title (lookup=$1)" ' + test_perf "server $title" ' git pack-objects --stdout --revs \ --thin --delta-base-offset \ tmp.pack @@ -42,13 +41,12 @@ test_fetch_bitmaps () { test_file_size tmp.pack ' - test_perf "client $title (lookup=$1)" ' + test_perf "client $title" ' git index-pack --stdin --fix-thin Date: Sun, 21 Jun 2026 19:03:03 -0400 Subject: [PATCH 70/95] pack-objects: support reachability bitmaps with `--path-walk` When 'pack-objects' is invoked with '--path-walk', it prevents us from using reachability bitmaps. This behavior dates back to 70664d2865c (pack-objects: add --path-walk option, 2025-05-16), which included a comment in the relevant portion of the command-line arguments handling that read as follows: /* * We must disable the bitmaps because we are removing * the --objects / --objects-edge[-aggressive] options. */ In fb2c309b7d3 (pack-objects: pass --objects with --path-walk, 2026-05-02), path-walk learned to pass '--objects' again, but still kept bitmap traversal disabled. That leaves two useful cases unsupported: * A path-walk repack that writes bitmaps does not give the bitmap selector any commits, because path-walk reveals commits through `add_objects_by_path()` rather than through `show_commit()`, where `index_commit_for_bitmap()` is normally called. * An invocation like "git pack-objects --use-bitmap-index --path-walk" never tries an existing bitmap, even when one is available and could answer the request. Fortunately for us, neither restriction is required. * On the writing side: teach the path-walk object callback to call `index_commit_for_bitmap()` for commits that it adds to the pack. That gives the bitmap selector the commit candidates it would have seen from the regular traversal. * For bitmap reading, keep passing '--objects' to the internal rev_list machinery, but stop clearing `use_bitmap_index`. If an existing bitmap can answer the request, use it; otherwise fall back to path-walk's own enumeration. As a result, we can see significantly reduced pack generation times from p5311 (with our `GIT_PERF_REPO` set to a recent clone of the fluentui repository) before this commit: Test HEAD^ HEAD ---------------------------------------------------------------------------------------- 5311.40: server (1 days, --path-walk) 1.43(1.39+0.04) 0.01(0.01+0.00) -99.3% 5311.41: size (1 days, --path-walk) 139.6K 139.7K +0.0% 5311.42: client (1 days, --path-walk) 0.02(0.02+0.00) 0.02(0.02+0.00) +0.0% 5311.44: server (2 days, --path-walk) 1.43(1.39+0.04) 0.01(0.00+0.00) -99.3% 5311.45: size (2 days, --path-walk) 139.6K 139.7K +0.0% 5311.46: client (2 days, --path-walk) 0.02(0.02+0.00) 0.02(0.02+0.00) +0.0% 5311.48: server (4 days, --path-walk) 1.44(1.39+0.04) 0.01(0.01+0.00) -99.3% 5311.49: size (4 days, --path-walk) 238.1K 238.1K +0.0% 5311.50: client (4 days, --path-walk) 0.03(0.03+0.00) 0.03(0.03+0.00) +0.0% 5311.52: server (8 days, --path-walk) 1.43(1.39+0.03) 0.01(0.00+0.00) -99.3% 5311.53: size (8 days, --path-walk) 344.9K 344.9K +0.0% 5311.54: client (8 days, --path-walk) 0.07(0.07+0.00) 0.07(0.08+0.00) +0.0% 5311.56: server (16 days, --path-walk) 1.47(1.44+0.03) 0.10(0.08+0.01) -93.2% 5311.57: size (16 days, --path-walk) 844.0K 844.0K +0.0% 5311.58: client (16 days, --path-walk) 0.09(0.09+0.00) 0.09(0.09+0.00) +0.0% 5311.60: server (32 days, --path-walk) 1.52(1.50+0.05) 0.14(0.15+0.02) -90.8% 5311.61: size (32 days, --path-walk) 4.2M 4.2M +0.1% 5311.62: client (32 days, --path-walk) 0.34(0.48+0.02) 0.34(0.45+0.05) +0.0% 5311.64: server (64 days, --path-walk) 1.55(1.52+0.06) 0.15(0.15+0.04) -90.3% 5311.65: size (64 days, --path-walk) 6.4M 6.4M -0.0% 5311.66: client (64 days, --path-walk) 0.51(0.79+0.05) 0.51(0.80+0.06) +0.0% 5311.68: server (128 days, --path-walk) 1.59(1.57+0.06) 0.16(0.21+0.01) -89.9% 5311.69: size (128 days, --path-walk) 8.4M 8.4M -0.0% 5311.70: client (128 days, --path-walk) 0.72(1.44+0.08) 0.71(1.47+0.09) -1.4% We get the same size of output pack, but this commit allows us to do so in a significantly shorter amount of time. Intuitively, we're generating the same pack (hence the unchanged 'test_size' output from run to run), but varying how we get there. Before this commit, pack-objects prefers '--path-walk' to '--use-bitmap-index', so we generate the output pack by performing a normal '--path-walk' traversal. With this commit, we are operating over a *repacked* state (that itself was done with a '--path-walk' traversal), but are able to perform pack-reuse on that repacked state via bitmaps. When comparing the size of the repacked pack with/without '--path-walk' on the previous commit versus this one, we see that (a) the repacked size improves significantly with '--path-walk', and that (b) writing bitmaps during repacking does not regress this improvement: Test HEAD^ HEAD ---------------------------------------------------------------------------------------- 5311.3: size of bitmapped pack 558.4M 558.5M +0.0% 5311.38: size of bitmapped pack (--path-walk) 164.4M 164.4M +0.0% (Note that to observe an improvement here, we must repack with '-F' in order to avoid reusing non-'--path-walk' deltas, which would otherwise skew our results.) There is one wrinkle when it comes to '--boundary', which we must not pass into the bitmap walk in the presence of both '--path-walk' and '--use-bitmap-index'. Path-walk needs boundary commits when it performs its own traversal, in order to discover bases for thin packs, but the bitmap traversal does not expect this. Work around this by setting `revs->boundary` as late as possible within the '--path-walk' traversal, after any bitmap attempt has either succeeded or declined to answer the request. Signed-off-by: Taylor Blau Signed-off-by: Junio C Hamano --- Documentation/git-pack-objects.adoc | 6 +++-- builtin/pack-objects.c | 18 +++++++++++++-- t/perf/p5311-pack-bitmaps-fetch.sh | 18 +++++++++++---- t/t5310-pack-bitmaps.sh | 36 +++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 8 deletions(-) diff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc index 8a27aa19fd3f1f..0adce8961a3ad7 100644 --- a/Documentation/git-pack-objects.adoc +++ b/Documentation/git-pack-objects.adoc @@ -402,8 +402,10 @@ will be automatically changed to version `1`. of filenames that cause collisions in Git's default name-hash algorithm. + -Incompatible with `--delta-islands`. The `--use-bitmap-index` option is -ignored in the presence of `--path-walk`. The `--path-walk` option +Incompatible with `--delta-islands`. When `--use-bitmap-index` is +specified with `--path-walk`, a successful bitmap traversal is used for +object enumeration, with path-walk remaining as the fallback traversal +when the bitmap cannot satisfy the request. The `--path-walk` option supports the `--filter=` forms `blob:none`, `blob:limit=`, `tree:0`, `object:type=`, and `sparse:`. These supported filter types can be combined with the `combine:+` form. diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index fe9fbecb30e100..b3822dc86c3282 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -4742,6 +4742,15 @@ static int add_objects_by_path(const char *path, continue; add_object_entry(oid, type, path, exclude); + + if (type == OBJ_COMMIT && write_bitmap_index) { + struct commit *commit; + + commit = lookup_commit(the_repository, oid); + if (!commit) + die(_("could not find commit %s"), oid_to_hex(oid)); + index_commit_for_bitmap(commit); + } } oe_end = to_pack.nr_objects; @@ -4774,6 +4783,13 @@ static int get_object_list_path_walk(struct rev_info *revs) info.path_fn = add_objects_by_path; info.path_fn_data = &processed; + /* + * Path-walk needs boundary commits to discover thin-pack bases, but + * bitmap traversal does not understand the boundary state. Set it + * here so any prior bitmap attempt sees the usual non-boundary walk. + */ + revs->boundary = 1; + /* * Allow the --[no-]sparse option to be interesting here, if only * for testing purposes. Paths with no interesting objects will not @@ -5205,9 +5221,7 @@ int cmd_pack_objects(int argc, } } if (path_walk) { - strvec_push(&rp, "--boundary"); strvec_push(&rp, "--objects"); - use_bitmap_index = 0; } else if (thin) { use_internal_rev_list = 1; strvec_push(&rp, shallow diff --git a/t/perf/p5311-pack-bitmaps-fetch.sh b/t/perf/p5311-pack-bitmaps-fetch.sh index 5bea5c64e7b44c..505062162270b4 100755 --- a/t/perf/p5311-pack-bitmaps-fetch.sh +++ b/t/perf/p5311-pack-bitmaps-fetch.sh @@ -4,15 +4,22 @@ test_description='performance of fetches from bitmapped packs' . ./perf-lib.sh test_fetch_bitmaps () { + argv=$1 + export argv + test_expect_success 'setup test directory' ' rm -fr * .git ' test_perf_default_repo - test_expect_success 'create bitmapped server repo' ' + test_expect_success "create bitmapped server repo ${argv:+($argv)}" ' git config pack.writebitmaps true && - git repack -ad + git repack -adF $argv + ' + + test_size "size of bitmapped pack ${argv:+($argv)}" ' + test_file_size .git/objects/pack/pack-*.pack ' # simulate a fetch from a repository that last fetched N days ago, for @@ -20,7 +27,7 @@ test_fetch_bitmaps () { # and assume the first entry in the chain that is N days older than the current # HEAD is where the HEAD would have been then. for days in 1 2 4 8 16 32 64 128; do - title=$(printf '%10s' "($days days)") + title=$(printf '%10s' "($days days${argv:+, $argv})") test_expect_success "setup revs from $days days ago" ' now=$(git log -1 --format=%ct HEAD) && then=$(($now - ($days * 86400))) && @@ -47,6 +54,9 @@ test_fetch_bitmaps () { done } -test_fetch_bitmaps +for argv in '' --path-walk +do + test_fetch_bitmaps $argv || return 1 +done test_done diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh index efeb71593bf7f6..e24347848ee9d4 100755 --- a/t/t5310-pack-bitmaps.sh +++ b/t/t5310-pack-bitmaps.sh @@ -577,6 +577,42 @@ test_bitmap_cases sane_unset GIT_TEST_PACK_USE_BITMAP_BOUNDARY_TRAVERSAL +test_expect_success 'path-walk repack can write and use bitmap indexes' ' + test_when_finished "rm -rf path-walk-bitmap" && + git init path-walk-bitmap && + ( + cd path-walk-bitmap && + test_commit first && + test_commit second && + test_commit third && + + git repack -a -d -b --path-walk && + git rev-list --test-bitmap --use-bitmap-index HEAD && + + git rev-parse HEAD >in && + + git rev-list --objects --no-object-names HEAD >expect.raw && + sort expect.raw >expect && + + for reuse in true false + do + : >trace.txt && + + GIT_TRACE2_EVENT="$(pwd)/trace.txt" \ + git -c pack.allowPackReuse=$reuse pack-objects \ + --stdout --revs --path-walk --use-bitmap-index \ + out.pack && + test_grep "\"category\":\"bitmap\",\"key\":\"bitmap/hits\"" trace.txt && + + git index-pack out.pack && + + list_packed_objects out.idx >actual.raw && + sort actual.raw >actual && + test_cmp expect actual || return 1 + done + ) +' + test_expect_success 'incremental repack fails when bitmaps are requested' ' test_commit more-1 && test_must_fail git repack -d 2>err && From 264efee401e04de50055d1519dda1fbd1e02428f Mon Sep 17 00:00:00 2001 From: Taylor Blau Date: Sun, 21 Jun 2026 19:03:07 -0400 Subject: [PATCH 71/95] pack-objects: extract `record_tree_depth()` helper Prepare for a subsequent change that needs to record tree depths from a second call site by factoring the delta-islands tree-depth bookkeeping out of `show_object()` and into a helper, `record_tree_depth()`. The helper looks up the object in `to_pack`, returns early when the object was not added there, computes the depth from the slash count in the supplied name, and preserves the existing max-depth-wins behavior when a tree is reached by more than one path. Signed-off-by: Taylor Blau Signed-off-by: Junio C Hamano --- builtin/pack-objects.c | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index b3822dc86c3282..db65d0e189ed31 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -2730,6 +2730,22 @@ static inline void oe_set_tree_depth(struct packing_data *pack, pack->tree_depth[e - pack->objects] = tree_depth; } +static void record_tree_depth(const struct object_id *oid, const char *name) +{ + const char *p; + unsigned depth; + struct object_entry *ent; + + /* the empty string is a root tree, which is depth 0 */ + depth = *name ? 1 : 0; + for (p = strchr(name, '/'); p; p = strchr(p + 1, '/')) + depth++; + + ent = packlist_find(&to_pack, oid); + if (ent && depth > oe_tree_depth(&to_pack, ent)) + oe_set_tree_depth(&to_pack, ent, depth); +} + /* * Return the size of the object without doing any delta * reconstruction (so non-deltas are true object sizes, but deltas @@ -4385,20 +4401,8 @@ static void show_object(struct object *obj, const char *name, add_preferred_base_object(name); add_object_entry(&obj->oid, obj->type, name, 0); - if (use_delta_islands) { - const char *p; - unsigned depth; - struct object_entry *ent; - - /* the empty string is a root tree, which is depth 0 */ - depth = *name ? 1 : 0; - for (p = strchr(name, '/'); p; p = strchr(p + 1, '/')) - depth++; - - ent = packlist_find(&to_pack, &obj->oid); - if (ent && depth > oe_tree_depth(&to_pack, ent)) - oe_set_tree_depth(&to_pack, ent, depth); - } + if (use_delta_islands) + record_tree_depth(&obj->oid, name); } static void show_object__ma_allow_any(struct object *obj, const char *name, void *data) From 7e6de2ac62817199a2c8dd464f9e33ae0b4966e3 Mon Sep 17 00:00:00 2001 From: Taylor Blau Date: Sun, 21 Jun 2026 19:03:10 -0400 Subject: [PATCH 72/95] pack-objects: support `--delta-islands` with `--path-walk` Since the inception of `--path-walk`, this option has had a documented incompatibility with `--delta-islands`. When discussing those original patches on the list, a message from Stolee in [1] noted the following: this could be remedied by [...] doing a separate walk to identify islands using the normal method In a related portion of the thread, Peff explains[2]: The delta islands code already does its own tree walk to propagate the bits down (it does rely on the base walk's show_commit() to propagate through the commits). Once each object has its island bitmaps, I think however you choose to come up with delta candidates [...] you should be able to use it. It's fundamentally just answering the question of "am I allowed to delta between these two objects". That is similar to what this patch does, and it turns out the cheaper option is sufficient: perform the same island side effects from the path-walk callback rather than doing a second walk. Recall how delta-islands are computed during a normal repack: - `show_commit()` calls `propagate_island_marks()` for each commit, which merges the commit's island bitset onto its root tree object and onto each of its parent commits. - `show_object()` for a tree records the tree's depth derived from the slash-separated pathname. Subsequent `resolve_tree_islands()` uses that depth to walk trees in increasing-depth order, propagating each tree's marks to its children. - At delta-search time, `in_same_island()` enforces that a delta target's island bitmap is a subset of its base's: every island that reaches the target must also reach the base. Path-walk's enumeration callback is `add_objects_by_path()`. It already adds objects to `to_pack`, but until now did not perform the island-related side effects. Two things are needed: - For each commit batch, call `propagate_island_marks()` on commits, exactly as `show_commit()` does. We have to be careful about the order in which we call this function, and we must see a commit before its parents in order to have island marks to propagate. The path-walk batch preserves that order. Path-walk appends commits to its `OBJ_COMMIT` batch as they come back from the same `get_revision()` loop the regular traversal uses, and `add_objects_by_path()` iterates the batch in array order. So every commit reaches `propagate_island_marks()` in the same sequence that `show_commit()` would have seen it, and the descendant-first chain that the algorithm relies on is intact. Skip island propagation for excluded commits to match the regular traversal, whose `show_commit()` callback is only invoked for interesting commits. Boundary commits may still be present in path-walk's callback so they can serve as thin-pack bases, but they should not contribute island marks. - For each tree batch, record the tree's depth from the path. Use the `record_tree_depth()` helper from the previous commit so both callbacks behave identically, including the max-depth-wins behavior when a tree is reached via more than one path. The helper accepts both the `show_object()` path shape ("foo", "foo/bar") and the path-walk shape with a trailing slash ("foo/", "foo/bar/"), so depths recorded from either traversal mode are directly comparable. This is implicit in the implementation sketch from Peff above. `resolve_tree_islands()` sorts trees by `oe->tree_depth` in increasing-depth order before propagating marks down, so that a parent tree's marks are finalized before its children inherit them. Without recording the depth at path-walk time, every path-walk-discovered tree would land at depth 0 in `to_pack`, the sort would lose its ordering, and children could inherit marks from parents whose own contributions had not yet been merged in. With those two pieces in place, `resolve_tree_islands()` receives the same island inputs from path-walk as it would from the regular traversal, so the existing island checks can be reused unchanged. Drop the documented incompatibility between `--path-walk` and `--delta-islands`, and add t5320 coverage for path-walk island repacks with and without bitmap writing, as well as the same-island case where a delta remains allowed. [1]: https://lore.kernel.org/git/9aa2471b-0850-4707-9733-d3b33609f5f2@gmail.com/ [2]: https://lore.kernel.org/git/20240911063203.GA1538586@coredump.intra.peff.net/ Signed-off-by: Taylor Blau Signed-off-by: Junio C Hamano --- Documentation/git-pack-objects.adoc | 14 +++++++------- builtin/pack-objects.c | 22 ++++++++++++++++++---- t/t5320-delta-islands.sh | 29 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc index 0adce8961a3ad7..65cd00c152f495 100644 --- a/Documentation/git-pack-objects.adoc +++ b/Documentation/git-pack-objects.adoc @@ -402,13 +402,13 @@ will be automatically changed to version `1`. of filenames that cause collisions in Git's default name-hash algorithm. + -Incompatible with `--delta-islands`. When `--use-bitmap-index` is -specified with `--path-walk`, a successful bitmap traversal is used for -object enumeration, with path-walk remaining as the fallback traversal -when the bitmap cannot satisfy the request. The `--path-walk` option -supports the `--filter=` forms `blob:none`, `blob:limit=`, -`tree:0`, `object:type=`, and `sparse:`. These supported filter -types can be combined with the `combine:+` form. +When `--use-bitmap-index` is specified with `--path-walk`, a successful +bitmap traversal is used for object enumeration, with path-walk +remaining as the fallback traversal when the bitmap cannot satisfy the +request. The `--path-walk` option supports the `--filter=` forms +`blob:none`, `blob:limit=`, `tree:0`, `object:type=`, and +`sparse:`. These supported filter types can be combined with the +`combine:+` form. DELTA ISLANDS diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index db65d0e189ed31..33db92be4c8a3b 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -4747,13 +4747,29 @@ static int add_objects_by_path(const char *path, add_object_entry(oid, type, path, exclude); - if (type == OBJ_COMMIT && write_bitmap_index) { + if (type == OBJ_COMMIT) { struct commit *commit; + if (!write_bitmap_index && !use_delta_islands) + continue; + commit = lookup_commit(the_repository, oid); if (!commit) die(_("could not find commit %s"), oid_to_hex(oid)); - index_commit_for_bitmap(commit); + if (write_bitmap_index) + index_commit_for_bitmap(commit); + /* + * Skip island propagation for boundary commits. + * The regular traversal's show_commit() is only + * called for interesting commits; matching that + * here keeps path-walk from doing extra work that + * would only be a no-op anyway (boundary commits + * are not in island_marks). + */ + if (use_delta_islands && !exclude) + propagate_island_marks(the_repository, commit); + } else if (type == OBJ_TREE && use_delta_islands) { + record_tree_depth(oid, path); } } @@ -5215,8 +5231,6 @@ int cmd_pack_objects(int argc, const char *option = NULL; if (!path_walk_filter_compatible(&filter_options)) option = "--filter"; - else if (use_delta_islands) - option = "--delta-islands"; if (option) { warning(_("cannot use %s with %s"), diff --git a/t/t5320-delta-islands.sh b/t/t5320-delta-islands.sh index 2c961c70963051..9b28344a0a3750 100755 --- a/t/t5320-delta-islands.sh +++ b/t/t5320-delta-islands.sh @@ -53,6 +53,35 @@ test_expect_success 'separate islands disallows delta' ' ! is_delta_base $two $one ' +test_expect_success 'path-walk island repack respects islands' ' + GIT_TRACE2_EVENT="$(pwd)/trace.path-walk-islands" \ + git -c "pack.island=refs/heads/(.*)" repack -adfi \ + --path-walk 2>err && + test_region pack-objects path-walk trace.path-walk-islands && + test_grep ! "cannot use --delta-islands with --path-walk" err && + ! is_delta_base $one $two && + ! is_delta_base $two $one +' + +test_expect_success 'path-walk island bitmap repack respects islands' ' + GIT_TRACE2_EVENT="$(pwd)/trace.path-walk-island-bitmap" \ + git -c "pack.island=refs/heads/(.*)" repack -a -d -f -i -b \ + --path-walk 2>err && + test_region pack-objects path-walk trace.path-walk-island-bitmap && + test_path_is_file .git/objects/pack/*.bitmap && + git rev-list --test-bitmap --use-bitmap-index one && + test_grep ! "cannot use --delta-islands with --path-walk" err && + ! is_delta_base $one $two && + ! is_delta_base $two $one +' + +test_expect_success 'path-walk same island allows delta' ' + GIT_TRACE2_EVENT="$(pwd)/trace.path-walk-same-island" \ + git -c "pack.island=refs/heads" repack -adfi --path-walk && + test_region pack-objects path-walk trace.path-walk-same-island && + is_delta_base $one $two +' + test_expect_success 'same island allows delta' ' git -c "pack.island=refs/heads" repack -adfi && is_delta_base $one $two From 304812ed33dec7ea7e68928feab38199d796ea25 Mon Sep 17 00:00:00 2001 From: Miklos Vajna Date: Mon, 22 Jun 2026 08:23:31 +0200 Subject: [PATCH 73/95] log: improve --follow following renames for non-linear history Have a repo with a subtree merge, do a 'git log --follow prefix/test.c', the output only contains history in the outer repo, not commits that were merged via a subtree merge. What happens is that 'git log --follow' stores the followed path only in opt->diffopt.pathspec, so in case the commit history is non-linear, and multiple parents have renames to the followed path, then the end result isn't really defined: the first commit that happens to be visited in one of the parents update opt->diffopt.pathspec, and from that point, only that updated path is visited. Fix the problem by introducing a commit -> path map (follow_pathspec_slab) that stores what will be a path to follow when visiting that parent. At the top of log_tree_commit(), if the slab has an entry for this commit, we replace opt->diffopt.pathspec with a path from this entry, so the correct path is followed, even if an unrelated sub-tree changed the path to be followed to something else. After log_tree_diff() runs, we record each parent's path in the slab. As a result, the walk order doesn't matter, which was exactly the source of problems previously. This helps with subtree merges (rename happens inside the merge commit), but also fixes the general case when the rename happens in the history of parents, not in the merge commit itself. Signed-off-by: Miklos Vajna Signed-off-by: Junio C Hamano --- Documentation/config/log.adoc | 3 +- log-tree.c | 116 ++++++++++++++++++++++++++++++ log-tree.h | 1 + revision.c | 2 + revision.h | 4 ++ t/meson.build | 1 + t/t4219-log-follow-merge.sh | 129 ++++++++++++++++++++++++++++++++++ 7 files changed, 254 insertions(+), 2 deletions(-) create mode 100755 t/t4219-log-follow-merge.sh diff --git a/Documentation/config/log.adoc b/Documentation/config/log.adoc index f20cc25cd7c3bf..757a7be196ab38 100644 --- a/Documentation/config/log.adoc +++ b/Documentation/config/log.adoc @@ -53,8 +53,7 @@ This is the same as the `--decorate` option of the `git log`. `log.follow`:: If `true`, `git log` will act as if the `--follow` option was used when a single is given. This has the same limitations as `--follow`, - i.e. it cannot be used to follow multiple files and does not work well - on non-linear history. + i.e. it cannot be used to follow multiple files. `log.graphColors`:: A list of colors, separated by commas, that can be used to draw diff --git a/log-tree.c b/log-tree.c index 88b3019293b725..83a3c4bf9b16b9 100644 --- a/log-tree.c +++ b/log-tree.c @@ -3,6 +3,7 @@ #include "git-compat-util.h" #include "commit-reach.h" +#include "commit-slab.h" #include "config.h" #include "diff.h" #include "diffcore.h" @@ -1089,6 +1090,96 @@ static int do_remerge_diff(struct rev_info *opt, return !opt->loginfo; } +/* Per-commit path storage for --follow across merges */ +define_commit_slab(follow_pathspec_slab, char *); + +static const char *pathspec_single_path(const struct pathspec *ps) +{ + if (ps->nr != 1) + return NULL; + return ps->items[0].match; +} + +static void set_pathspec_to_single_path(struct pathspec *ps, const char *path) +{ + const char *paths[2] = { path, NULL }; + + clear_pathspec(ps); + parse_pathspec(ps, + PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL, + PATHSPEC_LITERAL_PATH, "", paths); +} + +static void remember_follow_pathspec(struct rev_info *opt, + struct commit *c, const char *path) +{ + char **slot; + + if (!path) + return; + if (!opt->follow_pathspec_slab) { + opt->follow_pathspec_slab = xmalloc(sizeof(*opt->follow_pathspec_slab)); + init_follow_pathspec_slab(opt->follow_pathspec_slab); + } + slot = follow_pathspec_slab_at(opt->follow_pathspec_slab, c); + if (*slot && !strcmp(*slot, path)) + return; + free(*slot); + *slot = xstrdup(path); +} + +static const char *recall_follow_pathspec(struct rev_info *opt, + struct commit *c) +{ + char **slot; + + if (!opt->follow_pathspec_slab) + return NULL; + slot = follow_pathspec_slab_peek(opt->follow_pathspec_slab, c); + return slot ? *slot : NULL; +} + +static void free_follow_pathspec_slot(char **slot) +{ + FREE_AND_NULL(*slot); +} + +void release_follow_pathspec_slab(struct rev_info *opt) +{ + if (!opt->follow_pathspec_slab) + return; + deep_clear_follow_pathspec_slab(opt->follow_pathspec_slab, + free_follow_pathspec_slot); + FREE_AND_NULL(opt->follow_pathspec_slab); +} + +/* Compute a path to follow in parent, if there is one */ +static void propagate_follow_pathspec_to_parent(struct rev_info *opt, + struct commit *commit, + struct commit *parent) +{ + struct diff_options diff_opts; + const char *path; + + parse_commit_or_die(parent); + repo_diff_setup(opt->diffopt.repo, &diff_opts); + copy_pathspec(&diff_opts.pathspec, &opt->diffopt.pathspec); + diff_opts.flags.recursive = 1; + diff_opts.flags.follow_renames = 1; + diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT; + diff_setup_done(&diff_opts); + diff_tree_oid(get_commit_tree_oid(parent), + get_commit_tree_oid(commit), + "", &diff_opts); + + path = pathspec_single_path(&diff_opts.pathspec); + if (path) + remember_follow_pathspec(opt, parent, path); + + diff_queue_clear(&diff_queued_diff); + diff_free(&diff_opts); +} + /* * Show the diff of a commit. * @@ -1185,6 +1276,16 @@ int log_tree_commit(struct rev_info *opt, struct commit *commit) opt->loginfo = &log; opt->diffopt.no_free = 1; + /* Any recorded path for this commit? If so, restore it */ + if (opt->diffopt.flags.follow_renames) { + const char *stored = recall_follow_pathspec(opt, commit); + if (stored) { + const char *current = pathspec_single_path(&opt->diffopt.pathspec); + if (!current || strcmp(current, stored)) + set_pathspec_to_single_path(&opt->diffopt.pathspec, stored); + } + } + if (opt->track_linear && !opt->linear && !opt->reverse_output_stage) fprintf(opt->diffopt.file, "\n%s\n", opt->break_bar); shown = log_tree_diff(opt, commit, &log); @@ -1197,6 +1298,21 @@ int log_tree_commit(struct rev_info *opt, struct commit *commit) fprintf(opt->diffopt.file, "\n%s\n", opt->break_bar); if (shown) show_diff_of_diff(opt); + + /* Record what path each parent of this commit should use */ + if (opt->diffopt.flags.follow_renames) { + struct commit_list *parents = get_saved_parents(opt, commit); + if (parents && parents->next) { + struct commit_list *p; + for (p = parents; p; p = p->next) + propagate_follow_pathspec_to_parent(opt, commit, + p->item); + } else if (parents) { + remember_follow_pathspec(opt, parents->item, + pathspec_single_path(&opt->diffopt.pathspec)); + } + } + opt->loginfo = NULL; maybe_flush_or_die(opt->diffopt.file, "stdout"); opt->diffopt.no_free = no_free; diff --git a/log-tree.h b/log-tree.h index 07924be8bcea5e..e8679b6c4aa3a3 100644 --- a/log-tree.h +++ b/log-tree.h @@ -26,6 +26,7 @@ struct decoration_options { int parse_decorate_color_config(const char *var, const char *slot_name, const char *value); int log_tree_diff_flush(struct rev_info *); int log_tree_commit(struct rev_info *, struct commit *); +void release_follow_pathspec_slab(struct rev_info *); void show_log(struct rev_info *opt); void format_decorations(struct strbuf *sb, const struct commit *commit, enum git_colorbool use_color, const struct decoration_options *opts); diff --git a/revision.c b/revision.c index e91d7e1f11356a..0c95edef5947fa 100644 --- a/revision.c +++ b/revision.c @@ -26,6 +26,7 @@ #include "decorate.h" #include "string-list.h" #include "line-log.h" +#include "log-tree.h" #include "mailmap.h" #include "commit-slab.h" #include "cache-tree.h" @@ -3304,6 +3305,7 @@ void release_revisions(struct rev_info *revs) line_log_free(revs); oidset_clear(&revs->missing_commits); release_revisions_bloom_keyvecs(revs); + release_follow_pathspec_slab(revs); } static void add_child(struct rev_info *revs, struct commit *parent, struct commit *child) diff --git a/revision.h b/revision.h index 00c392be37195d..569b3fa1cb5a33 100644 --- a/revision.h +++ b/revision.h @@ -66,6 +66,7 @@ struct repository; struct rev_info; struct string_list; struct saved_parents; +struct follow_pathspec_slab; struct bloom_keyvec; struct bloom_filter_settings; struct option; @@ -363,6 +364,9 @@ struct rev_info { /* copies of the parent lists, for --full-diff display */ struct saved_parents *saved_parents_slab; + /* per-commit pathspec for --follow across merges */ + struct follow_pathspec_slab *follow_pathspec_slab; + struct commit_list *previous_parents; struct commit_list *ancestry_path_bottoms; const char *break_bar; diff --git a/t/meson.build b/t/meson.build index 3219264fe7d497..b6ac49b443372b 100644 --- a/t/meson.build +++ b/t/meson.build @@ -576,6 +576,7 @@ integration_tests = [ 't4215-log-skewed-merges.sh', 't4216-log-bloom.sh', 't4217-log-limit.sh', + 't4219-log-follow-merge.sh', 't4252-am-options.sh', 't4253-am-keep-cr-dos.sh', 't4254-am-corrupt.sh', diff --git a/t/t4219-log-follow-merge.sh b/t/t4219-log-follow-merge.sh new file mode 100755 index 00000000000000..e370f829556f71 --- /dev/null +++ b/t/t4219-log-follow-merge.sh @@ -0,0 +1,129 @@ +#!/bin/sh + +test_description='Test --follow follows renames across merges' + +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +. ./test-lib.sh + +test_expect_success 'setup subtree-merged repository' ' + git init inner && + echo inner >inner/inner.txt && + git -C inner add inner.txt && + git -C inner commit -m "inner init" && + + git init outer && + echo outer >outer/outer.txt && + git -C outer add outer.txt && + git -C outer commit -m "outer init" && + + git -C outer fetch ../inner master && + git -C outer merge -s ours --no-commit --allow-unrelated-histories \ + FETCH_HEAD && + git -C outer read-tree --prefix=inner/ -u FETCH_HEAD && + git -C outer commit -m "Merge inner repo into inner/ subdirectory" +' + +test_expect_success '--follow finds the pre-merge commit through a subtree merge' ' + git -C outer log --follow --pretty=tformat:%s inner/inner.txt >actual && + echo "inner init" >expect && + test_cmp expect actual +' + +test_expect_success 'setup merge of two branches that both renamed a file to README' ' + git init foo && + mkdir foo/foo && + echo "foo readme" >foo/foo/README && + git -C foo add foo/README && + git -C foo commit -m "add foo README" && + + git -C foo mv foo/README README && + git -C foo commit -m "promote foo README to toplevel" && + + echo "foo c" >foo/foo.c && + git -C foo add foo.c && + git -C foo commit -m "add foo C impl" && + + git init bar && + mkdir bar/bar && + echo "bar readme" >bar/bar/README && + git -C bar add bar/README && + git -C bar commit -m "add bar README" && + + git -C bar mv bar/README README && + git -C bar commit -m "promote bar README to toplevel" && + + echo "bar c" >bar/bar.c && + git -C bar add bar.c && + git -C bar commit -m "add bar C impl" && + + git -C foo fetch ../bar master && + git -C foo merge -s ours --no-commit --allow-unrelated-histories \ + FETCH_HEAD && + git -C foo checkout FETCH_HEAD -- bar.c && + git -C foo commit -m "merge bar into foo" +' + +test_expect_success '--follow follows renames across both sides of a merge' ' + git -C foo log --follow --pretty=tformat:%s README >actual && + sort actual >actual.sorted && + cat >expect <<-\EOF && + add bar README + add foo README + promote bar README to toplevel + promote foo README to toplevel + EOF + test_cmp expect actual.sorted +' + +test_expect_success 'setup diamond with renames on both sides of a fork' ' + git init diamond && + test_lines="line 1\nline 2\nline 3\nline 4\nline 5\n" && + + printf "$test_lines" >diamond/path0 && + git -C diamond add path0 && + git -C diamond commit -m "A: add path0" && + + git -C diamond checkout -b upper && + printf "line 1\nline 2\nline 3 modified by B\nline 4\nline 5\n" \ + >diamond/path0 && + git -C diamond commit -am "B: modify path0 on upper" && + git -C diamond mv path0 path1 && + git -C diamond commit -m "X: rename path0 to path1" && + + git -C diamond checkout -b lower master && + printf "line 1\nline 2\nline 3 modified by C\nline 4\nline 5\n" \ + >diamond/path0 && + git -C diamond commit -am "C: modify path0 on lower" && + git -C diamond mv path0 path2 && + git -C diamond commit -m "Y: rename path0 to path2" && + + git -C diamond checkout upper && + git -C diamond merge -s ours --no-commit lower && + git -C diamond rm path1 && + printf "line 1\nline 2\nline 3 merged\nline 4\nline 5\n" \ + >diamond/path && + git -C diamond add path && + git -C diamond commit -m "M: merge with rename to path" && + + printf "line 1\nline 2\nline 3 merged again\nline 4\nline 5\n" \ + >diamond/path && + git -C diamond commit -am "Z: modify path" +' + +test_expect_success '--follow follows renames through a fork in a single history' ' + git -C diamond log --follow --pretty=tformat:%s path >actual && + sort actual >actual.sorted && + cat >expect <<-\EOF && + A: add path0 + B: modify path0 on upper + C: modify path0 on lower + X: rename path0 to path1 + Y: rename path0 to path2 + Z: modify path + EOF + test_cmp expect actual.sorted +' + +test_done From 10c2678a2bfbd2a3e8d0a3623d1b71b6cc916253 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Tue, 23 Jun 2026 16:53:56 +0100 Subject: [PATCH 74/95] sequencer: factor out parsing of todo commands Move the code that parses todo commands into a separate function so that it can be shared with "git status" in the next commit. As we know the input is NUL terminated we do not pass a pointer to the end of the line and instead test for a blank line by looking for NUL, CR LF, or LF. We use starts_with() instead of starts_with_mem() for the same reason. This results in slightly different behavior when there a CR at the start of the line that is not followed by LF. Previously such a line was treated as a comment rather than an invalid line. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- sequencer.c | 39 +++++++++++++++++++++++++++------------ sequencer.h | 8 ++++++++ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/sequencer.c b/sequencer.c index b7d8dca47f4a58..b8e860434a8905 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2627,6 +2627,27 @@ static int is_command(enum todo_command command, const char **bol) return 0; } +bool sequencer_parse_todo_command(const char **p, enum todo_command *cmd) +{ + const char *s = *p; + + for (int i = 0; i < TODO_COMMENT; i++) + if (is_command(i, p)) { + *cmd = i; + return true; + } + + if (starts_with(s, comment_line_str)) { + *cmd = TODO_COMMENT; + return true; + } else if (s[0] == '\n' || (s[0] == '\r' && s[1] == '\n') || !s[0]) { + *cmd = TODO_COMMENT; + return true; + } + + return false; +} + static int check_label_or_ref_arg(enum todo_command command, const char *arg) { switch (command) { @@ -2716,30 +2737,24 @@ static int parse_insn_line(struct repository *r, struct replay_opts *opts, { struct object_id commit_oid; char *end_of_object_name; - int i, saved, status, padding; + int saved, status, padding; item->flags = 0; /* left-trim */ bol += strspn(bol, " \t"); - if (bol == eol || *bol == '\r' || starts_with_mem(bol, eol - bol, comment_line_str)) { - item->command = TODO_COMMENT; + if (!sequencer_parse_todo_command(&bol, &item->command)) + return error(_("invalid command '%.*s'"), + (int)strcspn(bol, " \t\r\n"), bol); + + if (item->command == TODO_COMMENT) { item->commit = NULL; item->arg_offset = bol - buf; item->arg_len = eol - bol; return 0; } - for (i = 0; i < TODO_COMMENT; i++) - if (is_command(i, &bol)) { - item->command = i; - break; - } - if (i >= TODO_COMMENT) - return error(_("invalid command '%.*s'"), - (int)strcspn(bol, " \t\r\n"), bol); - /* Eat up extra spaces/ tabs before object name */ padding = strspn(bol, " \t"); bol += padding; diff --git a/sequencer.h b/sequencer.h index a6fa670c7c1f99..28fabef926f351 100644 --- a/sequencer.h +++ b/sequencer.h @@ -262,6 +262,14 @@ int read_author_script(const char *path, char **name, char **email, char **date, int write_basic_state(struct replay_opts *opts, const char *head_name, struct commit *onto, const struct object_id *orig_head); void sequencer_post_commit_cleanup(struct repository *r, int verbose); + +/* + * Try to parse the todo command pointed to by *p. On success sets cmd, + * advances p and returns true. On failure returns false, leaves p and + * cmd unchanged. + */ +bool sequencer_parse_todo_command(const char **p, enum todo_command *cmd); + int sequencer_get_last_command(struct repository* r, enum replay_action *action); int sequencer_determine_whence(struct repository *r, enum commit_whence *whence); From 6f34e5f9e3b664fcd65a2a0695dff16b0ee04b35 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Tue, 23 Jun 2026 16:53:57 +0100 Subject: [PATCH 75/95] status: improve rebase todo list parsing When there is rebase in progress "git status" displays the last couple of completed and the next couple of pending commands from the todo list. When it does this it tries to abbreviate the object ids of the commits to be picked. Unfortunately it does not abbreviate the object ids when the line starts with "fixup -C" or "merge -C". It also mistakenly replaces the refname in "reset main" and "update-ref refs/heads/main" with the object id that the ref points to. Fix this by using the function added in the last commit to parse the command name and only try to abbreviate the argument for commands that take an object id. If a command accepts a label then try to resolve the object name as a label first and only if that fails try to resolve it as an object_id. When trying to abbreviate an object id, only replace the object name if it starts with the abbreviated object id so that tag or branch names that contain only hex digits are left unchanged. Comments are now processed after stripping any leading whitespace from the line. This matches what the sequencer does in parse_insn_line(). The existing test cases are updated to test a wider variety of commands. Only the pending commands in the tests are changed to avoid removing existing coverage. Helped-by: Elijah Newren Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- t/t7512-status-help.sh | 74 +++++++++++++-------- wt-status.c | 148 ++++++++++++++++++++++++++++++++++------- 2 files changed, 172 insertions(+), 50 deletions(-) diff --git a/t/t7512-status-help.sh b/t/t7512-status-help.sh index 08e82f79140841..aca4b6d33262df 100755 --- a/t/t7512-status-help.sh +++ b/t/t7512-status-help.sh @@ -224,7 +224,7 @@ test_expect_success 'status when splitting a commit' ' COMMIT3=$(git rev-parse --short split_commit) && test_commit four_split main.txt four && COMMIT4=$(git rev-parse --short split_commit) && - FAKE_LINES="1 edit 2 3" && + FAKE_LINES="reword 1 edit 2 fixup_-C 3" && export FAKE_LINES && test_when_finished "git rebase --abort" && ONTO=$(git rev-parse --short HEAD~3) && @@ -233,10 +233,10 @@ test_expect_success 'status when splitting a commit' ' cat >expected <todo <<-EOF && + edit several_edits^^ # two_edits + edit several_edits^ # three_edits + merge $(git rev-parse main) $(git rev-parse several_edits) + EOF + ( + set_replace_editor todo && + git rebase -i HEAD~3 + ) && git commit --amend -m "c" && git rebase --continue && git commit --amend -m "d" && @@ -477,7 +483,7 @@ Last commands done (2 commands done): edit $COMMIT2 # two_edits edit $COMMIT3 # three_edits Next command to do (1 remaining command): - pick $COMMIT4 # four_edits + merge $(git rev-parse --short main) $COMMIT4 (use "git rebase --edit-todo" to view and edit) You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''. (use "git commit --amend" to amend the current commit) @@ -525,14 +531,21 @@ EOF test_expect_success 'status: (split first edit) second edit and split' ' git reset --hard several_edits && - FAKE_LINES="edit 1 edit 2 3" && - export FAKE_LINES && test_when_finished "git rebase --abort" && COMMIT2=$(git rev-parse --short several_edits^^) && COMMIT3=$(git rev-parse --short several_edits^) && COMMIT4=$(git rev-parse --short several_edits) && + cat >todo <<-EOF && + edit several_edits^^ # two_edits + edit several_edits^ # three_edits + reset $(git rev-parse main) + merge -C several_edits topic # title + EOF ONTO=$(git rev-parse --short HEAD~3) && - git rebase -i HEAD~3 && + ( + set_replace_editor todo && + git rebase -i HEAD~3 + ) && git reset HEAD^ && git add main.txt && git commit --amend -m "f" && @@ -543,8 +556,9 @@ interactive rebase in progress; onto $ONTO Last commands done (2 commands done): edit $COMMIT2 # two_edits edit $COMMIT3 # three_edits -Next command to do (1 remaining command): - pick $COMMIT4 # four_edits +Next commands to do (2 remaining commands): + reset $(git rev-parse --short main) + merge -C $COMMIT4 topic # title (use "git rebase --edit-todo" to view and edit) You are currently splitting a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''. (Once your working directory is clean, run "git rebase --continue") @@ -563,14 +577,21 @@ EOF test_expect_success 'status: (split first edit) second edit and amend' ' git reset --hard several_edits && - FAKE_LINES="edit 1 edit 2 3" && - export FAKE_LINES && test_when_finished "git rebase --abort" && + git branch cafe main && COMMIT2=$(git rev-parse --short several_edits^^) && COMMIT3=$(git rev-parse --short several_edits^) && - COMMIT4=$(git rev-parse --short several_edits) && + cat >todo <<-EOF && + edit several_edits^^ # two_edits + edit several_edits^ # three_edits + update-ref refs/heads/main + reset cafe + EOF ONTO=$(git rev-parse --short HEAD~3) && - git rebase -i HEAD~3 && + ( + set_replace_editor todo && + git rebase -i HEAD~3 + ) && git reset HEAD^ && git add main.txt && git commit --amend -m "g" && @@ -581,8 +602,9 @@ interactive rebase in progress; onto $ONTO Last commands done (2 commands done): edit $COMMIT2 # two_edits edit $COMMIT3 # three_edits -Next command to do (1 remaining command): - pick $COMMIT4 # four_edits +Next commands to do (2 remaining commands): + update-ref refs/heads/main + reset cafe (use "git rebase --edit-todo" to view and edit) You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''. (use "git commit --amend" to amend the current commit) diff --git a/wt-status.c b/wt-status.c index 479ccc3304bc33..de36303c5264ce 100644 --- a/wt-status.c +++ b/wt-status.c @@ -1365,35 +1365,139 @@ static int split_commit_in_progress(struct wt_status *s) return split_in_progress; } +/* + * If the whitespace-delimited token starting at or just after *pp + * is a hex object id that is longer than its default abbreviation, + * abbreviate it in-place, shrinking `line` accordingly. On return + * *pp points one past the (possibly abbreviated) token. Leaves both + * `line` and *pp-advanced-past-the-token unchanged in all other cases + * (non-hex token, label name, unresolvable, or a refname that happens + * to consist only of hex digits). + */ +static void abbrev_oid_in_line(struct repository *r, struct strbuf *scratch, + struct strbuf *line, bool maybe_label, char **pp) +{ + char *p = *pp; + char *end_of_object_name, saved; + const char *abbrev; + struct object_id oid; + bool have_oid; + + p += strspn(p, " \t"); + end_of_object_name = p + strcspn(p, " \t"); + /* + * For "merge" and "reset" the object name may be a label or + * ref rather than a hex object id. Only abbreviate the object + * name if it is a hex object id. + */ + for (const char *q = p; q < end_of_object_name; q++) { + if (!isxdigit(*q)) + goto out; + } + if (maybe_label) { + strbuf_reset(scratch); + strbuf_addf(scratch, "refs/rewritten/%.*s", + (int)(end_of_object_name - p), p); + if (refs_ref_exists(get_main_ref_store(r), scratch->buf)) + goto out; /* object name was a label */ + } + saved = *end_of_object_name; + *end_of_object_name = '\0'; + have_oid = !repo_get_oid(r, p, &oid); + *end_of_object_name = saved; + if (!have_oid) + goto out; /* invalid object name */ + abbrev = repo_find_unique_abbrev(r, &oid, DEFAULT_ABBREV); + if (!starts_with(p, abbrev)) + goto out; /* object name was a refname containing only xdigits */ + p += strlen(abbrev); + strbuf_remove(line, p - line->buf, end_of_object_name - p); + end_of_object_name = p; +out: + *pp = end_of_object_name; +} + +/* Skip "[ \t]*(-[cC])?", returns true if "-c/-C" was skipped. */ +static bool skip_dash_c(char **pp) +{ + bool ret; + char *p = *pp; + + p += strspn(p, " \t"); + ret = skip_prefix(p, "-C", &p) || skip_prefix(p, "-c", &p); + *pp = p; + + return ret; +} + /* * Turn * "pick d6a2f0303e897ec257dd0e0a39a5ccb709bc2047 some message" * into * "pick d6a2f03 some message" * - * The function assumes that the line does not contain useless spaces - * before or after the command. + * Returns false on comment lines, true otherwise */ -static void abbrev_oid_in_line(struct repository *r, struct strbuf *line) +static bool format_todo_line(struct repository *r, struct strbuf *line) { - struct string_list split = STRING_LIST_INIT_DUP; - struct object_id oid; + enum todo_command cmd; + struct strbuf scratch = STRBUF_INIT; + char *p = line->buf; - if (starts_with(line->buf, "exec ") || - starts_with(line->buf, "x ") || - starts_with(line->buf, "label ") || - starts_with(line->buf, "l ")) - return; + if (!sequencer_parse_todo_command((const char **)&p, &cmd)) + return true; /* keep invalid lines */ + + switch (cmd) { + case TODO_COMMENT: + return false; + + case TODO_MERGE: { + /* + * The argument to -C cannot be a label, but the parents + * can be labels. + */ + bool maybe_label = !skip_dash_c(&p); + + while (true) { + p += strspn(p, " \t"); + if (!p[0] || (p[0] == '#' && (!p[1] || isspace(p[1])))) + break; + abbrev_oid_in_line(r, &scratch, line, maybe_label, &p); + maybe_label = true; + } + break; + } - if ((2 <= string_list_split(&split, line->buf, " ", 2)) && - !repo_get_oid(r, split.items[1].string, &oid)) { - strbuf_reset(line); - strbuf_addf(line, "%s ", split.items[0].string); - strbuf_add_unique_abbrev(line, &oid, DEFAULT_ABBREV); - for (size_t i = 2; i < split.nr; i++) - strbuf_addf(line, " %s", split.items[i].string); + case TODO_FIXUP: + skip_dash_c(&p); + /* fallthrough */ + case TODO_DROP: + case TODO_EDIT: + case TODO_PICK: + case TODO_REVERT: + case TODO_REWORD: + case TODO_SQUASH: + abbrev_oid_in_line(r, &scratch, line, false, &p); + break; + + case TODO_RESET: + abbrev_oid_in_line(r, &scratch, line, true, &p); + break; + /* + * Avoid "default" and instead list all the other commands so + * that -Wswitch (which is included in -Wall) warns if a new + * command is added without handling it in this function. + */ + case TODO_BREAK: + case TODO_EXEC: + case TODO_LABEL: + case TODO_NOOP: + case TODO_UPDATE_REF: + break; } - string_list_clear(&split, 0); + + strbuf_release(&scratch); + return true; } static int read_rebase_todolist(struct repository *r, const char *fname, struct string_list *lines) @@ -1411,13 +1515,9 @@ static int read_rebase_todolist(struct repository *r, const char *fname, struct repo_git_path_replace(r, &buf, "%s", fname)); } while (!strbuf_getline_lf(&buf, f)) { - if (starts_with(buf.buf, comment_line_str)) - continue; strbuf_trim(&buf); - if (!buf.len) - continue; - abbrev_oid_in_line(r, &buf); - string_list_append(lines, buf.buf); + if (format_todo_line(r, &buf)) + string_list_append(lines, buf.buf); } fclose(f); From 60cafea9078097f051f39c234fb6aa8f6051b2bd Mon Sep 17 00:00:00 2001 From: K Jayatheerth Date: Wed, 24 Jun 2026 09:07:46 +0530 Subject: [PATCH 76/95] path: extract format_path() and use in rev-parse Path formatting logic in builtin/rev-parse.c writes directly to stdout. Other builtins cannot reuse it. Extract this logic into format_path() in path.c and expose a path_format enum in path.h. Convert rev-parse to use the new helper in the same step to validate the API against existing tests and avoid introducing dead code. Mentored-by: Justin Tobler Mentored-by: Lucas Seiki Oshiro Signed-off-by: K Jayatheerth Signed-off-by: Junio C Hamano --- builtin/rev-parse.c | 79 +++++++++++++++++++++------------------------ path.c | 69 +++++++++++++++++++++++++++++++++++++++ path.h | 30 +++++++++++++++++ 3 files changed, 135 insertions(+), 43 deletions(-) diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c index bb882678fe2a9e..7d6ac920381dab 100644 --- a/builtin/rev-parse.c +++ b/builtin/rev-parse.c @@ -653,53 +653,46 @@ enum default_type { DEFAULT_UNMODIFIED, }; -static void print_path(const char *path, const char *prefix, enum format_type format, enum default_type def) +static void print_path(const char *path, const char *prefix, + enum format_type format, enum default_type def) { - char *cwd = NULL; - /* - * We don't ever produce a relative path if prefix is NULL, so set the - * prefix to the current directory so that we can produce a relative - * path whenever possible. If we're using RELATIVE_IF_SHARED mode, then - * we want an absolute path unless the two share a common prefix, so don't - * set it in that case, since doing so causes a relative path to always - * be produced if possible. - */ - if (!prefix && (format != FORMAT_DEFAULT || def != DEFAULT_RELATIVE_IF_SHARED)) - prefix = cwd = xgetcwd(); - if (format == FORMAT_DEFAULT && def == DEFAULT_UNMODIFIED) { - puts(path); - } else if (format == FORMAT_RELATIVE || - (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE)) { - /* - * In order for relative_path to work as expected, we need to - * make sure that both paths are absolute paths. If we don't, - * we can end up with an unexpected absolute path that the user - * didn't want. - */ - struct strbuf buf = STRBUF_INIT, realbuf = STRBUF_INIT, prefixbuf = STRBUF_INIT; - if (!is_absolute_path(path)) { - strbuf_realpath_forgiving(&realbuf, path, 1); - path = realbuf.buf; - } - if (!is_absolute_path(prefix)) { - strbuf_realpath_forgiving(&prefixbuf, prefix, 1); - prefix = prefixbuf.buf; + struct strbuf sb = STRBUF_INIT; + enum path_format fmt; + + if (format == FORMAT_DEFAULT) { + switch (def) { + case DEFAULT_RELATIVE: + fmt = PATH_FORMAT_RELATIVE; + break; + case DEFAULT_RELATIVE_IF_SHARED: + fmt = PATH_FORMAT_RELATIVE_IF_SHARED; + break; + case DEFAULT_CANONICAL: + fmt = PATH_FORMAT_CANONICAL; + break; + case DEFAULT_UNMODIFIED: + default: + fmt = PATH_FORMAT_UNMODIFIED; + break; } - puts(relative_path(path, prefix, &buf)); - strbuf_release(&buf); - strbuf_release(&realbuf); - strbuf_release(&prefixbuf); - } else if (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE_IF_SHARED) { - struct strbuf buf = STRBUF_INIT; - puts(relative_path(path, prefix, &buf)); - strbuf_release(&buf); } else { - struct strbuf buf = STRBUF_INIT; - strbuf_realpath_forgiving(&buf, path, 1); - puts(buf.buf); - strbuf_release(&buf); + switch (format) { + case FORMAT_RELATIVE: + fmt = PATH_FORMAT_RELATIVE; + break; + case FORMAT_CANONICAL: + fmt = PATH_FORMAT_CANONICAL; + break; + default: + fmt = PATH_FORMAT_UNMODIFIED; + break; + } } - free(cwd); + + format_path(&sb, path, prefix, fmt); + puts(sb.buf); + + strbuf_release(&sb); } int cmd_rev_parse(int argc, diff --git a/path.c b/path.c index d7e17bf17404de..c3a709a9284b7f 100644 --- a/path.c +++ b/path.c @@ -1579,6 +1579,75 @@ char *xdg_cache_home(const char *filename) return NULL; } +void format_path(struct strbuf *dest, const char *path, + const char *prefix, enum path_format format) +{ + strbuf_reset(dest); + + switch (format) { + case PATH_FORMAT_UNMODIFIED: + strbuf_addstr(dest, path); + break; + + case PATH_FORMAT_RELATIVE: { + struct strbuf relative_buf = STRBUF_INIT; + struct strbuf real_path = STRBUF_INIT; + struct strbuf real_prefix = STRBUF_INIT; + char *cwd = NULL; + + /* + * We don't ever produce a relative path if prefix is NULL, + * so set the prefix to the current directory so that we can + * produce a relative path whenever possible. + */ + if (!prefix) + prefix = cwd = xgetcwd(); + + if (!is_absolute_path(path)) { + strbuf_realpath_forgiving(&real_path, path, 1); + path = real_path.buf; + } + if (!is_absolute_path(prefix)) { + strbuf_realpath_forgiving(&real_prefix, prefix, 1); + prefix = real_prefix.buf; + } + + strbuf_addstr(dest, relative_path(path, prefix, &relative_buf)); + + strbuf_release(&relative_buf); + strbuf_release(&real_path); + strbuf_release(&real_prefix); + free(cwd); + break; + } + + case PATH_FORMAT_RELATIVE_IF_SHARED: { + struct strbuf relative_buf = STRBUF_INIT; + + /* + * If we're using RELATIVE_IF_SHARED mode, then we want an + * absolute path unless the two share a common prefix, so don't + * default the prefix to the current working directory. Doing so + * would cause a relative path to always be produced if possible. + */ + strbuf_addstr(dest, relative_path(path, prefix, &relative_buf)); + strbuf_release(&relative_buf); + break; + } + + case PATH_FORMAT_CANONICAL: + /* + * strbuf_realpath_forgiving inherently resets the destination + * buffer, safely aligning with our replace semantics. + */ + strbuf_realpath_forgiving(dest, path, 1); + break; + + default: + BUG("unknown path_format value %d", format); + } +} + REPO_GIT_PATH_FUNC(squash_msg, "SQUASH_MSG") REPO_GIT_PATH_FUNC(merge_msg, "MERGE_MSG") REPO_GIT_PATH_FUNC(merge_rr, "MERGE_RR") diff --git a/path.h b/path.h index 0434ba5e07e806..2b4089bb7cdbf6 100644 --- a/path.h +++ b/path.h @@ -262,6 +262,36 @@ enum scld_error safe_create_leading_directories_no_share(char *path); int safe_create_file_with_leading_directories(struct repository *repo, const char *path); +/** + * The formatting strategy to apply when writing a path into a buffer. + */ +enum path_format { + /* Output the path exactly as-is without any modifications. */ + PATH_FORMAT_UNMODIFIED, + + /* Output a path relative to the provided directory prefix. */ + PATH_FORMAT_RELATIVE, + + /* Output a relative path only if the path shares a root with the prefix. */ + PATH_FORMAT_RELATIVE_IF_SHARED, + + /* Output a fully resolved, absolute canonical path. */ + PATH_FORMAT_CANONICAL +}; + +/** + * Format a path according to the specified formatting strategy and store + * the result in the given strbuf, replacing any existing contents. + * + * `dest` : The string buffer to store the formatted path into. + * `path` : The path string that needs to be formatted. + * `prefix` : The directory prefix to calculate relative offsets against. + * Pass NULL to default to the current working directory where applicable. + * `format` : The formatting behavior rule to execute. + */ +void format_path(struct strbuf *dest, const char *path, + const char *prefix, enum path_format format); + # ifdef USE_THE_REPOSITORY_VARIABLE # include "strbuf.h" # include "repository.h" From 1efca6d0b2304360901dccfcbd5ea4f276ecf8c9 Mon Sep 17 00:00:00 2001 From: K Jayatheerth Date: Wed, 24 Jun 2026 09:07:47 +0530 Subject: [PATCH 77/95] repo: add path.commondir with absolute and relative suffix formatting Scripts working with worktree setups need a reliable way to discover the common directory, which diverges from the git directory when multiple worktrees are in use. There is no way to retrieve this path from git repo info today. Introduce path.commondir.absolute and path.commondir.relative keys. Exposing explicit format variants rather than a single key with a default avoids ambiguity for scripts that require predictable output. Mentored-by: Justin Tobler Mentored-by: Lucas Seiki Oshiro Signed-off-by: K Jayatheerth Signed-off-by: Junio C Hamano --- Documentation/git-repo.adoc | 9 +++++++ builtin/repo.c | 26 +++++++++++++++++++ t/t1900-repo-info.sh | 52 +++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc index 42262c198347e5..890c34051da8de 100644 --- a/Documentation/git-repo.adoc +++ b/Documentation/git-repo.adoc @@ -104,6 +104,15 @@ values that they return: `object.format`:: The object format (hash algorithm) used in the repository. +`path.commondir.absolute`:: + The canonical absolute path to the Git repository's common + directory (the shared `.git` directory containing objects, + refs, and global configuration). + +`path.commondir.relative`:: + The path to the Git repository's common directory relative to + the current working directory. + `references.format`:: The reference storage format. The valid values are: + diff --git a/builtin/repo.c b/builtin/repo.c index 71a5c1c29c05fe..4c3fbc26b967cf 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -7,12 +7,14 @@ #include "hex.h" #include "odb.h" #include "parse-options.h" +#include "path.h" #include "path-walk.h" #include "progress.h" #include "quote.h" #include "ref-filter.h" #include "refs.h" #include "revision.h" +#include "setup.h" #include "strbuf.h" #include "string-list.h" #include "shallow.h" @@ -75,6 +77,28 @@ static int get_object_format(struct repository *repo, struct strbuf *buf) return 0; } +static int get_path_commondir_absolute(struct repository *repo, struct strbuf *buf) +{ + const char *common_dir = repo_get_common_dir(repo); + + if (!common_dir) + return error(_("unable to get common directory")); + + format_path(buf, common_dir, startup_info->prefix, PATH_FORMAT_CANONICAL); + return 0; +} + +static int get_path_commondir_relative(struct repository *repo, struct strbuf *buf) +{ + const char *common_dir = repo_get_common_dir(repo); + + if (!common_dir) + return error(_("unable to get common directory")); + + format_path(buf, common_dir, startup_info->prefix, PATH_FORMAT_RELATIVE); + return 0; +} + static int get_references_format(struct repository *repo, struct strbuf *buf) { strbuf_addstr(buf, @@ -87,6 +111,8 @@ static const struct repo_info_field repo_info_field[] = { { "layout.bare", get_layout_bare }, { "layout.shallow", get_layout_shallow }, { "object.format", get_object_format }, + { "path.commondir.absolute", get_path_commondir_absolute }, + { "path.commondir.relative", get_path_commondir_relative }, { "references.format", get_references_format }, }; diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh index 39bb77dda0c327..09158d29f927a4 100755 --- a/t/t1900-repo-info.sh +++ b/t/t1900-repo-info.sh @@ -155,4 +155,56 @@ test_expect_success 'git repo info -h shows only repo info usage' ' test_grep ! "git repo structure" actual ' +# Helper function to test path keys in both absolute and relative formats. +# $1: label for the test +# $2: field_name (e.g., commondir) +# $3: expected_dir (the directory name, e.g., .git or custom-common) +# $4: init_command (extra setup like exporting env vars) +test_repo_info_path () { + label=$1 + field_name=$2 + expected_dir=$3 + init_command=$4 + + test_expect_success "absolute: $label" ' + test_when_finished "rm -rf repo" && + git init repo && + ( + mkdir -p repo/sub && + cd repo/sub && + ROOT="$(test-tool path-utils real_path ..)" && export ROOT && + eval "$init_command" && + echo "path.$field_name.absolute=$ROOT/$expected_dir" >expect && + git repo info "path.$field_name.absolute" >actual && + test_cmp expect actual + ) + ' + + test_expect_success "relative: $label" ' + test_when_finished "rm -rf repo" && + git init repo && + ( + mkdir -p repo/sub && + cd repo/sub && + ROOT="$(test-tool path-utils real_path ..)" && export ROOT && + eval "$init_command" && + echo "path.$field_name.relative=../$expected_dir" >expect && + git repo info "path.$field_name.relative" >actual && + test_cmp expect actual + ) + ' +} + +test_repo_info_path 'commondir standard' 'commondir' '.git' + +test_repo_info_path 'commondir with GIT_COMMON_DIR and GIT_DIR' 'commondir' \ + 'custom-common' \ + 'GIT_COMMON_DIR="$ROOT/custom-common" && export GIT_COMMON_DIR && + GIT_DIR="../.git" && export GIT_DIR && + git init --bare "$ROOT/custom-common"' + +test_repo_info_path 'commondir with only GIT_DIR' 'commondir' \ + '.git' \ + 'GIT_DIR="../.git" && export GIT_DIR' + test_done From 3ac28d832aefe06e0fb9390d3e20a9c32350e052 Mon Sep 17 00:00:00 2001 From: K Jayatheerth Date: Wed, 24 Jun 2026 09:07:48 +0530 Subject: [PATCH 78/95] repo: add path.gitdir with absolute and relative suffix formatting Scripts need a stable way to locate the git directory without parsing rev-parse output or relying on its flag-driven path format selection. There is no way to retrieve this path from git repo info today. Introduce path.gitdir.absolute and path.gitdir.relative keys, consistent with the path.commondir keys added in the previous patch. Reuse the test_repo_info_path helper introduced there to validate both variants. Mentored-by: Justin Tobler Mentored-by: Lucas Seiki Oshiro Signed-off-by: K Jayatheerth Signed-off-by: Junio C Hamano --- Documentation/git-repo.adoc | 6 ++++++ builtin/repo.c | 24 ++++++++++++++++++++++++ t/t1900-repo-info.sh | 6 ++++++ 3 files changed, 36 insertions(+) diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc index 890c34051da8de..ed7d80c690c720 100644 --- a/Documentation/git-repo.adoc +++ b/Documentation/git-repo.adoc @@ -113,6 +113,12 @@ values that they return: The path to the Git repository's common directory relative to the current working directory. +`path.gitdir.absolute`:: + The canonical absolute path to the Git repository directory (the `.git` directory). + +`path.gitdir.relative`:: + The path to the Git repository directory relative to the current working directory. + `references.format`:: The reference storage format. The valid values are: + diff --git a/builtin/repo.c b/builtin/repo.c index 4c3fbc26b967cf..27c8caff38c6a8 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -99,6 +99,28 @@ static int get_path_commondir_relative(struct repository *repo, struct strbuf *b return 0; } +static int get_path_gitdir_absolute(struct repository *repo, struct strbuf *buf) +{ + const char *git_dir = repo_get_git_dir(repo); + + if (!git_dir) + return error(_("unable to get git directory")); + + format_path(buf, git_dir, startup_info->prefix, PATH_FORMAT_CANONICAL); + return 0; +} + +static int get_path_gitdir_relative(struct repository *repo, struct strbuf *buf) +{ + const char *git_dir = repo_get_git_dir(repo); + + if (!git_dir) + return error(_("unable to get git directory")); + + format_path(buf, git_dir, startup_info->prefix, PATH_FORMAT_RELATIVE); + return 0; +} + static int get_references_format(struct repository *repo, struct strbuf *buf) { strbuf_addstr(buf, @@ -113,6 +135,8 @@ static const struct repo_info_field repo_info_field[] = { { "object.format", get_object_format }, { "path.commondir.absolute", get_path_commondir_absolute }, { "path.commondir.relative", get_path_commondir_relative }, + { "path.gitdir.absolute", get_path_gitdir_absolute }, + { "path.gitdir.relative", get_path_gitdir_relative }, { "references.format", get_references_format }, }; diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh index 09158d29f927a4..ae8c22c81749ba 100755 --- a/t/t1900-repo-info.sh +++ b/t/t1900-repo-info.sh @@ -207,4 +207,10 @@ test_repo_info_path 'commondir with only GIT_DIR' 'commondir' \ '.git' \ 'GIT_DIR="../.git" && export GIT_DIR' +test_repo_info_path 'gitdir standard' 'gitdir' '.git' + +test_repo_info_path 'gitdir with explicit GIT_DIR' 'gitdir' \ + '.git' \ + 'GIT_DIR="../.git" && export GIT_DIR' + test_done From d45f956f2022a90d39b1ce82aea668cce73f1d75 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 25 Jun 2026 11:57:39 +0200 Subject: [PATCH 79/95] odb/source-packed: extract logic to skip certain packs The caller can pass flags that allow them to filter out specific kinds of objects when iterating objects via `odb_for_each_object()`. This only works for "normal" iteration though, as we `BUG()` when the user passes flags and specifies an object prefix. This limitation will be lifted in the next commit. Prepare for this by extracting the logic that skips certain kinds of packs so that we can easily reuse it. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-packed.c | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/odb/source-packed.c b/odb/source-packed.c index 42c28fba0e34b2..3afc4bf01f74bc 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -126,6 +126,22 @@ static int match_hash(unsigned len, const unsigned char *a, const unsigned char return 1; } +static bool should_exclude_pack(struct packed_git *p, enum odb_for_each_object_flags flags) +{ + if ((flags & ODB_FOR_EACH_OBJECT_LOCAL_ONLY) && !p->pack_local) + return true; + if ((flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY) && + !p->pack_promisor) + return true; + if ((flags & ODB_FOR_EACH_OBJECT_SKIP_IN_CORE_KEPT_PACKS) && + p->pack_keep_in_core) + return true; + if ((flags & ODB_FOR_EACH_OBJECT_SKIP_ON_DISK_KEPT_PACKS) && + p->pack_keep) + return true; + return false; +} + static int for_each_prefixed_object_in_midx( struct odb_source_packed *store, struct multi_pack_index *m, @@ -306,17 +322,9 @@ static int odb_source_packed_for_each_object(struct odb_source *source, for (e = packfile_store_get_packs(packed); e; e = e->next) { struct packed_git *p = e->pack; - if ((opts->flags & ODB_FOR_EACH_OBJECT_LOCAL_ONLY) && !p->pack_local) - continue; - if ((opts->flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY) && - !p->pack_promisor) - continue; - if ((opts->flags & ODB_FOR_EACH_OBJECT_SKIP_IN_CORE_KEPT_PACKS) && - p->pack_keep_in_core) - continue; - if ((opts->flags & ODB_FOR_EACH_OBJECT_SKIP_ON_DISK_KEPT_PACKS) && - p->pack_keep) + if (should_exclude_pack(p, opts->flags)) continue; + if (open_pack_index(p)) { pack_errors = 1; continue; From 8ed957112de135f449698e9408f2582eabb9401e Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 25 Jun 2026 11:57:40 +0200 Subject: [PATCH 80/95] odb/source-packed: support flags when iterating an object prefix Callers of `odb_for_each_object()` can specify an optional object name prefix so that we only yield objects that match it. This is incompatible though with passing flags at the same time, as we don't yet know to handle them. Loosen this restriction by calling `should_exclude_pack()`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-packed.c | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/odb/source-packed.c b/odb/source-packed.c index 3afc4bf01f74bc..96fc436770afd1 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -148,6 +148,7 @@ static int for_each_prefixed_object_in_midx( const struct odb_for_each_object_options *opts, struct odb_source_packed_for_each_object_wrapper_data *data) { + bool pack_errors = false; int ret; for (; m; m = m->base_midx) { @@ -176,6 +177,20 @@ static int for_each_prefixed_object_in_midx( if (!match_hash(len, opts->prefix->hash, current->hash)) break; + if (opts->flags) { + uint32_t pack_id = nth_midxed_pack_int_id(m, i); + struct packed_git *pack; + + if (prepare_midx_pack(m, pack_id)) { + pack_errors = true; + continue; + } + + pack = nth_midxed_pack(m, pack_id); + if (should_exclude_pack(pack, opts->flags)) + continue; + } + if (data->request) { struct object_info oi = *data->request; @@ -198,6 +213,8 @@ static int for_each_prefixed_object_in_midx( ret = 0; out: + if (!ret && pack_errors) + ret = -1; return ret; } @@ -260,9 +277,6 @@ static int odb_source_packed_for_each_prefixed_object( bool pack_errors = false; int ret; - if (opts->flags) - BUG("flags unsupported"); - store->skip_mru_updates = true; m = get_multi_pack_index(store); @@ -275,6 +289,8 @@ static int odb_source_packed_for_each_prefixed_object( for (e = packfile_store_get_packs(store); e; e = e->next) { if (e->pack->multi_pack_index) continue; + if (should_exclude_pack(e->pack, opts->flags)) + continue; if (open_pack_index(e->pack)) { pack_errors = true; From 0a7f3389f4c16da05a1113ef0829a352af62dcbe Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 25 Jun 2026 11:57:41 +0200 Subject: [PATCH 81/95] connected: split out promisor-based connectivity check When performing a connectivity check in a partial clone we try to avoid doing the connectivity check by checking whether all new tips are part of a promisor pack. This makes use of the fact that we don't expect full connectivity for promised objects anyway, so it's basically fine if those objects are not fully connected. The logic that handles this promisor-based check is somewhat hard to read though as it uses nested loops and gotos. Pull it out into a standalone function, which makes it a bit easier to reason about. We'll also further simplify the function in the next commit. Suggested-by: Christian Couder Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- connected.c | 85 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/connected.c b/connected.c index 7e269768327238..d2b334173f0e76 100644 --- a/connected.c +++ b/connected.c @@ -11,6 +11,49 @@ #include "packfile.h" #include "promisor-remote.h" +/* + * For partial clones, we don't want to have to do a regular connectivity check + * because we have to enumerate and exclude all promisor objects (slow), and + * then the connectivity check itself becomes a no-op because in a partial + * clone every object is a promisor object. Instead, just make sure we + * received, in a promisor packfile, the objects pointed to by each wanted ref. + * + * Before checking for promisor packs, be sure we have the latest pack-files + * loaded into memory. + * + * Returns 1 when all object IDs have been found in promisor packs, in which + * case we're fully connected and thus done. Returns 0 when we have found + * objects in non-promisor packs, in which case we'll have to fall back to the + * rev-list-based connectivity checks. Returns a negative error code on error. + */ +static int check_connected_promisor(oid_iterate_fn fn, + void *cb_data, + const struct object_id **oid) +{ + odb_reprepare(the_repository->objects); + do { + struct packed_git *p; + + repo_for_each_pack(the_repository, p) { + if (!p->pack_promisor) + continue; + if (find_pack_entry_one(*oid, p)) + goto promisor_pack_found; + } + + /* + * We have found an object that is not part of a promisor pack, + * and thus we cannot skip the full connectivity check. + */ + return 0; + +promisor_pack_found: + ; + } while ((*oid = fn(cb_data)) != NULL); + + return 1; +} + /* * If we feed all the commits we want to verify to this command * @@ -46,42 +89,16 @@ int check_connected(oid_iterate_fn fn, void *cb_data, } if (repo_has_promisor_remote(the_repository)) { - /* - * For partial clones, we don't want to have to do a regular - * connectivity check because we have to enumerate and exclude - * all promisor objects (slow), and then the connectivity check - * itself becomes a no-op because in a partial clone every - * object is a promisor object. Instead, just make sure we - * received, in a promisor packfile, the objects pointed to by - * each wanted ref. - * - * Before checking for promisor packs, be sure we have the - * latest pack-files loaded into memory. - */ - odb_reprepare(the_repository->objects); - do { - struct packed_git *p; - - repo_for_each_pack(the_repository, p) { - if (!p->pack_promisor) - continue; - if (find_pack_entry_one(oid, p)) - goto promisor_pack_found; - } - /* - * Fallback to rev-list with oid and the rest of the - * object IDs provided by fn. - */ - goto no_promisor_pack_found; -promisor_pack_found: - ; - } while ((oid = fn(cb_data)) != NULL); - if (opt->err_fd) - close(opt->err_fd); - return 0; + err = check_connected_promisor(fn, cb_data, &oid); + if (err) { + if (opt->err_fd) + close(opt->err_fd); + if (err > 0) + err = 0; + return err; + } } -no_promisor_pack_found: if (opt->shallow_file) { strvec_push(&rev_list.args, "--shallow-file"); strvec_push(&rev_list.args, opt->shallow_file); From 66ee9cb93086b27721a36e2ba877921dcf177d91 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 25 Jun 2026 11:57:42 +0200 Subject: [PATCH 82/95] connected: search promisor objects generically When performing connectivity checks we have to figure out whether any of the new objects are promisor objects, as we cannot assume full connectivity if so. This check is performed by iterating through all packfiles in the repository and searching each of them for the given object. Of course, this mechanism is quite specific to implementation details of the object database, as we assume that it uses packfiles in the first place. Refactor the logic so that we instead use `odb_for_each_object_ext()` with an object prefix filter and the `ODB_FOR_EACH_OBJECT_PROMISOR_ONLY` flag. This will yield all objects that have the exact object name and that are part of a promisor pack in a generic way. Add a test to verify that we indeed use the optimization. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- connected.c | 35 ++++++++++++++++++++++++----------- t/t5616-partial-clone.sh | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/connected.c b/connected.c index d2b334173f0e76..929b9bd28d6fab 100644 --- a/connected.c +++ b/connected.c @@ -11,6 +11,15 @@ #include "packfile.h" #include "promisor-remote.h" +static int promised_object_cb(const struct object_id *oid UNUSED, + struct object_info *oi UNUSED, + void *payload) +{ + bool *found = payload; + *found = true; + return 1; +} + /* * For partial clones, we don't want to have to do a regular connectivity check * because we have to enumerate and exclude all promisor objects (slow), and @@ -30,25 +39,29 @@ static int check_connected_promisor(oid_iterate_fn fn, void *cb_data, const struct object_id **oid) { + struct odb_for_each_object_options opts = { + .flags = ODB_FOR_EACH_OBJECT_PROMISOR_ONLY, + .prefix_hex_len = the_repository->hash_algo->hexsz, + }; + int err; + odb_reprepare(the_repository->objects); do { - struct packed_git *p; + bool found = false; - repo_for_each_pack(the_repository, p) { - if (!p->pack_promisor) - continue; - if (find_pack_entry_one(*oid, p)) - goto promisor_pack_found; - } + opts.prefix = *oid; + + err = odb_for_each_object_ext(the_repository->objects, NULL, + promised_object_cb, &found, &opts); + if (err < 0) + return err; /* * We have found an object that is not part of a promisor pack, * and thus we cannot skip the full connectivity check. */ - return 0; - -promisor_pack_found: - ; + if (!found) + return 0; } while ((*oid = fn(cb_data)) != NULL); return 1; diff --git a/t/t5616-partial-clone.sh b/t/t5616-partial-clone.sh index 1c2805accac636..905052072db7c1 100755 --- a/t/t5616-partial-clone.sh +++ b/t/t5616-partial-clone.sh @@ -97,6 +97,30 @@ test_expect_success 'partial fetch inherits filter settings' ' test_line_count = 5 observed ' +test_expect_success 'partial fetch does not spawn rev-list connectivity check' ' + test_when_finished "rm -rf connectivity-remote connectivity-client" && + git init connectivity-remote && + test_commit -C connectivity-remote one && + git -C connectivity-remote config uploadpack.allowfilter 1 && + git -C connectivity-remote config uploadpack.allowanysha1inwant 1 && + + git clone --no-checkout --filter=blob:none \ + "file://$(pwd)/connectivity-remote" connectivity-client && + + # When doing a partial fetch where all tips are part of a promisor pack + # we want to skip the connectivity check, as these objects are allowed + # to not be fully connected. + test_commit -C connectivity-remote two && + GIT_TRACE2_EVENT="$(pwd)/partial.trace" git -C connectivity-client fetch origin && + test_subcommand_flex ! git rev-list --objects --stdin Date: Thu, 25 Jun 2026 11:19:59 +0200 Subject: [PATCH 83/95] setup: inline `check_and_apply_repository_format()` We have two callsites of `check_and_apply_repository_format()`. In a subsequent commit we'll want to adapt one of those callsites to change the order in which we read and apply the repository format, at which point the helper function will not really be a good fit for us anymore. Inline the function to both of the callsites. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- setup.c | 47 ++++++++++++++++------------------------------- 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/setup.c b/setup.c index b4652651dfd454..a9db1f2c23f32a 100644 --- a/setup.c +++ b/setup.c @@ -1788,32 +1788,6 @@ int apply_repository_format(struct repository *repo, return 0; } -/* - * Check the repository format version in the path found in repo_get_git_dir(repo), - * and die if it is a version we don't understand. Generally one would - * set_git_dir() before calling this, and use it only for "are we in a valid - * repo?". - * - * If successful and fmt is not NULL, fill fmt with data. - */ -static void check_and_apply_repository_format(struct repository *repo, - struct repository_format *fmt, - enum apply_repository_format_flags flags) -{ - struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT; - struct strbuf err = STRBUF_INIT; - - if (!fmt) - fmt = &repo_fmt; - - check_repository_format_gently(repo_get_git_dir(repo), fmt, NULL); - if (apply_repository_format(repo, fmt, flags, &err) < 0) - die("%s", err.buf); - startup_info->have_repository = 1; - - clear_repository_format(&repo_fmt); -} - const char *enter_repo(struct repository *repo, const char *path, unsigned flags) { static struct strbuf validated_path = STRBUF_INIT; @@ -1887,9 +1861,17 @@ const char *enter_repo(struct repository *repo, const char *path, unsigned flags } if (is_git_directory(".")) { + struct repository_format fmt = REPOSITORY_FORMAT_INIT; + struct strbuf err = STRBUF_INIT; + set_git_dir(repo, ".", 0); - check_and_apply_repository_format(repo, NULL, - APPLY_REPOSITORY_FORMAT_HONOR_ENV); + check_repository_format_gently(".", &fmt, NULL); + if (apply_repository_format(repo, &fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0) + die("%s", err.buf); + startup_info->have_repository = 1; + + clear_repository_format(&fmt); + strbuf_release(&err); return path; } @@ -2820,6 +2802,7 @@ int init_db(struct repository *repo, int exist_ok = flags & INIT_DB_EXIST_OK; char *original_git_dir = real_pathdup(git_dir, 1); struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT; + struct strbuf err = STRBUF_INIT; if (real_git_dir) { struct stat st; @@ -2846,9 +2829,10 @@ int init_db(struct repository *repo, * config file, so this will not fail. What we are catching * is an attempt to reinitialize new repository with an old tool. */ - check_and_apply_repository_format(repo, &repo_fmt, - APPLY_REPOSITORY_FORMAT_HONOR_ENV); - + check_repository_format_gently(repo_get_git_dir(repo), &repo_fmt, NULL); + if (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0) + die("%s", err.buf); + startup_info->have_repository = 1; repository_format_configure(repo, &repo_fmt, hash, ref_storage_format); /* @@ -2904,6 +2888,7 @@ int init_db(struct repository *repo, } clear_repository_format(&repo_fmt); + strbuf_release(&err); free(original_git_dir); return 0; } From 93c4361c41c3ac554ce8f61d8551d272d39ccd79 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 25 Jun 2026 11:20:00 +0200 Subject: [PATCH 84/95] setup: stop applying repository format twice When discovering the repository in "setup.c" we apply the final repository format multiple times: - Once via `repository_format_configure()`, where we apply the hash algorithm and ref storage format to both `struct repository_format` and `struct repository`. - And once via `apply_repository_format()`, where we apply these two settings from `struct repository_format` to `struct repository`. With the current flow both of these are in fact necessary. But this is only because we call `repository_format_configure()` after we have called `apply_repository_format()`. Consequently, if we only changed the repository format in `repository_format_configure()` it would never propagate to the repository. Refactor the code so that we first configure the repository format before applying it to the repository so that we can stop setting the hash and reference storage format multiple times. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- setup.c | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/setup.c b/setup.c index a9db1f2c23f32a..27481559644bc8 100644 --- a/setup.c +++ b/setup.c @@ -2710,8 +2710,7 @@ static int read_default_format_config(const char *key, const char *value, return ret; } -static void repository_format_configure(struct repository *repo, - struct repository_format *repo_fmt, +static void repository_format_configure(struct repository_format *repo_fmt, int hash, enum ref_storage_format ref_format) { struct default_format_config cfg = { @@ -2748,7 +2747,6 @@ static void repository_format_configure(struct repository *repo, } else if (cfg.hash != GIT_HASH_UNKNOWN) { repo_fmt->hash_algo = cfg.hash; } - repo_set_hash_algo(repo, repo_fmt->hash_algo); env = getenv("GIT_DEFAULT_REF_FORMAT"); if (repo_fmt->version >= 0 && @@ -2786,9 +2784,6 @@ static void repository_format_configure(struct repository *repo, free(backend); } - - repo_set_ref_storage_format(repo, repo_fmt->ref_storage_format, - repo_fmt->ref_storage_payload); } int init_db(struct repository *repo, @@ -2830,10 +2825,10 @@ int init_db(struct repository *repo, * is an attempt to reinitialize new repository with an old tool. */ check_repository_format_gently(repo_get_git_dir(repo), &repo_fmt, NULL); + repository_format_configure(&repo_fmt, hash, ref_storage_format); if (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0) die("%s", err.buf); startup_info->have_repository = 1; - repository_format_configure(repo, &repo_fmt, hash, ref_storage_format); /* * Ensure `core.hidedotfiles` is processed. This must happen after we From 7d9ffe68f2fc525c676a71996550bc9a4d350d11 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 25 Jun 2026 11:20:01 +0200 Subject: [PATCH 85/95] setup: don't apply "GIT_REFERENCE_BACKEND" without a repository When discovering a repository we eventually also apply the "GIT_REFERENCE_BACKEND" environment variable to the repository. There's two problems with that: - We do this unconditionally, which is rather pointless: we really only have to configure the repository when we have found one. - We have already applied the repository format at that point in time, so we need to manually reapply it. Move the logic around so that we only apply the environment variable when a repository was discovered. This also allows us to drop the explcit call to `repo_set_ref_storage_format()` because we now adjust the format before we apply it via `apply_repository_format()`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- setup.c | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/setup.c b/setup.c index 27481559644bc8..79125db56567b7 100644 --- a/setup.c +++ b/setup.c @@ -1906,7 +1906,6 @@ const char *setup_git_directory_gently(struct repository *repo, 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; /* @@ -2032,6 +2031,25 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok) if (startup_info->have_repository) { struct strbuf err = STRBUF_INIT; + const char *ref_backend_uri; + + /* + * The env variable should override the repository config + * for 'extensions.refStorage'. + */ + ref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT); + if (ref_backend_uri) { + char *format; + + free(repo_fmt.ref_storage_payload); + + parse_reference_uri(ref_backend_uri, &format, &repo_fmt.ref_storage_payload); + repo_fmt.ref_storage_format = ref_storage_format_by_name(format); + if (repo_fmt.ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN) + die(_("unknown ref storage format: '%s'"), format); + + free(format); + } if (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0) @@ -2057,25 +2075,6 @@ const char *setup_git_directory_gently(struct repository *repo, 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(repo, format, payload); - - free(backend); - free(payload); - } - setup_original_cwd(repo); strbuf_release(&dir); From 1f43ff2c7e4c1575960ad3b1338922d7fa837c0c Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 25 Jun 2026 11:20:02 +0200 Subject: [PATCH 86/95] refs: unregister reference stores from "chdir_notify" When creating reference stores we register them with the "chdir_notify" subsystem. This is required because some of the paths we track may be relative paths, so we have to reparent them in case the current working directory changes. But while we register the reference stores, we never unregister them. This can have multiple outcomes: - For a repository's main reference database we essentially keep the pointer alive. We never free that database, either, and our leak checker doesn't notice because it's still registered. - For submodule and worktree reference databases we do eventually free them in `repo_clear()`, so we may keep pointers to free'd memory registered. We never notice though as we don't tend to chdir around in the middle of the process. We never noticed either of these symptoms, but they are obviously bad. Partially fix those issues by unregistering the reference stores when releasing them. The leak of the main reference database will be fixed in a subsequent commit. Note that this requires us to use `chdir_notify_register()` instead of `chdir_notify_reparent()`, as there is no infrastructure to unregister the latter. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- refs/files-backend.c | 22 +++++++++++++++++++--- refs/packed-backend.c | 16 +++++++++++++++- refs/reftable-backend.c | 16 +++++++++++++++- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/refs/files-backend.c b/refs/files-backend.c index a4c7858787127d..296981584b6f99 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -100,6 +100,23 @@ static void clear_loose_ref_cache(struct files_ref_store *refs) } } +static void files_ref_store_reparent(const char *name UNUSED, + const char *old_cwd, + const char *new_cwd, + void *payload) +{ + struct files_ref_store *refs = payload; + char *tmp; + + tmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir); + free(refs->base.gitdir); + refs->base.gitdir = tmp; + + tmp = reparent_relative_path(old_cwd, new_cwd, refs->gitcommondir); + free(refs->gitcommondir); + refs->gitcommondir = tmp; +} + /* * Create a new submodule ref cache and add it to the internal * set of caches. @@ -128,9 +145,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo, repo_config_get_bool(repo, "core.prefersymlinkrefs", &refs->prefer_symlink_refs); - chdir_notify_reparent("files-backend $GIT_DIR", &refs->base.gitdir); - chdir_notify_reparent("files-backend $GIT_COMMONDIR", - &refs->gitcommondir); + chdir_notify_register(NULL, files_ref_store_reparent, refs); strbuf_release(&refdir); @@ -182,6 +197,7 @@ static void files_ref_store_release(struct ref_store *ref_store) free(refs->gitcommondir); ref_store_release(refs->packed_ref_store); free(refs->packed_ref_store); + chdir_notify_unregister(NULL, files_ref_store_reparent, refs); } static void files_reflog_path(struct files_ref_store *refs, diff --git a/refs/packed-backend.c b/refs/packed-backend.c index 0acde48c452513..499cb55dface4f 100644 --- a/refs/packed-backend.c +++ b/refs/packed-backend.c @@ -211,6 +211,19 @@ static size_t snapshot_hexsz(const struct snapshot *snapshot) return snapshot->refs->base.repo->hash_algo->hexsz; } +static void packed_ref_store_reparent(const char *name UNUSED, + const char *old_cwd, + const char *new_cwd, + void *payload) +{ + struct packed_ref_store *refs = payload; + char *tmp; + + tmp = reparent_relative_path(old_cwd, new_cwd, refs->path); + free(refs->path); + refs->path = tmp; +} + /* * 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. @@ -229,7 +242,7 @@ struct ref_store *packed_ref_store_init(struct repository *repo, strbuf_addf(&sb, "%s/packed-refs", gitdir); refs->path = strbuf_detach(&sb, NULL); - chdir_notify_reparent("packed-refs", &refs->path); + chdir_notify_register(NULL, packed_ref_store_reparent, refs); return ref_store; } @@ -274,6 +287,7 @@ static void packed_ref_store_release(struct ref_store *ref_store) clear_snapshot(refs); rollback_lock_file(&refs->lock); delete_tempfile(&refs->tempfile); + chdir_notify_unregister(NULL, packed_ref_store_reparent, refs); free(refs->path); } diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index 4ae22922de558b..8c93070677dcc3 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -365,6 +365,19 @@ static int reftable_be_config(const char *var, const char *value, return 0; } +static void reftable_be_reparent(const char *name UNUSED, + const char *old_cwd, + const char *new_cwd, + void *payload) +{ + struct reftable_ref_store *refs = payload; + char *tmp; + + tmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir); + free(refs->base.gitdir); + refs->base.gitdir = tmp; +} + static struct ref_store *reftable_be_init(struct repository *repo, const char *payload, const char *gitdir, @@ -447,7 +460,7 @@ static struct ref_store *reftable_be_init(struct repository *repo, goto done; } - chdir_notify_reparent("reftables-backend $GIT_DIR", &refs->base.gitdir); + chdir_notify_register(NULL, reftable_be_reparent, refs); done: assert(refs->err != REFTABLE_API_ERROR); @@ -474,6 +487,7 @@ static void reftable_be_release(struct ref_store *ref_store) free(be); } strmap_clear(&refs->worktree_backends, 0); + chdir_notify_unregister(NULL, reftable_be_reparent, refs); } static int reftable_be_create_on_disk(struct ref_store *ref_store, From 5bf546755c0a9d758773538d1bd730785189c9d7 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 25 Jun 2026 11:20:03 +0200 Subject: [PATCH 87/95] chdir-notify: drop unused `chdir_notify_reparent()` With the preceding commit we've removed all callers of `chdir_notify_reparent()`, so the function is unused now. Drop it. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- chdir-notify.c | 26 -------------------------- chdir-notify.h | 6 +----- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/chdir-notify.c b/chdir-notify.c index f8bfe3cbef9aba..1237a45e2e6492 100644 --- a/chdir-notify.c +++ b/chdir-notify.c @@ -43,32 +43,6 @@ void chdir_notify_unregister(const char *name, chdir_notify_callback cb, } } -static void reparent_cb(const char *name, - const char *old_cwd, - const char *new_cwd, - void *data) -{ - char **path = data; - char *tmp = *path; - - if (!tmp) - return; - - *path = reparent_relative_path(old_cwd, new_cwd, tmp); - free(tmp); - - if (name) { - trace_printf_key(&trace_setup_key, - "setup: reparent %s to '%s'", - name, *path); - } -} - -void chdir_notify_reparent(const char *name, char **path) -{ - chdir_notify_register(name, reparent_cb, path); -} - int chdir_notify(const char *new_cwd) { struct strbuf old_cwd = STRBUF_INIT; diff --git a/chdir-notify.h b/chdir-notify.h index 81eb69d846e45d..36b4114472e31d 100644 --- a/chdir-notify.h +++ b/chdir-notify.h @@ -19,10 +19,7 @@ * chdir_notify_register("description", foo, data); * * In practice most callers will want to move a relative path to the new root; - * they can use the reparent_relative_path() helper for that. If that's all - * you're doing, you can also use the convenience function: - * - * chdir_notify_reparent("description", &my_path); + * they can use the reparent_relative_path() helper for that. * * Whenever a chdir event occurs, that will update my_path (if it's relative) * to adjust for the new cwd by freeing any existing string and allocating a @@ -43,7 +40,6 @@ typedef void (*chdir_notify_callback)(const char *name, void chdir_notify_register(const char *name, chdir_notify_callback cb, void *data); void chdir_notify_unregister(const char *name, chdir_notify_callback cb, void *data); -void chdir_notify_reparent(const char *name, char **path); /* * From 432b2e4f9ebd2cbafab641619067dce4d3f223bc Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 25 Jun 2026 11:20:04 +0200 Subject: [PATCH 88/95] repository: free main reference database While we release worktree and submodule reference databases when clearing a repository, we don't ever release the main reference database. This memory leak went unnoticed because its pointer is kept alive by the "chdir_notify" subsystem. Fix the memory leak. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- repository.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/repository.c b/repository.c index 187dd471c4e607..e2b5c6712b9367 100644 --- a/repository.c +++ b/repository.c @@ -421,6 +421,11 @@ void repo_clear(struct repository *repo) FREE_AND_NULL(repo->remote_state); } + if (repo->refs_private) { + ref_store_release(repo->refs_private); + FREE_AND_NULL(repo->refs_private); + } + strmap_for_each_entry(&repo->submodule_ref_stores, &iter, e) ref_store_release(e->value); strmap_clear(&repo->submodule_ref_stores, 1); From a9e4ce0aec738ca3047d77f3781159424703a2a3 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 25 Jun 2026 11:20:05 +0200 Subject: [PATCH 89/95] refs: move parsing of "core.logAllRefUpdates" back into ref stores In cc42c88945 (refs: extract out reflog config to generic layer, 2026-05-04) we have refactored how we parse "core.logAllRefUpdates" so that it happens in the generic layer. Unfortunately, this has worsened a preexisting issue where we may recurse when creating the reference store because of a chicken-and-egg problem between parsing the configuration and evaluating "onbranch" conditions. Prepare for a fix by essentially reverting that change so that we handle this setting in the respective backends again. The backends are already parsing other configuration anyway, so by moving the logic back in there we can ensure that all backend configuration is parsed the same way. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/checkout.c | 7 +++++-- refs.c | 10 +++++++++- refs.h | 9 +++++++++ refs/files-backend.c | 20 +++++++++++++++++--- refs/refs-internal.h | 6 ------ refs/reftable-backend.c | 20 +++++++++++--------- repo-settings.c | 16 ---------------- repo-settings.h | 9 --------- setup.c | 6 +++++- 9 files changed, 56 insertions(+), 47 deletions(-) diff --git a/builtin/checkout.c b/builtin/checkout.c index b78b3a1d16def4..aee84ca89742b0 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -952,10 +952,13 @@ static void update_refs_for_switch(const struct checkout_opts *opts, const char *old_desc, *reflog_msg; if (opts->new_branch) { if (opts->new_orphan_branch) { - enum log_refs_config log_all_ref_updates = - repo_settings_get_log_all_ref_updates(the_repository); + enum log_refs_config log_all_ref_updates = LOG_REFS_UNSET; + const char *value; char *refname; + if (!repo_config_get_string_tmp(the_repository, "core.logallrefupdates", &value)) + log_all_ref_updates = refs_parse_log_all_ref_updates_config(value); + refname = mkpathdup("refs/heads/%s", opts->new_orphan_branch); if (opts->new_branch_log && !should_autocreate_reflog(log_all_ref_updates, refname)) { diff --git a/refs.c b/refs.c index d3caa9a633503f..5b773b1c151e94 100644 --- a/refs.c +++ b/refs.c @@ -1053,6 +1053,15 @@ static char *normalize_reflog_message(const char *msg) return strbuf_detach(&sb, NULL); } +enum log_refs_config refs_parse_log_all_ref_updates_config(const char *value) +{ + if (value && !strcasecmp(value, "always")) + return LOG_REFS_ALWAYS; + else if (git_config_bool("core.logallrefupdates", value)) + return LOG_REFS_NORMAL; + return LOG_REFS_NONE; +} + int should_autocreate_reflog(enum log_refs_config log_all_ref_updates, const char *refname) { @@ -2327,7 +2336,6 @@ static struct ref_store *ref_store_init(struct repository *repo, struct ref_store *refs; struct ref_store_init_options opts = { .access_flags = flags, - .log_all_ref_updates = repo_settings_get_log_all_ref_updates(repo), }; be = find_ref_storage_backend(format); diff --git a/refs.h b/refs.h index 71d5c186d044bb..a381022c77065a 100644 --- a/refs.h +++ b/refs.h @@ -146,6 +146,15 @@ enum ref_transaction_error refs_verify_refname_available(struct ref_store *refs, int refs_ref_exists(struct ref_store *refs, const char *refname); +enum log_refs_config { + LOG_REFS_UNSET = -1, + LOG_REFS_NONE = 0, + LOG_REFS_NORMAL, + LOG_REFS_ALWAYS +}; + +enum log_refs_config refs_parse_log_all_ref_updates_config(const char *value); + int should_autocreate_reflog(enum log_refs_config log_all_ref_updates, const char *refname); diff --git a/refs/files-backend.c b/refs/files-backend.c index 296981584b6f99..79fb6735e1e3a3 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -117,6 +117,21 @@ static void files_ref_store_reparent(const char *name UNUSED, refs->gitcommondir = tmp; } +static int files_ref_store_config(const char *var, const char *value, + const struct config_context *ctx UNUSED, + void *payload) +{ + struct files_ref_store *refs = payload; + + if (!strcmp(var, "core.prefersymlinkrefs")) { + refs->prefer_symlink_refs = git_config_bool(var, value); + } else if (!strcmp(var, "core.logallrefupdates")) { + refs->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value); + } + + return 0; +} + /* * Create a new submodule ref cache and add it to the internal * set of caches. @@ -141,10 +156,9 @@ static struct ref_store *files_ref_store_init(struct repository *repo, refs->packed_ref_store = packed_ref_store_init(repo, NULL, refs->gitcommondir, opts); refs->store_flags = opts->access_flags; - refs->log_all_ref_updates = opts->log_all_ref_updates; - - repo_config_get_bool(repo, "core.prefersymlinkrefs", &refs->prefer_symlink_refs); + refs->log_all_ref_updates = LOG_REFS_UNSET; + repo_config(repo, files_ref_store_config, refs); chdir_notify_register(NULL, files_ref_store_reparent, refs); strbuf_release(&refdir); diff --git a/refs/refs-internal.h b/refs/refs-internal.h index a08d58900eb644..c3ac7b556f3716 100644 --- a/refs/refs-internal.h +++ b/refs/refs-internal.h @@ -406,12 +406,6 @@ struct ref_store; struct ref_store_init_options { /* The kind of operations that the ref_store is allowed to perform. */ unsigned int access_flags; - - /* - * Denotes under what conditions reflogs should be created when updating - * references. - */ - enum log_refs_config log_all_ref_updates; }; /* diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index 8c93070677dcc3..5115a3f4ce6f5f 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -332,34 +332,36 @@ static void fill_reftable_log_record(struct reftable_log_record *log, const stru static int reftable_be_config(const char *var, const char *value, const struct config_context *ctx, - void *_opts) + void *payload) { - struct reftable_write_options *opts = _opts; + struct reftable_ref_store *refs = payload; if (!strcmp(var, "reftable.blocksize")) { unsigned long block_size = git_config_ulong(var, value, ctx->kvi); if (block_size > 16777215) die("reftable block size cannot exceed 16MB"); - opts->block_size = block_size; + refs->write_options.block_size = block_size; } else if (!strcmp(var, "reftable.restartinterval")) { unsigned long restart_interval = git_config_ulong(var, value, ctx->kvi); if (restart_interval > UINT16_MAX) die("reftable block size cannot exceed %u", (unsigned)UINT16_MAX); - opts->restart_interval = restart_interval; + refs->write_options.restart_interval = restart_interval; } else if (!strcmp(var, "reftable.indexobjects")) { - opts->skip_index_objects = !git_config_bool(var, value); + refs->write_options.skip_index_objects = !git_config_bool(var, value); } else if (!strcmp(var, "reftable.geometricfactor")) { unsigned long factor = git_config_ulong(var, value, ctx->kvi); if (factor > UINT8_MAX) die("reftable geometric factor cannot exceed %u", (unsigned)UINT8_MAX); - opts->auto_compaction_factor = factor; + refs->write_options.auto_compaction_factor = factor; } else if (!strcmp(var, "reftable.locktimeout")) { int64_t lock_timeout = git_config_int64(var, value, ctx->kvi); if (lock_timeout > LONG_MAX) die("reftable lock timeout cannot exceed %"PRIdMAX, (intmax_t)LONG_MAX); if (lock_timeout < 0 && lock_timeout != -1) die("reftable lock timeout does not support negative values other than -1"); - opts->lock_timeout_ms = lock_timeout; + refs->write_options.lock_timeout_ms = lock_timeout; + } else if (!strcmp(var, "core.logallrefupdates")) { + refs->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value); } return 0; @@ -398,7 +400,6 @@ static struct ref_store *reftable_be_init(struct repository *repo, base_ref_store_init(&refs->base, repo, refdir.buf, &refs_be_reftable); strmap_init(&refs->worktree_backends); - refs->log_all_ref_updates = opts->log_all_ref_updates; refs->store_flags = opts->access_flags; switch (repo->hash_algo->format_id) { @@ -415,8 +416,9 @@ static struct ref_store *reftable_be_init(struct repository *repo, refs->write_options.disable_auto_compact = !git_env_bool("GIT_TEST_REFTABLE_AUTOCOMPACTION", 1); refs->write_options.lock_timeout_ms = 100; + refs->log_all_ref_updates = LOG_REFS_UNSET; - repo_config(repo, reftable_be_config, &refs->write_options); + repo_config(repo, reftable_be_config, refs); /* * It is somewhat unfortunate that we have to mirror the default block diff --git a/repo-settings.c b/repo-settings.c index 208e09ff17fcee..f3be3b8c5a3d09 100644 --- a/repo-settings.c +++ b/repo-settings.c @@ -177,22 +177,6 @@ void repo_settings_set_big_file_threshold(struct repository *repo, unsigned long repo->settings.big_file_threshold = value; } -enum log_refs_config repo_settings_get_log_all_ref_updates(struct repository *repo) -{ - const char *value; - - if (!repo_config_get_string_tmp(repo, "core.logallrefupdates", &value)) { - if (value && !strcasecmp(value, "always")) - return LOG_REFS_ALWAYS; - else if (git_config_bool("core.logallrefupdates", value)) - return LOG_REFS_NORMAL; - else - return LOG_REFS_NONE; - } - - return LOG_REFS_UNSET; -} - int repo_settings_get_warn_ambiguous_refs(struct repository *repo) { prepare_repo_settings(repo); diff --git a/repo-settings.h b/repo-settings.h index cad9c3f0cc15f3..e5253ead025c83 100644 --- a/repo-settings.h +++ b/repo-settings.h @@ -16,13 +16,6 @@ enum fetch_negotiation_setting { FETCH_NEGOTIATION_NOOP, }; -enum log_refs_config { - LOG_REFS_UNSET = -1, - LOG_REFS_NONE = 0, - LOG_REFS_NORMAL, - LOG_REFS_ALWAYS -}; - struct repo_settings { int initialized; @@ -86,8 +79,6 @@ struct repo_settings { void prepare_repo_settings(struct repository *r); void repo_settings_clear(struct repository *r); -/* Read the value for "core.logAllRefUpdates". */ -enum log_refs_config repo_settings_get_log_all_ref_updates(struct repository *repo); /* Read the value for "core.warnAmbiguousRefs". */ int repo_settings_get_warn_ambiguous_refs(struct repository *repo); /* Read the value for "core.hooksPath". */ diff --git a/setup.c b/setup.c index 79125db56567b7..592753457c29a4 100644 --- a/setup.c +++ b/setup.c @@ -2584,10 +2584,14 @@ static int create_default_files(struct repository *repo, if (is_bare_repository()) repo_config_set(repo, "core.bare", "true"); else { + const char *value; + repo_config_set(repo, "core.bare", "false"); + /* allow template config file to override the default */ - if (repo_settings_get_log_all_ref_updates(repo) == LOG_REFS_UNSET) + if (repo_config_get_string_tmp(repo, "core.logallrefupdates", &value)) repo_config_set(repo, "core.logallrefupdates", "true"); + if (needs_work_tree_config(original_git_dir, work_tree)) repo_config_set(repo, "core.worktree", work_tree); } From f016ec17fbbde5ae7d208f0585bc02eb63416a53 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 25 Jun 2026 11:20:06 +0200 Subject: [PATCH 90/95] refs/files: lazy-load configuration to fix chicken-and-egg When initializing the "files" reference backend we read the repository's config to parse "core.preferSymlinkRefs" and "core.logAllRefUpdates". This results in a chicken-and-egg problem though, because parsing the configuration may require us to have access to the reference store already when an "onbranch" condition exists. Luckily, all the configuration that we honor only relates to writing references. Consequently, we don't strictly need that configuration to be readily available at initialization time, and we can easiliy defer parsing it to a later point in time. Implement this fix and add tests that verify that we can indeed properly parse these config knobs via an "onbranch" condition. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- refs/files-backend.c | 44 +++++++++++++++++++++++++++---------- t/t0600-reffiles-backend.sh | 21 ++++++++++++++++++ 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/refs/files-backend.c b/refs/files-backend.c index 79fb6735e1e3a3..7ffe489f6a1a24 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -84,12 +84,21 @@ struct files_ref_store { unsigned int store_flags; char *gitcommondir; - enum log_refs_config log_all_ref_updates; - int prefer_symlink_refs; - struct ref_cache *loose; - struct ref_store *packed_ref_store; + + /* + * Options used when writing references. These are parsed from the + * config lazily on first use via `files_ref_store_write_options()` so + * that we don't have to access the configuration when initializing the + * ref store. Do not access these fields directly, but use the accessor + * instead. + */ + struct files_ref_store_write_options { + enum log_refs_config log_all_ref_updates; + int prefer_symlink_refs; + bool initialized; + } write_opts_lazy_loaded; }; static void clear_loose_ref_cache(struct files_ref_store *refs) @@ -121,17 +130,31 @@ static int files_ref_store_config(const char *var, const char *value, const struct config_context *ctx UNUSED, void *payload) { - struct files_ref_store *refs = payload; + struct files_ref_store_write_options *opts = payload; if (!strcmp(var, "core.prefersymlinkrefs")) { - refs->prefer_symlink_refs = git_config_bool(var, value); + opts->prefer_symlink_refs = git_config_bool(var, value); } else if (!strcmp(var, "core.logallrefupdates")) { - refs->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value); + opts->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value); } return 0; } +static const struct files_ref_store_write_options *files_ref_store_write_options(struct files_ref_store *refs) +{ + struct files_ref_store_write_options *opts = &refs->write_opts_lazy_loaded; + + if (opts->initialized) + return opts; + + opts->log_all_ref_updates = LOG_REFS_UNSET; + repo_config(refs->base.repo, files_ref_store_config, opts); + + opts->initialized = true; + return opts; +} + /* * Create a new submodule ref cache and add it to the internal * set of caches. @@ -156,9 +179,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo, refs->packed_ref_store = packed_ref_store_init(repo, NULL, refs->gitcommondir, opts); refs->store_flags = opts->access_flags; - refs->log_all_ref_updates = LOG_REFS_UNSET; - repo_config(repo, files_ref_store_config, refs); chdir_notify_register(NULL, files_ref_store_reparent, refs); strbuf_release(&refdir); @@ -1890,7 +1911,7 @@ static int log_ref_setup(struct files_ref_store *refs, const char *refname, int force_create, int *logfd, struct strbuf *err) { - enum log_refs_config log_refs_cfg = refs->log_all_ref_updates; + enum log_refs_config log_refs_cfg = files_ref_store_write_options(refs)->log_all_ref_updates; struct strbuf logfile_sb = STRBUF_INIT; char *logfile; @@ -3301,6 +3322,7 @@ static int files_transaction_finish(struct ref_store *ref_store, { struct files_ref_store *refs = files_downcast(ref_store, 0, "ref_transaction_finish"); + const struct files_ref_store_write_options *write_opts = files_ref_store_write_options(refs); size_t i; int ret = 0; struct strbuf sb = STRBUF_INIT; @@ -3340,7 +3362,7 @@ static int files_transaction_finish(struct ref_store *ref_store, * We try creating a symlink, if that succeeds we continue to the * next update. If not, we try and create a regular symref. */ - if (update->new_target && refs->prefer_symlink_refs) + if (update->new_target && write_opts->prefer_symlink_refs) /* * By using the `NOT_CONSTANT()` trick, we can avoid * errors by `clang`'s `-Wunreachable` logic that would diff --git a/t/t0600-reffiles-backend.sh b/t/t0600-reffiles-backend.sh index 74bfa2e9ba060d..bbbf6fa4228cd7 100755 --- a/t/t0600-reffiles-backend.sh +++ b/t/t0600-reffiles-backend.sh @@ -519,4 +519,25 @@ test_expect_success 'symref transaction supports false symlink config' ' test_cmp expect actual ' +test_expect_success SYMLINKS,!MINGW,!WITH_BREAKING_CHANGES 'core.preferSymlinkRefs can be set up via onbranch condition' ' + test_when_finished "git symbolic-ref -d TEST_SYMREF_HEAD" && + test_when_finished "rm -f .git/include" && + git update-ref refs/heads/new @ && + cat >.git/include <<-\EOF && + [core] + preferSymlinkRefs = true + EOF + test_config includeIf.onbranch:"$(git branch --show-current)".path \ + "$(pwd)/.git/include" && + cat >stdin <<-EOF && + start + symref-create TEST_SYMREF_HEAD refs/heads/new + prepare + commit + EOF + git update-ref --no-deref --stdin Date: Thu, 25 Jun 2026 11:20:07 +0200 Subject: [PATCH 91/95] reftable: split up write options When initializing the reftable stack the caller may optionally pass some write options. These write options mix up two different concerns though: - Of course, they allow the caller to configure how new reftables are being written. - But they also allow the caller to configure the stack itself, like its hash ID and the `on_reload` callback. This is somewhat awkward, as it doesn't easily give the caller the flexibility to for example write multiple reftables with different options. Furthermore, this requires us to eagerly parse relevant configuration when initializing the reftable backend. Refactor the code by splitting out those options that configure the stack itself. Creating a new stack will thus only require this limited set of options, whereas the caller is expected to pass write options to all functions that end up writing tables. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- refs/reftable-backend.c | 29 +++-- reftable/reftable-stack.h | 30 ++++- reftable/reftable-writer.h | 17 +-- reftable/stack.c | 100 ++++++++++----- reftable/stack.h | 2 +- reftable/writer.c | 21 ++-- reftable/writer.h | 1 + t/helper/test-reftable.c | 2 +- t/unit-tests/lib-reftable.c | 8 +- t/unit-tests/lib-reftable.h | 2 + t/unit-tests/u-reftable-merged.c | 9 +- t/unit-tests/u-reftable-readwrite.c | 38 ++++-- t/unit-tests/u-reftable-stack.c | 189 +++++++++++++--------------- t/unit-tests/u-reftable-table.c | 8 +- 14 files changed, 258 insertions(+), 198 deletions(-) diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index 5115a3f4ce6f5f..608d71cf10c818 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -48,9 +48,9 @@ static void reftable_backend_on_reload(void *payload) static int reftable_backend_init(struct reftable_backend *be, const char *path, - const struct reftable_write_options *_opts) + const struct reftable_stack_options *_opts) { - struct reftable_write_options opts = *_opts; + struct reftable_stack_options opts = *_opts; opts.on_reload = reftable_backend_on_reload; opts.on_reload_payload = be; return reftable_new_stack(&be->stack, path, &opts); @@ -140,6 +140,7 @@ struct reftable_ref_store { * is populated lazily when we try to resolve `worktrees/$worktree` refs. */ struct strmap worktree_backends; + struct reftable_stack_options stack_options; struct reftable_write_options write_options; unsigned int store_flags; @@ -190,7 +191,7 @@ static int backend_for_worktree(struct reftable_backend **out, CALLOC_ARRAY(*out, 1); store->err = ret = reftable_backend_init(*out, worktree_dir.buf, - &store->write_options); + &store->stack_options); if (ret < 0) { free(*out); goto out; @@ -404,10 +405,10 @@ static struct ref_store *reftable_be_init(struct repository *repo, switch (repo->hash_algo->format_id) { case GIT_SHA1_FORMAT_ID: - refs->write_options.hash_id = REFTABLE_HASH_SHA1; + refs->stack_options.hash_id = REFTABLE_HASH_SHA1; break; case GIT_SHA256_FORMAT_ID: - refs->write_options.hash_id = REFTABLE_HASH_SHA256; + refs->stack_options.hash_id = REFTABLE_HASH_SHA256; break; default: BUG("unknown hash algorithm %d", repo->hash_algo->format_id); @@ -441,7 +442,7 @@ static struct ref_store *reftable_be_init(struct repository *repo, } strbuf_addstr(&path, "/reftable"); refs->err = reftable_backend_init(&refs->main_backend, path.buf, - &refs->write_options); + &refs->stack_options); if (refs->err) goto done; @@ -457,7 +458,7 @@ static struct ref_store *reftable_be_init(struct repository *repo, strbuf_addstr(&refdir, "/reftable"); refs->err = reftable_backend_init(&refs->worktree_backend, refdir.buf, - &refs->write_options); + &refs->stack_options); if (refs->err) goto done; } @@ -997,6 +998,7 @@ static int prepare_transaction_update(struct write_transaction_table_arg **out, struct reftable_addition *addition; ret = reftable_stack_new_addition(&addition, be->stack, + &refs->write_options, REFTABLE_STACK_NEW_ADDITION_RELOAD); if (ret) { if (ret == REFTABLE_LOCK_ERROR) @@ -1685,9 +1687,9 @@ static int reftable_be_optimize(struct ref_store *ref_store, stack = refs->main_backend.stack; if (opts->flags & REFS_OPTIMIZE_AUTO) - ret = reftable_stack_auto_compact(stack); + ret = reftable_stack_auto_compact(stack, &refs->write_options); else - ret = reftable_stack_compact_all(stack, NULL); + ret = reftable_stack_compact_all(stack, &refs->write_options, NULL); if (ret < 0) { ret = error(_("unable to compact stack: %s"), reftable_error_str(ret)); @@ -1721,8 +1723,8 @@ static int reftable_be_optimize_required(struct ref_store *ref_store, if (opts->flags & REFS_OPTIMIZE_AUTO) use_heuristics = true; - return reftable_stack_compaction_required(stack, use_heuristics, - required); + return reftable_stack_compaction_required(stack, &refs->write_options, + use_heuristics, required); } struct write_create_symref_arg { @@ -1979,6 +1981,7 @@ static int reftable_be_rename_ref(struct ref_store *ref_store, if (ret) goto done; ret = reftable_stack_add(arg.be->stack, &write_copy_table, &arg, + &refs->write_options, REFTABLE_STACK_NEW_ADDITION_RELOAD); done: @@ -2009,6 +2012,7 @@ static int reftable_be_copy_ref(struct ref_store *ref_store, if (ret) goto done; ret = reftable_stack_add(arg.be->stack, &write_copy_table, &arg, + &refs->write_options, REFTABLE_STACK_NEW_ADDITION_RELOAD); done: @@ -2374,6 +2378,7 @@ static int reftable_be_create_reflog(struct ref_store *ref_store, arg.stack = be->stack; ret = reftable_stack_add(be->stack, &write_reflog_existence_table, &arg, + &refs->write_options, REFTABLE_STACK_NEW_ADDITION_RELOAD); done: @@ -2446,6 +2451,7 @@ static int reftable_be_delete_reflog(struct ref_store *ref_store, arg.stack = be->stack; ret = reftable_stack_add(be->stack, &write_reflog_delete_table, &arg, + &refs->write_options, REFTABLE_STACK_NEW_ADDITION_RELOAD); assert(ret != REFTABLE_API_ERROR); @@ -2568,6 +2574,7 @@ static int reftable_be_reflog_expire(struct ref_store *ref_store, goto done; ret = reftable_stack_new_addition(&add, be->stack, + &refs->write_options, REFTABLE_STACK_NEW_ADDITION_RELOAD); if (ret < 0) goto done; diff --git a/reftable/reftable-stack.h b/reftable/reftable-stack.h index 5f7be573fabf8e..11f9963f4f1e9f 100644 --- a/reftable/reftable-stack.h +++ b/reftable/reftable-stack.h @@ -26,11 +26,29 @@ */ struct reftable_stack; +/* Options related to opening a stack. */ +struct reftable_stack_options { + /* + * 4-byte identifier ("sha1", "s256") of the hash. Defaults to SHA1 if + * unset. + */ + enum reftable_hash hash_id; + + /* + * Callback function to execute whenever the stack is being reloaded. + * This can be used e.g. to discard cached information that relies on + * the old stack's data. The payload data will be passed as argument to + * the callback. + */ + void (*on_reload)(void *payload); + void *on_reload_payload; +}; + /* open a new reftable stack. The tables along with the table list will be * stored in 'dir'. Typically, this should be .git/reftables. */ int reftable_new_stack(struct reftable_stack **dest, const char *dir, - const struct reftable_write_options *opts); + const struct reftable_stack_options *opts); /* returns the update_index at which a next table should be written. */ uint64_t reftable_stack_next_update_index(struct reftable_stack *st); @@ -52,6 +70,7 @@ enum { */ int reftable_stack_new_addition(struct reftable_addition **dest, struct reftable_stack *st, + const struct reftable_write_options *opts, unsigned int flags); /* Adds a reftable to transaction. */ @@ -77,7 +96,9 @@ void reftable_addition_destroy(struct reftable_addition *add); int reftable_stack_add(struct reftable_stack *st, int (*write_table)(struct reftable_writer *wr, void *write_arg), - void *write_arg, unsigned flags); + void *write_arg, + const struct reftable_write_options *opts, + unsigned flags); struct reftable_iterator; @@ -122,6 +143,7 @@ struct reftable_log_expiry_config { /* compacts all reftables into a giant table. Expire reflog entries if config is * non-NULL */ int reftable_stack_compact_all(struct reftable_stack *st, + const struct reftable_write_options *opts, struct reftable_log_expiry_config *config); /* @@ -132,11 +154,13 @@ int reftable_stack_compact_all(struct reftable_stack *st, * compacted to maintain geometric progression. */ int reftable_stack_compaction_required(struct reftable_stack *st, + const struct reftable_write_options *opts, bool use_heuristics, bool *required); /* heuristically compact unbalanced table stack. */ -int reftable_stack_auto_compact(struct reftable_stack *st); +int reftable_stack_auto_compact(struct reftable_stack *st, + const struct reftable_write_options *opts); /* delete stale .ref tables. */ int reftable_stack_clean(struct reftable_stack *st); diff --git a/reftable/reftable-writer.h b/reftable/reftable-writer.h index a66db415c87637..6ff4ddfc600461 100644 --- a/reftable/reftable-writer.h +++ b/reftable/reftable-writer.h @@ -28,11 +28,6 @@ struct reftable_write_options { /* how often to write complete keys in each block. */ uint16_t restart_interval; - /* 4-byte identifier ("sha1", "s256") of the hash. - * Defaults to SHA1 if unset - */ - enum reftable_hash hash_id; - /* Default mode for creating files. If unset, use 0666 (+umask) */ unsigned int default_permissions; @@ -60,15 +55,6 @@ struct reftable_write_options { * negative value will cause us to block indefinitely. */ long lock_timeout_ms; - - /* - * Callback function to execute whenever the stack is being reloaded. - * This can be used e.g. to discard cached information that relies on - * the old stack's data. The payload data will be passed as argument to - * the callback. - */ - void (*on_reload)(void *payload); - void *on_reload_payload; }; /* reftable_block_stats holds statistics for a single block type */ @@ -114,7 +100,8 @@ struct reftable_writer; int reftable_writer_new(struct reftable_writer **out, ssize_t (*writer_func)(void *, const void *, size_t), int (*flush_func)(void *), - void *writer_arg, const struct reftable_write_options *opts); + void *writer_arg, enum reftable_hash hash_id, + const struct reftable_write_options *opts); /* * Set the range of update indices for the records we will add. When writing a diff --git a/reftable/stack.c b/reftable/stack.c index 1fba96ddb3366f..ab129267089f29 100644 --- a/reftable/stack.c +++ b/reftable/stack.c @@ -501,10 +501,10 @@ static int reftable_stack_reload_maybe_reuse(struct reftable_stack *st, } int reftable_new_stack(struct reftable_stack **dest, const char *dir, - const struct reftable_write_options *_opts) + const struct reftable_stack_options *_opts) { struct reftable_buf list_file_name = REFTABLE_BUF_INIT; - struct reftable_write_options opts = { 0 }; + struct reftable_stack_options opts = { 0 }; struct reftable_stack *p; int err; @@ -629,6 +629,7 @@ int reftable_stack_reload(struct reftable_stack *st) struct reftable_addition { struct reftable_flock tables_list_lock; struct reftable_stack *stack; + struct reftable_write_options opts; char **new_tables; size_t new_tables_len, new_tables_cap; @@ -657,6 +658,7 @@ static void reftable_addition_close(struct reftable_addition *add) static int reftable_stack_init_addition(struct reftable_addition *add, struct reftable_stack *st, + const struct reftable_write_options *opts, unsigned int flags) { struct reftable_buf lock_file_name = REFTABLE_BUF_INIT; @@ -664,15 +666,17 @@ static int reftable_stack_init_addition(struct reftable_addition *add, memset(add, 0, sizeof(*add)); add->stack = st; + if (opts) + add->opts = *opts; err = flock_acquire(&add->tables_list_lock, st->list_file, - st->opts.lock_timeout_ms); + add->opts.lock_timeout_ms); if (err < 0) goto done; - if (st->opts.default_permissions) { + if (add->opts.default_permissions) { if (chmod(add->tables_list_lock.path, - st->opts.default_permissions) < 0) { + add->opts.default_permissions) < 0) { err = REFTABLE_IO_ERROR; goto done; } @@ -702,12 +706,14 @@ static int reftable_stack_init_addition(struct reftable_addition *add, static int stack_try_add(struct reftable_stack *st, int (*write_table)(struct reftable_writer *wr, void *arg), - void *arg, unsigned flags) + void *arg, + const struct reftable_write_options *opts, + unsigned flags) { struct reftable_addition add; int err; - err = reftable_stack_init_addition(&add, st, flags); + err = reftable_stack_init_addition(&add, st, opts, flags); if (err < 0) goto done; @@ -723,9 +729,11 @@ static int stack_try_add(struct reftable_stack *st, int reftable_stack_add(struct reftable_stack *st, int (*write)(struct reftable_writer *wr, void *arg), - void *arg, unsigned flags) + void *arg, + const struct reftable_write_options *opts, + unsigned flags) { - int err = stack_try_add(st, write, arg, flags); + int err = stack_try_add(st, write, arg, opts, flags); if (err < 0) { if (err == REFTABLE_OUTDATED_ERROR) { /* Ignore error return, we want to propagate @@ -810,7 +818,7 @@ int reftable_addition_commit(struct reftable_addition *add) if (err) goto done; - if (!add->stack->opts.disable_auto_compact) { + if (!add->opts.disable_auto_compact) { /* * Auto-compact the stack to keep the number of tables in * control. It is possible that a concurrent writer is already @@ -820,7 +828,7 @@ int reftable_addition_commit(struct reftable_addition *add) * concurrent writer, which causes `REFTABLE_OUTDATED_ERROR`. * Both of these errors are benign, so we simply ignore them. */ - err = reftable_stack_auto_compact(add->stack); + err = reftable_stack_auto_compact(add->stack, &add->opts); if (err < 0 && err != REFTABLE_LOCK_ERROR && err != REFTABLE_OUTDATED_ERROR) goto done; @@ -834,6 +842,7 @@ int reftable_addition_commit(struct reftable_addition *add) int reftable_stack_new_addition(struct reftable_addition **dest, struct reftable_stack *st, + const struct reftable_write_options *opts, unsigned int flags) { int err; @@ -842,7 +851,7 @@ int reftable_stack_new_addition(struct reftable_addition **dest, if (!*dest) return REFTABLE_OUT_OF_MEMORY_ERROR; - err = reftable_stack_init_addition(*dest, st, flags); + err = reftable_stack_init_addition(*dest, st, opts, flags); if (err) { reftable_free(*dest); *dest = NULL; @@ -862,7 +871,7 @@ int reftable_addition_add(struct reftable_addition *add, struct reftable_writer *wr = NULL; struct reftable_tmpfile tab_file = REFTABLE_TMPFILE_INIT; struct fd_writer writer = { - .opts = &add->stack->opts, + .opts = &add->opts, }; int err = 0; @@ -883,9 +892,9 @@ int reftable_addition_add(struct reftable_addition *add, err = tmpfile_from_pattern(&tab_file, temp_tab_file_name.buf); if (err < 0) goto done; - if (add->stack->opts.default_permissions) { + if (add->opts.default_permissions) { if (chmod(tab_file.path, - add->stack->opts.default_permissions)) { + add->opts.default_permissions)) { err = REFTABLE_IO_ERROR; goto done; } @@ -893,7 +902,7 @@ int reftable_addition_add(struct reftable_addition *add, writer.fd = tab_file.fd; err = reftable_writer_new(&wr, fd_writer_write, fd_writer_flush, - &writer, &add->stack->opts); + &writer, add->stack->opts.hash_id, &add->opts); if (err < 0) goto done; @@ -1066,13 +1075,14 @@ static int stack_write_compact(struct reftable_stack *st, static int stack_compact_locked(struct reftable_stack *st, size_t first, size_t last, struct reftable_log_expiry_config *config, + const struct reftable_write_options *opts, struct reftable_tmpfile *tab_file_out) { struct reftable_buf next_name = REFTABLE_BUF_INIT; struct reftable_buf tab_file_path = REFTABLE_BUF_INIT; struct reftable_writer *wr = NULL; struct fd_writer writer= { - .opts = &st->opts, + .opts = opts, }; struct reftable_tmpfile tab_file = REFTABLE_TMPFILE_INIT; int err = 0; @@ -1094,15 +1104,15 @@ static int stack_compact_locked(struct reftable_stack *st, if (err < 0) goto done; - if (st->opts.default_permissions && - chmod(tab_file.path, st->opts.default_permissions) < 0) { + if (opts->default_permissions && + chmod(tab_file.path, opts->default_permissions) < 0) { err = REFTABLE_IO_ERROR; goto done; } writer.fd = tab_file.fd; err = reftable_writer_new(&wr, fd_writer_write, fd_writer_flush, - &writer, &st->opts); + &writer, st->opts.hash_id, opts); if (err < 0) goto done; @@ -1150,6 +1160,7 @@ enum stack_compact_range_flags { static int stack_compact_range(struct reftable_stack *st, size_t first, size_t last, struct reftable_log_expiry_config *expiry, + const struct reftable_write_options *opts, unsigned int flags) { struct reftable_buf tables_list_buf = REFTABLE_BUF_INIT; @@ -1175,7 +1186,7 @@ static int stack_compact_range(struct reftable_stack *st, * Hold the lock so that we can read "tables.list" and lock all tables * which are part of the user-specified range. */ - err = flock_acquire(&tables_list_lock, st->list_file, st->opts.lock_timeout_ms); + err = flock_acquire(&tables_list_lock, st->list_file, opts->lock_timeout_ms); if (err < 0) goto done; @@ -1274,7 +1285,7 @@ static int stack_compact_range(struct reftable_stack *st, * these tables may end up with an empty new table in case tombstones * end up cancelling out all refs in that range. */ - err = stack_compact_locked(st, first, last, expiry, &new_table); + err = stack_compact_locked(st, first, last, expiry, opts, &new_table); if (err < 0) { if (err != REFTABLE_EMPTY_TABLE_ERROR) goto done; @@ -1286,13 +1297,13 @@ static int stack_compact_range(struct reftable_stack *st, * "tables.list". We'll then replace the compacted range of tables with * the new table. */ - err = flock_acquire(&tables_list_lock, st->list_file, st->opts.lock_timeout_ms); + err = flock_acquire(&tables_list_lock, st->list_file, opts->lock_timeout_ms); if (err < 0) goto done; - if (st->opts.default_permissions) { + if (opts->default_permissions) { if (chmod(tables_list_lock.path, - st->opts.default_permissions) < 0) { + opts->default_permissions) < 0) { err = REFTABLE_IO_ERROR; goto done; } @@ -1513,10 +1524,16 @@ static int stack_compact_range(struct reftable_stack *st, } int reftable_stack_compact_all(struct reftable_stack *st, + const struct reftable_write_options *opts, struct reftable_log_expiry_config *config) { + struct reftable_write_options opts_default = { 0 }; size_t last = st->merged->tables_len ? st->merged->tables_len - 1 : 0; - return stack_compact_range(st, 0, last, config, 0); + + if (!opts) + opts = &opts_default; + + return stack_compact_range(st, 0, last, config, opts, 0); } static int segment_size(struct segment *s) @@ -1601,6 +1618,7 @@ struct segment suggest_compaction_segment(uint64_t *sizes, size_t n, } static int stack_segments_for_compaction(struct reftable_stack *st, + const struct reftable_write_options *opts, struct segment *seg) { int version = (st->opts.hash_id == REFTABLE_HASH_SHA1) ? 1 : 2; @@ -1615,13 +1633,14 @@ static int stack_segments_for_compaction(struct reftable_stack *st, sizes[i] = st->tables[i]->size - overhead; *seg = suggest_compaction_segment(sizes, st->merged->tables_len, - st->opts.auto_compaction_factor); + opts->auto_compaction_factor); reftable_free(sizes); return 0; } static int update_segment_if_compaction_required(struct reftable_stack *st, + const struct reftable_write_options *opts, struct segment *seg, bool use_geometric, bool *required) @@ -1638,7 +1657,7 @@ static int update_segment_if_compaction_required(struct reftable_stack *st, return 0; } - err = stack_segments_for_compaction(st, seg); + err = stack_segments_for_compaction(st, opts, seg); if (err) return err; @@ -1647,27 +1666,40 @@ static int update_segment_if_compaction_required(struct reftable_stack *st, } int reftable_stack_compaction_required(struct reftable_stack *st, + const struct reftable_write_options *opts, bool use_heuristics, bool *required) { + struct reftable_write_options opts_default = { 0 }; struct segment seg; - return update_segment_if_compaction_required(st, &seg, use_heuristics, - required); + + if (!opts) + opts = &opts_default; + + return update_segment_if_compaction_required(st, opts, &seg, + use_heuristics, required); } -int reftable_stack_auto_compact(struct reftable_stack *st) +int reftable_stack_auto_compact(struct reftable_stack *st, + const struct reftable_write_options *opts) { + struct reftable_write_options opts_default = { 0 }; struct segment seg; bool required; int err; - err = update_segment_if_compaction_required(st, &seg, true, &required); + if (!opts) + opts = &opts_default; + + err = update_segment_if_compaction_required(st, opts, &seg, true, + &required); if (err) return err; if (required) return stack_compact_range(st, seg.start, seg.end - 1, - NULL, STACK_COMPACT_RANGE_BEST_EFFORT); + NULL, opts, + STACK_COMPACT_RANGE_BEST_EFFORT); return 0; } @@ -1807,7 +1839,7 @@ static int reftable_stack_clean_locked(struct reftable_stack *st) int reftable_stack_clean(struct reftable_stack *st) { struct reftable_addition *add = NULL; - int err = reftable_stack_new_addition(&add, st, 0); + int err = reftable_stack_new_addition(&add, st, NULL, 0); if (err < 0) { goto done; } diff --git a/reftable/stack.h b/reftable/stack.h index bc28f2998ac027..f7901e6c6f5a01 100644 --- a/reftable/stack.h +++ b/reftable/stack.h @@ -20,7 +20,7 @@ struct reftable_stack { char *reftable_dir; - struct reftable_write_options opts; + struct reftable_stack_options opts; struct reftable_table **tables; size_t tables_len; diff --git a/reftable/writer.c b/reftable/writer.c index 0133b649759bcf..f850e9d599e387 100644 --- a/reftable/writer.c +++ b/reftable/writer.c @@ -80,9 +80,6 @@ static void options_set_defaults(struct reftable_write_options *opts) opts->restart_interval = 16; } - if (opts->hash_id == 0) { - opts->hash_id = REFTABLE_HASH_SHA1; - } if (opts->block_size == 0) { opts->block_size = DEFAULT_BLOCK_SIZE; } @@ -90,7 +87,7 @@ static void options_set_defaults(struct reftable_write_options *opts) static int writer_version(struct reftable_writer *w) { - return (w->opts.hash_id == 0 || w->opts.hash_id == REFTABLE_HASH_SHA1) ? + return (w->hash_id == 0 || w->hash_id == REFTABLE_HASH_SHA1) ? 1 : 2; } @@ -107,7 +104,7 @@ static int writer_write_header(struct reftable_writer *w, uint8_t *dest) if (writer_version(w) == 2) { uint32_t hash_id; - switch (w->opts.hash_id) { + switch (w->hash_id) { case REFTABLE_HASH_SHA1: hash_id = REFTABLE_FORMAT_ID_SHA1; break; @@ -134,7 +131,7 @@ static int writer_reinit_block_writer(struct reftable_writer *w, uint8_t typ) reftable_buf_reset(&w->last_key); ret = block_writer_init(&w->block_writer_data, typ, w->block, w->opts.block_size, block_start, - hash_size(w->opts.hash_id)); + hash_size(w->hash_id)); if (ret < 0) return ret; @@ -147,7 +144,9 @@ static int writer_reinit_block_writer(struct reftable_writer *w, uint8_t typ) int reftable_writer_new(struct reftable_writer **out, ssize_t (*writer_func)(void *, const void *, size_t), int (*flush_func)(void *), - void *writer_arg, const struct reftable_write_options *_opts) + void *writer_arg, + enum reftable_hash hash_id, + const struct reftable_write_options *_opts) { struct reftable_write_options opts = {0}; struct reftable_writer *wp; @@ -162,6 +161,9 @@ int reftable_writer_new(struct reftable_writer **out, if (opts.block_size >= (1 << 24)) return REFTABLE_API_ERROR; + if (!hash_id) + hash_id = REFTABLE_HASH_SHA1; + reftable_buf_init(&wp->block_writer_data.last_key); reftable_buf_init(&wp->last_key); reftable_buf_init(&wp->scratch); @@ -173,6 +175,7 @@ int reftable_writer_new(struct reftable_writer **out, wp->write = writer_func; wp->write_arg = writer_arg; wp->opts = opts; + wp->hash_id = hash_id; wp->flush = flush_func; writer_reinit_block_writer(wp, REFTABLE_BLOCK_TYPE_REF); @@ -367,7 +370,7 @@ int reftable_writer_add_ref(struct reftable_writer *w, if (!w->opts.skip_index_objects && reftable_ref_record_val1(ref)) { reftable_buf_reset(&w->scratch); err = reftable_buf_add(&w->scratch, (char *)reftable_ref_record_val1(ref), - hash_size(w->opts.hash_id)); + hash_size(w->hash_id)); if (err < 0) goto out; @@ -379,7 +382,7 @@ int reftable_writer_add_ref(struct reftable_writer *w, if (!w->opts.skip_index_objects && reftable_ref_record_val2(ref)) { reftable_buf_reset(&w->scratch); err = reftable_buf_add(&w->scratch, reftable_ref_record_val2(ref), - hash_size(w->opts.hash_id)); + hash_size(w->hash_id)); if (err < 0) goto out; diff --git a/reftable/writer.h b/reftable/writer.h index 9f53610b27a161..c08fc413e19367 100644 --- a/reftable/writer.h +++ b/reftable/writer.h @@ -27,6 +27,7 @@ struct reftable_writer { uint64_t next; uint64_t min_update_index, max_update_index; struct reftable_write_options opts; + enum reftable_hash hash_id; /* memory buffer for writing */ uint8_t *block; diff --git a/t/helper/test-reftable.c b/t/helper/test-reftable.c index b16c0722c84aa8..fc49fafc3466f4 100644 --- a/t/helper/test-reftable.c +++ b/t/helper/test-reftable.c @@ -174,7 +174,7 @@ static int dump_table(struct reftable_merged_table *mt) static int dump_stack(const char *stackdir, uint32_t hash_id) { struct reftable_stack *stack = NULL; - struct reftable_write_options opts = { .hash_id = hash_id }; + struct reftable_stack_options opts = { .hash_id = hash_id }; struct reftable_merged_table *merged = NULL; int err = reftable_new_stack(&stack, stackdir, &opts); diff --git a/t/unit-tests/lib-reftable.c b/t/unit-tests/lib-reftable.c index fdb5b11a2034f2..19a3ac8b809410 100644 --- a/t/unit-tests/lib-reftable.c +++ b/t/unit-tests/lib-reftable.c @@ -25,11 +25,12 @@ static int strbuf_writer_flush(void *arg UNUSED) } struct reftable_writer *cl_reftable_strbuf_writer(struct reftable_buf *buf, + enum reftable_hash hash_id, struct reftable_write_options *opts) { struct reftable_writer *writer; int ret = reftable_writer_new(&writer, &strbuf_writer_write, &strbuf_writer_flush, - buf, opts); + buf, hash_id, opts); cl_assert(!ret); return writer; } @@ -39,6 +40,7 @@ void cl_reftable_write_to_buf(struct reftable_buf *buf, size_t nrefs, struct reftable_log_record *logs, size_t nlogs, + enum reftable_hash hash_id, struct reftable_write_options *_opts) { struct reftable_write_options opts = { 0 }; @@ -66,7 +68,7 @@ void cl_reftable_write_to_buf(struct reftable_buf *buf, min = ui; } - writer = cl_reftable_strbuf_writer(buf, &opts); + writer = cl_reftable_strbuf_writer(buf, hash_id, &opts); ret = reftable_writer_set_limits(writer, min, max); cl_assert(!ret); @@ -88,7 +90,7 @@ void cl_reftable_write_to_buf(struct reftable_buf *buf, size_t off = i * (opts.block_size ? opts.block_size : DEFAULT_BLOCK_SIZE); if (!off) - off = header_size(opts.hash_id == REFTABLE_HASH_SHA256 ? 2 : 1); + off = header_size(hash_id == REFTABLE_HASH_SHA256 ? 2 : 1); cl_assert(buf->buf[off] == 'r'); } diff --git a/t/unit-tests/lib-reftable.h b/t/unit-tests/lib-reftable.h index d7e6d3136f5d2f..caf443d147e1ed 100644 --- a/t/unit-tests/lib-reftable.h +++ b/t/unit-tests/lib-reftable.h @@ -10,6 +10,7 @@ struct reftable_buf; void cl_reftable_set_hash(uint8_t *p, int i, enum reftable_hash id); struct reftable_writer *cl_reftable_strbuf_writer(struct reftable_buf *buf, + enum reftable_hash hash_id, struct reftable_write_options *opts); void cl_reftable_write_to_buf(struct reftable_buf *buf, @@ -17,4 +18,5 @@ void cl_reftable_write_to_buf(struct reftable_buf *buf, size_t nrecords, struct reftable_log_record *logs, size_t nlogs, + enum reftable_hash hash_id, struct reftable_write_options *opts); diff --git a/t/unit-tests/u-reftable-merged.c b/t/unit-tests/u-reftable-merged.c index 54cb7fc2a71465..21232c1e4f7455 100644 --- a/t/unit-tests/u-reftable-merged.c +++ b/t/unit-tests/u-reftable-merged.c @@ -34,7 +34,8 @@ merged_table_from_records(struct reftable_ref_record **refs, cl_assert(*source != NULL); for (size_t i = 0; i < n; i++) { - cl_reftable_write_to_buf(&buf[i], refs[i], sizes[i], NULL, 0, &opts); + cl_reftable_write_to_buf(&buf[i], refs[i], sizes[i], NULL, 0, + REFTABLE_HASH_SHA1, &opts); block_source_from_buf(&(*source)[i], &buf[i]); err = reftable_table_new(&(*tables)[i], &(*source)[i], @@ -357,7 +358,8 @@ merged_table_from_log_records(struct reftable_log_record **logs, cl_assert(*source != NULL); for (size_t i = 0; i < n; i++) { - cl_reftable_write_to_buf(&buf[i], NULL, 0, logs[i], sizes[i], &opts); + cl_reftable_write_to_buf(&buf[i], NULL, 0, logs[i], sizes[i], + REFTABLE_HASH_SHA1, &opts); block_source_from_buf(&(*source)[i], &buf[i]); err = reftable_table_new(&(*tables)[i], &(*source)[i], @@ -487,7 +489,8 @@ void test_reftable_merged__default_write_opts(void) { struct reftable_write_options opts = { 0 }; struct reftable_buf buf = REFTABLE_BUF_INIT; - struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts); + struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, + REFTABLE_HASH_SHA1, &opts); struct reftable_ref_record rec = { .refname = (char *) "master", .update_index = 1, diff --git a/t/unit-tests/u-reftable-readwrite.c b/t/unit-tests/u-reftable-readwrite.c index 4d8c4be5f14010..5794b460c6020d 100644 --- a/t/unit-tests/u-reftable-readwrite.c +++ b/t/unit-tests/u-reftable-readwrite.c @@ -48,7 +48,6 @@ static void write_table(char ***names, struct reftable_buf *buf, int N, { struct reftable_write_options opts = { .block_size = block_size, - .hash_id = hash_id, }; struct reftable_ref_record *refs; struct reftable_log_record *logs; @@ -78,7 +77,7 @@ static void write_table(char ***names, struct reftable_buf *buf, int N, logs[i].value.update.message = (char *) "message"; } - cl_reftable_write_to_buf(buf, refs, N, logs, N, &opts); + cl_reftable_write_to_buf(buf, refs, N, logs, N, hash_id, &opts); reftable_free(refs); reftable_free(logs); @@ -103,6 +102,7 @@ void test_reftable_readwrite__log_buffer_size(void) .message = (char *) "commit: 9\n", } } }; struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, + REFTABLE_HASH_SHA1, &opts); /* This tests buffer extension for log compression. Must use a random @@ -143,6 +143,7 @@ void test_reftable_readwrite__log_overflow(void) }, }; struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, + REFTABLE_HASH_SHA1, &opts); memset(msg, 'x', sizeof(msg) - 1); @@ -157,6 +158,7 @@ void test_reftable_readwrite__log_write_limits(void) struct reftable_write_options opts = { 0 }; struct reftable_buf buf = REFTABLE_BUF_INIT; struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, + REFTABLE_HASH_SHA1, &opts); struct reftable_log_record log = { .refname = (char *)"refs/head/master", @@ -202,7 +204,9 @@ void test_reftable_readwrite__log_write_read(void) struct reftable_table *table; struct reftable_block_source source = { 0 }; struct reftable_buf buf = REFTABLE_BUF_INIT; - struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts); + struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, + REFTABLE_HASH_SHA1, + &opts); const struct reftable_stats *stats = NULL; int N = 2, i; char **names; @@ -299,6 +303,7 @@ void test_reftable_readwrite__log_zlib_corruption(void) struct reftable_block_source source = { 0 }; struct reftable_buf buf = REFTABLE_BUF_INIT; struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, + REFTABLE_HASH_SHA1, &opts); const struct reftable_stats *stats = NULL; char message[100] = { 0 }; @@ -531,6 +536,7 @@ static void t_table_refs_for(int indexed) struct reftable_block_source source = { 0 }; struct reftable_buf buf = REFTABLE_BUF_INIT; struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, + REFTABLE_HASH_SHA1, &opts); struct reftable_iterator it = { 0 }; int N = 50, j, i; @@ -622,7 +628,9 @@ void test_reftable_readwrite__write_empty_table(void) { struct reftable_write_options opts = { 0 }; struct reftable_buf buf = REFTABLE_BUF_INIT; - struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts); + struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, + REFTABLE_HASH_SHA1, + &opts); struct reftable_block_source source = { 0 }; struct reftable_table *table = NULL; struct reftable_ref_record rec = { 0 }; @@ -660,7 +668,9 @@ void test_reftable_readwrite__write_object_id_min_length(void) .block_size = 75, }; struct reftable_buf buf = REFTABLE_BUF_INIT; - struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts); + struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, + REFTABLE_HASH_SHA1, + &opts); struct reftable_ref_record ref = { .update_index = 1, .value_type = REFTABLE_REF_VAL1, @@ -691,7 +701,9 @@ void test_reftable_readwrite__write_object_id_length(void) .block_size = 75, }; struct reftable_buf buf = REFTABLE_BUF_INIT; - struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts); + struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, + REFTABLE_HASH_SHA1, + &opts); struct reftable_ref_record ref = { .update_index = 1, .value_type = REFTABLE_REF_VAL1, @@ -721,7 +733,9 @@ void test_reftable_readwrite__write_empty_key(void) { struct reftable_write_options opts = { 0 }; struct reftable_buf buf = REFTABLE_BUF_INIT; - struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts); + struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, + REFTABLE_HASH_SHA1, + &opts); struct reftable_ref_record ref = { .refname = (char *) "", .update_index = 1, @@ -740,7 +754,9 @@ void test_reftable_readwrite__write_key_order(void) { struct reftable_write_options opts = { 0 }; struct reftable_buf buf = REFTABLE_BUF_INIT; - struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts); + struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, + REFTABLE_HASH_SHA1, + &opts); struct reftable_ref_record refs[2] = { { .refname = (char *) "b", @@ -787,7 +803,8 @@ void test_reftable_readwrite__write_multiple_indices(void) int i; int err; - writer = cl_reftable_strbuf_writer(&writer_buf, &opts); + writer = cl_reftable_strbuf_writer(&writer_buf, REFTABLE_HASH_SHA1, + &opts); reftable_writer_set_limits(writer, 1, 1); for (i = 0; i < 100; i++) { struct reftable_ref_record ref = { @@ -861,7 +878,8 @@ void test_reftable_readwrite__write_multi_level_index(void) struct reftable_table *table; int err; - writer = cl_reftable_strbuf_writer(&writer_buf, &opts); + writer = cl_reftable_strbuf_writer(&writer_buf, REFTABLE_HASH_SHA1, + &opts); reftable_writer_set_limits(writer, 1, 1); for (size_t i = 0; i < 200; i++) { struct reftable_ref_record ref = { diff --git a/t/unit-tests/u-reftable-stack.c b/t/unit-tests/u-reftable-stack.c index b8110cdeee664b..e6c163594013d7 100644 --- a/t/unit-tests/u-reftable-stack.c +++ b/t/unit-tests/u-reftable-stack.c @@ -111,10 +111,9 @@ static int write_test_ref(struct reftable_writer *wr, void *arg) static void write_n_ref_tables(struct reftable_stack *st, size_t n) { - int disable_auto_compact; - - disable_auto_compact = st->opts.disable_auto_compact; - st->opts.disable_auto_compact = 1; + struct reftable_write_options opts = { + .disable_auto_compact = 1, + }; for (size_t i = 0; i < n; i++) { struct reftable_ref_record ref = { @@ -128,10 +127,8 @@ static void write_n_ref_tables(struct reftable_stack *st, cl_reftable_set_hash(ref.value.val1, i, REFTABLE_HASH_SHA1); cl_assert_equal_i(reftable_stack_add(st, - &write_test_ref, &ref, 0), 0); + &write_test_ref, &ref, &opts, 0), 0); } - - st->opts.disable_auto_compact = disable_auto_compact; } struct write_log_arg { @@ -168,10 +165,10 @@ void test_reftable_stack__add_one(void) struct stat stat_result = { 0 }; int err; - err = reftable_new_stack(&st, dir, &opts); + err = reftable_new_stack(&st, dir, NULL); cl_assert(!err); - err = reftable_stack_add(st, write_test_ref, &ref, 0); + err = reftable_stack_add(st, write_test_ref, &ref, &opts, 0); cl_assert(!err); err = reftable_stack_read_ref(st, ref.refname, &dest); @@ -210,7 +207,6 @@ void test_reftable_stack__add_one(void) void test_reftable_stack__uptodate(void) { - struct reftable_write_options opts = { 0 }; struct reftable_stack *st1 = NULL; struct reftable_stack *st2 = NULL; char *dir = get_tmp_dir(__LINE__); @@ -232,15 +228,15 @@ void test_reftable_stack__uptodate(void) /* simulate multi-process access to the same stack by creating two stacks for the same directory. */ - cl_assert_equal_i(reftable_new_stack(&st1, dir, &opts), 0); - cl_assert_equal_i(reftable_new_stack(&st2, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st1, dir, NULL), 0); + cl_assert_equal_i(reftable_new_stack(&st2, dir, NULL), 0); cl_assert_equal_i(reftable_stack_add(st1, write_test_ref, - &ref1, 0), 0); + &ref1, NULL, 0), 0); cl_assert_equal_i(reftable_stack_add(st2, write_test_ref, - &ref2, 0), REFTABLE_OUTDATED_ERROR); + &ref2, NULL, 0), REFTABLE_OUTDATED_ERROR); cl_assert_equal_i(reftable_stack_reload(st2), 0); cl_assert_equal_i(reftable_stack_add(st2, write_test_ref, - &ref2, 0), 0); + &ref2, NULL, 0), 0); reftable_stack_destroy(st1); reftable_stack_destroy(st2); clear_dir(dir); @@ -249,7 +245,6 @@ void test_reftable_stack__uptodate(void) void test_reftable_stack__transaction_api(void) { char *dir = get_tmp_dir(__LINE__); - struct reftable_write_options opts = { 0 }; struct reftable_stack *st = NULL; struct reftable_addition *add = NULL; @@ -261,11 +256,11 @@ void test_reftable_stack__transaction_api(void) }; struct reftable_ref_record dest = { 0 }; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); reftable_addition_destroy(add); - cl_assert_equal_i(reftable_stack_new_addition(&add, st, 0), 0); + cl_assert_equal_i(reftable_stack_new_addition(&add, st, NULL, 0), 0); cl_assert_equal_i(reftable_addition_add(add, write_test_ref, &ref), 0); cl_assert_equal_i(reftable_addition_commit(add), 0); @@ -306,7 +301,7 @@ void test_reftable_stack__transaction_with_reload(void) cl_assert_equal_i(reftable_new_stack(&st1, dir, NULL), 0); cl_assert_equal_i(reftable_new_stack(&st2, dir, NULL), 0); - cl_assert_equal_i(reftable_stack_new_addition(&add, st1, 0), 0); + cl_assert_equal_i(reftable_stack_new_addition(&add, st1, NULL, 0), 0); cl_assert_equal_i(reftable_addition_add(add, write_test_ref, &refs[0]), 0); cl_assert_equal_i(reftable_addition_commit(add), 0); @@ -317,9 +312,9 @@ void test_reftable_stack__transaction_with_reload(void) * create the addition and lock the stack by default, but allow the * reload to happen when REFTABLE_STACK_NEW_ADDITION_RELOAD is set. */ - cl_assert_equal_i(reftable_stack_new_addition(&add, st2, 0), + cl_assert_equal_i(reftable_stack_new_addition(&add, st2, NULL, 0), REFTABLE_OUTDATED_ERROR); - cl_assert_equal_i(reftable_stack_new_addition(&add, st2, + cl_assert_equal_i(reftable_stack_new_addition(&add, st2, NULL, REFTABLE_STACK_NEW_ADDITION_RELOAD), 0); cl_assert_equal_i(reftable_addition_add(add, write_test_ref, &refs[1]), 0); @@ -342,12 +337,11 @@ void test_reftable_stack__transaction_with_reload(void) void test_reftable_stack__transaction_api_performs_auto_compaction(void) { char *dir = get_tmp_dir(__LINE__); - struct reftable_write_options opts = {0}; struct reftable_addition *add = NULL; struct reftable_stack *st = NULL; size_t n = 20; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); for (size_t i = 0; i <= n; i++) { struct reftable_ref_record ref = { @@ -356,6 +350,9 @@ void test_reftable_stack__transaction_api_performs_auto_compaction(void) .value.symref = (char *) "master", }; char name[100]; + struct reftable_write_options write_opts = { + .disable_auto_compact = (i != n), + }; snprintf(name, sizeof(name), "branch%04"PRIuMAX, (uintmax_t)i); ref.refname = name; @@ -365,10 +362,8 @@ void test_reftable_stack__transaction_api_performs_auto_compaction(void) * we can ensure that we indeed honor this setting and have * better control over when exactly auto compaction runs. */ - st->opts.disable_auto_compact = i != n; - cl_assert_equal_i(reftable_stack_new_addition(&add, - st, 0), 0); + st, &write_opts, 0), 0); cl_assert_equal_i(reftable_addition_add(add, write_test_ref, &ref), 0); cl_assert_equal_i(reftable_addition_commit(add), 0); @@ -398,15 +393,14 @@ void test_reftable_stack__auto_compaction_fails_gracefully(void) .value_type = REFTABLE_REF_VAL1, .value.val1 = {0x01}, }; - struct reftable_write_options opts = { 0 }; struct reftable_stack *st; struct reftable_buf table_path = REFTABLE_BUF_INIT; char *dir = get_tmp_dir(__LINE__); int err; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); cl_assert_equal_i(reftable_stack_add(st, write_test_ref, - &ref, 0), 0); + &ref, NULL, 0), 0); cl_assert_equal_i(st->merged->tables_len, 1); cl_assert_equal_i(st->stats.attempts, 0); cl_assert_equal_i(st->stats.failures, 0); @@ -424,7 +418,7 @@ void test_reftable_stack__auto_compaction_fails_gracefully(void) write_file_buf(table_path.buf, "", 0); ref.update_index = 2; - err = reftable_stack_add(st, write_test_ref, &ref, 0); + err = reftable_stack_add(st, write_test_ref, &ref, NULL, 0); cl_assert(!err); cl_assert_equal_i(st->merged->tables_len, 2); cl_assert_equal_i(st->stats.attempts, 1); @@ -443,7 +437,6 @@ static int write_error(struct reftable_writer *wr UNUSED, void *arg) void test_reftable_stack__update_index_check(void) { char *dir = get_tmp_dir(__LINE__); - struct reftable_write_options opts = { 0 }; struct reftable_stack *st = NULL; struct reftable_ref_record ref1 = { .refname = (char *) "name1", @@ -458,11 +451,11 @@ void test_reftable_stack__update_index_check(void) .value.symref = (char *) "master", }; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); cl_assert_equal_i(reftable_stack_add(st, write_test_ref, - &ref1, 0), 0); + &ref1, NULL, 0), 0); cl_assert_equal_i(reftable_stack_add(st, write_test_ref, - &ref2, 0), REFTABLE_API_ERROR); + &ref2, NULL, 0), REFTABLE_API_ERROR); reftable_stack_destroy(st); clear_dir(dir); } @@ -470,14 +463,13 @@ void test_reftable_stack__update_index_check(void) void test_reftable_stack__lock_failure(void) { char *dir = get_tmp_dir(__LINE__); - struct reftable_write_options opts = { 0 }; struct reftable_stack *st = NULL; int i; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); for (i = -1; i != REFTABLE_EMPTY_TABLE_ERROR; i--) cl_assert_equal_i(reftable_stack_add(st, write_error, - &i, 0), i); + &i, NULL, 0), i); reftable_stack_destroy(st); clear_dir(dir); @@ -499,7 +491,7 @@ void test_reftable_stack__add(void) size_t i, N = ARRAY_SIZE(refs); int err = 0; - err = reftable_new_stack(&st, dir, &opts); + err = reftable_new_stack(&st, dir, NULL); cl_assert(!err); for (i = 0; i < N; i++) { @@ -521,7 +513,7 @@ void test_reftable_stack__add(void) for (i = 0; i < N; i++) cl_assert_equal_i(reftable_stack_add(st, write_test_ref, - &refs[i], 0), 0); + &refs[i], &opts, 0), 0); for (i = 0; i < N; i++) { struct write_log_arg arg = { @@ -529,10 +521,10 @@ void test_reftable_stack__add(void) .update_index = reftable_stack_next_update_index(st), }; cl_assert_equal_i(reftable_stack_add(st, write_test_log, - &arg, 0), 0); + &arg, &opts, 0), 0); } - cl_assert_equal_i(reftable_stack_compact_all(st, NULL), 0); + cl_assert_equal_i(reftable_stack_compact_all(st, &opts, NULL), 0); for (i = 0; i < N; i++) { struct reftable_ref_record dest = { 0 }; @@ -584,7 +576,6 @@ void test_reftable_stack__add(void) void test_reftable_stack__iterator(void) { - struct reftable_write_options opts = { 0 }; struct reftable_stack *st = NULL; char *dir = get_tmp_dir(__LINE__); struct reftable_ref_record refs[10] = { 0 }; @@ -593,7 +584,7 @@ void test_reftable_stack__iterator(void) size_t N = ARRAY_SIZE(refs), i; int err; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); for (i = 0; i < N; i++) { refs[i].refname = xstrfmt("branch%02"PRIuMAX, (uintmax_t)i); @@ -613,7 +604,7 @@ void test_reftable_stack__iterator(void) for (i = 0; i < N; i++) cl_assert_equal_i(reftable_stack_add(st, write_test_ref, - &refs[i], 0), 0); + &refs[i], NULL, 0), 0); for (i = 0; i < N; i++) { struct write_log_arg arg = { @@ -622,7 +613,7 @@ void test_reftable_stack__iterator(void) }; cl_assert_equal_i(reftable_stack_add(st, write_test_log, - &arg, 0), 0); + &arg, NULL, 0), 0); } reftable_stack_init_ref_iterator(st, &it); @@ -669,9 +660,6 @@ void test_reftable_stack__iterator(void) void test_reftable_stack__log_normalize(void) { - struct reftable_write_options opts = { - 0, - }; struct reftable_stack *st = NULL; char *dir = get_tmp_dir(__LINE__); struct reftable_log_record input = { @@ -693,15 +681,15 @@ void test_reftable_stack__log_normalize(void) .update_index = 1, }; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); input.value.update.message = (char *) "one\ntwo"; cl_assert_equal_i(reftable_stack_add(st, write_test_log, - &arg, 0), REFTABLE_API_ERROR); + &arg, NULL, 0), REFTABLE_API_ERROR); input.value.update.message = (char *) "one"; cl_assert_equal_i(reftable_stack_add(st, write_test_log, - &arg, 0), 0); + &arg, NULL, 0), 0); cl_assert_equal_i(reftable_stack_read_log(st, input.refname, &dest), 0); cl_assert_equal_s(dest.value.update.message, "one\n"); @@ -709,7 +697,7 @@ void test_reftable_stack__log_normalize(void) input.value.update.message = (char *) "two\n"; arg.update_index = 2; cl_assert_equal_i(reftable_stack_add(st, write_test_log, - &arg, 0), 0); + &arg, NULL, 0), 0); cl_assert_equal_i(reftable_stack_read_log(st, input.refname, &dest), 0); cl_assert_equal_s(dest.value.update.message, "two\n"); @@ -723,7 +711,6 @@ void test_reftable_stack__log_normalize(void) void test_reftable_stack__tombstone(void) { char *dir = get_tmp_dir(__LINE__); - struct reftable_write_options opts = { 0 }; struct reftable_stack *st = NULL; struct reftable_ref_record refs[2] = { 0 }; struct reftable_log_record logs[2] = { 0 }; @@ -731,7 +718,7 @@ void test_reftable_stack__tombstone(void) struct reftable_ref_record dest = { 0 }; struct reftable_log_record log_dest = { 0 }; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); /* even entries add the refs, odd entries delete them. */ for (i = 0; i < N; i++) { @@ -760,7 +747,7 @@ void test_reftable_stack__tombstone(void) } for (i = 0; i < N; i++) cl_assert_equal_i(reftable_stack_add(st, write_test_ref, - &refs[i], 0), 0); + &refs[i], NULL, 0), 0); for (i = 0; i < N; i++) { struct write_log_arg arg = { @@ -768,7 +755,7 @@ void test_reftable_stack__tombstone(void) .update_index = reftable_stack_next_update_index(st), }; cl_assert_equal_i(reftable_stack_add(st, write_test_log, - &arg, 0), 0); + &arg, NULL, 0), 0); } cl_assert_equal_i(reftable_stack_read_ref(st, "branch", @@ -779,7 +766,7 @@ void test_reftable_stack__tombstone(void) &log_dest), 1); reftable_log_record_release(&log_dest); - cl_assert_equal_i(reftable_stack_compact_all(st, NULL), 0); + cl_assert_equal_i(reftable_stack_compact_all(st, NULL, NULL), 0); cl_assert_equal_i(reftable_stack_read_ref(st, "branch", &dest), 1); cl_assert_equal_i(reftable_stack_read_log(st, "branch", @@ -799,7 +786,6 @@ void test_reftable_stack__tombstone(void) void test_reftable_stack__hash_id(void) { char *dir = get_tmp_dir(__LINE__); - struct reftable_write_options opts = { 0 }; struct reftable_stack *st = NULL; struct reftable_ref_record ref = { @@ -808,15 +794,14 @@ void test_reftable_stack__hash_id(void) .value.symref = (char *) "target", .update_index = 1, }; - struct reftable_write_options opts32 = { .hash_id = REFTABLE_HASH_SHA256 }; + struct reftable_stack_options opts32 = { .hash_id = REFTABLE_HASH_SHA256 }; struct reftable_stack *st32 = NULL; - struct reftable_write_options opts_default = { 0 }; struct reftable_stack *st_default = NULL; struct reftable_ref_record dest = { 0 }; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); cl_assert_equal_i(reftable_stack_add(st, write_test_ref, - &ref, 0), 0); + &ref, NULL, 0), 0); /* can't read it with the wrong hash ID. */ cl_assert_equal_i(reftable_new_stack(&st32, dir, @@ -824,7 +809,7 @@ void test_reftable_stack__hash_id(void) /* check that we can read it back with default opts too. */ cl_assert_equal_i(reftable_new_stack(&st_default, dir, - &opts_default), 0); + NULL), 0); cl_assert_equal_i(reftable_stack_read_ref(st_default, "master", &dest), 0); cl_assert(reftable_ref_record_equal(&ref, &dest, @@ -855,7 +840,6 @@ void test_reftable_stack__suggest_compaction_segment_nothing(void) void test_reftable_stack__reflog_expire(void) { char *dir = get_tmp_dir(__LINE__); - struct reftable_write_options opts = { 0 }; struct reftable_stack *st = NULL; struct reftable_log_record logs[20] = { 0 }; size_t i, N = ARRAY_SIZE(logs) - 1; @@ -864,7 +848,7 @@ void test_reftable_stack__reflog_expire(void) }; struct reftable_log_record log = { 0 }; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); for (i = 1; i <= N; i++) { char buf[256]; @@ -885,18 +869,18 @@ void test_reftable_stack__reflog_expire(void) .update_index = reftable_stack_next_update_index(st), }; cl_assert_equal_i(reftable_stack_add(st, write_test_log, - &arg, 0), 0); + &arg, NULL, 0), 0); } - cl_assert_equal_i(reftable_stack_compact_all(st, NULL), 0); - cl_assert_equal_i(reftable_stack_compact_all(st, &expiry), 0); + cl_assert_equal_i(reftable_stack_compact_all(st, NULL, NULL), 0); + cl_assert_equal_i(reftable_stack_compact_all(st, NULL, &expiry), 0); cl_assert_equal_i(reftable_stack_read_log(st, logs[9].refname, &log), 1); cl_assert_equal_i(reftable_stack_read_log(st, logs[11].refname, &log), 0); expiry.min_update_index = 15; - cl_assert_equal_i(reftable_stack_compact_all(st, &expiry), 0); + cl_assert_equal_i(reftable_stack_compact_all(st, NULL, &expiry), 0); cl_assert_equal_i(reftable_stack_read_log(st, logs[14].refname, &log), 1); cl_assert_equal_i(reftable_stack_read_log(st, logs[16].refname, @@ -918,15 +902,14 @@ static int write_nothing(struct reftable_writer *wr, void *arg UNUSED) void test_reftable_stack__empty_add(void) { - struct reftable_write_options opts = { 0 }; struct reftable_stack *st = NULL; char *dir = get_tmp_dir(__LINE__); struct reftable_stack *st2 = NULL; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); cl_assert_equal_i(reftable_stack_add(st, write_nothing, - NULL, 0), 0); - cl_assert_equal_i(reftable_new_stack(&st2, dir, &opts), 0); + NULL, NULL, 0), 0); + cl_assert_equal_i(reftable_new_stack(&st2, dir, NULL), 0); clear_dir(dir); reftable_stack_destroy(st); reftable_stack_destroy(st2); @@ -952,7 +935,7 @@ void test_reftable_stack__auto_compaction(void) size_t i, N = 100; int err; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); for (i = 0; i < N; i++) { char name[100]; @@ -964,10 +947,10 @@ void test_reftable_stack__auto_compaction(void) }; snprintf(name, sizeof(name), "branch%04"PRIuMAX, (uintmax_t)i); - err = reftable_stack_add(st, write_test_ref, &ref, 0); + err = reftable_stack_add(st, write_test_ref, &ref, &opts, 0); cl_assert(!err); - err = reftable_stack_auto_compact(st); + err = reftable_stack_auto_compact(st, &opts); cl_assert(!err); cl_assert(i < 2 || st->merged->tables_len < 2 * fastlogN(i, 2)); } @@ -989,7 +972,7 @@ void test_reftable_stack__auto_compaction_factor(void) size_t N = 100; int err; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); for (size_t i = 0; i < N; i++) { char name[20]; @@ -1000,7 +983,7 @@ void test_reftable_stack__auto_compaction_factor(void) }; xsnprintf(name, sizeof(name), "branch%04"PRIuMAX, (uintmax_t)i); - err = reftable_stack_add(st, &write_test_ref, &ref, 0); + err = reftable_stack_add(st, &write_test_ref, &ref, &opts, 0); cl_assert(!err); cl_assert(i < 5 || st->merged->tables_len < 5 * fastlogN(i, 5)); @@ -1020,7 +1003,7 @@ void test_reftable_stack__auto_compaction_with_locked_tables(void) char *dir = get_tmp_dir(__LINE__); int err; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); write_n_ref_tables(st, 5); cl_assert_equal_i(st->merged->tables_len, 5); @@ -1042,7 +1025,7 @@ void test_reftable_stack__auto_compaction_with_locked_tables(void) * would in theory compact all tables, due to the preexisting lock we * only compact the newest two tables. */ - err = reftable_stack_auto_compact(st); + err = reftable_stack_auto_compact(st, &opts); cl_assert(!err); cl_assert_equal_i(st->stats.failures, 0); cl_assert_equal_i(st->merged->tables_len, 4); @@ -1054,12 +1037,11 @@ void test_reftable_stack__auto_compaction_with_locked_tables(void) void test_reftable_stack__add_performs_auto_compaction(void) { - struct reftable_write_options opts = { 0 }; struct reftable_stack *st = NULL; char *dir = get_tmp_dir(__LINE__); size_t i, n = 20; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); for (i = 0; i <= n; i++) { struct reftable_ref_record ref = { @@ -1067,6 +1049,9 @@ void test_reftable_stack__add_performs_auto_compaction(void) .value_type = REFTABLE_REF_SYMREF, .value.symref = (char *) "master", }; + struct reftable_write_options write_opts = { + .disable_auto_compact = (i != n), + }; bool required = false; char buf[128]; @@ -1075,20 +1060,18 @@ void test_reftable_stack__add_performs_auto_compaction(void) * we can ensure that we indeed honor this setting and have * better control over when exactly auto compaction runs. */ - st->opts.disable_auto_compact = i != n; - snprintf(buf, sizeof(buf), "branch-%04"PRIuMAX, (uintmax_t)i); ref.refname = buf; cl_assert_equal_i(reftable_stack_add(st, write_test_ref, - &ref, 0), 0); + &ref, &write_opts, 0), 0); /* * The stack length should grow continuously for all runs where * auto compaction is disabled. When enabled, we should merge * all tables in the stack. */ - cl_assert_equal_i(reftable_stack_compaction_required(st, true, &required), 0); + cl_assert_equal_i(reftable_stack_compaction_required(st, NULL, true, &required), 0); if (i != n) { cl_assert_equal_i(st->merged->tables_len, i + 1); if (i < 1) @@ -1115,7 +1098,7 @@ void test_reftable_stack__compaction_with_locked_tables(void) char *dir = get_tmp_dir(__LINE__); int err; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); write_n_ref_tables(st, 3); cl_assert_equal_i(st->merged->tables_len, 3); @@ -1131,7 +1114,7 @@ void test_reftable_stack__compaction_with_locked_tables(void) * Compaction is expected to fail given that we were not able to * compact all tables. */ - err = reftable_stack_compact_all(st, NULL); + err = reftable_stack_compact_all(st, &opts, NULL); cl_assert_equal_i(err, REFTABLE_LOCK_ERROR); cl_assert_equal_i(st->stats.failures, 1); cl_assert_equal_i(st->merged->tables_len, 3); @@ -1143,15 +1126,14 @@ void test_reftable_stack__compaction_with_locked_tables(void) void test_reftable_stack__compaction_concurrent(void) { - struct reftable_write_options opts = { 0 }; struct reftable_stack *st1 = NULL, *st2 = NULL; char *dir = get_tmp_dir(__LINE__); - cl_assert_equal_i(reftable_new_stack(&st1, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st1, dir, NULL), 0); write_n_ref_tables(st1, 3); - cl_assert_equal_i(reftable_new_stack(&st2, dir, &opts), 0); - cl_assert_equal_i(reftable_stack_compact_all(st1, NULL), 0); + cl_assert_equal_i(reftable_new_stack(&st2, dir, NULL), 0); + cl_assert_equal_i(reftable_stack_compact_all(st1, NULL, NULL), 0); reftable_stack_destroy(st1); reftable_stack_destroy(st2); @@ -1171,20 +1153,19 @@ static void unclean_stack_close(struct reftable_stack *st) void test_reftable_stack__compaction_concurrent_clean(void) { - struct reftable_write_options opts = { 0 }; struct reftable_stack *st1 = NULL, *st2 = NULL, *st3 = NULL; char *dir = get_tmp_dir(__LINE__); - cl_assert_equal_i(reftable_new_stack(&st1, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st1, dir, NULL), 0); write_n_ref_tables(st1, 3); - cl_assert_equal_i(reftable_new_stack(&st2, dir, &opts), 0); - cl_assert_equal_i(reftable_stack_compact_all(st1, NULL), 0); + cl_assert_equal_i(reftable_new_stack(&st2, dir, NULL), 0); + cl_assert_equal_i(reftable_stack_compact_all(st1, NULL, NULL), 0); unclean_stack_close(st1); unclean_stack_close(st2); - cl_assert_equal_i(reftable_new_stack(&st3, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st3, dir, NULL), 0); cl_assert_equal_i(reftable_stack_clean(st3), 0); cl_assert_equal_i(count_dir_entries(dir), 2); @@ -1197,7 +1178,6 @@ void test_reftable_stack__compaction_concurrent_clean(void) void test_reftable_stack__read_across_reload(void) { - struct reftable_write_options opts = { 0 }; struct reftable_stack *st1 = NULL, *st2 = NULL; struct reftable_ref_record rec = { 0 }; struct reftable_iterator it = { 0 }; @@ -1205,17 +1185,17 @@ void test_reftable_stack__read_across_reload(void) int err; /* Create a first stack and set up an iterator for it. */ - cl_assert_equal_i(reftable_new_stack(&st1, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st1, dir, NULL), 0); write_n_ref_tables(st1, 2); cl_assert_equal_i(st1->merged->tables_len, 2); reftable_stack_init_ref_iterator(st1, &it); cl_assert_equal_i(reftable_iterator_seek_ref(&it, ""), 0); /* Set up a second stack for the same directory and compact it. */ - err = reftable_new_stack(&st2, dir, &opts); + err = reftable_new_stack(&st2, dir, NULL); cl_assert(!err); cl_assert_equal_i(st2->merged->tables_len, 2); - err = reftable_stack_compact_all(st2, NULL); + err = reftable_stack_compact_all(st2, NULL, NULL); cl_assert(!err); cl_assert_equal_i(st2->merged->tables_len, 1); @@ -1244,7 +1224,6 @@ void test_reftable_stack__read_across_reload(void) void test_reftable_stack__reload_with_missing_table(void) { - struct reftable_write_options opts = { 0 }; struct reftable_stack *st = NULL; struct reftable_ref_record rec = { 0 }; struct reftable_iterator it = { 0 }; @@ -1253,7 +1232,7 @@ void test_reftable_stack__reload_with_missing_table(void) int err; /* Create a first stack and set up an iterator for it. */ - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); write_n_ref_tables(st, 2); cl_assert_equal_i(st->merged->tables_len, 2); reftable_stack_init_ref_iterator(st, &it); @@ -1320,11 +1299,11 @@ void test_reftable_stack__invalid_limit_updates(void) char *dir = get_tmp_dir(__LINE__); struct reftable_stack *st = NULL; - cl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0); + cl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0); reftable_addition_destroy(add); - cl_assert_equal_i(reftable_stack_new_addition(&add, st, 0), 0); + cl_assert_equal_i(reftable_stack_new_addition(&add, st, &opts, 0), 0); /* * write_limits_after_ref also updates the update indexes after adding diff --git a/t/unit-tests/u-reftable-table.c b/t/unit-tests/u-reftable-table.c index 14fae8b199c13c..fae478ee044c48 100644 --- a/t/unit-tests/u-reftable-table.c +++ b/t/unit-tests/u-reftable-table.c @@ -22,7 +22,8 @@ void test_reftable_table__seek_once(void) struct reftable_buf buf = REFTABLE_BUF_INIT; int ret; - cl_reftable_write_to_buf(&buf, records, ARRAY_SIZE(records), NULL, 0, NULL); + cl_reftable_write_to_buf(&buf, records, ARRAY_SIZE(records), NULL, 0, + REFTABLE_HASH_SHA1, NULL); block_source_from_buf(&source, &buf); ret = reftable_table_new(&table, &source, "name"); @@ -64,7 +65,7 @@ void test_reftable_table__reseek(void) int ret; cl_reftable_write_to_buf(&buf, records, ARRAY_SIZE(records), - NULL, 0, NULL); + NULL, 0, REFTABLE_HASH_SHA1, NULL); block_source_from_buf(&source, &buf); ret = reftable_table_new(&table, &source, "name"); @@ -147,7 +148,8 @@ void test_reftable_table__block_iterator(void) (uintmax_t) i); } - cl_reftable_write_to_buf(&buf, records, nrecords, NULL, 0, NULL); + cl_reftable_write_to_buf(&buf, records, nrecords, NULL, 0, + REFTABLE_HASH_SHA1, NULL); block_source_from_buf(&source, &buf); ret = reftable_table_new(&table, &source, "name"); From 79fa75d499d767540fae8d85ab62391cae6591f9 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 25 Jun 2026 11:20:08 +0200 Subject: [PATCH 92/95] refs/reftable: lazy-load configuration to fix chicken-and-egg Same as with the "files" backend, the "reftable" backend also has a chicken-and-egg problem with "onbranch" conditions. Fix this issue the same as we did with the "files" backend by lazy-loading configuration. Now that both the "files" and the "reftable" backend handle this properly, add a generic test to t1400 that verifies that the user can configure "core.logAllRefUpdates" via an "onbranch" condition. This is mostly a nonsensical thing to do in the first place, but it serves as a good sanity check. Note that we had to move `should_write_log()` around so that it can access the new `reftable_be_write_options()` function. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- refs/reftable-backend.c | 146 +++++++++++++++++------------- t/t0613-reftable-write-options.sh | 19 ++++ t/t1400-update-ref.sh | 12 +++ 3 files changed, 116 insertions(+), 61 deletions(-) diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index 608d71cf10c818..d74131a5ae0c19 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -141,10 +141,21 @@ struct reftable_ref_store { */ struct strmap worktree_backends; struct reftable_stack_options stack_options; - struct reftable_write_options write_options; + + /* + * Options used when writing to or compacting the reftable stacks. + * These are parsed from the configuration lazily on first use via + * `reftable_be_write_options()` so that we don't have to access the + * configuration when initializing the ref store. Do not access these + * fields directly, but use the accessor instead. + */ + struct reftable_be_write_options { + struct reftable_write_options opts; + enum log_refs_config log_all_ref_updates; + bool initialized; + } write_opts_lazy_loaded; unsigned int store_flags; - enum log_refs_config log_all_ref_updates; int err; }; @@ -285,26 +296,6 @@ static int backend_for(struct reftable_backend **out, return ret; } -static int should_write_log(struct reftable_ref_store *refs, const char *refname) -{ - enum log_refs_config log_refs_cfg = refs->log_all_ref_updates; - if (log_refs_cfg == LOG_REFS_UNSET) - log_refs_cfg = is_bare_repository() ? LOG_REFS_NONE : LOG_REFS_NORMAL; - - switch (log_refs_cfg) { - case LOG_REFS_NONE: - return refs_reflog_exists(&refs->base, refname); - case LOG_REFS_ALWAYS: - return 1; - case LOG_REFS_NORMAL: - if (should_autocreate_reflog(log_refs_cfg, refname)) - return 1; - return refs_reflog_exists(&refs->base, refname); - default: - BUG("unhandled core.logAllRefUpdates value %d", log_refs_cfg); - } -} - static void fill_reftable_log_record(struct reftable_log_record *log, const struct ident_split *split) { const char *tz_begin; @@ -336,38 +327,72 @@ static int reftable_be_config(const char *var, const char *value, void *payload) { struct reftable_ref_store *refs = payload; + struct reftable_be_write_options *opts = &refs->write_opts_lazy_loaded; if (!strcmp(var, "reftable.blocksize")) { unsigned long block_size = git_config_ulong(var, value, ctx->kvi); if (block_size > 16777215) die("reftable block size cannot exceed 16MB"); - refs->write_options.block_size = block_size; + opts->opts.block_size = block_size; } else if (!strcmp(var, "reftable.restartinterval")) { unsigned long restart_interval = git_config_ulong(var, value, ctx->kvi); if (restart_interval > UINT16_MAX) die("reftable block size cannot exceed %u", (unsigned)UINT16_MAX); - refs->write_options.restart_interval = restart_interval; + opts->opts.restart_interval = restart_interval; } else if (!strcmp(var, "reftable.indexobjects")) { - refs->write_options.skip_index_objects = !git_config_bool(var, value); + opts->opts.skip_index_objects = !git_config_bool(var, value); } else if (!strcmp(var, "reftable.geometricfactor")) { unsigned long factor = git_config_ulong(var, value, ctx->kvi); if (factor > UINT8_MAX) die("reftable geometric factor cannot exceed %u", (unsigned)UINT8_MAX); - refs->write_options.auto_compaction_factor = factor; + opts->opts.auto_compaction_factor = factor; } else if (!strcmp(var, "reftable.locktimeout")) { int64_t lock_timeout = git_config_int64(var, value, ctx->kvi); if (lock_timeout > LONG_MAX) die("reftable lock timeout cannot exceed %"PRIdMAX, (intmax_t)LONG_MAX); if (lock_timeout < 0 && lock_timeout != -1) die("reftable lock timeout does not support negative values other than -1"); - refs->write_options.lock_timeout_ms = lock_timeout; + opts->opts.lock_timeout_ms = lock_timeout; } else if (!strcmp(var, "core.logallrefupdates")) { - refs->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value); + opts->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value); } return 0; } +static const struct reftable_be_write_options *reftable_be_write_options(struct reftable_ref_store *refs) +{ + struct reftable_be_write_options *opts = &refs->write_opts_lazy_loaded; + mode_t mask; + + if (opts->initialized) + return opts; + + mask = umask(0); + umask(mask); + + opts->opts.default_permissions = calc_shared_perm(refs->base.repo, 0666 & ~mask); + opts->opts.disable_auto_compact = + !git_env_bool("GIT_TEST_REFTABLE_AUTOCOMPACTION", 1); + opts->opts.lock_timeout_ms = 100; + opts->log_all_ref_updates = LOG_REFS_UNSET; + + repo_config(refs->base.repo, reftable_be_config, refs); + + /* + * It is somewhat unfortunate that we have to mirror the default block + * size of the reftable library here. But given that the write options + * wouldn't be updated by the library here, and given that we require + * the proper block size to trim reflog message so that they fit, we + * must set up a proper value here. + */ + if (!opts->opts.block_size) + opts->opts.block_size = 4096; + + opts->initialized = true; + return opts; +} + static void reftable_be_reparent(const char *name UNUSED, const char *old_cwd, const char *new_cwd, @@ -391,10 +416,6 @@ static struct ref_store *reftable_be_init(struct repository *repo, struct strbuf refdir = STRBUF_INIT; struct strbuf path = STRBUF_INIT; bool is_worktree; - mode_t mask; - - mask = umask(0); - umask(mask); refs_compute_filesystem_location(gitdir, payload, &is_worktree, &refdir, &ref_common_dir); @@ -413,23 +434,6 @@ static struct ref_store *reftable_be_init(struct repository *repo, default: BUG("unknown hash algorithm %d", repo->hash_algo->format_id); } - refs->write_options.default_permissions = calc_shared_perm(repo, 0666 & ~mask); - refs->write_options.disable_auto_compact = - !git_env_bool("GIT_TEST_REFTABLE_AUTOCOMPACTION", 1); - refs->write_options.lock_timeout_ms = 100; - refs->log_all_ref_updates = LOG_REFS_UNSET; - - repo_config(repo, reftable_be_config, refs); - - /* - * It is somewhat unfortunate that we have to mirror the default block - * size of the reftable library here. But given that the write options - * wouldn't be updated by the library here, and given that we require - * the proper block size to trim reflog message so that they fit, we - * must set up a proper value here. - */ - if (!refs->write_options.block_size) - refs->write_options.block_size = 4096; /* * Set up the main reftable stack that is hosted in GIT_COMMON_DIR. @@ -998,7 +1002,7 @@ static int prepare_transaction_update(struct write_transaction_table_arg **out, struct reftable_addition *addition; ret = reftable_stack_new_addition(&addition, be->stack, - &refs->write_options, + &reftable_be_write_options(refs)->opts, REFTABLE_STACK_NEW_ADDITION_RELOAD); if (ret) { if (ret == REFTABLE_LOCK_ERROR) @@ -1437,6 +1441,26 @@ static int transaction_update_cmp(const void *a, const void *b) return strcmp(update_a->update->refname, update_b->update->refname); } +static int should_write_log(struct reftable_ref_store *refs, const char *refname) +{ + enum log_refs_config log_refs_cfg = reftable_be_write_options(refs)->log_all_ref_updates; + if (log_refs_cfg == LOG_REFS_UNSET) + log_refs_cfg = is_bare_repository() ? LOG_REFS_NONE : LOG_REFS_NORMAL; + + switch (log_refs_cfg) { + case LOG_REFS_NONE: + return refs_reflog_exists(&refs->base, refname); + case LOG_REFS_ALWAYS: + return 1; + case LOG_REFS_NORMAL: + if (should_autocreate_reflog(log_refs_cfg, refname)) + return 1; + return refs_reflog_exists(&refs->base, refname); + default: + BUG("unhandled core.logAllRefUpdates value %d", log_refs_cfg); + } +} + static int write_transaction_table(struct reftable_writer *writer, void *cb_data) { struct write_transaction_table_arg *arg = cb_data; @@ -1571,7 +1595,7 @@ static int write_transaction_table(struct reftable_writer *writer, void *cb_data memcpy(log->value.update.old_hash, tx_update->current_oid.hash, GIT_MAX_RAWSZ); log->value.update.message = - xstrndup(u->msg, arg->refs->write_options.block_size / 2); + xstrndup(u->msg, reftable_be_write_options(arg->refs)->opts.block_size / 2); } } @@ -1687,9 +1711,9 @@ static int reftable_be_optimize(struct ref_store *ref_store, stack = refs->main_backend.stack; if (opts->flags & REFS_OPTIMIZE_AUTO) - ret = reftable_stack_auto_compact(stack, &refs->write_options); + ret = reftable_stack_auto_compact(stack, &reftable_be_write_options(refs)->opts); else - ret = reftable_stack_compact_all(stack, &refs->write_options, NULL); + ret = reftable_stack_compact_all(stack, &reftable_be_write_options(refs)->opts, NULL); if (ret < 0) { ret = error(_("unable to compact stack: %s"), reftable_error_str(ret)); @@ -1723,7 +1747,7 @@ static int reftable_be_optimize_required(struct ref_store *ref_store, if (opts->flags & REFS_OPTIMIZE_AUTO) use_heuristics = true; - return reftable_stack_compaction_required(stack, &refs->write_options, + return reftable_stack_compaction_required(stack, &reftable_be_write_options(refs)->opts, use_heuristics, required); } @@ -1843,7 +1867,7 @@ static int write_copy_table(struct reftable_writer *writer, void *cb_data) logs[logs_nr].refname = xstrdup(arg->newname); logs[logs_nr].update_index = deletion_ts; logs[logs_nr].value.update.message = - xstrndup(arg->logmsg, arg->refs->write_options.block_size / 2); + xstrndup(arg->logmsg, reftable_be_write_options(arg->refs)->opts.block_size / 2); memcpy(logs[logs_nr].value.update.old_hash, old_ref.value.val1, GIT_MAX_RAWSZ); logs_nr++; @@ -1882,7 +1906,7 @@ static int write_copy_table(struct reftable_writer *writer, void *cb_data) logs[logs_nr].refname = xstrdup(arg->newname); logs[logs_nr].update_index = creation_ts; logs[logs_nr].value.update.message = - xstrndup(arg->logmsg, arg->refs->write_options.block_size / 2); + xstrndup(arg->logmsg, reftable_be_write_options(arg->refs)->opts.block_size / 2); memcpy(logs[logs_nr].value.update.new_hash, old_ref.value.val1, GIT_MAX_RAWSZ); logs_nr++; @@ -1981,7 +2005,7 @@ static int reftable_be_rename_ref(struct ref_store *ref_store, if (ret) goto done; ret = reftable_stack_add(arg.be->stack, &write_copy_table, &arg, - &refs->write_options, + &reftable_be_write_options(refs)->opts, REFTABLE_STACK_NEW_ADDITION_RELOAD); done: @@ -2012,7 +2036,7 @@ static int reftable_be_copy_ref(struct ref_store *ref_store, if (ret) goto done; ret = reftable_stack_add(arg.be->stack, &write_copy_table, &arg, - &refs->write_options, + &reftable_be_write_options(refs)->opts, REFTABLE_STACK_NEW_ADDITION_RELOAD); done: @@ -2378,7 +2402,7 @@ static int reftable_be_create_reflog(struct ref_store *ref_store, arg.stack = be->stack; ret = reftable_stack_add(be->stack, &write_reflog_existence_table, &arg, - &refs->write_options, + &reftable_be_write_options(refs)->opts, REFTABLE_STACK_NEW_ADDITION_RELOAD); done: @@ -2451,7 +2475,7 @@ static int reftable_be_delete_reflog(struct ref_store *ref_store, arg.stack = be->stack; ret = reftable_stack_add(be->stack, &write_reflog_delete_table, &arg, - &refs->write_options, + &reftable_be_write_options(refs)->opts, REFTABLE_STACK_NEW_ADDITION_RELOAD); assert(ret != REFTABLE_API_ERROR); @@ -2574,7 +2598,7 @@ static int reftable_be_reflog_expire(struct ref_store *ref_store, goto done; ret = reftable_stack_new_addition(&add, be->stack, - &refs->write_options, + &reftable_be_write_options(refs)->opts, REFTABLE_STACK_NEW_ADDITION_RELOAD); if (ret < 0) goto done; diff --git a/t/t0613-reftable-write-options.sh b/t/t0613-reftable-write-options.sh index 26b716c75f219d..a65960d0488640 100755 --- a/t/t0613-reftable-write-options.sh +++ b/t/t0613-reftable-write-options.sh @@ -278,4 +278,23 @@ test_expect_success 'object index can be disabled' ' ) ' +test_expect_success 'write options can be set up via onbranch condition' ' + test_config_global core.logAllRefUpdates false && + test_when_finished "rm -rf repo" && + init_repo && + ( + cd repo && + test_commit A && + test_commit B && + cat >.git/include <<-\EOF && + [reftable] + blockSize = 123 + EOF + git config includeIf.onbranch:master.path "$(pwd)/.git/include" && + git refs optimize && + test-tool dump-reftable -b .git/reftable/*.ref >stats && + test_grep "block_size: 123" stats + ) +' + test_done diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh index 1015f335e31611..b8c3be66310e02 100755 --- a/t/t1400-update-ref.sh +++ b/t/t1400-update-ref.sh @@ -178,6 +178,18 @@ test_expect_success '--no-create-reflog overrides core.logAllRefUpdates=always' test_must_fail git reflog exists $outside ' +test_expect_success 'core.logAllRefUpdates can be set up via onbranch condition' ' + test_when_finished "git update-ref -d $outside" && + test_when_finished "rm -f .git/include" && + cat >.git/include <<-\EOF && + [core] + logAllRefUpdates = always + EOF + test_config includeIf.onbranch:main.path "$(pwd)/.git/include" && + git update-ref $outside $A && + git reflog exists $outside +' + test_expect_success "create $m (by HEAD)" ' git update-ref HEAD $A && test $A = $(git show-ref -s --verify $m) From d6522d01dfd147d18246d308dd6ea24b32d095d2 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 25 Jun 2026 11:20:09 +0200 Subject: [PATCH 93/95] refs: protect against chicken-and-egg recursion In the preceding commits we have fixed recursion when creating the reference backends due to a chicken-and-egg situation with "onbranch" conditions. Unfortunately, this issue has existed for a while, and we didn't really have a good mechanism to detect this recursion. Improve the status quo by detecting the recursion when creating the main reference store. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- refs.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/refs.c b/refs.c index 5b773b1c151e94..1d2463789167c9 100644 --- a/refs.c +++ b/refs.c @@ -2359,15 +2359,22 @@ void ref_store_release(struct ref_store *ref_store) struct ref_store *get_main_ref_store(struct repository *r) { + static bool initializing; + if (r->refs_private) return r->refs_private; if (!r->gitdir) BUG("attempting to get main_ref_store outside of repository"); + if (initializing) + BUG("initialization of main ref store is recursing"); + initializing = true; r->refs_private = ref_store_init(r, r->ref_storage_format, r->gitdir, REF_STORE_ALL_CAPS); r->refs_private = maybe_debug_wrap_ref_store(r->gitdir, r->refs_private); + initializing = false; + return r->refs_private; } From eaad121fefb3c5845a52aba8952435096a172154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SZEDER=20G=C3=A1bor?= Date: Sun, 10 Oct 2021 19:28:09 +0200 Subject: [PATCH 94/95] t3420-rebase-autostash: don't try to grep non-existing files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several tests in 't3420-rebase-autostash.sh' start various rebase processes that are expected to fail because of merge conflicts. The tests [1] checking that 'git rebase --quit' and autostash work together as expected after such a failure then run '! grep ...' to ensure that the dirty contents of the file is gone. However, due to the test repo's history and the choice of upstream branch that file shouldn't exist in the conflicted state at all, and thus it shouldn't exist after the subsequent 'git rebase --quit' either. Consequently, this 'grep' doesn't fail as expected, i.e. because it can't find the dirty content, but instead it fails, because it can't open the file. Thighten this check by using 'test_path_is_missing' instead, thereby avoiding unexpected errors from 'grep' as well. Previously 2745817028 (t3420-rebase-autostash: don't try to grep non-existing files, 2018-08-22) fixed a couple of similar issues; this one was added later in 9b2df3e8d0 (rebase: save autostash entry into stash reflog on --quit, 2020-04-28). [1] This patch modifies only a single test, but that test is run several times with different strategies ('--apply', '--merge', and '--interactive'), hence the plural "tests". Signed-off-by: SZEDER Gábor Signed-off-by: Junio C Hamano --- t/t3420-rebase-autostash.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/t3420-rebase-autostash.sh b/t/t3420-rebase-autostash.sh index 43fcb68f27e439..bbe82d2c0c90d5 100755 --- a/t/t3420-rebase-autostash.sh +++ b/t/t3420-rebase-autostash.sh @@ -200,7 +200,7 @@ testrebase () { git rebase --quit && test_when_finished git stash drop && test_path_is_missing $dotest/autostash && - ! grep dirty file3 && + test_path_is_missing file3 && git stash show -p >actual && test_cmp expect actual && git reset --hard && From f85a7e662054a7b0d9070e432508831afa214b47 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 6 Jul 2026 15:49:17 -0700 Subject: [PATCH 95/95] Start Git 2.56 cycle This time, do not forget to update GIT-VERSION-GEN to say 2.55.GIT Signed-off-by: Junio C Hamano --- Documentation/RelNotes/2.56.0.adoc | 113 +++++++++++++++++++++++++++++ GIT-VERSION-GEN | 2 +- RelNotes | 2 +- 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 Documentation/RelNotes/2.56.0.adoc diff --git a/Documentation/RelNotes/2.56.0.adoc b/Documentation/RelNotes/2.56.0.adoc new file mode 100644 index 00000000000000..fda10a014f85ba --- /dev/null +++ b/Documentation/RelNotes/2.56.0.adoc @@ -0,0 +1,113 @@ +Git v2.56 Release Notes +======================= + +UI, Workflows & Features +------------------------ + + * Advice shown by "git status" when the local branch is behind or has + diverged from its push branch has been updated to suggest "git pull + ". + + * The handling of promisor-remote protocol capability has been updated + to allow the other side to add to the list of promisor remotes via the + 'promisor.acceptFromServerURL' configuration variable. + + * The 'ort' merge backend has been hardened against corrupt trees by + ensuring it aborts under appropriate error conditions. + + * The `fetch.followRemoteHEAD` configuration variable has been added to + provide a default for the per-remote `remote..followRemoteHEAD` + setting. + + * "git log --follow" has been updated to better handle non-linear + history, in which the path being tracked gets renamed differently in + multiple history lines. + + * The "git repo info" command has been taught new keys to output both + absolute and relative paths for "gitdir" and "commondir", supported by + a new path-formatting helper extracted from "git rev-parse". + + +Performance, Internal Implementation, Development Support etc. +-------------------------------------------------------------- + + * The refactoring of 'setup.c' has been continued to drop remaining + global state (`git_work_tree_cfg`, `is_bare_repository_cfg`), updating + `is_bare_repository()` to no longer implicitly rely on + `the_repository`. + + * Project-specific configuration for b4 has been introduced, and the + documentation has been updated to recommend using it as a + streamlined method for submitting patches. + + * The default format path of git cat-file --batch has been optimized + to use strbuf_add_oid_hex() and strbuf_add_uint() instead of + strbuf_addf(), yielding a noticeable speedup. + + * Commands that list branches and tags (like git branch and git tag) + have been optimized to pass the namespace prefix when initializing + their ref iterator, avoiding a loose-ref scaling regression in + repositories with many unrelated loose references. + + * The packed object source has been refactored into a proper struct + odb_source. + + * The global configuration variables protect_hfs and protect_ntfs have + been migrated into struct repo_config_values to tie them to + per-repository configuration state. + + * The trailer sections in SubmittingPatches have been updated to + encourage use of standard trailers. + + * The documentation in SubmittingPatches has been updated to clarify how + patch contributors should respond to design and viability critiques, + and how the resolution of such critiques should be recorded in the + final commit messages. + + * The pack-objects command has been updated to support reachability + bitmaps and delta-islands concurrently with the `--path-walk` option, + allowing faster packaging by falling back to path-walk when bitmaps + cannot fully satisfy the request. + + * Documentation on community contribution guidelines has been updated to + encourage replying to review comments before rerolling, and to advise + a default limit of at most one reroll per day to give reviewers across + different time zones enough time to participate. + + +Fixes since v2.55 +----------------- + + * A regression in the error diagnosis code for invalid .git files has + been fixed, avoiding a potential NULL-pointer crash when reporting + that a .git file does not point to a valid repository. + (merge 54a441bcea jk/setup-gitfile-diag-fix later to maint). + + * Support for hashing loose or packed objects larger than 4GB on Windows + and other LLP64 platforms has been improved by converting object header + buffers and data-handling functions from 'unsigned long' to 'size_t'. + (merge d99e13d0be po/hash-object-size-t later to maint). + + * The display of the rebase todo list in "git status" has been + improved to correctly abbreviate object IDs for more commands and + avoid misinterpreting refs as object IDs. + (merge 6f34e5f9e3 pw/status-rebase-todo later to maint). + + * Reference backend configuration has been updated to load lazily to + avoid recursive calls during repository initialization when 'onbranch' + configuration conditions are evaluated. This has also fixed a memory + leak and allowed the unused `chdir_notify_reparent()` machinery to be + dropped. + (merge d6522d01df ps/refs-onbranch-fixes later to maint). + + * The connectivity check has been refactored to search for promisor + objects in a generic way using the object database interface, + rather than iterating packfiles directly. This allows connectivity + checks to work properly in repositories that do not use packfiles. + (merge 66ee9cb930 ps/connected-generic-promisor-checks later to maint). + + * A test checking interactions between git rebase --quit and + autostash in t3420-rebase-autostash.sh has been corrected to use + test_path_is_missing instead of ! grep on a file that shouldn't + exist in the conflicted state. + (merge eaad121fef sg/t3420-do-not-grep-in-missing-file later to maint). diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 6e4df991932910..a72f090fe2b8d2 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,6 +1,6 @@ #!/bin/sh -DEF_VER=v2.55.0 +DEF_VER=v2.55.GIT LF=' ' diff --git a/RelNotes b/RelNotes index 159e44a9490cc3..752580e69384ba 120000 --- a/RelNotes +++ b/RelNotes @@ -1 +1 @@ -Documentation/RelNotes/2.55.0.adoc \ No newline at end of file +Documentation/RelNotes/2.56.0.adoc \ No newline at end of file