Skip to content

Commit d6bb0fc

Browse files
committed
revision: introduce --toplevel-branches
In large repositories which are served at central hubs (such as at hosting forges), there tend to be huge numbers of refs, and even huge numbers of branches. These branches are often of forms like refs/heads/${username}/${topic} refs/heads/${bot}/${unique_name} Sometimes, the sheer number of branches is itself a scaling challenge. There are cases where we'd like a cheap approximation of "the union of the main integration branches" (e.g. "main develop next maint"), and it's okay to also take other branches along for the ride but it's nice to limit how many extra refs we include. Introduce a new pseudo-revision `--toplevel-branches` for this purpose. It is similar to the existing `--branches` pseudo-revision and its `--branches=<pattern>` sibling, but there's been no ergonomic way to exclude the large maze of branches in sub-hierarchies (`*` as a pattern matches across `/`). The next commit will use this to speed up push time reachability checks. Add tests to `t6018-rev-list-glob.sh`, which already constructs a fixture with a mix of top-level branches (`main`, `someref`, `subspace-x`) and sub-directory branches (`other/three`, `subspace/one`, `subspace/two`), making it a natural home for the new coverage. Signed-off-by: Elijah Newren <newren@gmail.com>
1 parent 1666c12 commit d6bb0fc

7 files changed

Lines changed: 103 additions & 0 deletions

File tree

Documentation/git-rev-parse.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ If a `pattern` is given, only refs matching the given shell glob are
199199
shown. If the pattern does not contain a globbing character (`?`,
200200
`*`, or `[`), it is turned into a prefix match by appending `/*`.
201201

202+
--toplevel-branches::
203+
Show every ref directly under `refs/heads/` (that is, a branch
204+
whose short name does not contain a `/`).
205+
202206
--glob=<pattern>::
203207
Show all refs matching the shell glob pattern `pattern`. If
204208
the pattern does not start with `refs/`, this is automatically

Documentation/rev-list-options.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,12 @@ endif::git-log[]
170170
branches to ones matching given shell glob. If _<pattern>_ lacks '?',
171171
'{asterisk}', or '[', '/{asterisk}' at the end is implied.
172172

173+
`--toplevel-branches`::
174+
Pretend as if every ref directly under `refs/heads/` is listed on
175+
the command line as _<commit>_. This is a subset of `--branches`
176+
which excludes branches in deeper hierarchies, i.e. excluding
177+
branches whose short name contains a `/`.
178+
173179
`--tags[=<pattern>]`::
174180
Pretend as if all the refs in `refs/tags` are listed
175181
on the command line as _<commit>_. If _<pattern>_ is given, limit

