promisor-remote: introduce promisor.acceptFromServerUrl

The "promisor-remote" protocol capability allows servers to advertise promisor remotes, but doesn't allow these remotes to be automatically configured on the client. Let's introduce a new `promisor.acceptFromServerUrl` config variable which contains a glob pattern, so that advertised remotes with a URL matching that pattern will be automatically configured. The glob pattern can optionally be prefixed with a remote name which will be used as the name of the new local remote. For now though, let's only introduce the functions to read and validate the glob patterns and the optional prefixes. Checking if the URLs of the advertised remotes match the glob patterns and taking the appropriate action is left for a following commit. Signed-off-by: Christian Couder <chriscool@tuxfamily.org> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Christian Couder committed May 27, 2026 at 16:08 UTC 78e0d9b0e4649659cc38545450174d293cb1ec0c
2 files changed +111
promisor-remote.c
+90
@@ -12,6 +12,7 @@
12 #include "packfile.h"
13 #include "environment.h"
14 #include "url.h"
15 +#include "urlmatch.h"
16 #include "version.h"
17
18 struct promisor_remote_config {
@@ -657,6 +658,90 @@ static bool has_control_char(const char *s)
658 return false;
659 }
660
661 +struct allowed_url {
662 + char *remote_name;
663 + char *url_pattern;
664 + struct url_info pattern_info;
665 +};
666 +
667 +static void allowed_url_free(void *util, const char *str UNUSED)
668 +{
669 + struct allowed_url *allowed = util;
670 +
671 + if (!allowed)
672 + return;
673 +
674 + /* Depending on prefix, free either remote_name or url_pattern */
675 + free(allowed->remote_name ? allowed->remote_name : allowed->url_pattern);
676 + free(allowed->pattern_info.url);
677 + free(allowed);
678 +}
679 +
680 +static struct allowed_url *valid_accept_url(const char *url)
681 +{
682 + char *dup, *p;
683 + struct allowed_url *allowed;
684 +
685 + if (!url)
686 + return NULL;
687 +
688 + dup = xstrdup(url);
689 + p = strchr(dup, '=');
690 + if (p) {
691 + *p = '\0';
692 + if (!valid_remote_name(dup)) {
693 + warning(_("invalid remote name '%s' before '=' sign "
694 + "in '%s' from promisor.acceptFromServerUrl config"),
695 + dup, url);
696 + free(dup);
697 + return NULL;
698 + }
699 + p++;
700 + } else {
701 + p = dup;
702 + }
703 +
704 + if (has_control_char(p)) {
705 + warning(_("invalid url pattern '%s' "
706 + "in '%s' from promisor.acceptFromServerUrl config"), p, url);
707 + free(dup);
708 + return NULL;
709 + }
710 +
711 + allowed = xmalloc(sizeof(*allowed));
712 + allowed->remote_name = (p == dup) ? NULL : dup;
713 + allowed->url_pattern = p;
714 + allowed->pattern_info.url = url_normalize_pattern(p, &allowed->pattern_info);
715 + if (!allowed->pattern_info.url) {
716 + warning(_("invalid url pattern '%s' "
717 + "in '%s' from promisor.acceptFromServerUrl config"), p, url);
718 + free(dup);
719 + free(allowed);
720 + return NULL;
721 + }
722 +
723 + return allowed;
724 +}
725 +
726 +static void load_accept_from_server_url(struct repository *repo,
727 + struct string_list *accept_urls)
728 +{
729 + const struct string_list *config_urls;
730 +
731 + if (!repo_config_get_string_multi(repo, "promisor.acceptfromserverurl", &config_urls)) {
732 + struct string_list_item *item;
733 +
734 + for_each_string_list_item(item, config_urls) {
735 + struct allowed_url *allowed = valid_accept_url(item->string);
736 + if (allowed) {
737 + struct string_list_item *new;
738 + new = string_list_append(accept_urls, item->string);
739 + new->util = allowed;
740 + }
741 + }
742 + }
743 +}
744 +
745 static int should_accept_remote(enum accept_promisor accept,
746 struct promisor_info *advertised,
747 struct string_list *config_info)
@@ -901,6 +986,10 @@ static void filter_promisor_remote(struct repository *repo,
986 struct string_list_item *item;
987 bool reload_config = false;
988 enum accept_promisor accept = accept_from_server(repo);
989 + struct string_list accept_urls = STRING_LIST_INIT_DUP;
990 +
991 + /* Load and validate the acceptFromServerUrl config */
992 + load_accept_from_server_url(repo, &accept_urls);
993
994 if (accept == ACCEPT_NONE)
995 return;
@@ -934,6 +1023,7 @@ static void filter_promisor_remote(struct repository *repo,
1023 }
1024 }
1025
1026 + string_list_clear_func(&accept_urls, allowed_url_free);
1027 promisor_info_list_clear(&config_info);
1028 string_list_clear(&remote_info, 0);
1029 store_info_free(store_info);
t/t5710-promisor-remote-capability.sh
+21
@@ -387,6 +387,27 @@ test_expect_success "clone with 'KnownUrl' and empty url, so not advertised" '
387 check_missing_objects server 1 "$oid"
388 '
389
390 +test_expect_success "clone with invalid promisor.acceptFromServerUrl" '
391 + git -C server config promisor.advertise true &&
392 + test_when_finished "rm -rf client" &&
393 +
394 + # As "bad name" contains a space, which is not a valid remote name,
395 + # the pattern should be rejected with a warning and no remote created.
396 + GIT_NO_LAZY_FETCH=0 git clone \
397 + -c promisor.acceptfromserver=None \
398 + -c "promisor.acceptFromServerUrl=bad name=https://example.com/*" \
399 + --no-local --filter="blob:limit=5k" server client 2>err &&
400 +
401 + # Check that a warning was emitted
402 + test_grep "invalid remote name '\''bad name'\''" err &&
403 +
404 + # Check that the largest object is not missing on the server
405 + check_missing_objects server 0 "" &&
406 +
407 + # Reinitialize server so that the largest object is missing again
408 + initialize_server 1 "$oid"
409 +'
410 +
411 test_expect_success "clone with promisor.sendFields" '
412 git -C server config promisor.advertise true &&
413 test_when_finished "rm -rf client" &&