From 5c8976e16de25ba24517ce725401bc13115e80a4 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Wed, 14 Oct 2026 20:11:33 +0200 Subject: [PATCH 1/2] connected: negate alternate refs in deepening fetches too The connectivity check spawned by check_connected() uses git rev-list --objects --stdin --not --all --quiet --alternate-refs where 39b44ba771 (check_everything_connected: assume alternate ref tips are valid, 2019-07-01) added "--alternate-refs" so that the walk from the fetched tips stops at the refs of any alternate object store, on the assumption that the alternate is already fully connected. That only works because "--alternate-refs" comes after "--not". cf1e7c0770 (fetch-pack: write shallow, then check connectivity, 2018-07-02) drops "--not --all" for deepening fetches, since those need to verify the whole chain up to the new shallow boundary rather than stopping at refs we already had. For such fetches we therefore run git --shallow-file ... rev-list --objects --stdin --quiet --alternate-refs with the alternate's refs as positive revisions, and rev-list enumerates every object reachable from them. Unless one of those refs happens to lie inside the newly fetched shallow range, where the grafts cut the walk short, that is the alternate's entire history. This is slow for setups that attach a large alternate to a fresh repository and then do a shallow fetch of a ref that is a few commits ahead of the alternate, which is how CI runners share a clone of a big repository between jobs. With a 7M-object alternate (a bare clone of gitlab-org/gitlab), "git fetch --depth 1" transfers an empty pack in under a second and the connectivity check then takes 227s. With the alternate's refs negated the same rev-list finishes in 0.0s. Pass "--not" in both cases, before the branch that adds "--all" for non-deepening fetches, so that "--alternate-refs" is negated for deepening fetches as well. Such a fetch still has to reach the new shallow boundary from the fetched tips without stopping at our own refs, which is what cf1e7c0770 wanted. It stops at the alternate's refs, as every other fetch already does. Move "--alternate-refs" up next to "--all" so that the arguments that bound the walk are in one place. Add tests to t5618 that check the arguments of the rev-list child via GIT_TRACE2_EVENT for a regular and for a deepening fetch. The result of the check is the same either way for a complete alternate, so there is no behavioural difference to assert on. The difference is only in which objects are walked. Helped-by: Patrick Steinhardt Helped-by: Junio C Hamano Signed-off-by: Igor Wiedler Signed-off-by: Junio C Hamano --- connected.c | 6 ++++-- t/t5618-alternate-refs.sh | 12 ++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/connected.c b/connected.c index 7e269768327238..cf2647c94244b6 100644 --- a/connected.c +++ b/connected.c @@ -91,15 +91,17 @@ int check_connected(oid_iterate_fn fn, void *cb_data, strvec_push(&rev_list.args, "--stdin"); if (repo_has_promisor_remote(the_repository)) strvec_push(&rev_list.args, "--exclude-promisor-objects"); + + /* define where the traversal should stop */ + strvec_push(&rev_list.args, "--not"); if (!opt->is_deepening_fetch) { - strvec_push(&rev_list.args, "--not"); if (opt->exclude_hidden_refs_section) strvec_pushf(&rev_list.args, "--exclude-hidden=%s", opt->exclude_hidden_refs_section); strvec_push(&rev_list.args, "--all"); } - strvec_push(&rev_list.args, "--quiet"); strvec_push(&rev_list.args, "--alternate-refs"); + strvec_push(&rev_list.args, "--quiet"); if (opt->progress) strvec_pushf(&rev_list.args, "--progress=%s", _("Checking connectivity")); diff --git a/t/t5618-alternate-refs.sh b/t/t5618-alternate-refs.sh index 2fb6d549d394ee..4cc39af0f08400 100755 --- a/t/t5618-alternate-refs.sh +++ b/t/t5618-alternate-refs.sh @@ -58,4 +58,16 @@ test_expect_success 'log --source shows .alternate marker' ' test_cmp expect actual ' +test_expect_success 'connectivity check of a fetch excludes alternate refs' ' + GIT_TRACE2_EVENT="$PWD/fetch.trace" git fetch origin && + test_subcommand_flex git rev-list --objects --stdin --not --all \ + --alternate-refs --quiet Date: Wed, 14 Oct 2026 20:11:34 +0200 Subject: [PATCH 2/2] odb: ignore refs of shallow and grafted alternates odb_for_each_alternate_ref() reports the refs of every alternate object store that is a repository. Its three callers use them as commits whose history can be assumed to be present: "rev-list --alternate-refs" (and through it the connectivity check after a fetch), fetch-pack, which sends them as "have"s so that the server does not resend what the alternate has, and receive-pack, which advertises them as ".have". That assumption does not hold for a shallow repository, whose refs point at commits with no history behind them below its shallow boundary, nor for a grafted one. A repository borrowing objects from such an alternate does not see the alternate's shallow file, so the history that is missing there is missing here as well. The fetch then succeeds, the connectivity check stops at the alternate's refs, and a later "git log" fails with "Could not read ". "git clone --reference" already rejects a shallow or grafted reference repository for this reason, but nothing prevents such a repository from being added via objects/info/alternates or GIT_ALTERNATE_OBJECT_DIRECTORIES, or from being made shallow after other repositories started borrowing from it. Skip the refs of an alternate that has a "shallow" file or an "info/grafts" file, the same checks clone uses. Its objects are still available through the object store. Only its refs are no longer used as a boundary. Partial clones have the same problem for the objects they lack, but detecting one requires reading the alternate's config, so this patch does not handle them. Helped-by: Junio C Hamano Signed-off-by: Igor Wiedler Signed-off-by: Junio C Hamano --- Documentation/rev-list-options.adoc | 7 ++++--- odb.c | 11 +++++++++++ t/t5618-alternate-refs.sh | 13 +++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc index eaee6ee8399c57..6df0e536ad7ce6 100644 --- a/Documentation/rev-list-options.adoc +++ b/Documentation/rev-list-options.adoc @@ -223,9 +223,10 @@ explicitly. Pretend as if all objects mentioned as ref tips of alternate repositories were listed on the command line. An alternate repository is any repository whose object directory is specified - in `objects/info/alternates`. The set of included objects may - be modified by `core.alternateRefsCommand`, etc. See - linkgit:git-config[1]. + in `objects/info/alternates`. The refs of shallow and grafted + alternate repositories are ignored, as the history behind them + may be missing. The set of included objects may be modified by + `core.alternateRefsCommand`, etc. See linkgit:git-config[1]. `--single-worktree`:: By default, all working trees will be examined by the diff --git a/odb.c b/odb.c index 7d555be09feaea..e431bcb84c80ed 100644 --- a/odb.c +++ b/odb.c @@ -453,6 +453,17 @@ static int refs_from_alternate_cb(struct odb_source *alternate, goto out; strbuf_setlen(&path, base_len); + /* A shallow or grafted repository may lack the history behind its refs. */ + strbuf_addstr(&path, "/shallow"); + if (!access(path.buf, F_OK)) + goto out; + strbuf_setlen(&path, base_len); + + strbuf_addstr(&path, "/info/grafts"); + if (!access(path.buf, F_OK)) + goto out; + strbuf_setlen(&path, base_len); + read_alternate_refs(alternate->odb->repo, path.buf, cb->fn, cb->payload); out: diff --git a/t/t5618-alternate-refs.sh b/t/t5618-alternate-refs.sh index 4cc39af0f08400..9c3862709a6d08 100755 --- a/t/t5618-alternate-refs.sh +++ b/t/t5618-alternate-refs.sh @@ -70,4 +70,17 @@ test_expect_success 'connectivity check of a deepening fetch excludes alternate --alternate-refs --quiet shallow-tip && + cp .git/objects/info/alternates alternates.bak && + test_when_finished "mv alternates.bak .git/objects/info/alternates" && + echo "$TRASH_DIRECTORY/shallow/.git/objects" >>.git/objects/info/alternates && + git cat-file -e $(cat shallow-tip) && + git rev-list --remotes=origin >expect && + git rev-list --alternate-refs >actual && + test_cmp expect actual +' + test_done