diff --git a/Documentation/git-rev-parse.adoc b/Documentation/git-rev-parse.adoc index 5398691f3f15f7..f84659c809759a 100644 --- a/Documentation/git-rev-parse.adoc +++ b/Documentation/git-rev-parse.adoc @@ -199,6 +199,10 @@ If a `pattern` is given, only refs matching the given shell glob are shown. If the pattern does not contain a globbing character (`?`, `*`, or `[`), it is turned into a prefix match by appending `/*`. +--toplevel-branches:: + Show every ref directly under `refs/heads/` (that is, a branch + whose short name does not contain a `/`). + --glob=:: Show all refs matching the shell glob pattern `pattern`. If the pattern does not start with `refs/`, this is automatically diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc index 94a7b1c065dba8..a1e0c7838535f9 100644 --- a/Documentation/rev-list-options.adoc +++ b/Documentation/rev-list-options.adoc @@ -170,6 +170,12 @@ endif::git-log[] branches to ones matching given shell glob. If __ lacks '?', '{asterisk}', or '[', '/{asterisk}' at the end is implied. +`--toplevel-branches`:: + Pretend as if every ref directly under `refs/heads/` is listed on + the command line as __. This is a subset of --branches + which excludes branches in deeper hierarchies, i.e. excluding + branches whose short name contains a `/`. + `--tags[=]`:: Pretend as if all the refs in `refs/tags` are listed on the command line as __. If __ is given, limit 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/builtin/rev-parse.c b/builtin/rev-parse.c index bb882678fe2a9e..a2ce2c5078495a 100644 --- a/builtin/rev-parse.c +++ b/builtin/rev-parse.c @@ -70,6 +70,7 @@ static int is_rev_argument(const char *arg) "--dense", "--branches=", "--branches", + "--toplevel-branches", "--header", "--ignore-missing", "--max-age=", @@ -960,6 +961,15 @@ int cmd_rev_parse(int argc, free(term_bad); continue; } + if (!strcmp(arg, "--toplevel-branches")) { + if (ref_excludes.hidden_refs_configured) + return error(_("options '%s' and '%s' cannot be used together"), + "--exclude-hidden", "--toplevel-branches"); + refs_for_each_toplevel_branch_ref(get_main_ref_store(the_repository), + show_reference, NULL); + clear_ref_exclusions(&ref_excludes); + continue; + } if (opt_with_value(arg, "--branches", &arg)) { if (ref_excludes.hidden_refs_configured) return error(_("options '%s' and '%s' cannot be used together"), diff --git a/connected.c b/connected.c index 7e269768327238..3d1708075760a2 100644 --- a/connected.c +++ b/connected.c @@ -8,6 +8,7 @@ #include "sigchain.h" #include "connected.h" #include "transport.h" +#include "object-name.h" #include "packfile.h" #include "promisor-remote.h" @@ -93,10 +94,17 @@ 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, "--toplevel-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"); diff --git a/connected.h b/connected.h index 16b2c84f2e35fc..0ab84e5c1013f0 100644 --- a/connected.h +++ b/connected.h @@ -50,9 +50,19 @@ 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. This avoids the linear-in-refcount + * enumeration of every visible ref in repositories with many + * branches/tags, 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/refs.c b/refs.c index 0f3355d2ee0be1..f7e18cdf6d216d 100644 --- a/refs.c +++ b/refs.c @@ -552,6 +552,41 @@ int refs_for_each_branch_ref(struct ref_store *refs, refs_for_each_cb cb, void * return refs_for_each_ref_ext(refs, cb, cb_data, &opts); } +struct toplevel_branch_filter { + refs_for_each_cb *fn; + void *cb_data; +}; + +static int filter_toplevel_branch(const struct reference *ref, void *data) +{ + struct toplevel_branch_filter *filter = data; + + /* + * ref->name has had the "refs/heads/" prefix trimmed, so a + * top-level branch like refs/heads/main appears as "main", + * while a sub-directory branch like refs/heads/dscho/wip + * appears as "dscho/wip". + */ + if (strchr(ref->name, '/')) + return 0; + return filter->fn(ref, filter->cb_data); +} + +int refs_for_each_toplevel_branch_ref(struct ref_store *refs, + refs_for_each_cb cb, void *cb_data) +{ + struct toplevel_branch_filter filter = { + .fn = cb, + .cb_data = cb_data, + }; + struct refs_for_each_ref_options opts = { + .prefix = "refs/heads/", + .trim_prefix = strlen("refs/heads/"), + }; + return refs_for_each_ref_ext(refs, filter_toplevel_branch, &filter, + &opts); +} + int refs_for_each_remote_ref(struct ref_store *refs, refs_for_each_cb cb, void *cb_data) { struct refs_for_each_ref_options opts = { diff --git a/refs.h b/refs.h index 71d5c186d044bb..449751b3a1e8dd 100644 --- a/refs.h +++ b/refs.h @@ -506,6 +506,8 @@ int refs_for_each_tag_ref(struct ref_store *refs, refs_for_each_cb fn, void *cb_data); int refs_for_each_branch_ref(struct ref_store *refs, refs_for_each_cb fn, void *cb_data); +int refs_for_each_toplevel_branch_ref(struct ref_store *refs, + refs_for_each_cb fn, void *cb_data); int refs_for_each_remote_ref(struct ref_store *refs, refs_for_each_cb fn, void *cb_data); int refs_for_each_replace_ref(struct ref_store *refs, diff --git a/revision.c b/revision.c index 5693618be4ec81..c527bae0ff920a 100644 --- a/revision.c +++ b/revision.c @@ -2328,6 +2328,7 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg /* pseudo revision arguments */ if (!strcmp(arg, "--all") || !strcmp(arg, "--branches") || + !strcmp(arg, "--toplevel-branches") || !strcmp(arg, "--tags") || !strcmp(arg, "--remotes") || !strcmp(arg, "--reflog") || !strcmp(arg, "--not") || !strcmp(arg, "--no-walk") || !strcmp(arg, "--do-walk") || @@ -2807,6 +2808,12 @@ static int handle_revision_pseudo_opt(struct rev_info *revs, "--exclude-hidden", "--branches"); handle_refs(refs, revs, *flags, refs_for_each_branch_ref); clear_ref_exclusions(&revs->ref_excludes); + } else if (!strcmp(arg, "--toplevel-branches")) { + if (revs->ref_excludes.hidden_refs_configured) + return error(_("options '%s' and '%s' cannot be used together"), + "--exclude-hidden", "--toplevel-branches"); + handle_refs(refs, revs, *flags, refs_for_each_toplevel_branch_ref); + clear_ref_exclusions(&revs->ref_excludes); } else if (!strcmp(arg, "--bisect")) { read_bisect_terms(&term_bad, &term_good); handle_refs(refs, revs, *flags, for_each_bad_bisect_ref); diff --git a/t/t6018-rev-list-glob.sh b/t/t6018-rev-list-glob.sh index bb55c7e3c3c30d..77d6965fd5f0a0 100755 --- a/t/t6018-rev-list-glob.sh +++ b/t/t6018-rev-list-glob.sh @@ -423,4 +423,30 @@ test_expect_failure 'shortlog --glob is not confused by option-like argument' ' ' +test_expect_success 'rev-parse --toplevel-branches matches only top-level branches' ' + + git rev-parse main someref subspace-x | sort >expect && + git rev-parse --toplevel-branches | sort >actual && + test_cmp expect actual + +' + +test_expect_success 'rev-list --toplevel-branches matches only top-level branches' ' + + git rev-list --no-walk main someref subspace-x | sort >expect && + git rev-list --no-walk --toplevel-branches | sort >actual && + test_cmp expect actual + +' + +test_expect_success 'rev-list --not --toplevel-branches excludes top-level reachable' ' + + # "main" itself is a top-level branch, so excluding all + # top-level branches from the walk starting at main leaves + # nothing to enumerate. + git rev-list main --not --toplevel-branches >actual && + test_must_be_empty actual + +' + test_done