refs: properly apply exclude patterns to namespaced refs

Reference namespaces allow commands like git-upload-pack(1) to serve different sets of references to the client depending on which namespace is enabled, which is for example useful in fork networks. Namespaced refs are stored with a `refs/namespaces/$namespace` prefix, but all the user will ultimately see is a stripped version where that prefix is removed. The way that this interacts with "transfer.hideRefs" is not immediately obvious: the hidden refs can either apply to the stripped references, or to the non-stripped ones that still have the namespace prefix. In fact, the "transfer.hideRefs" machinery does the former and applies to the stripped reference by default, but rules can have "^" prefixed to switch this behaviour to instead match against the full reference name. Namespaces are exclusively handled at the generic "refs" layer, the respective backends have no clue that such a thing even exists. This also has the consequence that they cannot handle hiding references as soon as reference namespaces come into play because they neither know whether a namespace is active, nor do they know how to strip references if they are active. Handling such exclude patterns in `refs_for_each_namespaced_ref()` and `refs_for_each_fullref_in_prefixes()` is broken though, as both support that the user passes both namespaces and exclude patterns. In the case where both are set we will exclude references with unstripped names, even though we really wanted to exclude references based on their stripped names. This only surfaces when: - A repository uses reference namespaces. - "transfer.hideRefs" is active. - The namespaced references are packed into the "packed-refs" file. None of our tests exercise this scenario, and thus we haven't ever hit it. While t5509 exercises both (1) and (2), it does not happen to hit (3). It is trivial to demonstrate the bug though by explicitly packing refs in the tests, and then we indeed surface the breakage. Fix this bug by prefixing exclude patterns with the namespace in the generic layer. The newly introduced function will be used outside of "refs.c" in the next patch, so we add a declaration to "refs.h". Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Sep 16, 2024 at 10:50 UTC 155dc8447d3590ea856bb17919bfc85172b52e09
3 files changed +41 -4
refs.c
+31 -4
@@ -1517,6 +1517,19 @@ const char **hidden_refs_to_excludes(const struct strvec *hide_refs)
1517 return hide_refs->v;
1518 }
1519
1520 +const char **get_namespaced_exclude_patterns(const char **exclude_patterns,
1521 + const char *namespace,
1522 + struct strvec *out)
1523 +{
1524 + if (!namespace || !*namespace || !exclude_patterns || !*exclude_patterns)
1525 + return exclude_patterns;
1526 +
1527 + for (size_t i = 0; exclude_patterns[i]; i++)
1528 + strvec_pushf(out, "%s%s", namespace, exclude_patterns[i]);
1529 +
1530 + return out->v;
1531 +}
1532 +
1533 const char *find_descendant_ref(const char *dirname,
1534 const struct string_list *extras,
1535 const struct string_list *skip)
@@ -1634,11 +1647,19 @@ int refs_for_each_namespaced_ref(struct ref_store *refs,
1647 const char **exclude_patterns,
1648 each_ref_fn fn, void *cb_data)
1649 {
1637 - struct strbuf buf = STRBUF_INIT;
1650 + struct strvec namespaced_exclude_patterns = STRVEC_INIT;
1651 + struct strbuf prefix = STRBUF_INIT;
1652 int ret;
1639 - strbuf_addf(&buf, "%srefs/", get_git_namespace());
1640 - ret = do_for_each_ref(refs, buf.buf, exclude_patterns, fn, 0, 0, cb_data);
1641 - strbuf_release(&buf);
1653 +
1654 + exclude_patterns = get_namespaced_exclude_patterns(exclude_patterns,
1655 + get_git_namespace(),
1656 + &namespaced_exclude_patterns);
1657 +
1658 + strbuf_addf(&prefix, "%srefs/", get_git_namespace());
1659 + ret = do_for_each_ref(refs, prefix.buf, exclude_patterns, fn, 0, 0, cb_data);
1660 +
1661 + strvec_clear(&namespaced_exclude_patterns);
1662 + strbuf_release(&prefix);
1663 return ret;
1664 }
1665
@@ -1719,6 +1740,7 @@ int refs_for_each_fullref_in_prefixes(struct ref_store *ref_store,
1740 const char **exclude_patterns,
1741 each_ref_fn fn, void *cb_data)
1742 {
1743 + struct strvec namespaced_exclude_patterns = STRVEC_INIT;
1744 struct string_list prefixes = STRING_LIST_INIT_DUP;
1745 struct string_list_item *prefix;
1746 struct strbuf buf = STRBUF_INIT;
@@ -1730,6 +1752,10 @@ int refs_for_each_fullref_in_prefixes(struct ref_store *ref_store,
1752 strbuf_addstr(&buf, namespace);
1753 namespace_len = buf.len;
1754
1755 + exclude_patterns = get_namespaced_exclude_patterns(exclude_patterns,
1756 + namespace,
1757 + &namespaced_exclude_patterns);
1758 +
1759 for_each_string_list_item(prefix, &prefixes) {
1760 strbuf_addstr(&buf, prefix->string);
1761 ret = refs_for_each_fullref_in(ref_store, buf.buf,
@@ -1739,6 +1765,7 @@ int refs_for_each_fullref_in_prefixes(struct ref_store *ref_store,
1765 strbuf_setlen(&buf, namespace_len);
1766 }
1767
1768 + strvec_clear(&namespaced_exclude_patterns);
1769 string_list_clear(&prefixes, 0);
1770 strbuf_release(&buf);
1771 return ret;
refs.h
+9
@@ -859,6 +859,15 @@ int ref_is_hidden(const char *, const char *, const struct strvec *);
859 */
860 const char **hidden_refs_to_excludes(const struct strvec *hide_refs);
861
862 +/*
863 + * Prefix all exclude patterns with the namespace, if any. This is required
864 + * because exclude patterns apply to the stripped reference name, not the full
865 + * reference name with the namespace.
866 + */
867 +const char **get_namespaced_exclude_patterns(const char **exclude_patterns,
868 + const char *namespace,
869 + struct strvec *out);
870 +
871 /* Is this a per-worktree ref living in the refs/ namespace? */
872 int is_per_worktree_ref(const char *refname);
873
t/t5509-fetch-push-namespaces.sh
+1
@@ -96,6 +96,7 @@ test_expect_success 'hide namespaced refs with transfer.hideRefs' '
96 '
97
98 test_expect_success 'check that transfer.hideRefs does not match unstripped refs' '
99 + git -C pushee pack-refs --all &&
100 GIT_NAMESPACE=namespace \
101 git -C pushee -c transfer.hideRefs=refs/namespaces/namespace/refs/tags \
102 ls-remote "ext::git %s ." >actual &&