diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 71e1f3dcd4a204..0ae75b9c210973 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -2058,7 +2058,7 @@ static void execute_commands(struct command *commands, opt.err_fd = err_fd; opt.progress = err_fd && !quiet; opt.env = tmp_objdir_env(tmp_objdir); - opt.exclude_hidden_refs_section = "receive"; + opt.use_toplevel_branches_for_reachability = 1; if (check_connected(iterate_receive_command_list, &data, &opt)) set_connectivity_errors(commands, si); diff --git a/connected.c b/connected.c index 7e269768327238..f05ed1c2788daa 100644 --- a/connected.c +++ b/connected.c @@ -8,13 +8,14 @@ #include "sigchain.h" #include "connected.h" #include "transport.h" +#include "object-name.h" #include "packfile.h" #include "promisor-remote.h" /* * If we feed all the commits we want to verify to this command * - * $ git rev-list --objects --stdin --not --all + * $ git rev-list --objects --stdin --not ${main_branches} * * and if it does not error out, that means everything reachable from * these commits locally exists and is connected to our existing refs. @@ -93,13 +94,22 @@ int check_connected(oid_iterate_fn fn, void *cb_data, strvec_push(&rev_list.args, "--exclude-promisor-objects"); 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"); + if (opt->use_toplevel_branches_for_reachability) { + struct object_id head_oid; + strvec_push(&rev_list.args, "--exclude=*/*"); + strvec_push(&rev_list.args, "--branches"); + if (!repo_get_oid(the_repository, "HEAD", &head_oid)) + strvec_push(&rev_list.args, "HEAD"); + } else { + 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"); + if (!opt->use_toplevel_branches_for_reachability) + strvec_push(&rev_list.args, "--alternate-refs"); if (opt->progress) strvec_pushf(&rev_list.args, "--progress=%s", _("Checking connectivity")); diff --git a/connected.h b/connected.h index 16b2c84f2e35fc..f898e251fca147 100644 --- a/connected.h +++ b/connected.h @@ -50,9 +50,22 @@ struct check_connected_options { /* * If not NULL, use `--exclude-hidden=$section` to exclude all refs * hidden via the `$section.hideRefs` config from the set of - * already-reachable refs. + * already-reachable refs; irrelevant if + * use_toplevel_branches_for_reachability is set. */ const char *exclude_hidden_refs_section; + + /* + * If set, use only toplevel branches (and HEAD) for the + * reachability check, and skip the `--alternate-refs` stoppers + * that the fetch/clone code path relies on. This avoids the + * linear-in-refcount enumeration of every visible ref in + * repositories with many branches/tags (and the analogous + * expansion of the alternate's ref set in fork-network + * setups), at the cost of walking a little further into + * already-reachable history. + */ + unsigned use_toplevel_branches_for_reachability : 1; }; #define CHECK_CONNECTED_INIT { 0 } diff --git a/t/t5410-receive-pack.sh b/t/t5410-receive-pack.sh index 09d6bfd2a10f5a..f0ee5c095308f8 100755 --- a/t/t5410-receive-pack.sh +++ b/t/t5410-receive-pack.sh @@ -97,4 +97,62 @@ test_expect_success TEE_DOES_NOT_HANG \ test_must_fail git -C remote.git rev-list $(git -C repo rev-parse HEAD) ' +test_expect_success 'connectivity check uses narrow stopper, omits --alternate-refs' ' + test_when_finished "rm -rf src dst trace" && + + git init src && + test_commit -C src one && + + # Clone first so that HEAD in the bare repo resolves; this lets us + # observe HEAD in the stopper args. + git clone --bare src dst && + test_commit -C src two && + + GIT_TRACE="$(pwd)/trace" \ + git -C src push ../dst HEAD:refs/heads/main && + + # Receive-pack should request the narrow stopper. GIT_TRACE may + # single-quote arguments containing shell metacharacters, so allow + # an optional quote before/after --exclude=*/*. + grep "rev-list .*--not .\{0,1\}--exclude=\*/\*.\{0,1\} --branches HEAD" \ + trace && + # And not fall back to --alternate-refs in this code path. + ! grep "rev-list .*--alternate-refs" trace +' + +test_expect_success 'push to bare repo with unborn HEAD succeeds' ' + test_when_finished "rm -rf src dst" && + + git init src && + test_commit -C src one && + + # Fresh bare repo: HEAD points at an unborn branch and no + # branches exist yet, so the narrow stopper code must omit HEAD + # and "--branches" expands to nothing. + git init --bare dst && + git -C src push ../dst HEAD:refs/heads/main && + git -C src rev-parse HEAD >expect && + git -C dst rev-parse refs/heads/main >actual && + test_cmp expect actual +' + +test_expect_success 'push to bare repo whose HEAD resolves to a nested branch succeeds' ' + test_when_finished "rm -rf src dst" && + + git init src && + test_commit -C src one && + + git init --bare dst && + git -C src push ../dst HEAD:refs/heads/team/feature && + # Point HEAD at the nested branch so HEAD resolves, but the + # only branch in the repo is excluded by "--exclude=*/*". + git -C dst symbolic-ref HEAD refs/heads/team/feature && + + test_commit -C src two && + git -C src push ../dst HEAD:refs/heads/team/feature && + git -C src rev-parse HEAD >expect && + git -C dst rev-parse refs/heads/team/feature >actual && + test_cmp expect actual +' + test_done