@@ -2210,12 +2210,14 @@ static int for_each_fullref_in_pattern(struct ref_filter *filter,
if (!filter->name_patterns[0]) {
/* no patterns; we have to look at everything */
- return for_each_fullref_in("", cb, cb_data);
+ return refs_for_each_fullref_in(get_main_ref_store(the_repository),
+ "", filter->exclude.v, cb, cb_data);
}
return refs_for_each_fullref_in_prefixes(get_main_ref_store(the_repository),
NULL, filter->name_patterns,
- NULL, cb, cb_data);
+ filter->exclude.v,
+ cb, cb_data);
}
/*
@@ -343,6 +343,10 @@ int for_each_ref(each_ref_fn fn, void *cb_data);
*/
int for_each_ref_in(const char *prefix, each_ref_fn fn, void *cb_data);
+/*
+ * references matching any pattern in "exclude_patterns" are omitted from the
+ * result set on a best-effort basis.
+ */
int refs_for_each_fullref_in(struct ref_store *refs, const char *prefix,
const char **exclude_patterns,
each_ref_fn fn, void *cb_data);
@@ -304,7 +304,8 @@ static int cmp_packed_ref_records(const void *v1, const void *v2)
* Compare a snapshot record at `rec` to the specified NUL-terminated
* refname.
*/
-static int cmp_record_to_refname(const char *rec, const char *refname)
+static int cmp_record_to_refname(const char *rec, const char *refname,
+ int start)
{
const char *r1 = rec + the_hash_algo->hexsz + 1;
const char *r2 = refname;
@@ -313,7 +314,7 @@ static int cmp_record_to_refname(const char *rec, const char *refname)
if (*r1 == '\n')
return *r2 ? -1 : 0;
if (!*r2)
- return 1;
+ return start ? 1 : -1;
if (*r1 != *r2)
return (unsigned char)*r1 < (unsigned char)*r2 ? -1 : +1;
r1++;
@@ -529,7 +530,8 @@ static int load_contents(struct snapshot *snapshot)
}
static const char *find_reference_location_1(struct snapshot *snapshot,
- const char *refname, int mustexist)
+ const char *refname, int mustexist,
+ int start)
{
/*
* This is not *quite* a garden-variety binary search, because
@@ -559,7 +561,7 @@ static const char *find_reference_location_1(struct snapshot *snapshot,
mid = lo + (hi - lo) / 2;
rec = find_start_of_record(lo, mid);
- cmp = cmp_record_to_refname(rec, refname);
+ cmp = cmp_record_to_refname(rec, refname, start);
if (cmp < 0) {
lo = find_end_of_record(mid, hi);
} else if (cmp > 0) {
@@ -592,7 +594,22 @@ static const char *find_reference_location_1(struct snapshot *snapshot,
static const char *find_reference_location(struct snapshot *snapshot,
const char *refname, int mustexist)
{
- return find_reference_location_1(snapshot, refname, mustexist);
+ return find_reference_location_1(snapshot, refname, mustexist, 1);
+}
+
+/*
+ * Find the place in `snapshot->buf` after the end of the record for
+ * `refname`. In other words, find the location of first thing *after*
+ * `refname`.
+ *
+ * Other semantics are identical to the ones in
+ * `find_reference_location()`.
+ */
+static const char *find_reference_location_end(struct snapshot *snapshot,
+ const char *refname,
+ int mustexist)
+{
+ return find_reference_location_1(snapshot, refname, mustexist, 0);
}
/*
@@ -786,6 +803,13 @@ struct packed_ref_iterator {
/* The end of the part of the buffer that will be iterated over: */
const char *eof;
+ struct jump_list_entry {
+ const char *start;
+ const char *end;
+ } *jump;
+ size_t jump_nr, jump_alloc;
+ size_t jump_cur;
+
/* Scratch space for current values: */
struct object_id oid, peeled;
struct strbuf refname_buf;
@@ -803,14 +827,35 @@ struct packed_ref_iterator {
*/
static int next_record(struct packed_ref_iterator *iter)
{
- const char *p = iter->pos, *eol;
+ const char *p, *eol;
strbuf_reset(&iter->refname_buf);
+ /*
+ * If iter->pos is contained within a skipped region, jump past
+ * it.
+ *
+ * Note that each skipped region is considered at most once,
+ * since they are ordered based on their starting position.
+ */
+ while (iter->jump_cur < iter->jump_nr) {
+ struct jump_list_entry *curr = &iter->jump[iter->jump_cur];
+ if (iter->pos < curr->start)
+ break; /* not to the next jump yet */
+
+ iter->jump_cur++;
+ if (iter->pos < curr->end) {
+ iter->pos = curr->end;
+ /* jumps are coalesced, so only one jump is necessary */
+ break;
+ }
+ }
+
if (iter->pos == iter->eof)
return ITER_DONE;
iter->base.flags = REF_ISPACKED;
+ p = iter->pos;
if (iter->eof - p < the_hash_algo->hexsz + 2 ||
parse_oid_hex(p, &iter->oid, &p) ||
@@ -918,6 +963,7 @@ static int packed_ref_iterator_abort(struct ref_iterator *ref_iterator)
int ok = ITER_DONE;
strbuf_release(&iter->refname_buf);
+ free(iter->jump);
release_snapshot(iter->snapshot);
base_ref_iterator_free(ref_iterator);
return ok;
@@ -929,6 +975,108 @@ static struct ref_iterator_vtable packed_ref_iterator_vtable = {
.abort = packed_ref_iterator_abort
};
+static int jump_list_entry_cmp(const void *va, const void *vb)
+{
+ const struct jump_list_entry *a = va;
+ const struct jump_list_entry *b = vb;
+
+ if (a->start < b->start)
+ return -1;
+ if (a->start > b->start)
+ return 1;
+ return 0;
+}
+
+static int has_glob_special(const char *str)
+{
+ const char *p;
+ for (p = str; *p; p++) {
+ if (is_glob_special(*p))
+ return 1;
+ }
+ return 0;
+}
+
+static void populate_excluded_jump_list(struct packed_ref_iterator *iter,
+ struct snapshot *snapshot,
+ const char **excluded_patterns)
+{
+ size_t i, j;
+ const char **pattern;
+ struct jump_list_entry *last_disjoint;
+
+ if (!excluded_patterns)
+ return;
+
+ for (pattern = excluded_patterns; *pattern; pattern++) {
+ struct jump_list_entry *e;
+ const char *start, *end;
+
+ /*
+ * We can't feed any excludes with globs in them to the
+ * refs machinery. It only understands prefix matching.
+ * We likewise can't even feed the string leading up to
+ * the first meta-character, as something like "foo[a]"
+ * should not exclude "foobar" (but the prefix "foo"
+ * would match that and mark it for exclusion).
+ */
+ if (has_glob_special(*pattern))
+ continue;
+
+ start = find_reference_location(snapshot, *pattern, 0);
+ end = find_reference_location_end(snapshot, *pattern, 0);
+
+ if (start == end)
+ continue; /* nothing to jump over */
+
+ ALLOC_GROW(iter->jump, iter->jump_nr + 1, iter->jump_alloc);
+
+ e = &iter->jump[iter->jump_nr++];
+ e->start = start;
+ e->end = end;
+ }
+
+ if (!iter->jump_nr) {
+ /*
+ * Every entry in exclude_patterns has a meta-character,
+ * nothing to do here.
+ */
+ return;
+ }
+
+ QSORT(iter->jump, iter->jump_nr, jump_list_entry_cmp);
+
+ /*
+ * As an optimization, merge adjacent entries in the jump list
+ * to jump forwards as far as possible when entering a skipped
+ * region.
+ *
+ * For example, if we have two skipped regions:
+ *
+ * [[A, B], [B, C]]
+ *
+ * we want to combine that into a single entry jumping from A to
+ * C.
+ */
+ last_disjoint = iter->jump;
+
+ for (i = 1, j = 1; i < iter->jump_nr; i++) {
+ struct jump_list_entry *ours = &iter->jump[i];
+ if (ours->start <= last_disjoint->end) {
+ /* overlapping regions extend the previous one */
+ last_disjoint->end = last_disjoint->end > ours->end
+ ? last_disjoint->end : ours->end;
+ } else {
+ /* otherwise, insert a new region */
+ iter->jump[j++] = *ours;
+ last_disjoint = ours;
+ }
+ }
+
+ iter->jump_nr = j;
+ iter->jump_cur = 0;
+}
+
static struct ref_iterator *packed_ref_iterator_begin(
struct ref_store *ref_store,
const char *prefix, const char **exclude_patterns,
@@ -964,6 +1112,9 @@ static struct ref_iterator *packed_ref_iterator_begin(
ref_iterator = &iter->base;
base_ref_iterator_init(ref_iterator, &packed_ref_iterator_vtable, 1);
+ if (exclude_patterns)
+ populate_excluded_jump_list(iter, snapshot, exclude_patterns);
+
iter->snapshot = snapshot;
acquire_snapshot(snapshot);
@@ -186,6 +186,15 @@ static int cmd_for_each_ref(struct ref_store *refs, const char **argv)
return refs_for_each_ref_in(refs, prefix, each_ref, NULL);
}
+static int cmd_for_each_ref__exclude(struct ref_store *refs, const char **argv)
+{
+ const char *prefix = notnull(*argv++, "prefix");
+ const char **exclude_patterns = argv;
+
+ return refs_for_each_fullref_in(refs, prefix, exclude_patterns, each_ref,
+ NULL);
+}
+
static int cmd_resolve_ref(struct ref_store *refs, const char **argv)
{
struct object_id oid = *null_oid();
@@ -318,6 +327,7 @@ static struct command commands[] = {
{ "delete-refs", cmd_delete_refs },
{ "rename-ref", cmd_rename_ref },
{ "for-each-ref", cmd_for_each_ref },
+ { "for-each-ref--exclude", cmd_for_each_ref__exclude },
{ "resolve-ref", cmd_resolve_ref },
{ "verify-ref", cmd_verify_ref },
{ "for-each-reflog", cmd_for_each_reflog },
new file mode 100755
@@ -0,0 +1,101 @@
+#!/bin/sh
+
+test_description='test exclude_patterns functionality in main ref store'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+TEST_PASSES_SANITIZE_LEAK=true
+. ./test-lib.sh
+
+for_each_ref__exclude () {
+ test-tool ref-store main for-each-ref--exclude "$@" >actual.raw
+ cut -d ' ' -f 2 actual.raw
+}
+
+for_each_ref () {
+ git for-each-ref --format='%(refname)' "$@"
+}
+
+test_expect_success 'setup' '
+ test_commit --no-tag base &&
+ base="$(git rev-parse HEAD)" &&
+
+ for name in foo bar baz quux
+ do
+ for i in 1 2 3
+ do
+ echo "create refs/heads/$name/$i $base" || return 1
+ done || return 1
+ done >in &&
+ echo "delete refs/heads/main" >>in &&
+
+ git update-ref --stdin <in &&
+ git pack-refs --all
+'
+
+test_expect_success 'excluded region in middle' '
+ for_each_ref__exclude refs/heads refs/heads/foo >actual &&
+ for_each_ref refs/heads/bar refs/heads/baz refs/heads/quux >expect &&
+
+ test_cmp expect actual
+'
+
+test_expect_success 'excluded region at beginning' '
+ for_each_ref__exclude refs/heads refs/heads/bar >actual &&
+ for_each_ref refs/heads/baz refs/heads/foo refs/heads/quux >expect &&
+
+ test_cmp expect actual
+'
+
+test_expect_success 'excluded region at end' '
+ for_each_ref__exclude refs/heads refs/heads/quux >actual &&
+ for_each_ref refs/heads/foo refs/heads/bar refs/heads/baz >expect &&
+
+ test_cmp expect actual
+'
+
+test_expect_success 'disjoint excluded regions' '
+ for_each_ref__exclude refs/heads refs/heads/bar refs/heads/quux >actual &&
+ for_each_ref refs/heads/baz refs/heads/foo >expect &&
+
+ test_cmp expect actual
+'
+
+test_expect_success 'adjacent, non-overlapping excluded regions' '
+ for_each_ref__exclude refs/heads refs/heads/bar refs/heads/baz >actual &&
+ for_each_ref refs/heads/foo refs/heads/quux >expect &&
+
+ test_cmp expect actual
+'
+
+test_expect_success 'overlapping excluded regions' '
+ for_each_ref__exclude refs/heads refs/heads/ba refs/heads/baz >actual &&
+ for_each_ref refs/heads/foo refs/heads/quux >expect &&
+
+ test_cmp expect actual
+'
+
+test_expect_success 'several overlapping excluded regions' '
+ for_each_ref__exclude refs/heads \
+ refs/heads/bar refs/heads/baz refs/heads/foo >actual &&
+ for_each_ref refs/heads/quux >expect &&
+
+ test_cmp expect actual
+'
+
+test_expect_success 'non-matching excluded section' '
+ for_each_ref__exclude refs/heads refs/heads/does/not/exist >actual &&
+ for_each_ref >expect &&
+
+ test_cmp expect actual
+'
+
+test_expect_success 'meta-characters are discarded' '
+ for_each_ref__exclude refs/heads "refs/heads/ba*" >actual &&
+ for_each_ref >expect &&
+
+ test_cmp expect actual
+'
+
+test_done