builtin/rev-parse.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ static int is_rev_argument(const char *arg)
7070
"--dense",
7171
"--branches=",
7272
"--branches",
73+
"--toplevel-branches",
7374
"--header",
7475
"--ignore-missing",
7576
"--max-age=",
@@ -960,6 +961,15 @@ int cmd_rev_parse(int argc,
960961
free(term_bad);
961962
continue;
962963
}
964+
if (!strcmp(arg, "--toplevel-branches")) {
965+
if (ref_excludes.hidden_refs_configured)
966+
return error(_("options '%s' and '%s' cannot be used together"),
967+
"--exclude-hidden", "--toplevel-branches");
968+
refs_for_each_toplevel_branch_ref(get_main_ref_store(the_repository),
969+
show_reference, NULL);
970+
clear_ref_exclusions(&ref_excludes);
971+
continue;
972+
}
963973
if (opt_with_value(arg, "--branches", &arg)) {
964974
if (ref_excludes.hidden_refs_configured)
965975
return error(_("options '%s' and '%s' cannot be used together"),

refs.c

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,41 @@ int refs_for_each_branch_ref(struct ref_store *refs, refs_for_each_cb cb, void *
552552
return refs_for_each_ref_ext(refs, cb, cb_data, &opts);
553553
}
554554

555+
struct toplevel_branch_filter {
556+
refs_for_each_cb *fn;
557+
void *cb_data;
558+
};
559+
560+
static int filter_toplevel_branch(const struct reference *ref, void *data)
561+
{
562+
struct toplevel_branch_filter *filter = data;
563+
564+
/*
565+
* ref->name has had the "refs/heads/" prefix trimmed, so a
566+
* top-level branch like refs/heads/main appears as "main",
567+
* while a sub-directory branch like refs/heads/dscho/wip
568+
* appears as "dscho/wip".
569+
*/
570+
if (strchr(ref->name, '/'))
571+
return 0;
572+
return filter->fn(ref, filter->cb_data);
573+
}
574+
575+
int refs_for_each_toplevel_branch_ref(struct ref_store *refs,
576+
refs_for_each_cb cb, void *cb_data)
577+
{
578+
struct toplevel_branch_filter filter = {
579+
.fn = cb,
580+
.cb_data = cb_data,
581+
};
582+
struct refs_for_each_ref_options opts = {
583+
.prefix = "refs/heads/",
584+
.trim_prefix = strlen("refs/heads/"),
585+
};
586+
return refs_for_each_ref_ext(refs, filter_toplevel_branch, &filter,
587+
&opts);
588+
}
589+
555590
int refs_for_each_remote_ref(struct ref_store *refs, refs_for_each_cb cb, void *cb_data)
556591
{
557592
struct refs_for_each_ref_options opts = {

refs.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,8 @@ int refs_for_each_tag_ref(struct ref_store *refs,
506506
refs_for_each_cb fn, void *cb_data);
507507
int refs_for_each_branch_ref(struct ref_store *refs,
508508
refs_for_each_cb fn, void *cb_data);
509+
int refs_for_each_toplevel_branch_ref(struct ref_store *refs,
510+
refs_for_each_cb fn, void *cb_data);
509511
int refs_for_each_remote_ref(struct ref_store *refs,
510512
refs_for_each_cb fn, void *cb_data);
511513
int refs_for_each_replace_ref(struct ref_store *refs,

revision.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2328,6 +2328,7 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
23282328

23292329
/* pseudo revision arguments */
23302330
if (!strcmp(arg, "--all") || !strcmp(arg, "--branches") ||
2331+
!strcmp(arg, "--toplevel-branches") ||
23312332
!strcmp(arg, "--tags") || !strcmp(arg, "--remotes") ||
23322333
!strcmp(arg, "--reflog") || !strcmp(arg, "--not") ||
23332334
!strcmp(arg, "--no-walk") || !strcmp(arg, "--do-walk") ||
@@ -2807,6 +2808,12 @@ static int handle_revision_pseudo_opt(struct rev_info *revs,
28072808
"--exclude-hidden", "--branches");
28082809
handle_refs(refs, revs, *flags, refs_for_each_branch_ref);
28092810
clear_ref_exclusions(&revs->ref_excludes);
2811+
} else if (!strcmp(arg, "--toplevel-branches")) {
2812+
if (revs->ref_excludes.hidden_refs_configured)
2813+
return error(_("options '%s' and '%s' cannot be used together"),
2814+
"--exclude-hidden", "--toplevel-branches");
2815+
handle_refs(refs, revs, *flags, refs_for_each_toplevel_branch_ref);
2816+
clear_ref_exclusions(&revs->ref_excludes);
28102817
} else if (!strcmp(arg, "--bisect")) {
28112818
read_bisect_terms(&term_bad, &term_good);
28122819
handle_refs(refs, revs, *flags, for_each_bad_bisect_ref);

t/t6018-rev-list-glob.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,4 +423,43 @@ test_expect_failure 'shortlog --glob is not confused by option-like argument' '
423423
424424
'
425425

426+
test_expect_success 'rev-parse --toplevel-branches matches only top-level branches' '
427+
428+
git rev-parse main someref subspace-x | sort >expect &&
429+
git rev-parse --toplevel-branches | sort >actual &&
430+
test_cmp expect actual
431+
432+
'
433+
434+
test_expect_success 'rev-list --toplevel-branches matches only top-level branches' '
435+
436+
git rev-list --no-walk main someref subspace-x | sort >expect &&
437+
git rev-list --no-walk --toplevel-branches | sort >actual &&
438+
test_cmp expect actual
439+
440+
'
441+
442+
test_expect_success 'rev-list --not --toplevel-branches excludes top-level reachable' '
443+
444+
# "main" itself is a top-level branch, so excluding all
445+
# top-level branches from the walk starting at main leaves
446+
# nothing to enumerate.
447+
git rev-list main --not --toplevel-branches >actual &&
448+
test_must_be_empty actual
449+
450+
'
451+
452+
for section in fetch receive uploadpack
453+
do
454+
test_expect_success "rev-parse --exclude-hidden=$section fails with --toplevel-branches" '
455+
test_must_fail git rev-parse --exclude-hidden=$section --toplevel-branches 2>err &&
456+
test_grep "error: options .--exclude-hidden. and .--toplevel-branches. cannot be used together" err
457+
'
458+
459+
test_expect_success "rev-list --exclude-hidden=$section fails with --toplevel-branches" '
460+
test_must_fail git rev-list --exclude-hidden=$section --toplevel-branches 2>err &&
461+
test_grep "error: options .--exclude-hidden. and .--toplevel-branches. cannot be used together" err
462+
'
463+
done
464+
426465
test_done

0 commit comments

Comments
 (0)